From 803382c821eddce6c46bb3b7f864aaa7f1828431 Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:19:29 -0500 Subject: [PATCH] Keep typography PR focused on text preferences Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> --- .../channels/lib/threadViewModePreference.ts | 13 +- .../ui/AppearanceSettingsControls.tsx | 369 ++++++------------ .../shared/lib/linkPreviewStylePreference.ts | 11 +- .../tests/e2e/buzz-theme-screenshots.spec.ts | 20 +- 4 files changed, 134 insertions(+), 279 deletions(-) diff --git a/desktop/src/features/channels/lib/threadViewModePreference.ts b/desktop/src/features/channels/lib/threadViewModePreference.ts index efda042da..684b73b11 100644 --- a/desktop/src/features/channels/lib/threadViewModePreference.ts +++ b/desktop/src/features/channels/lib/threadViewModePreference.ts @@ -23,7 +23,6 @@ const DEFAULT_THREAD_VIEW_MODE: ThreadViewMode = "split"; const listeners = new Set<() => void>(); let threadViewMode = readStoredThreadViewMode(); -let previewOverride: ThreadViewMode | null = null; function parseThreadViewMode(value: string | null | undefined): ThreadViewMode { return value === "focus" || value === "split" @@ -47,7 +46,7 @@ function subscribe(listener: () => void): () => void { } function getSnapshot(): ThreadViewMode { - return previewOverride ?? threadViewMode; + return threadViewMode; } function getServerSnapshot(): ThreadViewMode { @@ -61,7 +60,6 @@ export function getThreadViewMode(): ThreadViewMode { /** Update the thread layout preference and notify all subscribed components. */ export function setThreadViewMode(mode: ThreadViewMode): void { - previewOverride = null; threadViewMode = mode; try { @@ -75,15 +73,6 @@ export function setThreadViewMode(mode: ThreadViewMode): void { } } -/** Temporarily apply a layout without changing the saved preference. */ -export function previewThreadViewMode(mode: ThreadViewMode | null): void { - if (previewOverride === mode) return; - previewOverride = mode; - for (const listener of listeners) { - listener(); - } -} - /** How threads should open in a channel: as a focus drawer or a split pane. */ export function useThreadViewMode(): ThreadViewMode { return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); diff --git a/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx b/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx index a8e657bee..4892a6458 100644 --- a/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx +++ b/desktop/src/features/settings/ui/AppearanceSettingsControls.tsx @@ -1,9 +1,7 @@ -import * as React from "react"; import type { ReactNode } from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; -import { Eye } from "lucide-react"; +import { ChevronDown, Eye } from "lucide-react"; import { - previewThreadViewMode, setThreadViewMode, useThreadViewMode, type ThreadViewMode, @@ -12,14 +10,10 @@ import { useCommunities } from "@/features/communities/useCommunities"; import { AvatarFramingSlider } from "@/features/profile/ui/AnimatedAvatarControls"; import { contrastColorForBackground } from "@/features/profile/ui/ProfileAvatarEditor.utils"; import { - previewLinkPreviewStyle, setLinkPreviewStyle, useLinkPreviewStyle, type LinkPreviewStyle, } from "@/shared/lib/linkPreviewStylePreference"; -import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews"; -import { LinkPreviewAttachment } from "@/shared/ui/link-preview-attachment"; -import type { LinkPreviewImageLightboxProps } from "@/shared/ui/rich-link-preview-attachment"; import { previewConversationDensity, setConversationDensity, @@ -41,6 +35,14 @@ import { useTheme, } from "@/shared/theme/ThemeProvider"; +import { Button } from "@/shared/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/shared/ui/dropdown-menu"; import { Switch } from "@/shared/ui/switch"; import { SettingsOptionRow } from "./SettingsOptionGroup"; import { SegmentedControl } from "@/shared/ui/segmented-control"; @@ -254,84 +256,6 @@ export function ConversationDisplaySettings() { ); } -/** - * Static sample used by the settings preview card. The thumbnail is an inline - * SVG data URL so the preview needs no network fetch or native image pipeline. - */ -const LINK_PREVIEW_SAMPLE_BASE: Omit = { - kind: "generic-link", - href: "https://example.com/product-updates", - provider: "example.com", - title: "Product updates — a fresh look at conversations", - typeLabel: "link", - description: - "Highlights from this release: refreshed conversation layout, quicker link handling, and readability improvements.", - imageState: "image", - imageDomain: "example.com", -}; - -/** - * Build the sample thumbnail as an SVG data URL from the Buzz gradient - * tokens. Data-URL images cannot resolve CSS variables, so the token values - * are read from the live stylesheet and baked in per render — if the Buzz - * gradient ever changes in `theme.css`, this preview follows automatically. - */ -function buzzGradientSampleImage(isDark: boolean): string { - const styles = globalThis.document - ? getComputedStyle(document.documentElement) - : null; - const readToken = (token: string, fallback: string): string => - styles?.getPropertyValue(token).trim() || fallback; - const top = isDark - ? readToken("--buzz-gradient-dark-top", "#4a4616") - : readToken("--buzz-gradient-light-top", "#e6e6b6"); - const bottom = isDark - ? readToken("--buzz-gradient-dark-bottom", "#0a1423") - : readToken("--buzz-gradient-light-bottom", "#c4d0da"); - const shape = isDark ? "#ffffff" : "#fcfcf9"; - const svg = ``; - return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; -} - -/** Lightbox stand-in for the settings sample — renders the image inert. */ -function SampleImageLightbox({ - children, - className, -}: LinkPreviewImageLightboxProps) { - return
{children}
; -} - -function LinkPreviewSample() { - const { isDark } = useTheme(); - const preview = React.useMemo( - () => ({ - ...LINK_PREVIEW_SAMPLE_BASE, - imageDataUrl: buzzGradientSampleImage(isDark), - }), - [isDark], - ); - return ( -
- -
- ); -} - export function LinkPreviewStyleSetting() { const style = useLinkPreviewStyle(); const activeOption = @@ -339,30 +263,58 @@ export function LinkPreviewStyleSetting() { LINK_PREVIEW_STYLE_OPTIONS[0]; return ( -
- -
-

Link previews

-

+

+

Links

+

+ {activeOption.description} +

+
+ + +
- -
- -
+ {activeOption.label} + + + + + + setLinkPreviewStyle(next as LinkPreviewStyle) + } + value={style} + > + {LINK_PREVIEW_STYLE_OPTIONS.map((option) => ( + + + {option.label} + + {option.description} + + + + ))} + + + + ); } @@ -482,119 +434,6 @@ export function GlassBackgroundSetting() { } /** Compact thread preference row in the Appearance preferences card. */ -/** - * Abstract diagram for the thread layout preview, in the same soft-block - * style as the links sample: a rounded frame holding a channel surface and a - * thread surface, with light skeleton bars. Inline SVG (not a data-URL image) - * so fills reference theme tokens directly and follow light/dark and accent - * changes automatically. Only the panel proportions change between modes. - */ -function ThreadLayoutDiagram({ mode }: { mode: ThreadViewMode }) { - const { isDark } = useTheme(); - const gradientId = React.useId(); - // Inline SVG resolves CSS variables, so the frame gradient references the - // Buzz gradient tokens directly and follows theme.css automatically. - const gradientTop = isDark - ? "var(--buzz-gradient-dark-top, #4a4616)" - : "var(--buzz-gradient-light-top, #e6e6b6)"; - const gradientBottom = isDark - ? "var(--buzz-gradient-dark-bottom, #0a1423)" - : "var(--buzz-gradient-light-bottom, #c4d0da)"; - const shape = isDark ? "#ffffff" : "#fcfcf9"; - const channel = shape; - const channelOpacity = isDark ? 0.3 : 0.45; - const thread = shape; - const threadOpacity = isDark ? 0.85 : 0.92; - const bar = "hsl(var(--muted-foreground) / 0.18)"; - const barSoft = "hsl(var(--muted-foreground) / 0.11)"; - - const isFocus = mode === "focus"; - // Inner content area: 10..230 x 10..122 (inside the frame padding). - // Split: channel and thread share the area side by side with a gap. - // Focus: the thread surface dominates, leaving a narrow channel sliver. - const gap = 6; - const threadX = isFocus ? 42 : 124; - const channelWidth = threadX - 10 - gap; - const threadWidth = 230 - threadX; - - /** Two skeleton text bars, clipped to the panel they sit in. */ - const skeleton = (x: number, y: number, width: number) => ( - <> - - - - ); - - return ( - - ); -} - -function ThreadLayoutPreview() { - const mode = useThreadViewMode(); - return ( -
- -
- ); -} - export function ThreadLayoutSetting() { const threadViewMode = useThreadViewMode(); const { communities } = useCommunities(); @@ -605,38 +444,64 @@ export function ThreadLayoutSetting() { ) ?? THREAD_VIEW_MODE_OPTIONS[0]; return ( -
- -
-

- Thread layout - {showCommunityScope ? ( - - {" "} - (all communities) - - ) : null} -

-

+

+

+ Thread layout + {showCommunityScope ? ( + + {" "} + (all communities) + + ) : null} +

+

+ {activeOption.description} +

+
+ + +
- -
- -
+ {activeOption.label} + + + + + setThreadViewMode(next as ThreadViewMode)} + value={threadViewMode} + > + {THREAD_VIEW_MODE_OPTIONS.map((option) => ( + + + {option.label} + + {option.description} + + + + ))} + + + + ); } diff --git a/desktop/src/shared/lib/linkPreviewStylePreference.ts b/desktop/src/shared/lib/linkPreviewStylePreference.ts index 374f0cd74..3a3c5d789 100644 --- a/desktop/src/shared/lib/linkPreviewStylePreference.ts +++ b/desktop/src/shared/lib/linkPreviewStylePreference.ts @@ -9,7 +9,6 @@ export const DEFAULT_LINK_PREVIEW_STYLE: LinkPreviewStyle = "compact"; const listeners = new Set<() => void>(); let linkPreviewStyle = readStoredLinkPreviewStyle(); -let previewOverride: LinkPreviewStyle | null = null; export function parseLinkPreviewStyle( value: string | null | undefined, @@ -35,11 +34,10 @@ function subscribe(listener: () => void): () => void { } export function getLinkPreviewStyle(): LinkPreviewStyle { - return previewOverride ?? linkPreviewStyle; + return linkPreviewStyle; } export function setLinkPreviewStyle(style: LinkPreviewStyle): void { - previewOverride = null; linkPreviewStyle = style; try { globalThis.localStorage?.setItem(LINK_PREVIEW_STYLE_STORAGE_KEY, style); @@ -49,13 +47,6 @@ export function setLinkPreviewStyle(style: LinkPreviewStyle): void { for (const listener of listeners) listener(); } -/** Temporarily apply a style without changing the saved preference. */ -export function previewLinkPreviewStyle(style: LinkPreviewStyle | null): void { - if (previewOverride === style) return; - previewOverride = style; - for (const listener of listeners) listener(); -} - export function useLinkPreviewStyle(): LinkPreviewStyle { return React.useSyncExternalStore( subscribe, diff --git a/desktop/tests/e2e/buzz-theme-screenshots.spec.ts b/desktop/tests/e2e/buzz-theme-screenshots.spec.ts index b41cd3e12..6563c0441 100644 --- a/desktop/tests/e2e/buzz-theme-screenshots.spec.ts +++ b/desktop/tests/e2e/buzz-theme-screenshots.spec.ts @@ -505,7 +505,7 @@ test("appearance groups theme and preferences into labeled rows", async ({ preferencesCard.getByTestId("prominent-active-tab-toggle"), ).toHaveCount(0); await expect( - preferencesCard.getByRole("group", { name: "Thread layout" }), + preferencesCard.getByTestId("thread-layout-trigger"), ).toBeVisible(); const themeStyleTrigger = themeCard.getByTestId("theme-style-trigger"); const themeStyleOptions = themeCard.getByTestId("theme-style-options"); @@ -1541,14 +1541,24 @@ test("glass background keeps the content panel solid", async ({ page }) => { page.getByTestId("conversation-density-control"), page.getByTestId("conversation-density-control-indicator"), page.getByTestId("theme-style-trigger"), - page.getByTestId("link-preview-style-control"), - page.getByTestId("link-preview-style-control-indicator"), - page.getByTestId("thread-layout-control"), - page.getByTestId("thread-layout-control-indicator"), + page.getByTestId("link-preview-style-trigger"), + page.getByTestId("thread-layout-trigger"), ]; for (const control of matchingRadiusControls) { await expect(control).toHaveCSS("border-radius", "8px"); } + await page.getByTestId("link-preview-style-trigger").click(); + await expect(page.getByTestId("link-preview-style-menu")).toHaveCSS( + "border-radius", + "8px", + ); + await page.keyboard.press("Escape"); + await page.getByTestId("thread-layout-trigger").click(); + await expect(page.getByTestId("thread-layout-menu")).toHaveCSS( + "border-radius", + "8px", + ); + await page.keyboard.press("Escape"); await expect(page.getByTestId("glass-opacity-value")).toHaveCount(0); await expect( opacitySlider.locator(".buzz-avatar-framing-slider-handle"),