feat: add warm sand theme colors and light / dark mode toggle (#302)
This commit was merged in pull request #302.
This commit is contained in:
@@ -172,7 +172,7 @@ export function AttachmentLightbox({
|
||||
}}
|
||||
>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/90 backdrop-blur-sm duration-100" />
|
||||
<DialogPrimitive.Overlay className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-scrim/90 fixed inset-0 z-50 backdrop-blur-sm duration-100" />
|
||||
<DialogPrimitive.Content
|
||||
aria-describedby={undefined}
|
||||
className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 flex items-center justify-center p-16 outline-none duration-100"
|
||||
|
||||
@@ -62,7 +62,7 @@ function AttachmentThumbnail({
|
||||
tabIndex={previewable ? 0 : undefined}
|
||||
onClick={previewable && onPreview ? onPreview : undefined}
|
||||
className={cn(
|
||||
'group relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-white/10',
|
||||
'bg-muted group relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg',
|
||||
previewable && 'cursor-pointer',
|
||||
isError && 'ring-1 ring-red-400/50',
|
||||
)}
|
||||
@@ -75,19 +75,19 @@ function AttachmentThumbnail({
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-0.5 px-1">
|
||||
<FileIcon className="size-5 text-white/60" />
|
||||
<span className="max-w-full truncate text-[9px] text-white/50">
|
||||
<FileIcon className="text-muted-foreground size-5" />
|
||||
<span className="text-muted-foreground max-w-full truncate text-[9px]">
|
||||
{attachment.file.name}
|
||||
</span>
|
||||
<span className="text-[9px] text-white/40">
|
||||
<span className="text-muted-foreground text-[9px]">
|
||||
{formatFileSize(attachment.file.size)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
||||
<Loader2 className="size-4 animate-spin text-white/70" />
|
||||
<div className="bg-scrim/50 absolute inset-0 flex items-center justify-center">
|
||||
<Loader2 className="size-4 animate-spin text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -97,7 +97,7 @@ function AttachmentThumbnail({
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
className="absolute right-0.5 top-0.5 hidden rounded-full bg-black/70 p-0.5 text-white/70 hover:text-white group-hover:block"
|
||||
className="bg-scrim/70 absolute right-0.5 top-0.5 hidden rounded-full p-0.5 text-white/80 hover:text-white group-hover:block"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
@@ -108,9 +108,9 @@ function AttachmentThumbnail({
|
||||
function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
|
||||
if (entry.isLoading) {
|
||||
return (
|
||||
<div className="flex h-16 w-28 shrink-0 flex-col gap-1.5 rounded-lg bg-white/10 p-2">
|
||||
<Skeleton className="h-2 w-16 bg-white/10" />
|
||||
<Skeleton className="h-3 w-24 bg-white/10" />
|
||||
<div className="bg-muted flex h-16 w-28 shrink-0 flex-col gap-1.5 rounded-lg p-2">
|
||||
<Skeleton className="h-2 w-16" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,9 +124,9 @@ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => platform.link.openExternal(metadata?.url ?? entry.url)}
|
||||
className="flex h-16 w-28 shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-lg bg-white/10 px-2 py-1.5 text-left transition-colors hover:bg-white/15"
|
||||
className="bg-muted hover:bg-accent flex h-16 w-28 shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-lg px-2 py-1.5 text-left transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-1 text-[10px] text-white/40">
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-[10px]">
|
||||
{metadata?.favicon ? (
|
||||
<img
|
||||
src={metadata.favicon}
|
||||
@@ -142,7 +142,7 @@ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
|
||||
<span className="truncate">{domain}</span>
|
||||
</div>
|
||||
{title && (
|
||||
<p className="line-clamp-2 text-[11px] font-medium leading-tight text-white/80">
|
||||
<p className="text-foreground line-clamp-2 text-[11px] font-medium leading-tight">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
@@ -197,7 +197,7 @@ export function AttachmentStrip({
|
||||
e.stopPropagation();
|
||||
onAddClick();
|
||||
}}
|
||||
className="flex h-16 w-16 shrink-0 items-center justify-center rounded-lg border border-dashed border-white/20 text-white/40 transition-colors hover:border-white/40 hover:text-white/60"
|
||||
className="border-border text-muted-foreground hover:border-foreground/40 hover:text-foreground flex h-16 w-16 shrink-0 items-center justify-center rounded-lg border border-dashed transition-colors"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -768,7 +768,7 @@ export function ComposeOverlay({
|
||||
/>
|
||||
)}
|
||||
{step === 'recording' && recordingSource === 'screen' && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90">
|
||||
<div className="bg-scrim/90 absolute inset-0 z-50 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-8 z-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 animate-pulse rounded-full bg-red-500" />
|
||||
@@ -840,8 +840,8 @@ export function ComposeOverlay({
|
||||
/>
|
||||
)}
|
||||
{step === 'submitting' && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90">
|
||||
<span className="animate-pulse text-sm text-white/60">
|
||||
<div className="bg-background/95 absolute inset-0 z-50 flex flex-col items-center justify-center backdrop-blur-sm">
|
||||
<span className="text-muted-foreground animate-pulse text-sm">
|
||||
Sending...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -77,13 +77,13 @@ export function ConfigureContainerStep({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex flex-col bg-black/90 pt-16"
|
||||
className="bg-background/95 absolute inset-0 z-50 flex flex-col pt-16 backdrop-blur-sm"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-sm space-y-4 px-6">
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">
|
||||
<Label className="text-muted-foreground mb-1 text-xs">
|
||||
{kind === 'folder' ? 'Folder name' : 'Stream name'}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -94,21 +94,22 @@ export function ConfigureContainerStep({
|
||||
setName(e.target.value);
|
||||
}}
|
||||
placeholder="Give it a name..."
|
||||
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Visibility */}
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Visible to</Label>
|
||||
<div className="rounded-md border border-white/10">
|
||||
<Label className="text-muted-foreground mb-1 text-xs">
|
||||
Visible to
|
||||
</Label>
|
||||
<div className="border-border rounded-md border">
|
||||
{/* Everyone in network */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => setEveryone((prev) => !prev)}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors',
|
||||
'text-white/70 hover:bg-white/5',
|
||||
'hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -137,7 +138,7 @@ export function ConfigureContainerStep({
|
||||
onClick={() => toggleMember(member.id)}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center gap-2.5 rounded px-3 py-1.5 text-left text-sm transition-colors',
|
||||
'text-white/70 hover:bg-white/5',
|
||||
'hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -145,7 +146,7 @@ export function ConfigureContainerStep({
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-white/10 text-[10px] font-medium">
|
||||
<span className="bg-muted flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-medium">
|
||||
{initials}
|
||||
</span>
|
||||
<span className="flex-1 truncate">
|
||||
@@ -162,7 +163,7 @@ export function ConfigureContainerStep({
|
||||
</div>
|
||||
|
||||
{/* Keyboard hints */}
|
||||
<div className="absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm">
|
||||
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
||||
cancel
|
||||
</KeyHint>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* Theme Milkdown Crepe to blend into the app's translucent "glass" surfaces.
|
||||
* Crepe's frame-dark theme is a grayscale palette driven by --crepe-* custom
|
||||
* properties; we override those to white-on-transparent with a blue accent so
|
||||
* the editor (and the read-only display, which uses the same engine) sits on
|
||||
/* Theme Milkdown Crepe to the app's design tokens so it follows light/dark.
|
||||
* Crepe's theme is a palette driven by --crepe-* custom properties; we map
|
||||
* those onto our CSS variables (globals.css) so the editor (and the read-only
|
||||
* display, which uses the same engine) inherits the active theme and sits on
|
||||
* top of the existing card instead of painting its own opaque background.
|
||||
*
|
||||
* The floating menus (slash menu, selection toolbar, link tooltip) are portaled
|
||||
* to <body>, OUTSIDE .milkdown — so the variables must be declared on those
|
||||
* selectors too, otherwise they fall back to transparent and the menu is
|
||||
* unreadable over the content behind it. */
|
||||
* unreadable over the content behind it. Our tokens resolve against the
|
||||
* :root/.dark cascade on <html>, so the portaled menus stay themed too. */
|
||||
.llink-crepe .milkdown,
|
||||
.milkdown-slash-menu,
|
||||
.milkdown-toolbar,
|
||||
@@ -15,24 +16,30 @@
|
||||
.milkdown-link-preview,
|
||||
.milkdown-block-handle {
|
||||
--crepe-color-background: transparent;
|
||||
--crepe-color-on-background: rgb(255 255 255 / 0.92);
|
||||
/* Surface backs code blocks, tables, and the floating menus — keep it
|
||||
* (near-)opaque so menus read clearly over content. */
|
||||
--crepe-color-surface: rgb(24 24 28 / 0.96);
|
||||
--crepe-color-surface-low: rgb(42 42 50 / 0.96);
|
||||
--crepe-color-on-surface: #ffffff;
|
||||
--crepe-color-on-surface-variant: rgb(255 255 255 / 0.65);
|
||||
--crepe-color-outline: rgb(255 255 255 / 0.2);
|
||||
--crepe-color-primary: #60a5fa;
|
||||
--crepe-color-secondary: rgb(96 165 250 / 0.25);
|
||||
--crepe-color-on-secondary: #ffffff;
|
||||
--crepe-color-inverse: #ffffff;
|
||||
--crepe-color-on-inverse: #0b0b0b;
|
||||
--crepe-color-inline-code: #fca5a5;
|
||||
--crepe-color-inline-area: rgb(255 255 255 / 0.1);
|
||||
--crepe-color-error: #f87171;
|
||||
--crepe-color-hover: rgb(255 255 255 / 0.08);
|
||||
--crepe-color-selected: rgb(96 165 250 / 0.25);
|
||||
--crepe-color-on-background: var(--card-foreground);
|
||||
/* Surface backs code blocks, tables, and the floating menus — use an opaque
|
||||
* themed surface so menus read clearly over content. */
|
||||
--crepe-color-surface: var(--popover);
|
||||
--crepe-color-surface-low: var(--muted);
|
||||
--crepe-color-on-surface: var(--popover-foreground);
|
||||
--crepe-color-on-surface-variant: var(--muted-foreground);
|
||||
--crepe-color-outline: var(--border);
|
||||
--crepe-color-primary: var(--primary);
|
||||
--crepe-color-secondary: color-mix(in oklch, var(--primary) 22%, transparent);
|
||||
--crepe-color-on-secondary: var(--foreground);
|
||||
--crepe-color-inverse: var(--foreground);
|
||||
--crepe-color-on-inverse: var(--background);
|
||||
--crepe-color-inline-code: var(--primary);
|
||||
--crepe-color-inline-area: var(--muted);
|
||||
--crepe-color-error: var(--destructive);
|
||||
--crepe-color-hover: var(--accent);
|
||||
--crepe-color-selected: color-mix(in oklch, var(--primary) 22%, transparent);
|
||||
/* frame-dark ships white "glow" shadows that vanish on light surfaces;
|
||||
* use a conventional dark drop shadow so floating menus lift in both themes. */
|
||||
--crepe-shadow-1:
|
||||
0px 1px 2px 0px rgb(0 0 0 / 0.12), 0px 1px 3px 1px rgb(0 0 0 / 0.08);
|
||||
--crepe-shadow-2:
|
||||
0px 1px 2px 0px rgb(0 0 0 / 0.16), 0px 2px 6px 2px rgb(0 0 0 / 0.12);
|
||||
/* Use the application font, not Crepe's bundled Noto Sans / Noto Serif. */
|
||||
--crepe-font-default: inherit;
|
||||
--crepe-font-title: inherit;
|
||||
@@ -57,7 +64,7 @@
|
||||
.llink-crepe .milkdown .ProseMirror {
|
||||
padding: 0;
|
||||
outline: none;
|
||||
caret-color: white;
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Crepe's default heading scale (42px h1) is sized for a document editor;
|
||||
@@ -103,7 +110,7 @@
|
||||
line-height: 1.375rem;
|
||||
font-weight: 700;
|
||||
margin-top: 12px;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.llink-crepe .milkdown .ProseMirror > :first-child {
|
||||
|
||||
@@ -178,7 +178,7 @@ export function RecordingOverlay({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90',
|
||||
'bg-scrim/90 absolute inset-0 z-50 flex flex-col items-center justify-center',
|
||||
isReviewing && isDragging && 'ring-2 ring-inset ring-white/30',
|
||||
isRecording && isWarning && 'record-warning-glow',
|
||||
)}
|
||||
@@ -230,7 +230,7 @@ export function RecordingOverlay({
|
||||
|
||||
{/* Bottom gradient scrim for keyboard hint readability */}
|
||||
{(isRecording || isReviewing) && !isLoading && (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="from-scrim/60 pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t to-transparent" />
|
||||
)}
|
||||
|
||||
{/* Top center: recording indicator */}
|
||||
|
||||
@@ -63,37 +63,38 @@ export function TaskComposeStep({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex flex-col bg-black/90 pt-16"
|
||||
className="bg-background/95 absolute inset-0 z-50 flex flex-col pt-16 backdrop-blur-sm"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-sm space-y-4 px-6">
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Task</Label>
|
||||
<Label className="text-muted-foreground mb-1 text-xs">Task</Label>
|
||||
<Input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="What needs to get done?"
|
||||
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Notes</Label>
|
||||
<Label className="text-muted-foreground mb-1 text-xs">Notes</Label>
|
||||
<Textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Optional details…"
|
||||
className="min-h-20 border-white/10 bg-white/5 text-white placeholder:text-white/30 focus-visible:border-white/30 focus-visible:ring-0 dark:bg-white/5"
|
||||
className="min-h-20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Assign to</Label>
|
||||
<Label className="text-muted-foreground mb-1 text-xs">
|
||||
Assign to
|
||||
</Label>
|
||||
<Select value={assignedTo} onValueChange={setAssignedTo}>
|
||||
<SelectTrigger className="w-full border-white/10 bg-white/5 text-white">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Unassigned" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -108,7 +109,7 @@ export function TaskComposeStep({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm">
|
||||
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
||||
cancel
|
||||
</KeyHint>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function TextEditor({
|
||||
);
|
||||
|
||||
const keyboardHints = (
|
||||
<div className="absolute bottom-4 flex items-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground absolute bottom-4 flex items-center gap-4 text-sm">
|
||||
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
||||
cancel
|
||||
</KeyHint>
|
||||
@@ -161,8 +161,8 @@ export function TextEditor({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 flex items-center justify-center bg-black/90',
|
||||
isDragging && 'ring-2 ring-inset ring-white/30',
|
||||
'bg-background/95 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm',
|
||||
isDragging && 'ring-primary/40 ring-2 ring-inset',
|
||||
)}
|
||||
{...dropZoneProps}
|
||||
>
|
||||
@@ -174,7 +174,7 @@ export function TextEditor({
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message..."
|
||||
className={cn(
|
||||
'w-full resize-none border-none bg-transparent p-8 text-center text-white placeholder-white/40 outline-none',
|
||||
'text-foreground placeholder-muted-foreground w-full resize-none border-none bg-transparent p-8 text-center outline-none',
|
||||
style.size,
|
||||
style.weight,
|
||||
)}
|
||||
@@ -189,13 +189,13 @@ export function TextEditor({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 flex items-center justify-center bg-black/90',
|
||||
isDragging && 'ring-2 ring-inset ring-white/30',
|
||||
'bg-background/95 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm',
|
||||
isDragging && 'ring-primary/40 ring-2 ring-inset',
|
||||
)}
|
||||
{...dropZoneProps}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
>
|
||||
<div className="mx-8 flex h-[calc(100%-8rem)] w-full min-w-0 max-w-[calc(var(--message-content-width)_+_var(--message-editor-gutter)*2)] flex-col overflow-hidden rounded border border-white/10 bg-white/5 backdrop-blur-xl animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="bg-card text-card-foreground border-border animate-in fade-in zoom-in-95 mx-8 flex h-[calc(100%-8rem)] w-full min-w-0 max-w-[calc(var(--message-content-width)_+_var(--message-editor-gutter)*2)] flex-col overflow-hidden rounded border duration-200">
|
||||
{/* No padding here: the editor's own scroll box hosts the slash menu,
|
||||
so we pad inside the editor (ProseMirror) instead. That keeps the
|
||||
menu's clipping bounds the full card rather than the inset box. */}
|
||||
@@ -208,7 +208,7 @@ export function TextEditor({
|
||||
</div>
|
||||
|
||||
{hasEnrichments && strip && (
|
||||
<div className="shrink-0 border-t border-white/10 px-5 py-3">
|
||||
<div className="border-border shrink-0 border-t px-5 py-3">
|
||||
{strip}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -230,7 +230,7 @@ function ContainerControls({
|
||||
}) {
|
||||
const requestIntent = useComposeIntentStore((s) => s.request);
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
<KeyHint keys={['↑↓', 'Enter']}>navigate</KeyHint>
|
||||
<KeyHint keys="1–9">jump</KeyHint>
|
||||
<VideoAudioToggle />
|
||||
|
||||
@@ -38,14 +38,16 @@ export function DeletedParticleView({
|
||||
}, [paused, onEnded, particle.id]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8">
|
||||
<div className="flex h-full w-full items-center justify-center px-8">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<Trash2 className="text-white/40 size-6" />
|
||||
<p className="text-white/70 text-base font-medium">
|
||||
<Trash2 className="text-muted-foreground size-6" />
|
||||
<p className="text-foreground text-base font-medium">
|
||||
This particle was deleted
|
||||
</p>
|
||||
{deleter && (
|
||||
<p className="text-white/40 text-xs">by {deleter.displayName}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
by {deleter.displayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ export const MediaParticleView = forwardRef<
|
||||
|
||||
if (isAudio) {
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center bg-black/90">
|
||||
<div className="bg-scrim/90 relative flex h-full w-full items-center justify-center">
|
||||
<audio
|
||||
ref={(el) => {
|
||||
audioRef.current = el;
|
||||
@@ -186,7 +186,7 @@ export const MediaParticleView = forwardRef<
|
||||
playsInline
|
||||
onEnded={onEnded}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
className="h-full w-full object-contain bg-black"
|
||||
className="bg-black h-full w-full object-contain"
|
||||
/>
|
||||
|
||||
{transcript && (
|
||||
|
||||
@@ -63,7 +63,7 @@ function ImageAttachment({
|
||||
);
|
||||
|
||||
if (isLoading || !url) {
|
||||
return <Skeleton className="h-20 w-20 shrink-0 rounded-lg bg-white/10" />;
|
||||
return <Skeleton className="h-20 w-20 shrink-0 rounded-lg" />;
|
||||
}
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
@@ -78,7 +78,7 @@ function ImageAttachment({
|
||||
e.stopPropagation();
|
||||
onPreview();
|
||||
}}
|
||||
className="group relative block h-30 w-40 shrink-0 overflow-hidden rounded-lg bg-white/10"
|
||||
className="bg-muted group relative block h-30 w-40 shrink-0 overflow-hidden rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
@@ -89,7 +89,7 @@ function ImageAttachment({
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleDownload}
|
||||
className="absolute bottom-1 right-1 rounded-full bg-black/60 p-1 text-white/70 opacity-0 transition-opacity hover:text-white group-hover:opacity-100"
|
||||
className="bg-scrim/60 absolute bottom-1 right-1 rounded-full p-1 text-white/80 opacity-0 transition-opacity hover:text-white group-hover:opacity-100"
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
</span>
|
||||
@@ -123,14 +123,14 @@ function FileAttachment({
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleOpen}
|
||||
className="flex shrink-0 cursor-pointer flex-col gap-1.5 rounded-lg bg-white/10 px-3 py-2 transition-colors hover:bg-white/15"
|
||||
className="bg-muted hover:bg-accent flex shrink-0 cursor-pointer flex-col gap-1.5 rounded-lg px-3 py-2 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon className="size-4 shrink-0 text-white/60" />
|
||||
<span className="max-w-[10rem] truncate text-xs font-medium text-white/90">
|
||||
<FileIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
<span className="text-foreground max-w-[10rem] truncate text-xs font-medium">
|
||||
{particle.properties.filename}
|
||||
</span>
|
||||
<span className="text-[10px] text-white/40">
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{formatFileSize(particle.properties.size_bytes)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@ function FileAttachment({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-white/70 hover:bg-white/10 hover:text-white"
|
||||
className="text-muted-foreground"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<ExternalLink data-icon="inline-start" />
|
||||
@@ -147,7 +147,7 @@ function FileAttachment({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-white/70 hover:bg-white/10 hover:text-white"
|
||||
className="text-muted-foreground"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<Download data-icon="inline-start" />
|
||||
@@ -177,7 +177,7 @@ function CompactAttachmentItem({
|
||||
e.stopPropagation();
|
||||
openParticle(particle, index, url, onPreview);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md bg-white/10 px-2.5 py-1.5 text-left transition-colors hover:bg-white/15"
|
||||
className="bg-muted hover:bg-accent flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left transition-colors"
|
||||
>
|
||||
{isImage ? (
|
||||
url ? (
|
||||
@@ -187,15 +187,15 @@ function CompactAttachmentItem({
|
||||
className="size-5 shrink-0 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="size-4 shrink-0 text-white/50" />
|
||||
<ImageIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
)
|
||||
) : (
|
||||
<FileIcon className="size-4 shrink-0 text-white/50" />
|
||||
<FileIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
)}
|
||||
<span className="min-w-0 truncate text-xs text-white/80">
|
||||
<span className="text-foreground min-w-0 truncate text-xs">
|
||||
{particle.properties.filename}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-white/40">
|
||||
<span className="text-muted-foreground shrink-0 text-[10px]">
|
||||
{formatFileSize(particle.properties.size_bytes)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -191,7 +191,7 @@ const StreamRow = memo(function StreamRow({
|
||||
)}
|
||||
>
|
||||
{shortcutKey && (
|
||||
<kbd className="flex size-5 shrink-0 items-center justify-center rounded bg-white/10 font-mono text-xs text-muted-foreground">
|
||||
<kbd className="bg-muted text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded font-mono text-xs">
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -220,8 +220,8 @@ const StreamRow = memo(function StreamRow({
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
{hasActiveHuddle && (
|
||||
<span className="flex items-center gap-1 rounded-full bg-red-500/15 px-1.5 py-0.5">
|
||||
<Headphones className="size-3 text-red-400" />
|
||||
<span className="text-[10px] font-medium text-red-400">
|
||||
<Headphones className="size-3 text-red-600 dark:text-red-400" />
|
||||
<span className="text-[10px] font-medium text-red-600 dark:text-red-400">
|
||||
{huddleCount}
|
||||
</span>
|
||||
</span>
|
||||
@@ -298,7 +298,7 @@ const FolderRow = memo(function FolderRow({
|
||||
)}
|
||||
>
|
||||
{shortcutKey && (
|
||||
<kbd className="flex size-5 shrink-0 items-center justify-center rounded bg-white/10 font-mono text-xs text-muted-foreground">
|
||||
<kbd className="bg-muted text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded font-mono text-xs">
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -356,7 +356,7 @@ const LeafRow = memo(function LeafRow({
|
||||
)}
|
||||
>
|
||||
{shortcutKey && (
|
||||
<kbd className="flex size-5 shrink-0 items-center justify-center rounded bg-white/10 font-mono text-xs text-muted-foreground">
|
||||
<kbd className="bg-muted text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded font-mono text-xs">
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
)}
|
||||
|
||||
@@ -69,9 +69,9 @@ function MediaPreview({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-black/90">
|
||||
<Mic className="h-8 w-8 text-white/60" />
|
||||
<span className="font-mono text-xs text-white/50">{durationLabel}</span>
|
||||
<div className="bg-muted text-muted-foreground flex h-full w-full flex-col items-center justify-center gap-2">
|
||||
<Mic className="h-8 w-8" />
|
||||
<span className="font-mono text-xs">{durationLabel}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,8 +103,8 @@ function VideoThumbnail({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-black/80">
|
||||
<Video className="h-8 w-8 text-white/40" />
|
||||
<div className="bg-muted flex h-full w-full items-center justify-center">
|
||||
<Video className="text-muted-foreground h-8 w-8" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -114,7 +114,7 @@ function VideoThumbnail({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-black">
|
||||
<div className="bg-muted relative h-full w-full">
|
||||
<video
|
||||
src={`${url}#t=2`}
|
||||
preload="metadata"
|
||||
@@ -122,7 +122,7 @@ function VideoThumbnail({
|
||||
playsInline
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<span className="absolute right-1.5 bottom-1.5 rounded bg-black/70 px-1.5 py-0.5 font-mono text-[10px] text-white/80">
|
||||
<span className="bg-scrim/70 absolute bottom-1.5 right-1.5 rounded px-1.5 py-0.5 font-mono text-[10px] text-white">
|
||||
{duration}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -138,7 +138,7 @@ function LeafParticleView({
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 bg-black text-white [--stream-safe-top:2rem] [--stream-safe-bottom:2rem]">
|
||||
<div className="bg-background min-h-0 flex-1 [--stream-safe-top:2rem] [--stream-safe-bottom:2rem]">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,10 +77,10 @@ export function PlaybackPageIndicator({
|
||||
className="group relative block h-3 w-full"
|
||||
>
|
||||
{/* Dim track */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/30 transition-all group-hover:h-1.5" />
|
||||
<div className="bg-muted-foreground/30 absolute inset-x-0 bottom-0 h-[3px] transition-all group-hover:h-1.5" />
|
||||
{/* Fill */}
|
||||
<div
|
||||
className="absolute left-0 bottom-0 h-[3px] bg-white/90 transition-all group-hover:h-1.5"
|
||||
className="bg-foreground absolute bottom-0 left-0 h-[3px] transition-all group-hover:h-1.5"
|
||||
style={{
|
||||
width:
|
||||
i < current
|
||||
@@ -107,7 +107,7 @@ export function PlaybackPageIndicator({
|
||||
)}
|
||||
</div>
|
||||
{paginated && showTracks && current >= 0 && (
|
||||
<div className="pointer-events-none pt-1 text-center text-[10px] font-medium tabular-nums tracking-wide text-white/40">
|
||||
<div className="text-muted-foreground pointer-events-none pt-1 text-center text-[10px] font-medium tabular-nums tracking-wide">
|
||||
{current + 1} / {total}
|
||||
</div>
|
||||
)}
|
||||
@@ -141,7 +141,7 @@ function GhostStub({
|
||||
className="group relative block h-3 w-2 shrink-0"
|
||||
aria-label="Jump to adjacent page"
|
||||
>
|
||||
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/15 transition-all group-hover:h-1.5 group-hover:bg-white/40" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-muted-foreground/40 absolute inset-x-0 bottom-0 h-[3px] transition-all group-hover:h-1.5" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -166,7 +166,7 @@ function SegmentPresenceAvatars({
|
||||
className={
|
||||
onlineHumanIds?.has(human.humanId)
|
||||
? 'ring-2 ring-green-500'
|
||||
: 'ring-1 ring-black/50'
|
||||
: 'ring-1 ring-background'
|
||||
}
|
||||
avatarObjectId={human.avatarObjectId}
|
||||
initials={human.emailPrefix.slice(0, 2).toUpperCase()}
|
||||
@@ -178,7 +178,9 @@ function SegmentPresenceAvatars({
|
||||
</Tooltip>
|
||||
))}
|
||||
{overflow > 0 && (
|
||||
<span className="text-[10px] text-white/70 pl-1">+{overflow}</span>
|
||||
<span className="text-muted-foreground pl-1 text-[10px]">
|
||||
+{overflow}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,12 +77,12 @@ export function ReactionBar({
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-full px-2 py-0.5 text-xs backdrop-blur-sm transition-colors',
|
||||
isMine
|
||||
? 'bg-white/20 ring-1 ring-white/40'
|
||||
: 'bg-black/40 hover:bg-black/50',
|
||||
? 'bg-primary/15 text-foreground ring-1 ring-primary/40'
|
||||
: 'bg-card/70 text-foreground border border-border hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<span className="text-sm">{emoji}</span>
|
||||
<span className="text-white/80">{reactors.length}</span>
|
||||
<span className="text-muted-foreground">{reactors.length}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
@@ -109,8 +109,8 @@ export function ReactionBar({
|
||||
className={cn(
|
||||
'flex max-w-[200px] items-center gap-1.5 rounded-full py-0.5 pl-0.5 pr-2.5 text-xs backdrop-blur-sm transition-colors',
|
||||
isMine
|
||||
? 'bg-white/20 ring-1 ring-white/40'
|
||||
: 'bg-black/40 hover:bg-black/50',
|
||||
? 'bg-primary/15 text-foreground ring-1 ring-primary/40'
|
||||
: 'bg-card/70 text-foreground border border-border hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<HumanAvatar
|
||||
@@ -118,11 +118,11 @@ export function ReactionBar({
|
||||
className="shrink-0"
|
||||
avatarObjectId={firstReactor.avatarObjectId}
|
||||
initials={firstReactor.initials}
|
||||
fallbackClassName="bg-white/15 text-[9px] font-medium text-white"
|
||||
fallbackClassName="bg-muted text-muted-foreground text-[9px] font-medium"
|
||||
/>
|
||||
<span className="truncate text-white/90">{text}</span>
|
||||
<span className="truncate text-foreground">{text}</span>
|
||||
{reactors.length > 1 && (
|
||||
<span className="shrink-0 text-white/60">
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{reactors.length}
|
||||
</span>
|
||||
)}
|
||||
@@ -154,7 +154,7 @@ export function ReactionBar({
|
||||
|
||||
{/* Picker / actions */}
|
||||
{expanded ? (
|
||||
<div className="flex flex-col items-center gap-0.5 rounded-full bg-black/40 px-0.5 py-1.5 backdrop-blur-sm">
|
||||
<div className="bg-card/80 border-border flex flex-col items-center gap-0.5 rounded-full border px-0.5 py-1.5 backdrop-blur-sm">
|
||||
{REACTION_EMOJIS.map((emoji) => {
|
||||
if (activeEmojis.includes(emoji)) return null;
|
||||
return (
|
||||
@@ -164,7 +164,7 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
handleToggle(emoji);
|
||||
}}
|
||||
className="rounded-full px-0.5 py-1 text-sm transition-colors hover:bg-white/15"
|
||||
className="hover:bg-accent rounded-full px-0.5 py-1 text-sm transition-colors"
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
@@ -175,9 +175,9 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
setExpanded(false);
|
||||
}}
|
||||
className="flex size-5 items-center justify-center rounded-full transition-colors hover:bg-white/15"
|
||||
className="hover:bg-accent flex size-5 items-center justify-center rounded-full transition-colors"
|
||||
>
|
||||
<X className="size-3 text-white/60" />
|
||||
<X className="text-muted-foreground size-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -189,14 +189,14 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
onOpenTextReaction();
|
||||
}}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-black/40 backdrop-blur-sm transition-colors hover:bg-black/50"
|
||||
className="bg-card/70 border-border hover:bg-accent flex size-6 items-center justify-center rounded-full border backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Type className="size-3 text-white/60" />
|
||||
<Type className="text-muted-foreground size-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
Quick reply{' '}
|
||||
<kbd className="ml-1 rounded bg-white/10 px-1 font-mono text-[10px]">
|
||||
<kbd className="bg-muted ml-1 rounded px-1 font-mono text-[10px]">
|
||||
R
|
||||
</kbd>
|
||||
</TooltipContent>
|
||||
@@ -206,9 +206,9 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
setExpanded(true);
|
||||
}}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-black/40 backdrop-blur-sm transition-colors hover:bg-black/50"
|
||||
className="bg-card/70 border-border hover:bg-accent flex size-6 items-center justify-center rounded-full border backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Plus className="size-3 text-white/60" />
|
||||
<Plus className="text-muted-foreground size-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,17 +56,17 @@ export function RenameStreamOverlay({
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-scrim/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-popover/95 text-popover-foreground border-border absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white/70">Rename stream</h2>
|
||||
<h2 className="text-sm font-semibold">Rename stream</h2>
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
onClick={onClose}
|
||||
title="Close (or press Esc)"
|
||||
className="text-xs text-white/30"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -85,7 +85,6 @@ export function RenameStreamOverlay({
|
||||
}
|
||||
}}
|
||||
placeholder="Stream name"
|
||||
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
|
||||
@@ -61,7 +61,7 @@ export function BottomBar({
|
||||
<div className="flex items-center justify-center px-3 pt-2 gap-2">
|
||||
{exitRemainingMs !== null && (
|
||||
<div className="flex justify-center">
|
||||
<span className="rounded-full bg-black/30 px-2 text-xs text-white/70 backdrop-blur-sm">
|
||||
<span className="bg-card/70 text-muted-foreground border-border rounded-full border px-2 text-xs backdrop-blur-sm">
|
||||
Closing in {Math.ceil(exitRemainingMs / 1000)}s
|
||||
</span>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ export function StreamViewControls({
|
||||
}) {
|
||||
const requestIntent = useComposeIntentStore((s) => s.request);
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
{showEscape && (
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
|
||||
@@ -39,17 +39,17 @@ export function StreamListSidebar({
|
||||
}, [currentIndex]);
|
||||
|
||||
return (
|
||||
<aside className="dark flex w-60 shrink-0 flex-col overflow-hidden border-l border-white/10 bg-zinc-950">
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-white/10 px-4 py-3">
|
||||
<List className="size-3.5 text-white/40" />
|
||||
<span className="truncate text-sm font-medium text-white/90 mr-auto">
|
||||
<aside className="bg-sidebar text-sidebar-foreground border-border flex w-60 shrink-0 flex-col overflow-hidden border-l">
|
||||
<div className="border-border flex shrink-0 items-center gap-2 border-b px-4 py-3">
|
||||
<List className="text-muted-foreground size-3.5" />
|
||||
<span className="mr-auto truncate text-sm font-medium">
|
||||
{items.length} messages
|
||||
</span>
|
||||
<KeyHint
|
||||
keys="L"
|
||||
onClick={onToggle}
|
||||
title="Hide list (or press L)"
|
||||
className="text-xs text-white/40"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -72,7 +72,7 @@ export function StreamListSidebar({
|
||||
</div>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<p className="px-2 py-8 text-center text-sm text-white/40">
|
||||
<p className="text-muted-foreground px-2 py-8 text-center text-sm">
|
||||
No particles in this stream yet.
|
||||
</p>
|
||||
)}
|
||||
@@ -106,7 +106,7 @@ function ChatRow({
|
||||
}}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
|
||||
isSelected ? 'bg-white/10' : 'hover:bg-white/5',
|
||||
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
|
||||
)}
|
||||
>
|
||||
<HumanAvatar
|
||||
@@ -114,14 +114,14 @@ function ChatRow({
|
||||
className="mt-0.5 shrink-0"
|
||||
initials={sender.initials}
|
||||
avatarObjectId={sender.avatarObjectId}
|
||||
fallbackClassName="bg-white/10 text-white/80 text-[10px] font-medium"
|
||||
fallbackClassName="bg-muted text-muted-foreground text-[10px] font-medium"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="truncate text-xs font-semibold text-white/90">
|
||||
<span className="truncate text-xs font-semibold">
|
||||
{sender.displayName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-white/35">
|
||||
<span className="text-muted-foreground shrink-0 text-[10px]">
|
||||
<RelativeTimestamp date={particle.created_at} />
|
||||
</span>
|
||||
</div>
|
||||
@@ -134,14 +134,16 @@ function ChatRow({
|
||||
function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
if (isParticleDeleted(particle)) {
|
||||
return (
|
||||
<p className="text-xs italic text-white/35">This particle was deleted</p>
|
||||
<p className="text-muted-foreground text-xs italic">
|
||||
This particle was deleted
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
switch (particle.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<p className="line-clamp-2 text-xs leading-relaxed [overflow-wrap:anywhere] whitespace-pre-line text-white/70">
|
||||
<p className="text-muted-foreground line-clamp-2 whitespace-pre-line text-xs leading-relaxed [overflow-wrap:anywhere]">
|
||||
{particle.properties.content}
|
||||
</p>
|
||||
);
|
||||
@@ -166,13 +168,13 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
: '';
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<Icon className="size-3.5 shrink-0 text-white/50" />
|
||||
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<Icon className="text-muted-foreground size-3.5 shrink-0" />
|
||||
{label}
|
||||
{duration}
|
||||
</span>
|
||||
{transcript && (
|
||||
<p className="line-clamp-2 text-xs leading-relaxed text-white/45">
|
||||
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
|
||||
{transcript}
|
||||
</p>
|
||||
)}
|
||||
@@ -181,8 +183,8 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
}
|
||||
case 'file':
|
||||
return (
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<FileText className="size-3.5 shrink-0 text-white/50" />
|
||||
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<FileText className="text-muted-foreground size-3.5 shrink-0" />
|
||||
<span className="truncate">{particle.properties.filename}</span>
|
||||
</span>
|
||||
);
|
||||
@@ -191,21 +193,26 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
const doneCount = checklist.filter((item) => item.done).length;
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<CircleCheck
|
||||
className={cn(
|
||||
'size-3.5 shrink-0',
|
||||
done ? 'text-emerald-400' : 'text-white/50',
|
||||
done
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn('truncate', done && 'text-white/40 line-through')}
|
||||
className={cn(
|
||||
'truncate',
|
||||
done && 'text-muted-foreground line-through',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
{checklist.length > 0 && (
|
||||
<span className="pl-5 text-[10px] text-white/40">
|
||||
<span className="text-muted-foreground pl-5 text-[10px]">
|
||||
{doneCount} / {checklist.length} subtasks
|
||||
</span>
|
||||
)}
|
||||
@@ -214,11 +221,11 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
}
|
||||
case 'paper':
|
||||
return (
|
||||
<p className="truncate text-xs text-white/70">
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{particle.properties.title}
|
||||
</p>
|
||||
);
|
||||
default:
|
||||
return <p className="text-xs text-white/45">{particle.type}</p>;
|
||||
return <p className="text-muted-foreground text-xs">{particle.type}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,18 +97,18 @@ export function StreamMembersOverlay({
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="bg-scrim/60 absolute inset-0 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 flex max-h-[80vh] w-full max-w-sm -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-popover/95 text-popover-foreground border-border absolute left-1/2 top-1/2 flex max-h-[80vh] w-full max-w-sm -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl border p-6 shadow-2xl backdrop-blur-xl">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white/70">Members</h2>
|
||||
<h2 className="text-sm font-semibold">Members</h2>
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
onClick={onClose}
|
||||
title="Close (or press Esc)"
|
||||
className="text-xs text-white/30"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -116,11 +116,11 @@ export function StreamMembersOverlay({
|
||||
|
||||
{/* Visibility */}
|
||||
<section className="mb-4">
|
||||
<h3 className="mb-2 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<h3 className="text-muted-foreground mb-2 text-[10px] font-medium uppercase tracking-widest">
|
||||
Visibility
|
||||
</h3>
|
||||
{isCreator ? (
|
||||
<div className="grid grid-cols-2 gap-1 rounded-lg bg-white/5 p-1">
|
||||
<div className="bg-muted grid grid-cols-2 gap-1 rounded-lg p-1">
|
||||
<VisibilityPill
|
||||
active={visibility.mode === 'network'}
|
||||
icon={<Globe className="size-3.5" />}
|
||||
@@ -135,15 +135,15 @@ export function StreamMembersOverlay({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-sm text-white/70">
|
||||
<div className="text-foreground flex items-center gap-2 text-sm">
|
||||
{visibility.mode === 'network' ? (
|
||||
<>
|
||||
<Globe className="size-3.5 text-white/40" />
|
||||
<Globe className="text-muted-foreground size-3.5" />
|
||||
<span>Everyone in {network?.name ?? 'network'}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="size-3.5 text-white/40" />
|
||||
<Lock className="text-muted-foreground size-3.5" />
|
||||
<span>{memberIds.length} specific people</span>
|
||||
</>
|
||||
)}
|
||||
@@ -153,9 +153,11 @@ export function StreamMembersOverlay({
|
||||
|
||||
{/* Member list */}
|
||||
<section className="flex min-h-0 flex-1 flex-col">
|
||||
<h3 className="mb-2 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<h3 className="text-muted-foreground mb-2 text-[10px] font-medium uppercase tracking-widest">
|
||||
{visibility.mode === 'network' ? 'Has access' : 'People'}{' '}
|
||||
<span className="ml-1 text-white/20">{memberIds.length}</span>
|
||||
<span className="text-muted-foreground ml-1">
|
||||
{memberIds.length}
|
||||
</span>
|
||||
</h3>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<ul className="flex flex-col gap-0.5 pr-2">
|
||||
@@ -167,7 +169,7 @@ export function StreamMembersOverlay({
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="group flex items-center gap-2.5 rounded px-2 py-1.5 text-sm text-white/70"
|
||||
className="text-foreground group flex items-center gap-2.5 rounded px-2 py-1.5 text-sm"
|
||||
>
|
||||
<HumanAvatar
|
||||
size="sm"
|
||||
@@ -178,13 +180,13 @@ export function StreamMembersOverlay({
|
||||
<span
|
||||
className={cn(
|
||||
'flex-1 truncate',
|
||||
!display.exists && 'italic text-white/40',
|
||||
!display.exists && 'text-muted-foreground italic',
|
||||
)}
|
||||
>
|
||||
{display.displayName}
|
||||
</span>
|
||||
{isCreatorRow && (
|
||||
<span className="text-[10px] uppercase tracking-wider text-white/30">
|
||||
<span className="text-muted-foreground text-[10px] uppercase tracking-wider">
|
||||
Creator
|
||||
</span>
|
||||
)}
|
||||
@@ -192,7 +194,7 @@ export function StreamMembersOverlay({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeMember(id)}
|
||||
className="rounded p-1 text-white/30 opacity-0 transition-opacity hover:bg-white/10 hover:text-white/70 group-hover:opacity-100"
|
||||
className="text-muted-foreground hover:bg-accent hover:text-foreground rounded p-1 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-label={`Remove ${display.displayName}`}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
@@ -209,8 +211,8 @@ export function StreamMembersOverlay({
|
||||
{isCreator &&
|
||||
visibility.mode === 'custom' &&
|
||||
availableToAdd.length > 0 && (
|
||||
<section className="mt-4 border-t border-white/5 pt-4">
|
||||
<h3 className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<section className="border-border mt-4 border-t pt-4">
|
||||
<h3 className="text-muted-foreground mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest">
|
||||
<UserPlus className="size-3" />
|
||||
Add people
|
||||
</h3>
|
||||
@@ -222,7 +224,7 @@ export function StreamMembersOverlay({
|
||||
type="button"
|
||||
onClick={() => addMember(human.id)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded px-2 py-1.5 text-left text-sm text-white/70 transition-colors hover:bg-white/5',
|
||||
'text-foreground hover:bg-accent flex w-full items-center gap-2.5 rounded px-2 py-1.5 text-left text-sm transition-colors',
|
||||
)}
|
||||
>
|
||||
<HumanAvatar
|
||||
@@ -234,7 +236,7 @@ export function StreamMembersOverlay({
|
||||
<span className="flex-1 truncate">
|
||||
{human.email_prefix}
|
||||
</span>
|
||||
<UserPlus className="size-3.5 text-white/30" />
|
||||
<UserPlus className="text-muted-foreground size-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -246,7 +248,7 @@ export function StreamMembersOverlay({
|
||||
{isCreator &&
|
||||
visibility.mode === 'custom' &&
|
||||
availableToAdd.length === 0 && (
|
||||
<p className="mt-4 text-center text-xs text-white/30">
|
||||
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||
<Users className="mr-1 inline size-3" />
|
||||
Everyone in the network is already a member
|
||||
</p>
|
||||
@@ -275,8 +277,8 @@ function VisibilityPill({
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs transition-colors',
|
||||
active
|
||||
? 'bg-white/10 text-white/90'
|
||||
: 'text-white/50 hover:text-white/80',
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function TopBar({
|
||||
<div className="drag-region flex flex-row px-2 gap-1 items-center">
|
||||
<WindowControls />
|
||||
|
||||
<Breadcrumb className="no-drag rounded-full bg-black/30 backdrop-blur-sm px-3 py-1 mx-auto">
|
||||
<Breadcrumb className="no-drag bg-card/70 border-border mx-auto rounded-full border px-3 py-1 backdrop-blur-sm">
|
||||
<BreadcrumbList>
|
||||
{streamParticle && (
|
||||
<>
|
||||
@@ -139,7 +139,7 @@ export function TopBar({
|
||||
size="sm"
|
||||
avatarObjectId={display.avatarObjectId}
|
||||
initials={display.initials}
|
||||
fallbackClassName="bg-red-500/30 text-[8px] text-red-200"
|
||||
fallbackClassName="bg-red-500/30 text-[8px] text-red-700 dark:text-red-200"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{display.email}</TooltipContent>
|
||||
@@ -147,12 +147,14 @@ export function TopBar({
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
<span className="text-xs font-medium text-red-200">Join</span>
|
||||
<span className="text-xs font-medium text-red-600 dark:text-red-200">
|
||||
Join
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isStreamOpen(streamParticle) && (
|
||||
<span className="no-drag text-muted-foreground flex items-center gap-1 rounded-full bg-white/10 px-2 py-0.5 text-xs backdrop-blur-sm">
|
||||
<span className="no-drag text-muted-foreground bg-muted border-border flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs backdrop-blur-sm">
|
||||
<CircleCheckBig className="size-3" />
|
||||
Closed
|
||||
</span>
|
||||
@@ -280,16 +282,16 @@ function MembersIndicator({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="no-drag flex items-center gap-1.5 rounded-full bg-white/5 px-2 py-1 text-xs text-white/70 backdrop-blur-sm transition-colors hover:bg-white/10"
|
||||
className="no-drag bg-card/70 text-muted-foreground border-border hover:bg-accent flex items-center gap-1.5 rounded-full border px-2 py-1 text-xs backdrop-blur-sm transition-colors"
|
||||
>
|
||||
{isNetworkWide ? (
|
||||
<>
|
||||
<Globe className="size-3 text-white/50" />
|
||||
<Globe className="text-muted-foreground size-3" />
|
||||
<span>Everyone</span>
|
||||
</>
|
||||
) : isPrivate ? (
|
||||
<>
|
||||
<Lock className="size-3 text-white/50" />
|
||||
<Lock className="text-muted-foreground size-3" />
|
||||
<span>Private</span>
|
||||
</>
|
||||
) : (
|
||||
@@ -306,7 +308,7 @@ function MembersIndicator({
|
||||
))}
|
||||
</AvatarGroup>
|
||||
{overflow > 0 && (
|
||||
<span className="text-white/50">+{overflow}</span>
|
||||
<span className="text-muted-foreground">+{overflow}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -381,7 +381,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
|
||||
if (children.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 bg-black text-white">
|
||||
<div className="bg-background flex h-full flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
No particles in this stream yet
|
||||
</p>
|
||||
@@ -460,15 +460,15 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-black">
|
||||
<div className="bg-background flex h-screen overflow-hidden">
|
||||
{/* Stream chrome — immersive playback column */}
|
||||
<div
|
||||
className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-black text-white [--stream-safe-top:4rem] [--stream-safe-bottom:5rem]"
|
||||
className="bg-background relative flex min-w-0 flex-1 flex-col overflow-hidden [--stream-safe-top:4rem] [--stream-safe-bottom:5rem]"
|
||||
onMouseMove={handleMouseActivity}
|
||||
onMouseLeave={() => setShowControls(false)}
|
||||
>
|
||||
{/* Top gradient safe zone */}
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b from-black/60 to-transparent" />
|
||||
<div className="from-background/80 pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b to-transparent" />
|
||||
|
||||
{/* TopBar — always visible */}
|
||||
<div className="z-10 absolute left-0 right-0 pt-2">
|
||||
@@ -488,7 +488,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
|
||||
<div className="pointer-events-none absolute right-4 top-14 z-20 flex flex-col items-end gap-1.5">
|
||||
{fastPlayback && (
|
||||
<div className="rounded-full bg-black/50 px-2.5 py-1 text-xs font-medium text-white backdrop-blur-sm">
|
||||
<div className="bg-card/70 text-foreground border-border rounded-full border px-2.5 py-1 text-xs font-medium backdrop-blur-sm">
|
||||
1.5x
|
||||
</div>
|
||||
)}
|
||||
@@ -497,7 +497,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
type="button"
|
||||
onClick={resume}
|
||||
title="Resume (or press Space)"
|
||||
className="pointer-events-auto flex items-center gap-1.5 rounded-full bg-black/40 px-2.5 py-1 text-xs font-medium text-white/70 backdrop-blur-sm transition-colors hover:bg-black/60 hover:text-white"
|
||||
className="bg-card/70 text-muted-foreground border-border hover:bg-accent hover:text-foreground pointer-events-auto flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Play className="size-3 fill-current" />
|
||||
Paused
|
||||
@@ -541,7 +541,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
/>
|
||||
|
||||
{/* Bottom gradient safe zone for keyboard hints */}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="from-background/80 pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t to-transparent" />
|
||||
|
||||
{/* BottomBar — pinned visible while browsing, mouse-activity in player */}
|
||||
<BottomBar
|
||||
|
||||
@@ -212,20 +212,20 @@ export function TaskParticleView({
|
||||
const doneCount = checklist.filter((item) => item.done).length;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="flex h-full w-full items-center justify-center px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
{immersive && editing && (
|
||||
<div
|
||||
className="animate-in fade-in-0 fixed inset-0 z-30 bg-black/50 backdrop-blur-md duration-200"
|
||||
className="animate-in fade-in-0 fixed inset-0 z-30 bg-scrim/50 backdrop-blur-md duration-200"
|
||||
onMouseDown={exitFocus}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'scrollbar-card flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-5 overflow-y-auto overscroll-contain rounded bg-white/10 p-[var(--message-card-padding)] backdrop-blur-md transition-shadow',
|
||||
'scrollbar-card bg-card text-card-foreground border-border flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-5 overflow-y-auto overscroll-contain rounded border p-[var(--message-card-padding)] transition-shadow',
|
||||
immersive &&
|
||||
editing &&
|
||||
'relative z-40 shadow-2xl shadow-black/50 ring-1 ring-white/15',
|
||||
'relative z-40 shadow-2xl shadow-black/50 ring-1 ring-border',
|
||||
)}
|
||||
onFocusCapture={() => setEditing(true)}
|
||||
onBlurCapture={(e) => {
|
||||
@@ -236,7 +236,7 @@ export function TaskParticleView({
|
||||
<Checkbox
|
||||
checked={done}
|
||||
onCheckedChange={(checked) => handleToggleDone(checked === true)}
|
||||
className="mt-1.5 size-5 rounded-full border-white/40 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
className="mt-1.5 size-5 rounded-full border-muted-foreground/40 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
aria-label={done ? 'Mark task as not done' : 'Mark task as done'}
|
||||
/>
|
||||
<input
|
||||
@@ -246,8 +246,8 @@ export function TaskParticleView({
|
||||
onBlur={titleField.onBlur}
|
||||
placeholder="Task title"
|
||||
className={cn(
|
||||
'w-full bg-transparent text-2xl font-semibold text-white outline-none placeholder:text-white/30',
|
||||
done && 'text-white/50 line-through',
|
||||
'text-foreground placeholder:text-muted-foreground w-full bg-transparent text-2xl font-semibold outline-none',
|
||||
done && 'text-muted-foreground line-through',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -258,12 +258,12 @@ export function TaskParticleView({
|
||||
onFocus={notesField.onFocus}
|
||||
onBlur={notesField.onBlur}
|
||||
placeholder="Add notes…"
|
||||
className="min-h-16 resize-none border-none bg-transparent p-0 text-sm text-white/80 shadow-none placeholder:text-white/30 focus-visible:ring-0 dark:bg-transparent"
|
||||
className="text-foreground placeholder:text-muted-foreground min-h-16 resize-none border-none bg-transparent p-0 text-sm shadow-none focus-visible:ring-0 dark:bg-transparent"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{checklist.length > 0 && (
|
||||
<span className="text-xs text-white/40">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{doneCount} / {checklist.length} done
|
||||
</span>
|
||||
)}
|
||||
@@ -286,10 +286,7 @@ export function TaskParticleView({
|
||||
value={assigned_to ?? UNASSIGNED}
|
||||
onValueChange={handleAssign}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-fit gap-2 border-white/15 bg-white/5 text-white/80"
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-fit gap-2">
|
||||
{assigned_to && assignee.exists && (
|
||||
<HumanAvatar
|
||||
size="sm"
|
||||
@@ -315,7 +312,7 @@ export function TaskParticleView({
|
||||
keys="Esc"
|
||||
onClick={exitFocus}
|
||||
title="Finish editing (or press Esc)"
|
||||
className="fixed bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 z-40 -translate-x-1/2 text-xs text-white/40"
|
||||
className="fixed bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 z-40 -translate-x-1/2 text-xs text-white/60"
|
||||
>
|
||||
to finish
|
||||
</KeyHint>
|
||||
@@ -345,7 +342,7 @@ function ChecklistItemRow({
|
||||
<Checkbox
|
||||
checked={item.done}
|
||||
onCheckedChange={(checked) => onToggle(checked === true)}
|
||||
className="border-white/30 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
className="border-muted-foreground/40 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
aria-label={
|
||||
item.done ? 'Mark subtask as not done' : 'Mark subtask as done'
|
||||
}
|
||||
@@ -357,14 +354,14 @@ function ChecklistItemRow({
|
||||
onBlur={textField.onBlur}
|
||||
placeholder="Subtask"
|
||||
className={cn(
|
||||
'w-full bg-transparent text-sm text-white/90 outline-none placeholder:text-white/30',
|
||||
item.done && 'text-white/40 line-through',
|
||||
'text-foreground placeholder:text-muted-foreground w-full bg-transparent text-sm outline-none',
|
||||
item.done && 'text-muted-foreground line-through',
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-white/30 opacity-0 transition-opacity hover:text-white/70 group-hover/item:opacity-100"
|
||||
className="text-muted-foreground hover:text-foreground opacity-0 transition-opacity group-hover/item:opacity-100"
|
||||
aria-label="Remove subtask"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
@@ -385,7 +382,7 @@ function AddChecklistItemRow({ onAdd }: { onAdd: (text: string) => void }) {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Plus className="size-4 text-white/30" />
|
||||
<Plus className="text-muted-foreground size-4" />
|
||||
<input
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
@@ -397,7 +394,7 @@ function AddChecklistItemRow({ onAdd }: { onAdd: (text: string) => void }) {
|
||||
}}
|
||||
onBlur={submit}
|
||||
placeholder="Add subtask…"
|
||||
className="w-full bg-transparent text-sm text-white/70 outline-none placeholder:text-white/30"
|
||||
className="text-foreground placeholder:text-muted-foreground w-full bg-transparent text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ export function TextParticleView({
|
||||
setIsEditing(true);
|
||||
}}
|
||||
title="Edit"
|
||||
className="absolute bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 rounded-full bg-black/40 px-3 py-1.5 text-xs text-white/70 backdrop-blur-sm transition-colors hover:bg-black/60 hover:text-white"
|
||||
className="bg-card/70 text-muted-foreground border-border hover:bg-accent hover:text-foreground absolute bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 z-30 flex -translate-x-1/2 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
Edit
|
||||
@@ -130,7 +130,7 @@ export function TextParticleView({
|
||||
);
|
||||
|
||||
const editedLabel = particle.properties.edited_at && (
|
||||
<span className="text-xs text-white/40">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
edited <RelativeTimestamp date={particle.properties.edited_at} />
|
||||
</span>
|
||||
);
|
||||
@@ -146,7 +146,7 @@ export function TextParticleView({
|
||||
// Mode 1: bare URLs only — show link cards centered
|
||||
if (linksOnly && !hasAttachments) {
|
||||
return (
|
||||
<div className="group relative flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="group relative flex h-full w-full items-center justify-center px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<LinkPreviews entries={linkPreviews} />
|
||||
{editedLabel && (
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2">
|
||||
@@ -167,10 +167,10 @@ export function TextParticleView({
|
||||
) {
|
||||
const style = getImmersiveTextStyle(content.length);
|
||||
return (
|
||||
<div className="group relative flex h-full w-full flex-col items-center justify-center gap-4 bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="group relative flex h-full w-full flex-col items-center justify-center gap-4 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<p
|
||||
className={cn(
|
||||
'max-w-2xl break-words text-center leading-relaxed text-white select-text cursor-text',
|
||||
'text-foreground max-w-2xl cursor-text select-text break-words text-center leading-relaxed',
|
||||
style.size,
|
||||
style.weight,
|
||||
)}
|
||||
@@ -186,8 +186,8 @@ export function TextParticleView({
|
||||
|
||||
// Mode 3: card layout
|
||||
return (
|
||||
<div className="group relative flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="scrollbar-card flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-4 overflow-y-auto overscroll-contain rounded bg-white/10 p-[var(--message-card-padding)] backdrop-blur-md">
|
||||
<div className="group relative flex h-full w-full items-center justify-center px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="scrollbar-card bg-card text-card-foreground border-border flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-4 overflow-y-auto overscroll-contain rounded border p-[var(--message-card-padding)]">
|
||||
<MarkdownEditor
|
||||
key={content}
|
||||
value={content}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function TextReactionInput({
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex items-center gap-1 rounded-full bg-black/60 py-1 pl-3 pr-1 shadow-lg ring-1 ring-white/15 backdrop-blur-md"
|
||||
className="bg-popover/90 text-popover-foreground border-border flex items-center gap-1 rounded-full border py-1 pl-3 pr-1 shadow-lg backdrop-blur-md"
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -74,12 +74,14 @@ export function TextReactionInput({
|
||||
}}
|
||||
placeholder="Quick reply…"
|
||||
maxLength={MAX_LENGTH}
|
||||
className="w-24 bg-transparent text-sm text-white outline-none placeholder:text-white/40"
|
||||
className="text-foreground placeholder:text-muted-foreground w-24 bg-transparent text-sm outline-none"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-[1.5ch] text-right text-[10px] tabular-nums',
|
||||
remaining <= 8 ? 'text-amber-300/80' : 'text-white/30',
|
||||
remaining <= 8
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{remaining}
|
||||
@@ -91,8 +93,8 @@ export function TextReactionInput({
|
||||
className={cn(
|
||||
'ml-1 flex size-6 items-center justify-center rounded-full transition-colors',
|
||||
canSubmit
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/30',
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
aria-label="Send reaction"
|
||||
>
|
||||
|
||||
@@ -12,11 +12,16 @@ import {
|
||||
Volume2,
|
||||
ArrowLeft,
|
||||
Camera,
|
||||
Sun,
|
||||
Moon,
|
||||
Monitor,
|
||||
Palette,
|
||||
} from 'lucide-react';
|
||||
import { HumanAvatar } from '@/components/human-avatar';
|
||||
import { AvatarEditDialog } from '@/features/settings/avatar-edit-dialog';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { WindowControls } from '@/components/window-controls';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Muted } from '@/components/ui/typography';
|
||||
@@ -24,6 +29,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { CopyableEmail } from '@/components/copyable-email';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useSoundEffectsStore } from '@/stores/sound-effects-store';
|
||||
import { useThemeStore, type ThemeMode } from '@/stores/theme-store';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { logError, toUserMessage } from '@/lib/errors';
|
||||
import { toast } from 'sonner';
|
||||
@@ -80,6 +86,50 @@ function SettingsGroup({
|
||||
);
|
||||
}
|
||||
|
||||
const THEME_OPTIONS: {
|
||||
value: ThemeMode;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}[] = [
|
||||
{ value: 'system', label: 'System', icon: <Monitor className="size-3.5" /> },
|
||||
{ value: 'light', label: 'Light', icon: <Sun className="size-3.5" /> },
|
||||
{ value: 'dark', label: 'Dark', icon: <Moon className="size-3.5" /> },
|
||||
];
|
||||
|
||||
function ThemeSetting() {
|
||||
const mode = useThemeStore((s) => s.mode);
|
||||
const setMode = useThemeStore((s) => s.setMode);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3 px-4 py-3">
|
||||
<span className="text-muted-foreground flex size-5 items-center justify-center">
|
||||
<Palette className="size-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 text-sm font-medium">Theme</span>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
value={mode}
|
||||
onValueChange={(value) => value && setMode(value as ThemeMode)}
|
||||
className="border-border bg-muted/50 border p-0.5"
|
||||
spacing={2}
|
||||
>
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<ToggleGroupItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
aria-label={option.label}
|
||||
className="data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm gap-1.5 rounded-md px-2.5 text-xs"
|
||||
>
|
||||
{option.icon}
|
||||
{option.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
@@ -150,7 +200,7 @@ export default function SettingsPage() {
|
||||
initials={initials}
|
||||
fallbackClassName="bg-primary/10 text-primary text-xl font-medium"
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<span className="bg-scrim/40 absolute inset-0 flex items-center justify-center rounded-full opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<span className="bg-primary text-primary-foreground ring-background absolute bottom-0 right-0 flex size-5 items-center justify-center rounded-full ring-2">
|
||||
<Camera className="size-2.5" />
|
||||
</span>
|
||||
@@ -165,6 +215,12 @@ export default function SettingsPage() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsGroup title="Appearance">
|
||||
<ThemeSetting />
|
||||
</SettingsGroup>
|
||||
|
||||
<Separator className="mt-4" />
|
||||
|
||||
<SettingsGroup title="Notifications">
|
||||
<div className="flex w-full items-center gap-3 px-4 py-3">
|
||||
<span className="text-muted-foreground flex size-5 items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user