From d14cec5885e2ad9fa4792f9782bb8a61e2dd2a22 Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Sun, 21 Jun 2026 10:56:59 -0700 Subject: [PATCH] feat: add warm sand theme colors and light / dark mode toggle (#302) --- js/desktop/index.html | 18 ++- .../src/components/composing-indicator.tsx | 10 +- .../confirm-destructive-overlay.tsx | 10 +- js/desktop/src/components/key-hint.tsx | 7 +- .../src/components/keybindings-overlay.tsx | 18 +-- .../src/components/link-preview-card.tsx | 22 +-- .../src/components/screen-source-picker.tsx | 26 ++-- js/desktop/src/components/ui/dialog.tsx | 2 +- js/desktop/src/components/ui/slider.tsx | 2 +- js/desktop/src/components/ui/sonner.tsx | 4 +- js/desktop/src/components/window-controls.tsx | 6 +- .../attachments/attachment-lightbox.tsx | 2 +- .../src/features/compose/attachment-strip.tsx | 28 ++-- .../src/features/compose/compose-overlay.tsx | 6 +- .../compose/configure-container-step.tsx | 19 +-- .../src/features/compose/markdown-editor.css | 57 ++++--- .../features/compose/recording-overlay.tsx | 4 +- .../features/compose/task-compose-step.tsx | 17 +- .../src/features/compose/text-editor.tsx | 16 +- .../src/features/particles/container-view.tsx | 2 +- .../particles/deleted-particle-view.tsx | 10 +- .../particles/media-particle-view.tsx | 4 +- .../particles/particle-attachments.tsx | 28 ++-- .../particles/particle-children-list.tsx | 10 +- .../features/particles/particle-preview.tsx | 14 +- .../particles/particle-view-resolver.tsx | 2 +- .../particles/playback-page-indicator.tsx | 14 +- .../src/features/particles/reaction-bar.tsx | 34 ++-- .../particles/rename-stream-overlay.tsx | 9 +- .../features/particles/stream-bottom-bar.tsx | 4 +- .../particles/stream-list-sidebar.tsx | 53 ++++--- .../particles/stream-members-overlay.tsx | 46 +++--- .../src/features/particles/stream-top-bar.tsx | 18 ++- .../src/features/particles/stream-view.tsx | 14 +- .../features/particles/task-particle-view.tsx | 37 ++--- .../features/particles/text-particle-view.tsx | 14 +- .../particles/text-reaction-input.tsx | 12 +- js/desktop/src/features/settings-page.tsx | 58 ++++++- js/desktop/src/renderer.tsx | 1 + js/desktop/src/stores/theme-store.ts | 63 ++++++++ js/desktop/src/styles/globals.css | 147 ++++++++++-------- js/desktop/src/web/index.html | 18 ++- js/desktop/src/web/renderer.tsx | 1 + 43 files changed, 540 insertions(+), 347 deletions(-) create mode 100644 js/desktop/src/stores/theme-store.ts diff --git a/js/desktop/index.html b/js/desktop/index.html index 9beb6ad..f750307 100644 --- a/js/desktop/index.html +++ b/js/desktop/index.html @@ -1,8 +1,24 @@ - + llink +
diff --git a/js/desktop/src/components/composing-indicator.tsx b/js/desktop/src/components/composing-indicator.tsx index bb4aae5..062bd8d 100644 --- a/js/desktop/src/components/composing-indicator.tsx +++ b/js/desktop/src/components/composing-indicator.tsx @@ -29,14 +29,14 @@ export function ComposingIndicator({ return (
- - - + + + - + {displayName} {modeLabel}
diff --git a/js/desktop/src/components/confirm-destructive-overlay.tsx b/js/desktop/src/components/confirm-destructive-overlay.tsx index 8e19909..8bf14c6 100644 --- a/js/desktop/src/components/confirm-destructive-overlay.tsx +++ b/js/desktop/src/components/confirm-destructive-overlay.tsx @@ -38,23 +38,23 @@ export function ConfirmDestructiveOverlay({ return createPortal(
-
+
-

{title}

+

{title}

to close
-
{description}
+
{description}
@@ -48,7 +48,7 @@ export function ScreenSourcePicker({
{loading ? ( -

+

Loading sources...

) : ( @@ -73,17 +73,17 @@ export function ScreenSourcePicker({ )}
-
+
@@ -106,7 +106,7 @@ function SourceSection({ }) { return (
-

+

{title}

@@ -116,8 +116,8 @@ function SourceSection({ onClick={() => onSelect(source.id)} className={`overflow-hidden rounded-lg border-2 text-left transition-colors ${ selectedId === source.id - ? 'border-blue-500 bg-zinc-800' - : 'border-transparent bg-zinc-800/50 hover:border-zinc-600' + ? 'border-primary bg-accent' + : 'border-transparent bg-muted hover:border-border' }`} > {source.name} -

+

{source.name}

diff --git a/js/desktop/src/components/ui/dialog.tsx b/js/desktop/src/components/ui/dialog.tsx index 836c0a1..30a27be 100644 --- a/js/desktop/src/components/ui/dialog.tsx +++ b/js/desktop/src/components/ui/dialog.tsx @@ -39,7 +39,7 @@ function DialogOverlay({ ))} diff --git a/js/desktop/src/components/ui/sonner.tsx b/js/desktop/src/components/ui/sonner.tsx index ff59bd9..559d8bd 100644 --- a/js/desktop/src/components/ui/sonner.tsx +++ b/js/desktop/src/components/ui/sonner.tsx @@ -6,11 +6,13 @@ import { OctagonXIcon, Loader2Icon, } from 'lucide-react'; +import { useThemeStore } from '@/stores/theme-store'; const Toaster = ({ ...props }: ToasterProps) => { + const isDark = useThemeStore((s) => s.isDark); return ( , diff --git a/js/desktop/src/components/window-controls.tsx b/js/desktop/src/components/window-controls.tsx index 5cfd30d..818ef31 100644 --- a/js/desktop/src/components/window-controls.tsx +++ b/js/desktop/src/components/window-controls.tsx @@ -21,7 +21,7 @@ export function WindowControls() { size="icon-sm" onClick={() => platform.window.minimize()} aria-label="Minimize" - className="dark:hover:bg-white/10 rounded text-white/50" + className="text-muted-foreground rounded" > @@ -34,7 +34,7 @@ export function WindowControls() { size="icon-sm" onClick={() => platform.window.maximize()} aria-label={isMaximized ? 'Restore' : 'Maximize'} - className="dark:hover:bg-white/10 rounded text-white/50" + className="text-muted-foreground rounded" > {isMaximized ? : } @@ -47,7 +47,7 @@ export function WindowControls() { size="icon-sm" onClick={() => platform.window.close()} aria-label="Close" - className="hover:text-destructive dark:hover:bg-white/10 rounded text-white/50" + className="hover:text-destructive text-muted-foreground rounded" > diff --git a/js/desktop/src/features/attachments/attachment-lightbox.tsx b/js/desktop/src/features/attachments/attachment-lightbox.tsx index f46b8c9..2018f7c 100644 --- a/js/desktop/src/features/attachments/attachment-lightbox.tsx +++ b/js/desktop/src/features/attachments/attachment-lightbox.tsx @@ -172,7 +172,7 @@ export function AttachmentLightbox({ }} > - + ) : (
- - + + {attachment.file.name} - + {formatFileSize(attachment.file.size)}
)} {isUploading && ( -
- +
+
)} @@ -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" > @@ -108,9 +108,9 @@ function AttachmentThumbnail({ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) { if (entry.isLoading) { return ( -
- - +
+ +
); } @@ -124,9 +124,9 @@ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) { diff --git a/js/desktop/src/features/compose/compose-overlay.tsx b/js/desktop/src/features/compose/compose-overlay.tsx index c4a0bdd..d361a8d 100644 --- a/js/desktop/src/features/compose/compose-overlay.tsx +++ b/js/desktop/src/features/compose/compose-overlay.tsx @@ -768,7 +768,7 @@ export function ComposeOverlay({ /> )} {step === 'recording' && recordingSource === 'screen' && ( -
+
@@ -840,8 +840,8 @@ export function ComposeOverlay({ /> )} {step === 'submitting' && ( -
- +
+ Sending...
diff --git a/js/desktop/src/features/compose/configure-container-step.tsx b/js/desktop/src/features/compose/configure-container-step.tsx index b0d96de..b31ccee 100644 --- a/js/desktop/src/features/compose/configure-container-step.tsx +++ b/js/desktop/src/features/compose/configure-container-step.tsx @@ -77,13 +77,13 @@ export function ConfigureContainerStep({ return (
-
{/* Visibility */}
- -
+ +
{/* Everyone in network */}
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', )} > 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', )} > - + {initials} @@ -162,7 +163,7 @@ export function ConfigureContainerStep({
{/* Keyboard hints */} -
+
cancel diff --git a/js/desktop/src/features/compose/markdown-editor.css b/js/desktop/src/features/compose/markdown-editor.css index 1262acc..e91ed84 100644 --- a/js/desktop/src/features/compose/markdown-editor.css +++ b/js/desktop/src/features/compose/markdown-editor.css @@ -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 , 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 , 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 { diff --git a/js/desktop/src/features/compose/recording-overlay.tsx b/js/desktop/src/features/compose/recording-overlay.tsx index bdb1644..f2f012f 100644 --- a/js/desktop/src/features/compose/recording-overlay.tsx +++ b/js/desktop/src/features/compose/recording-overlay.tsx @@ -178,7 +178,7 @@ export function RecordingOverlay({ return (
+
)} {/* Top center: recording indicator */} diff --git a/js/desktop/src/features/compose/task-compose-step.tsx b/js/desktop/src/features/compose/task-compose-step.tsx index 9562ad9..3961a3f 100644 --- a/js/desktop/src/features/compose/task-compose-step.tsx +++ b/js/desktop/src/features/compose/task-compose-step.tsx @@ -63,37 +63,38 @@ export function TaskComposeStep({ return (
- + 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" />
- +