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:
Arjun Patel
2026-06-21 10:56:59 -07:00
committed by GitHub
parent 3d80ac2993
commit d14cec5885
43 changed files with 540 additions and 347 deletions
@@ -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="19">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"
>
+57 -1
View File
@@ -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">