From 011e7d8115a121b0bc26ad145899ca27748a4f54 Mon Sep 17 00:00:00 2001 From: Maofeng Date: Wed, 29 Jul 2026 21:22:47 +0800 Subject: [PATCH] feat(blocks): add reusable application shell blocks - add appearance state, theme presets, preference controls, and preview components - add responsive primary, nested, mobile, and flyout navigation with route-aware breadcrumbs - add application layout, sidebar state, header actions, user menu, query refresh, and scroll utilities - add configurable chat and notification surfaces backed by consumer-provided data and queries - expose package entry points and cover theme, state, and command behavior with tests --- packages/blocks/package.json | 44 + .../src/blocks/appearance/controller.tsx | 88 ++ .../blocks/src/blocks/appearance/dialog.tsx | 17 + .../blocks/src/blocks/appearance/index.ts | 20 + .../src/blocks/appearance/preferences.tsx | 86 ++ .../src/blocks/appearance/state.test.ts | 67 + .../blocks/src/blocks/appearance/state.ts | 125 ++ .../blocks/appearance/theme-color-picker.tsx | 114 ++ .../blocks/appearance/theme-config-panel.tsx | 57 + .../src/blocks/appearance/theme-preview.tsx | 103 ++ .../src/blocks/appearance/theme.test.ts | 44 + .../blocks/src/blocks/appearance/theme.ts | 58 + .../blocks/src/blocks/appearance/themes.ts | 1126 +++++++++++++++++ .../blocks/src/blocks/appearance/ui-state.tsx | 213 ++++ .../src/blocks/chats/chat-popover-handle.ts | 3 + .../src/blocks/chats/chat-popover-trigger.tsx | 13 + .../blocks/src/blocks/chats/chat-popover.tsx | 43 + .../src/blocks/chats/chat-thread-item.tsx | 45 + packages/blocks/src/blocks/chats/index.ts | 4 + packages/blocks/src/blocks/chats/types.ts | 5 + .../src/blocks/layout/app-breadcrumb.tsx | 450 +++++++ .../blocks/src/blocks/layout/app-layout.tsx | 246 ++++ .../src/blocks/layout/app-query-indicator.tsx | 47 + .../blocks/src/blocks/layout/app-sidebar.tsx | 35 + .../blocks/layout/header-action-button.tsx | 35 + packages/blocks/src/blocks/layout/index.ts | 14 + .../src/blocks/layout/sidebar-state.tsx | 53 + .../blocks/src/blocks/layout/user-menu.tsx | 243 ++++ .../blocks/navigation/flyout-navigation.tsx | 67 + .../blocks/src/blocks/navigation/index.ts | 19 + .../blocks/src/blocks/navigation/item-icon.ts | 10 + .../blocks/navigation/mobile-navigation.tsx | 175 +++ .../blocks/navigation/navigation-button.tsx | 224 ++++ .../blocks/navigation/nested-navigation.tsx | 109 ++ .../blocks/navigation/primary-navigation.tsx | 192 +++ .../src/blocks/navigation/route-state.ts | 22 + .../blocks/src/blocks/navigation/types.ts | 17 + .../src/blocks/navigation/use-navigation.tsx | 146 +++ .../blocks/src/blocks/notifications/index.ts | 31 + .../notifications/notification-center.tsx | 145 +++ .../notifications/notification-item.tsx | 221 ++++ .../notifications/notification-sheet.tsx | 82 ++ .../blocks/src/blocks/notifications/types.ts | 46 + .../blocks/notifications/use-notifications.ts | 107 ++ packages/blocks/src/components/icon.tsx | 30 + packages/blocks/src/hooks/use-on-scroll.ts | 70 + packages/blocks/src/hooks/use-refresh.ts | 33 + .../blocks/src/lib/command-actions.test.tsx | 35 + packages/blocks/src/lib/command-actions.tsx | 55 + packages/blocks/src/styles/globals.css | 1 + packages/blocks/tsconfig.json | 18 + 51 files changed, 5253 insertions(+) create mode 100644 packages/blocks/package.json create mode 100644 packages/blocks/src/blocks/appearance/controller.tsx create mode 100644 packages/blocks/src/blocks/appearance/dialog.tsx create mode 100644 packages/blocks/src/blocks/appearance/index.ts create mode 100644 packages/blocks/src/blocks/appearance/preferences.tsx create mode 100644 packages/blocks/src/blocks/appearance/state.test.ts create mode 100644 packages/blocks/src/blocks/appearance/state.ts create mode 100644 packages/blocks/src/blocks/appearance/theme-color-picker.tsx create mode 100644 packages/blocks/src/blocks/appearance/theme-config-panel.tsx create mode 100644 packages/blocks/src/blocks/appearance/theme-preview.tsx create mode 100644 packages/blocks/src/blocks/appearance/theme.test.ts create mode 100644 packages/blocks/src/blocks/appearance/theme.ts create mode 100644 packages/blocks/src/blocks/appearance/themes.ts create mode 100644 packages/blocks/src/blocks/appearance/ui-state.tsx create mode 100644 packages/blocks/src/blocks/chats/chat-popover-handle.ts create mode 100644 packages/blocks/src/blocks/chats/chat-popover-trigger.tsx create mode 100644 packages/blocks/src/blocks/chats/chat-popover.tsx create mode 100644 packages/blocks/src/blocks/chats/chat-thread-item.tsx create mode 100644 packages/blocks/src/blocks/chats/index.ts create mode 100644 packages/blocks/src/blocks/chats/types.ts create mode 100644 packages/blocks/src/blocks/layout/app-breadcrumb.tsx create mode 100644 packages/blocks/src/blocks/layout/app-layout.tsx create mode 100644 packages/blocks/src/blocks/layout/app-query-indicator.tsx create mode 100644 packages/blocks/src/blocks/layout/app-sidebar.tsx create mode 100644 packages/blocks/src/blocks/layout/header-action-button.tsx create mode 100644 packages/blocks/src/blocks/layout/index.ts create mode 100644 packages/blocks/src/blocks/layout/sidebar-state.tsx create mode 100644 packages/blocks/src/blocks/layout/user-menu.tsx create mode 100644 packages/blocks/src/blocks/navigation/flyout-navigation.tsx create mode 100644 packages/blocks/src/blocks/navigation/index.ts create mode 100644 packages/blocks/src/blocks/navigation/item-icon.ts create mode 100644 packages/blocks/src/blocks/navigation/mobile-navigation.tsx create mode 100644 packages/blocks/src/blocks/navigation/navigation-button.tsx create mode 100644 packages/blocks/src/blocks/navigation/nested-navigation.tsx create mode 100644 packages/blocks/src/blocks/navigation/primary-navigation.tsx create mode 100644 packages/blocks/src/blocks/navigation/route-state.ts create mode 100644 packages/blocks/src/blocks/navigation/types.ts create mode 100644 packages/blocks/src/blocks/navigation/use-navigation.tsx create mode 100644 packages/blocks/src/blocks/notifications/index.ts create mode 100644 packages/blocks/src/blocks/notifications/notification-center.tsx create mode 100644 packages/blocks/src/blocks/notifications/notification-item.tsx create mode 100644 packages/blocks/src/blocks/notifications/notification-sheet.tsx create mode 100644 packages/blocks/src/blocks/notifications/types.ts create mode 100644 packages/blocks/src/blocks/notifications/use-notifications.ts create mode 100644 packages/blocks/src/components/icon.tsx create mode 100644 packages/blocks/src/hooks/use-on-scroll.ts create mode 100644 packages/blocks/src/hooks/use-refresh.ts create mode 100644 packages/blocks/src/lib/command-actions.test.tsx create mode 100644 packages/blocks/src/lib/command-actions.tsx create mode 100644 packages/blocks/src/styles/globals.css create mode 100644 packages/blocks/tsconfig.json diff --git a/packages/blocks/package.json b/packages/blocks/package.json new file mode 100644 index 0000000..c18baf1 --- /dev/null +++ b/packages/blocks/package.json @@ -0,0 +1,44 @@ +{ + "name": "@workspace/blocks", + "version": "0.0.0", + "type": "module", + "private": true, + "scripts": { + "format": "prettier --write \"**/*.{ts,tsx}\"", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@base-ui/react": "^1.6.0", + "@base-ui/utils": "^0.3.1", + "@floating-ui/utils": "^0.2.12", + "@hugeicons/core-free-icons": "^4.2.3", + "@hugeicons/react": "^1.1.9", + "@tanstack/react-hotkeys": "^0.10.0", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-router": "^1.170.18", + "@workspace/ui": "workspace:*", + "lucide-react": "^1.27.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@testing-library/react": "^16.3.2", + "@types/react": "^19", + "@types/react-dom": "^19", + "jsdom": "^30.0.1", + "typescript": "~6", + "vitest": "^4.1.10" + }, + "exports": { + "./globals.css": "./src/styles/globals.css", + "./appearance": "./src/blocks/appearance/index.ts", + "./chats": "./src/blocks/chats/index.ts", + "./components/*": "./src/components/*.tsx", + "./hooks/*": "./src/hooks/*.ts", + "./lib/*": "./src/lib/*.tsx", + "./layout": "./src/blocks/layout/index.ts", + "./navigation": "./src/blocks/navigation/index.ts", + "./notifications": "./src/blocks/notifications/index.ts" + } +} diff --git a/packages/blocks/src/blocks/appearance/controller.tsx b/packages/blocks/src/blocks/appearance/controller.tsx new file mode 100644 index 0000000..3950108 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/controller.tsx @@ -0,0 +1,88 @@ +import { useCallback, useState } from "react" +import { useHotkey } from "@tanstack/react-hotkeys" +import { useResolvedTheme, useUiState } from "./ui-state" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@workspace/ui/components/dialog" +import { + NavigationActionTarget, + navigationActions, +} from "../../lib/command-actions" +import { appearanceDialogHandle } from "./dialog" +import { CompactPreference, ModePreference } from "./preferences" +import { ThemeConfigPanel } from "./theme-config-panel" + +export function AppearanceController() { + const [isOpen, setIsOpen] = useState(false) + const [, setThemeMode] = useUiState("theme-mode") + const resolvedTheme = useResolvedTheme() + + const toggleDialog = useCallback(() => { + if (isOpen) { + appearanceDialogHandle.close() + } else { + appearanceDialogHandle.open(null) + } + }, [isOpen]) + + const toggleTheme = useCallback(() => { + setThemeMode(resolvedTheme === "dark" ? "light" : "dark") + }, [resolvedTheme, setThemeMode]) + + useHotkey("Mod+,", toggleDialog, { + ignoreInputs: true, + preventDefault: true, + stopPropagation: false, + meta: { + name: "界面外观", + description: "打开主题与布局偏好", + }, + }) + + useHotkey("Mod+D", toggleTheme, { + ignoreInputs: true, + preventDefault: true, + stopPropagation: false, + meta: { + name: "切换主题", + description: "在浅色与深色主题之间切换", + }, + }) + + return ( + + appearanceDialogHandle.open(null)} + /> + + + 界面外观 + 主题与布局偏好 + +
+
+ + +
+
+ + +
+
+
+
+ ) +} diff --git a/packages/blocks/src/blocks/appearance/dialog.tsx b/packages/blocks/src/blocks/appearance/dialog.tsx new file mode 100644 index 0000000..075ede3 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/dialog.tsx @@ -0,0 +1,17 @@ +import type { ComponentProps } from "react" +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" +import { DialogTrigger } from "@workspace/ui/components/dialog" + +export const appearanceDialogHandle = DialogPrimitive.createHandle() + +export function AppearanceTrigger( + props: Omit, "handle" | "nativeButton"> +) { + return ( + + ) +} diff --git a/packages/blocks/src/blocks/appearance/index.ts b/packages/blocks/src/blocks/appearance/index.ts new file mode 100644 index 0000000..3331a14 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/index.ts @@ -0,0 +1,20 @@ +export { appearanceDialogHandle, AppearanceTrigger } from "./dialog" +export { AppearanceController } from "./controller" +export { + UiStateProvider, + type UiStateProviderProps, + type UiStateChangeHandler, + useResolvedTheme, + useUiState, +} from "./ui-state" +export { + defaultUiState, + isUiStateUpdate, + type ThemeMode, + type UiState, + uiStateDefinitions, + type UiStateKey, + type UiStateUpdate, + type UiStateValue, +} from "./state" +export * from "./theme" diff --git a/packages/blocks/src/blocks/appearance/preferences.tsx b/packages/blocks/src/blocks/appearance/preferences.tsx new file mode 100644 index 0000000..46c263b --- /dev/null +++ b/packages/blocks/src/blocks/appearance/preferences.tsx @@ -0,0 +1,86 @@ +import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@workspace/ui/components/select" +import { Switch } from "@workspace/ui/components/switch" + +import type { ThemeMode } from "./state" +import { useUiState } from "./ui-state" + +const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [ + { label: "使用浅色", value: "light" }, + { label: "使用深色", value: "dark" }, + { label: "跟随系统", value: "system" }, +] + +function ModeIcon({ mode }: { mode: ThemeMode | null }) { + if (mode === "light") return + if (mode === "dark") return + return +} + +export function ModePreference() { + const [themeMode, setThemeMode] = useUiState("theme-mode") + const description = { + light: "界面将始终使用浅色主题", + dark: "界面将始终使用深色主题", + system: "界面主题将跟随系统外观设置", + }[themeMode] + + return ( +
+

主题模式

+

{description}

+ +
+ ) +} + +export function CompactPreference() { + const [isCompacted, setIsCompacted] = useUiState("main-compacted") + + return ( +
+

紧凑布局

+

+ 启用后,页面主内容将使用紧凑宽度 +

+ +
+ ) +} diff --git a/packages/blocks/src/blocks/appearance/state.test.ts b/packages/blocks/src/blocks/appearance/state.test.ts new file mode 100644 index 0000000..79fa38a --- /dev/null +++ b/packages/blocks/src/blocks/appearance/state.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest" +import { defaultUiState, isUiStateUpdate, uiStateDefinitions } from "./state" + +describe("UI state", () => { + it("keeps the main content full width by default", () => { + expect(defaultUiState["main-compacted"]).toBe(false) + }) + + it("serializes and parses the compacted main state", () => { + const definition = uiStateDefinitions["main-compacted"] + + expect(definition.serialize(true)).toBe("1") + expect(definition.serialize(false)).toBe("0") + expect(definition.parse("1")).toBe(true) + expect(definition.parse("0")).toBe(false) + expect(definition.parse(undefined)).toBe(false) + }) + + it("validates compacted main updates", () => { + expect(isUiStateUpdate({ key: "main-compacted", value: true })).toBe(true) + expect(isUiStateUpdate({ key: "main-compacted", value: "true" })).toBe( + false + ) + }) + + it("keeps and persists the sidebar collapsed state", () => { + const definition = uiStateDefinitions["sidebar-collapsed"] + + expect(defaultUiState["sidebar-collapsed"]).toBe(false) + expect(definition.serialize(true)).toBe("1") + expect(definition.parse("1")).toBe(true) + expect(definition.parse("true")).toBe(true) + expect(definition.parse(undefined)).toBe(false) + expect(isUiStateUpdate({ key: "sidebar-collapsed", value: true })).toBe( + true + ) + }) + + it("serializes and parses theme color settings", () => { + const definition = uiStateDefinitions["theme-light"] + const value = { + baseColor: "zinc", + accentColor: "violet", + } as const + + expect(definition.parse(definition.serialize(value))).toEqual(value) + expect(definition.parse("invalid json")).toEqual({ + baseColor: "neutral", + accentColor: "blue", + }) + }) + + it("validates light and dark theme updates", () => { + expect( + isUiStateUpdate({ + key: "theme-dark", + value: { baseColor: "stone", accentColor: "amber" }, + }) + ).toBe(true) + expect( + isUiStateUpdate({ + key: "theme-light", + value: { baseColor: "unknown", accentColor: "blue" }, + }) + ).toBe(false) + }) +}) diff --git a/packages/blocks/src/blocks/appearance/state.ts b/packages/blocks/src/blocks/appearance/state.ts new file mode 100644 index 0000000..0a6772f --- /dev/null +++ b/packages/blocks/src/blocks/appearance/state.ts @@ -0,0 +1,125 @@ +import { accentColors, baseColors, type ThemeColorConfig } from "./theme" + +export type ThemeMode = "light" | "dark" | "system" + +export type UiState = { + "main-compacted": boolean + "sidebar-collapsed": boolean + "theme-mode": ThemeMode + "theme-light": ThemeColorConfig + "theme-dark": ThemeColorConfig +} + +export type UiStateKey = keyof UiState +export type UiStateValue = UiState[K] + +type UiStateDefinition = { + cookie: string + defaultValue: T + parse: (value: string | undefined) => T + serialize: (value: T) => string +} + +const defaultThemeColorConfig: ThemeColorConfig = { + baseColor: "neutral", + accentColor: "blue", +} + +function isThemeColorConfig(value: unknown): value is ThemeColorConfig { + if (!value || typeof value !== "object") return false + + const config = value as { + baseColor?: unknown + accentColor?: unknown + } + + return ( + typeof config.baseColor === "string" && + baseColors.some((color) => color === config.baseColor) && + typeof config.accentColor === "string" && + accentColors.some((color) => color === config.accentColor) + ) +} + +function parseThemeColorConfig(value: string | undefined): ThemeColorConfig { + if (!value) return { ...defaultThemeColorConfig } + + try { + const parsed: unknown = JSON.parse(value) + return isThemeColorConfig(parsed) ? parsed : { ...defaultThemeColorConfig } + } catch { + return { ...defaultThemeColorConfig } + } +} + +export const uiStateDefinitions: { + [K in UiStateKey]: UiStateDefinition +} = { + "main-compacted": { + cookie: "ui-main-compacted", + defaultValue: false, + parse: (value) => value === "1", + serialize: (value) => (value ? "1" : "0"), + }, + "sidebar-collapsed": { + cookie: "ui-sidebar-collapsed", + defaultValue: false, + parse: (value) => value === "true" || value === "1", + serialize: (value) => (value ? "1" : "0"), + }, + "theme-mode": { + cookie: "ui-theme", + defaultValue: "system", + parse: (value) => + value === "light" || value === "dark" || value === "system" + ? value + : "system", + serialize: (value) => value, + }, + "theme-light": { + cookie: "ui-theme-light", + defaultValue: { ...defaultThemeColorConfig }, + parse: parseThemeColorConfig, + serialize: JSON.stringify, + }, + "theme-dark": { + cookie: "ui-theme-dark", + defaultValue: { ...defaultThemeColorConfig }, + parse: parseThemeColorConfig, + serialize: JSON.stringify, + }, +} + +export const defaultUiState: UiState = { + "main-compacted": uiStateDefinitions["main-compacted"].defaultValue, + "sidebar-collapsed": uiStateDefinitions["sidebar-collapsed"].defaultValue, + "theme-mode": uiStateDefinitions["theme-mode"].defaultValue, + "theme-light": uiStateDefinitions["theme-light"].defaultValue, + "theme-dark": uiStateDefinitions["theme-dark"].defaultValue, +} + +export type UiStateUpdate = { + [K in UiStateKey]: { key: K; value: UiState[K] } +}[UiStateKey] + +export function isUiStateUpdate(value: unknown): value is UiStateUpdate { + if (!value || typeof value !== "object") return false + + const update = value as { key?: unknown; value?: unknown } + switch (update.key) { + case "main-compacted": + case "sidebar-collapsed": + return typeof update.value === "boolean" + case "theme-mode": + return ( + update.value === "light" || + update.value === "dark" || + update.value === "system" + ) + case "theme-light": + case "theme-dark": + return isThemeColorConfig(update.value) + default: + return false + } +} diff --git a/packages/blocks/src/blocks/appearance/theme-color-picker.tsx b/packages/blocks/src/blocks/appearance/theme-color-picker.tsx new file mode 100644 index 0000000..98d523d --- /dev/null +++ b/packages/blocks/src/blocks/appearance/theme-color-picker.tsx @@ -0,0 +1,114 @@ +import type { CSSProperties } from "react" +import { cn } from "@workspace/ui/lib/utils" +import { + accentColors, + baseColors, + getTheme, + type AccentColor, + type BaseColor, +} from "./theme" + +type ThemeColor = BaseColor | AccentColor + +type ThemeColorSwatch = { + name: T + title: string + light: { + background: string + foreground: string + } + dark: { + background: string + foreground: string + } +} + +function createSwatches( + colors: readonly T[] +): ThemeColorSwatch[] { + return colors.map((name) => { + const theme = getTheme(name)! + + return { + name, + title: theme.title, + light: { + background: theme.cssVars.light.primary, + foreground: theme.cssVars.light["primary-foreground"], + }, + dark: { + background: theme.cssVars.dark.primary, + foreground: theme.cssVars.dark["primary-foreground"], + }, + } + }) +} + +export const baseColorSwatches = createSwatches(baseColors) +export const accentColorSwatches = createSwatches(accentColors) + +export function ThemeColorPicker({ + swatches, + title, + value, + onChange, +}: { + swatches: readonly ThemeColorSwatch[] + title: string + value: T + onChange: (color: T) => void +}) { + return ( +
+
+ {title} +
+
+ {swatches.map((swatch) => ( + onChange(swatch.name)} + /> + ))} +
+
+ ) +} + +function ThemeColorButton({ + swatch, + active, + onClick, +}: { + swatch: ThemeColorSwatch + active: boolean + onClick: () => void +}) { + const style = { + "--light-bg": swatch.light.background, + "--light-fg": swatch.light.foreground, + "--dark-bg": swatch.dark.background, + "--dark-fg": swatch.dark.foreground, + } as CSSProperties + + return ( + + ) +} diff --git a/packages/blocks/src/blocks/appearance/theme-config-panel.tsx b/packages/blocks/src/blocks/appearance/theme-config-panel.tsx new file mode 100644 index 0000000..f85fccb --- /dev/null +++ b/packages/blocks/src/blocks/appearance/theme-config-panel.tsx @@ -0,0 +1,57 @@ +import { MoonIcon, SunIcon } from "lucide-react" +import { useUiState } from "./ui-state" +import { Badge } from "@workspace/ui/components/badge" +import { + accentColorSwatches, + baseColorSwatches, + ThemeColorPicker, +} from "./theme-color-picker" +import { ThemePreview } from "./theme-preview" +import type { ThemeScheme } from "./theme" + +export function ThemeConfigPanel({ + scheme, + active, + description, +}: { + scheme: ThemeScheme + active: boolean + description: string +}) { + const stateKey = scheme === "light" ? "theme-light" : "theme-dark" + const [config, setConfig] = useUiState(stateKey) + + return ( +
+
+ {scheme === "light" ? ( + + ) : ( + + )} + {scheme === "light" ? "浅色主题" : "深色主题"} + {active && 使用中} +
+
+

{description}

+ + + setConfig((current) => ({ ...current, baseColor })) + } + /> + + setConfig((current) => ({ ...current, accentColor })) + } + /> +
+
+ ) +} diff --git a/packages/blocks/src/blocks/appearance/theme-preview.tsx b/packages/blocks/src/blocks/appearance/theme-preview.tsx new file mode 100644 index 0000000..021ed04 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/theme-preview.tsx @@ -0,0 +1,103 @@ +import type { CSSProperties } from "react" +import { + getTheme, + getThemeCssVariables, + type ThemeColorConfig, + type ThemeScheme, +} from "./theme" +import { cn } from "@workspace/ui/lib/utils" +import { useUiState } from "./ui-state" + +export function ThemePreview({ + scheme, + config, + className, +}: { + scheme: ThemeScheme + config: ThemeColorConfig + className?: string +}) { + const [isCompacted] = useUiState("main-compacted") + const themeVariables = getThemeCssVariables(scheme, config) + const baseTheme = getTheme(config.baseColor) ?? getTheme("neutral")! + const style = { + ...Object.fromEntries( + Object.entries(themeVariables).map(([key, value]) => [`--${key}`, value]) + ), + "--preview-base": baseTheme.cssVars[scheme].primary, + } as CSSProperties + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/packages/blocks/src/blocks/appearance/theme.test.ts b/packages/blocks/src/blocks/appearance/theme.test.ts new file mode 100644 index 0000000..c1c5223 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/theme.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest" +import { + accentColors, + baseColors, + buildThemeStyleSheet, + getTheme, + getThemeCssVariables, + themes, +} from "./theme" + +describe("appearance themes", () => { + it("defines every selectable base and accent color exactly once", () => { + const selectableColors = [...baseColors, ...accentColors] + + expect(themes.map((theme) => theme.name)).toEqual(selectableColors) + expect(new Set(selectableColors).size).toBe(selectableColors.length) + expect(themes.every((theme) => theme.title.length > 0)).toBe(true) + }) + + it("combines the base palette with the selected accent palette", () => { + const variables = getThemeCssVariables("light", { + baseColor: "stone", + accentColor: "rose", + }) + + expect(variables.background).toBe( + getTheme("stone")?.cssVars.light.background + ) + expect(variables.primary).toBe(getTheme("rose")?.cssVars.light.primary) + }) + + it("builds explicit and system color-scheme rules", () => { + const styleSheet = buildThemeStyleSheet( + { baseColor: "neutral", accentColor: "blue" }, + { baseColor: "zinc", accentColor: "violet" } + ) + + expect(styleSheet).toContain(":root,.ui\\:light{") + expect(styleSheet).toContain(".ui\\:dark{") + expect(styleSheet).toContain("@media (prefers-color-scheme:dark)") + expect(styleSheet).toContain("--background:") + expect(styleSheet).toContain("--primary:") + }) +}) diff --git a/packages/blocks/src/blocks/appearance/theme.ts b/packages/blocks/src/blocks/appearance/theme.ts new file mode 100644 index 0000000..25bf25d --- /dev/null +++ b/packages/blocks/src/blocks/appearance/theme.ts @@ -0,0 +1,58 @@ +import { + themes, + type AccentColor, + type BaseColor, + type ThemeColorConfig, + type ThemeScheme, +} from "./themes" + +export { + accentColors, + baseColors, + themes, + type AccentColor, + type BaseColor, + type Theme, + type ThemeColorConfig, + type ThemeScheme, +} from "./themes" + +export const THEME_STYLE_ELEMENT_ID = "ui-theme-variables" + +export const getTheme = (name: BaseColor | AccentColor) => + themes.find((theme) => theme.name === name) + +export function getThemeCssVariables( + scheme: ThemeScheme, + config: ThemeColorConfig +) { + const base = getTheme(config.baseColor) ?? getTheme("neutral")! + const accent = getTheme(config.accentColor) ?? getTheme("blue")! + + return { + ...base.cssVars[scheme], + ...accent.cssVars[scheme], + } +} + +function serializeThemeVariables(theme: Record): string { + return Object.entries(theme) + .map(([key, value]) => `--${key}:${value}`) + .join(";") +} + +export function buildThemeStyleSheet( + lightConfig: ThemeColorConfig, + darkConfig: ThemeColorConfig +) { + const light = serializeThemeVariables( + getThemeCssVariables("light", lightConfig) + ) + const dark = serializeThemeVariables(getThemeCssVariables("dark", darkConfig)) + + return [ + `:root,.ui\\:light{${light}}`, + `.ui\\:dark{${dark}}`, + `@media (prefers-color-scheme:dark){:root:not(.ui\\:light):not(.ui\\:dark){${dark}}}`, + ].join("") +} diff --git a/packages/blocks/src/blocks/appearance/themes.ts b/packages/blocks/src/blocks/appearance/themes.ts new file mode 100644 index 0000000..7f8c6fd --- /dev/null +++ b/packages/blocks/src/blocks/appearance/themes.ts @@ -0,0 +1,1126 @@ +export type BaseColor = + "neutral" | "stone" | "zinc" | "mauve" | "olive" | "mist" | "taupe" + +export type AccentColor = + | "amber" + | "blue" + | "cyan" + | "emerald" + | "fuchsia" + | "green" + | "indigo" + | "lime" + | "orange" + | "pink" + | "purple" + | "red" + | "rose" + | "sky" + | "teal" + | "violet" + | "yellow" + +export type ThemeScheme = "light" | "dark" + +export type ThemeColorConfig = { + baseColor: BaseColor + accentColor: AccentColor +} + +export const accentColors: AccentColor[] = [ + "amber", + "blue", + "cyan", + "emerald", + "fuchsia", + "green", + "indigo", + "lime", + "orange", + "pink", + "purple", + "red", + "rose", + "sky", + "teal", + "violet", + "yellow", +] + +export const baseColors: BaseColor[] = [ + "neutral", + "stone", + "zinc", + "mauve", + "olive", + "mist", + "taupe", +] + +export type Theme = { + name: BaseColor | AccentColor + title: string + cssVars: { + light: Record + dark: Record + } +} + +export const themes: Theme[] = [ + { + name: "neutral", + title: "中性灰", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0 0)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.145 0 0)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.145 0 0)", + primary: "oklch(0.205 0 0)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.97 0 0)", + "secondary-foreground": "oklch(0.205 0 0)", + muted: "oklch(0.97 0 0)", + "muted-foreground": "oklch(0.556 0 0)", + accent: "oklch(0.97 0 0)", + "accent-foreground": "oklch(0.205 0 0)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0 0)", + input: "oklch(0.922 0 0)", + ring: "oklch(0.708 0 0)", + "chart-1": "oklch(0.87 0 0)", + "chart-2": "oklch(0.556 0 0)", + "chart-3": "oklch(0.439 0 0)", + "chart-4": "oklch(0.371 0 0)", + "chart-5": "oklch(0.269 0 0)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.145 0 0)", + "sidebar-primary": "oklch(0.205 0 0)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.97 0 0)", + "sidebar-accent-foreground": "oklch(0.205 0 0)", + "sidebar-border": "oklch(0.922 0 0)", + "sidebar-ring": "oklch(0.708 0 0)", + }, + dark: { + background: "oklch(0.145 0 0)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.205 0 0)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.205 0 0)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.922 0 0)", + "primary-foreground": "oklch(0.205 0 0)", + secondary: "oklch(0.269 0 0)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.269 0 0)", + "muted-foreground": "oklch(0.708 0 0)", + accent: "oklch(0.269 0 0)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.556 0 0)", + "chart-1": "oklch(0.87 0 0)", + "chart-2": "oklch(0.556 0 0)", + "chart-3": "oklch(0.439 0 0)", + "chart-4": "oklch(0.371 0 0)", + "chart-5": "oklch(0.269 0 0)", + sidebar: "oklch(0.205 0 0)", + "sidebar-foreground": "oklch(0.985 0 0)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.269 0 0)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.556 0 0)", + }, + }, + }, + { + name: "stone", + title: "石灰", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.147 0.004 49.25)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.147 0.004 49.25)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.147 0.004 49.25)", + primary: "oklch(0.216 0.006 56.043)", + "primary-foreground": "oklch(0.985 0.001 106.423)", + secondary: "oklch(0.97 0.001 106.424)", + "secondary-foreground": "oklch(0.216 0.006 56.043)", + muted: "oklch(0.97 0.001 106.424)", + "muted-foreground": "oklch(0.553 0.013 58.071)", + accent: "oklch(0.97 0.001 106.424)", + "accent-foreground": "oklch(0.216 0.006 56.043)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.923 0.003 48.717)", + input: "oklch(0.923 0.003 48.717)", + ring: "oklch(0.709 0.01 56.259)", + "chart-1": "oklch(0.869 0.005 56.366)", + "chart-2": "oklch(0.553 0.013 58.071)", + "chart-3": "oklch(0.444 0.011 73.639)", + "chart-4": "oklch(0.374 0.01 67.558)", + "chart-5": "oklch(0.268 0.007 34.298)", + radius: "0.625rem", + sidebar: "oklch(0.985 0.001 106.423)", + "sidebar-foreground": "oklch(0.147 0.004 49.25)", + "sidebar-primary": "oklch(0.216 0.006 56.043)", + "sidebar-primary-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-accent": "oklch(0.97 0.001 106.424)", + "sidebar-accent-foreground": "oklch(0.216 0.006 56.043)", + "sidebar-border": "oklch(0.923 0.003 48.717)", + "sidebar-ring": "oklch(0.709 0.01 56.259)", + }, + dark: { + background: "oklch(0.147 0.004 49.25)", + foreground: "oklch(0.985 0.001 106.423)", + card: "oklch(0.216 0.006 56.043)", + "card-foreground": "oklch(0.985 0.001 106.423)", + popover: "oklch(0.216 0.006 56.043)", + "popover-foreground": "oklch(0.985 0.001 106.423)", + primary: "oklch(0.923 0.003 48.717)", + "primary-foreground": "oklch(0.216 0.006 56.043)", + secondary: "oklch(0.268 0.007 34.298)", + "secondary-foreground": "oklch(0.985 0.001 106.423)", + muted: "oklch(0.268 0.007 34.298)", + "muted-foreground": "oklch(0.709 0.01 56.259)", + accent: "oklch(0.268 0.007 34.298)", + "accent-foreground": "oklch(0.985 0.001 106.423)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.553 0.013 58.071)", + "chart-1": "oklch(0.869 0.005 56.366)", + "chart-2": "oklch(0.553 0.013 58.071)", + "chart-3": "oklch(0.444 0.011 73.639)", + "chart-4": "oklch(0.374 0.01 67.558)", + "chart-5": "oklch(0.268 0.007 34.298)", + sidebar: "oklch(0.216 0.006 56.043)", + "sidebar-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-accent": "oklch(0.268 0.007 34.298)", + "sidebar-accent-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.553 0.013 58.071)", + }, + }, + }, + { + name: "zinc", + title: "锌灰", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.141 0.005 285.823)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.141 0.005 285.823)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.141 0.005 285.823)", + primary: "oklch(0.21 0.006 285.885)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + muted: "oklch(0.967 0.001 286.375)", + "muted-foreground": "oklch(0.552 0.016 285.938)", + accent: "oklch(0.967 0.001 286.375)", + "accent-foreground": "oklch(0.21 0.006 285.885)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.92 0.004 286.32)", + input: "oklch(0.92 0.004 286.32)", + ring: "oklch(0.705 0.015 286.067)", + "chart-1": "oklch(0.871 0.006 286.286)", + "chart-2": "oklch(0.552 0.016 285.938)", + "chart-3": "oklch(0.442 0.017 285.786)", + "chart-4": "oklch(0.37 0.013 285.805)", + "chart-5": "oklch(0.274 0.006 286.033)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.141 0.005 285.823)", + "sidebar-primary": "oklch(0.21 0.006 285.885)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.967 0.001 286.375)", + "sidebar-accent-foreground": "oklch(0.21 0.006 285.885)", + "sidebar-border": "oklch(0.92 0.004 286.32)", + "sidebar-ring": "oklch(0.705 0.015 286.067)", + }, + dark: { + background: "oklch(0.141 0.005 285.823)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.21 0.006 285.885)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.21 0.006 285.885)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.92 0.004 286.32)", + "primary-foreground": "oklch(0.21 0.006 285.885)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.274 0.006 286.033)", + "muted-foreground": "oklch(0.705 0.015 286.067)", + accent: "oklch(0.274 0.006 286.033)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.552 0.016 285.938)", + "chart-1": "oklch(0.871 0.006 286.286)", + "chart-2": "oklch(0.552 0.016 285.938)", + "chart-3": "oklch(0.442 0.017 285.786)", + "chart-4": "oklch(0.37 0.013 285.805)", + "chart-5": "oklch(0.274 0.006 286.033)", + sidebar: "oklch(0.21 0.006 285.885)", + "sidebar-foreground": "oklch(0.985 0 0)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.274 0.006 286.033)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.552 0.016 285.938)", + }, + }, + }, + { + name: "mauve", + title: "紫灰", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0.008 326)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.145 0.008 326)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.145 0.008 326)", + primary: "oklch(0.212 0.019 322.12)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.96 0.003 325.6)", + "secondary-foreground": "oklch(0.212 0.019 322.12)", + muted: "oklch(0.96 0.003 325.6)", + "muted-foreground": "oklch(0.542 0.034 322.5)", + accent: "oklch(0.96 0.003 325.6)", + "accent-foreground": "oklch(0.212 0.019 322.12)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0.005 325.62)", + input: "oklch(0.922 0.005 325.62)", + ring: "oklch(0.711 0.019 323.02)", + "chart-1": "oklch(0.865 0.012 325.68)", + "chart-2": "oklch(0.542 0.034 322.5)", + "chart-3": "oklch(0.435 0.029 321.78)", + "chart-4": "oklch(0.364 0.029 323.89)", + "chart-5": "oklch(0.263 0.024 320.12)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.145 0.008 326)", + "sidebar-primary": "oklch(0.212 0.019 322.12)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.96 0.003 325.6)", + "sidebar-accent-foreground": "oklch(0.212 0.019 322.12)", + "sidebar-border": "oklch(0.922 0.005 325.62)", + "sidebar-ring": "oklch(0.711 0.019 323.02)", + }, + dark: { + background: "oklch(0.145 0.008 326)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.212 0.019 322.12)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.212 0.019 322.12)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.922 0.005 325.62)", + "primary-foreground": "oklch(0.212 0.019 322.12)", + secondary: "oklch(0.263 0.024 320.12)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.263 0.024 320.12)", + "muted-foreground": "oklch(0.711 0.019 323.02)", + accent: "oklch(0.263 0.024 320.12)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.542 0.034 322.5)", + "chart-1": "oklch(0.865 0.012 325.68)", + "chart-2": "oklch(0.542 0.034 322.5)", + "chart-3": "oklch(0.435 0.029 321.78)", + "chart-4": "oklch(0.364 0.029 323.89)", + "chart-5": "oklch(0.263 0.024 320.12)", + sidebar: "oklch(0.212 0.019 322.12)", + "sidebar-foreground": "oklch(0.985 0 0)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.263 0.024 320.12)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.542 0.034 322.5)", + }, + }, + }, + { + name: "olive", + title: "橄榄灰", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.153 0.006 107.1)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.153 0.006 107.1)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.153 0.006 107.1)", + primary: "oklch(0.228 0.013 107.4)", + "primary-foreground": "oklch(0.988 0.003 106.5)", + secondary: "oklch(0.966 0.005 106.5)", + "secondary-foreground": "oklch(0.228 0.013 107.4)", + muted: "oklch(0.966 0.005 106.5)", + "muted-foreground": "oklch(0.58 0.031 107.3)", + accent: "oklch(0.966 0.005 106.5)", + "accent-foreground": "oklch(0.228 0.013 107.4)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.93 0.007 106.5)", + input: "oklch(0.93 0.007 106.5)", + ring: "oklch(0.737 0.021 106.9)", + "chart-1": "oklch(0.88 0.011 106.6)", + "chart-2": "oklch(0.58 0.031 107.3)", + "chart-3": "oklch(0.466 0.025 107.3)", + "chart-4": "oklch(0.394 0.023 107.4)", + "chart-5": "oklch(0.286 0.016 107.4)", + radius: "0.625rem", + sidebar: "oklch(0.988 0.003 106.5)", + "sidebar-foreground": "oklch(0.153 0.006 107.1)", + "sidebar-primary": "oklch(0.228 0.013 107.4)", + "sidebar-primary-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-accent": "oklch(0.966 0.005 106.5)", + "sidebar-accent-foreground": "oklch(0.228 0.013 107.4)", + "sidebar-border": "oklch(0.93 0.007 106.5)", + "sidebar-ring": "oklch(0.737 0.021 106.9)", + }, + dark: { + background: "oklch(0.153 0.006 107.1)", + foreground: "oklch(0.988 0.003 106.5)", + card: "oklch(0.228 0.013 107.4)", + "card-foreground": "oklch(0.988 0.003 106.5)", + popover: "oklch(0.228 0.013 107.4)", + "popover-foreground": "oklch(0.988 0.003 106.5)", + primary: "oklch(0.93 0.007 106.5)", + "primary-foreground": "oklch(0.228 0.013 107.4)", + secondary: "oklch(0.286 0.016 107.4)", + "secondary-foreground": "oklch(0.988 0.003 106.5)", + muted: "oklch(0.286 0.016 107.4)", + "muted-foreground": "oklch(0.737 0.021 106.9)", + accent: "oklch(0.286 0.016 107.4)", + "accent-foreground": "oklch(0.988 0.003 106.5)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.58 0.031 107.3)", + "chart-1": "oklch(0.88 0.011 106.6)", + "chart-2": "oklch(0.58 0.031 107.3)", + "chart-3": "oklch(0.466 0.025 107.3)", + "chart-4": "oklch(0.394 0.023 107.4)", + "chart-5": "oklch(0.286 0.016 107.4)", + sidebar: "oklch(0.228 0.013 107.4)", + "sidebar-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-accent": "oklch(0.286 0.016 107.4)", + "sidebar-accent-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.58 0.031 107.3)", + }, + }, + }, + { + name: "mist", + title: "雾灰", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.148 0.004 228.8)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.148 0.004 228.8)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.148 0.004 228.8)", + primary: "oklch(0.218 0.008 223.9)", + "primary-foreground": "oklch(0.987 0.002 197.1)", + secondary: "oklch(0.963 0.002 197.1)", + "secondary-foreground": "oklch(0.218 0.008 223.9)", + muted: "oklch(0.963 0.002 197.1)", + "muted-foreground": "oklch(0.56 0.021 213.5)", + accent: "oklch(0.963 0.002 197.1)", + "accent-foreground": "oklch(0.218 0.008 223.9)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.925 0.005 214.3)", + input: "oklch(0.925 0.005 214.3)", + ring: "oklch(0.723 0.014 214.4)", + "chart-1": "oklch(0.872 0.007 219.6)", + "chart-2": "oklch(0.56 0.021 213.5)", + "chart-3": "oklch(0.45 0.017 213.2)", + "chart-4": "oklch(0.378 0.015 216)", + "chart-5": "oklch(0.275 0.011 216.9)", + radius: "0.625rem", + sidebar: "oklch(0.987 0.002 197.1)", + "sidebar-foreground": "oklch(0.148 0.004 228.8)", + "sidebar-primary": "oklch(0.218 0.008 223.9)", + "sidebar-primary-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-accent": "oklch(0.963 0.002 197.1)", + "sidebar-accent-foreground": "oklch(0.218 0.008 223.9)", + "sidebar-border": "oklch(0.925 0.005 214.3)", + "sidebar-ring": "oklch(0.723 0.014 214.4)", + }, + dark: { + background: "oklch(0.148 0.004 228.8)", + foreground: "oklch(0.987 0.002 197.1)", + card: "oklch(0.218 0.008 223.9)", + "card-foreground": "oklch(0.987 0.002 197.1)", + popover: "oklch(0.218 0.008 223.9)", + "popover-foreground": "oklch(0.987 0.002 197.1)", + primary: "oklch(0.925 0.005 214.3)", + "primary-foreground": "oklch(0.218 0.008 223.9)", + secondary: "oklch(0.275 0.011 216.9)", + "secondary-foreground": "oklch(0.987 0.002 197.1)", + muted: "oklch(0.275 0.011 216.9)", + "muted-foreground": "oklch(0.723 0.014 214.4)", + accent: "oklch(0.275 0.011 216.9)", + "accent-foreground": "oklch(0.987 0.002 197.1)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.56 0.021 213.5)", + "chart-1": "oklch(0.872 0.007 219.6)", + "chart-2": "oklch(0.56 0.021 213.5)", + "chart-3": "oklch(0.45 0.017 213.2)", + "chart-4": "oklch(0.378 0.015 216)", + "chart-5": "oklch(0.275 0.011 216.9)", + sidebar: "oklch(0.218 0.008 223.9)", + "sidebar-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-accent": "oklch(0.275 0.011 216.9)", + "sidebar-accent-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.56 0.021 213.5)", + }, + }, + }, + { + name: "taupe", + title: "灰褐", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.147 0.004 49.3)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.147 0.004 49.3)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.147 0.004 49.3)", + primary: "oklch(0.214 0.009 43.1)", + "primary-foreground": "oklch(0.986 0.002 67.8)", + secondary: "oklch(0.96 0.002 17.2)", + "secondary-foreground": "oklch(0.214 0.009 43.1)", + muted: "oklch(0.96 0.002 17.2)", + "muted-foreground": "oklch(0.547 0.021 43.1)", + accent: "oklch(0.96 0.002 17.2)", + "accent-foreground": "oklch(0.214 0.009 43.1)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0.005 34.3)", + input: "oklch(0.922 0.005 34.3)", + ring: "oklch(0.714 0.014 41.2)", + "chart-1": "oklch(0.868 0.007 39.5)", + "chart-2": "oklch(0.547 0.021 43.1)", + "chart-3": "oklch(0.438 0.017 39.3)", + "chart-4": "oklch(0.367 0.016 35.7)", + "chart-5": "oklch(0.268 0.011 36.5)", + radius: "0.625rem", + sidebar: "oklch(0.986 0.002 67.8)", + "sidebar-foreground": "oklch(0.147 0.004 49.3)", + "sidebar-primary": "oklch(0.214 0.009 43.1)", + "sidebar-primary-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-accent": "oklch(0.96 0.002 17.2)", + "sidebar-accent-foreground": "oklch(0.214 0.009 43.1)", + "sidebar-border": "oklch(0.922 0.005 34.3)", + "sidebar-ring": "oklch(0.714 0.014 41.2)", + }, + dark: { + background: "oklch(0.147 0.004 49.3)", + foreground: "oklch(0.986 0.002 67.8)", + card: "oklch(0.214 0.009 43.1)", + "card-foreground": "oklch(0.986 0.002 67.8)", + popover: "oklch(0.214 0.009 43.1)", + "popover-foreground": "oklch(0.986 0.002 67.8)", + primary: "oklch(0.922 0.005 34.3)", + "primary-foreground": "oklch(0.214 0.009 43.1)", + secondary: "oklch(0.268 0.011 36.5)", + "secondary-foreground": "oklch(0.986 0.002 67.8)", + muted: "oklch(0.268 0.011 36.5)", + "muted-foreground": "oklch(0.714 0.014 41.2)", + accent: "oklch(0.268 0.011 36.5)", + "accent-foreground": "oklch(0.986 0.002 67.8)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.547 0.021 43.1)", + "chart-1": "oklch(0.868 0.007 39.5)", + "chart-2": "oklch(0.547 0.021 43.1)", + "chart-3": "oklch(0.438 0.017 39.3)", + "chart-4": "oklch(0.367 0.016 35.7)", + "chart-5": "oklch(0.268 0.011 36.5)", + sidebar: "oklch(0.214 0.009 43.1)", + "sidebar-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-accent": "oklch(0.268 0.011 36.5)", + "sidebar-accent-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.547 0.021 43.1)", + }, + }, + }, + { + name: "amber", + title: "琥珀色", + cssVars: { + light: { + primary: "oklch(0.555 0.163 48.998)", + "primary-foreground": "oklch(0.987 0.022 95.277)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.879 0.169 91.605)", + "chart-2": "oklch(0.769 0.188 70.08)", + "chart-3": "oklch(0.666 0.179 58.318)", + "chart-4": "oklch(0.555 0.163 48.998)", + "chart-5": "oklch(0.473 0.137 46.201)", + "sidebar-primary": "oklch(0.666 0.179 58.318)", + "sidebar-primary-foreground": "oklch(0.987 0.022 95.277)", + }, + dark: { + primary: "oklch(0.473 0.137 46.201)", + "primary-foreground": "oklch(0.987 0.022 95.277)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.879 0.169 91.605)", + "chart-2": "oklch(0.769 0.188 70.08)", + "chart-3": "oklch(0.666 0.179 58.318)", + "chart-4": "oklch(0.555 0.163 48.998)", + "chart-5": "oklch(0.473 0.137 46.201)", + "sidebar-primary": "oklch(0.769 0.188 70.08)", + "sidebar-primary-foreground": "oklch(0.279 0.077 45.635)", + }, + }, + }, + { + name: "blue", + title: "蓝色", + cssVars: { + light: { + primary: "oklch(0.488 0.243 264.376)", + "primary-foreground": "oklch(0.97 0.014 254.604)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.809 0.105 251.813)", + "chart-2": "oklch(0.623 0.214 259.815)", + "chart-3": "oklch(0.546 0.245 262.881)", + "chart-4": "oklch(0.488 0.243 264.376)", + "chart-5": "oklch(0.424 0.199 265.638)", + "sidebar-primary": "oklch(0.546 0.245 262.881)", + "sidebar-primary-foreground": "oklch(0.97 0.014 254.604)", + }, + dark: { + primary: "oklch(0.424 0.199 265.638)", + "primary-foreground": "oklch(0.97 0.014 254.604)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.809 0.105 251.813)", + "chart-2": "oklch(0.623 0.214 259.815)", + "chart-3": "oklch(0.546 0.245 262.881)", + "chart-4": "oklch(0.488 0.243 264.376)", + "chart-5": "oklch(0.424 0.199 265.638)", + "sidebar-primary": "oklch(0.623 0.214 259.815)", + "sidebar-primary-foreground": "oklch(0.97 0.014 254.604)", + }, + }, + }, + { + name: "cyan", + title: "青色", + cssVars: { + light: { + primary: "oklch(0.52 0.105 223.128)", + "primary-foreground": "oklch(0.984 0.019 200.873)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.865 0.127 207.078)", + "chart-2": "oklch(0.715 0.143 215.221)", + "chart-3": "oklch(0.609 0.126 221.723)", + "chart-4": "oklch(0.52 0.105 223.128)", + "chart-5": "oklch(0.45 0.085 224.283)", + "sidebar-primary": "oklch(0.609 0.126 221.723)", + "sidebar-primary-foreground": "oklch(0.984 0.019 200.873)", + }, + dark: { + primary: "oklch(0.45 0.085 224.283)", + "primary-foreground": "oklch(0.984 0.019 200.873)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.865 0.127 207.078)", + "chart-2": "oklch(0.715 0.143 215.221)", + "chart-3": "oklch(0.609 0.126 221.723)", + "chart-4": "oklch(0.52 0.105 223.128)", + "chart-5": "oklch(0.45 0.085 224.283)", + "sidebar-primary": "oklch(0.715 0.143 215.221)", + "sidebar-primary-foreground": "oklch(0.302 0.056 229.695)", + }, + }, + }, + { + name: "emerald", + title: "翠绿色", + cssVars: { + light: { + primary: "oklch(0.508 0.118 165.612)", + "primary-foreground": "oklch(0.979 0.021 166.113)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.845 0.143 164.978)", + "chart-2": "oklch(0.696 0.17 162.48)", + "chart-3": "oklch(0.596 0.145 163.225)", + "chart-4": "oklch(0.508 0.118 165.612)", + "chart-5": "oklch(0.432 0.095 166.913)", + "sidebar-primary": "oklch(0.596 0.145 163.225)", + "sidebar-primary-foreground": "oklch(0.979 0.021 166.113)", + }, + dark: { + primary: "oklch(0.432 0.095 166.913)", + "primary-foreground": "oklch(0.979 0.021 166.113)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.845 0.143 164.978)", + "chart-2": "oklch(0.696 0.17 162.48)", + "chart-3": "oklch(0.596 0.145 163.225)", + "chart-4": "oklch(0.508 0.118 165.612)", + "chart-5": "oklch(0.432 0.095 166.913)", + "sidebar-primary": "oklch(0.696 0.17 162.48)", + "sidebar-primary-foreground": "oklch(0.262 0.051 172.552)", + }, + }, + }, + { + name: "fuchsia", + title: "紫红色", + cssVars: { + light: { + primary: "oklch(0.518 0.253 323.949)", + "primary-foreground": "oklch(0.977 0.017 320.058)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.833 0.145 321.434)", + "chart-2": "oklch(0.667 0.295 322.15)", + "chart-3": "oklch(0.591 0.293 322.896)", + "chart-4": "oklch(0.518 0.253 323.949)", + "chart-5": "oklch(0.452 0.211 324.591)", + "sidebar-primary": "oklch(0.591 0.293 322.896)", + "sidebar-primary-foreground": "oklch(0.977 0.017 320.058)", + }, + dark: { + primary: "oklch(0.452 0.211 324.591)", + "primary-foreground": "oklch(0.977 0.017 320.058)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.833 0.145 321.434)", + "chart-2": "oklch(0.667 0.295 322.15)", + "chart-3": "oklch(0.591 0.293 322.896)", + "chart-4": "oklch(0.518 0.253 323.949)", + "chart-5": "oklch(0.452 0.211 324.591)", + "sidebar-primary": "oklch(0.667 0.295 322.15)", + "sidebar-primary-foreground": "oklch(0.977 0.017 320.058)", + }, + }, + }, + { + name: "green", + title: "绿色", + cssVars: { + light: { + primary: "oklch(0.527 0.154 150.069)", + "primary-foreground": "oklch(0.982 0.018 155.826)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.871 0.15 154.449)", + "chart-2": "oklch(0.723 0.219 149.579)", + "chart-3": "oklch(0.627 0.194 149.214)", + "chart-4": "oklch(0.527 0.154 150.069)", + "chart-5": "oklch(0.448 0.119 151.328)", + "sidebar-primary": "oklch(0.627 0.194 149.214)", + "sidebar-primary-foreground": "oklch(0.982 0.018 155.826)", + }, + dark: { + primary: "oklch(0.448 0.119 151.328)", + "primary-foreground": "oklch(0.982 0.018 155.826)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.871 0.15 154.449)", + "chart-2": "oklch(0.723 0.219 149.579)", + "chart-3": "oklch(0.627 0.194 149.214)", + "chart-4": "oklch(0.527 0.154 150.069)", + "chart-5": "oklch(0.448 0.119 151.328)", + "sidebar-primary": "oklch(0.723 0.219 149.579)", + "sidebar-primary-foreground": "oklch(0.982 0.018 155.826)", + }, + }, + }, + { + name: "indigo", + title: "靛蓝色", + cssVars: { + light: { + primary: "oklch(0.457 0.24 277.023)", + "primary-foreground": "oklch(0.962 0.018 272.314)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.785 0.115 274.713)", + "chart-2": "oklch(0.585 0.233 277.117)", + "chart-3": "oklch(0.511 0.262 276.966)", + "chart-4": "oklch(0.457 0.24 277.023)", + "chart-5": "oklch(0.398 0.195 277.366)", + "sidebar-primary": "oklch(0.511 0.262 276.966)", + "sidebar-primary-foreground": "oklch(0.962 0.018 272.314)", + }, + dark: { + primary: "oklch(0.398 0.195 277.366)", + "primary-foreground": "oklch(0.962 0.018 272.314)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.785 0.115 274.713)", + "chart-2": "oklch(0.585 0.233 277.117)", + "chart-3": "oklch(0.511 0.262 276.966)", + "chart-4": "oklch(0.457 0.24 277.023)", + "chart-5": "oklch(0.398 0.195 277.366)", + "sidebar-primary": "oklch(0.585 0.233 277.117)", + "sidebar-primary-foreground": "oklch(0.962 0.018 272.314)", + }, + }, + }, + { + name: "lime", + title: "青柠色", + cssVars: { + light: { + primary: "oklch(0.841 0.238 128.85)", + "primary-foreground": "oklch(0.405 0.101 131.063)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.897 0.196 126.665)", + "chart-2": "oklch(0.768 0.233 130.85)", + "chart-3": "oklch(0.648 0.2 131.684)", + "chart-4": "oklch(0.532 0.157 131.589)", + "chart-5": "oklch(0.453 0.124 130.933)", + "sidebar-primary": "oklch(0.648 0.2 131.684)", + "sidebar-primary-foreground": "oklch(0.986 0.031 120.757)", + }, + dark: { + primary: "oklch(0.768 0.233 130.85)", + "primary-foreground": "oklch(0.405 0.101 131.063)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.897 0.196 126.665)", + "chart-2": "oklch(0.768 0.233 130.85)", + "chart-3": "oklch(0.648 0.2 131.684)", + "chart-4": "oklch(0.532 0.157 131.589)", + "chart-5": "oklch(0.453 0.124 130.933)", + "sidebar-primary": "oklch(0.768 0.233 130.85)", + "sidebar-primary-foreground": "oklch(0.274 0.072 132.109)", + }, + }, + }, + { + name: "orange", + title: "橙色", + cssVars: { + light: { + primary: "oklch(0.553 0.195 38.402)", + "primary-foreground": "oklch(0.98 0.016 73.684)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.837 0.128 66.29)", + "chart-2": "oklch(0.705 0.213 47.604)", + "chart-3": "oklch(0.646 0.222 41.116)", + "chart-4": "oklch(0.553 0.195 38.402)", + "chart-5": "oklch(0.47 0.157 37.304)", + "sidebar-primary": "oklch(0.646 0.222 41.116)", + "sidebar-primary-foreground": "oklch(0.98 0.016 73.684)", + }, + dark: { + primary: "oklch(0.47 0.157 37.304)", + "primary-foreground": "oklch(0.98 0.016 73.684)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.837 0.128 66.29)", + "chart-2": "oklch(0.705 0.213 47.604)", + "chart-3": "oklch(0.646 0.222 41.116)", + "chart-4": "oklch(0.553 0.195 38.402)", + "chart-5": "oklch(0.47 0.157 37.304)", + "sidebar-primary": "oklch(0.705 0.213 47.604)", + "sidebar-primary-foreground": "oklch(0.98 0.016 73.684)", + }, + }, + }, + { + name: "pink", + title: "粉色", + cssVars: { + light: { + primary: "oklch(0.525 0.223 3.958)", + "primary-foreground": "oklch(0.971 0.014 343.198)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.823 0.12 346.018)", + "chart-2": "oklch(0.656 0.241 354.308)", + "chart-3": "oklch(0.592 0.249 0.584)", + "chart-4": "oklch(0.525 0.223 3.958)", + "chart-5": "oklch(0.459 0.187 3.815)", + "sidebar-primary": "oklch(0.592 0.249 0.584)", + "sidebar-primary-foreground": "oklch(0.971 0.014 343.198)", + }, + dark: { + primary: "oklch(0.459 0.187 3.815)", + "primary-foreground": "oklch(0.971 0.014 343.198)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.823 0.12 346.018)", + "chart-2": "oklch(0.656 0.241 354.308)", + "chart-3": "oklch(0.592 0.249 0.584)", + "chart-4": "oklch(0.525 0.223 3.958)", + "chart-5": "oklch(0.459 0.187 3.815)", + "sidebar-primary": "oklch(0.656 0.241 354.308)", + "sidebar-primary-foreground": "oklch(0.971 0.014 343.198)", + }, + }, + }, + { + name: "purple", + title: "紫色", + cssVars: { + light: { + primary: "oklch(0.496 0.265 301.924)", + "primary-foreground": "oklch(0.977 0.014 308.299)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.827 0.119 306.383)", + "chart-2": "oklch(0.627 0.265 303.9)", + "chart-3": "oklch(0.558 0.288 302.321)", + "chart-4": "oklch(0.496 0.265 301.924)", + "chart-5": "oklch(0.438 0.218 303.724)", + "sidebar-primary": "oklch(0.558 0.288 302.321)", + "sidebar-primary-foreground": "oklch(0.977 0.014 308.299)", + }, + dark: { + primary: "oklch(0.438 0.218 303.724)", + "primary-foreground": "oklch(0.977 0.014 308.299)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.827 0.119 306.383)", + "chart-2": "oklch(0.627 0.265 303.9)", + "chart-3": "oklch(0.558 0.288 302.321)", + "chart-4": "oklch(0.496 0.265 301.924)", + "chart-5": "oklch(0.438 0.218 303.724)", + "sidebar-primary": "oklch(0.627 0.265 303.9)", + "sidebar-primary-foreground": "oklch(0.977 0.014 308.299)", + }, + }, + }, + { + name: "red", + title: "红色", + cssVars: { + light: { + primary: "oklch(0.505 0.213 27.518)", + "primary-foreground": "oklch(0.971 0.013 17.38)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.808 0.114 19.571)", + "chart-2": "oklch(0.637 0.237 25.331)", + "chart-3": "oklch(0.577 0.245 27.325)", + "chart-4": "oklch(0.505 0.213 27.518)", + "chart-5": "oklch(0.444 0.177 26.899)", + "sidebar-primary": "oklch(0.577 0.245 27.325)", + "sidebar-primary-foreground": "oklch(0.971 0.013 17.38)", + }, + dark: { + primary: "oklch(0.444 0.177 26.899)", + "primary-foreground": "oklch(0.971 0.013 17.38)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.808 0.114 19.571)", + "chart-2": "oklch(0.637 0.237 25.331)", + "chart-3": "oklch(0.577 0.245 27.325)", + "chart-4": "oklch(0.505 0.213 27.518)", + "chart-5": "oklch(0.444 0.177 26.899)", + "sidebar-primary": "oklch(0.637 0.237 25.331)", + "sidebar-primary-foreground": "oklch(0.971 0.013 17.38)", + }, + }, + }, + { + name: "rose", + title: "玫红色", + cssVars: { + light: { + primary: "oklch(0.514 0.222 16.935)", + "primary-foreground": "oklch(0.969 0.015 12.422)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.81 0.117 11.638)", + "chart-2": "oklch(0.645 0.246 16.439)", + "chart-3": "oklch(0.586 0.253 17.585)", + "chart-4": "oklch(0.514 0.222 16.935)", + "chart-5": "oklch(0.455 0.188 13.697)", + "sidebar-primary": "oklch(0.586 0.253 17.585)", + "sidebar-primary-foreground": "oklch(0.969 0.015 12.422)", + }, + dark: { + primary: "oklch(0.455 0.188 13.697)", + "primary-foreground": "oklch(0.969 0.015 12.422)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.81 0.117 11.638)", + "chart-2": "oklch(0.645 0.246 16.439)", + "chart-3": "oklch(0.586 0.253 17.585)", + "chart-4": "oklch(0.514 0.222 16.935)", + "chart-5": "oklch(0.455 0.188 13.697)", + sidebar: "oklch(0.21 0.006 285.885)", + "sidebar-primary": "oklch(0.645 0.246 16.439)", + "sidebar-primary-foreground": "oklch(0.969 0.015 12.422)", + }, + }, + }, + { + name: "sky", + title: "天蓝色", + cssVars: { + light: { + primary: "oklch(0.5 0.134 242.749)", + "primary-foreground": "oklch(0.977 0.013 236.62)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.828 0.111 230.318)", + "chart-2": "oklch(0.685 0.169 237.323)", + "chart-3": "oklch(0.588 0.158 241.966)", + "chart-4": "oklch(0.5 0.134 242.749)", + "chart-5": "oklch(0.443 0.11 240.79)", + "sidebar-primary": "oklch(0.588 0.158 241.966)", + "sidebar-primary-foreground": "oklch(0.977 0.013 236.62)", + }, + dark: { + primary: "oklch(0.443 0.11 240.79)", + "primary-foreground": "oklch(0.977 0.013 236.62)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.828 0.111 230.318)", + "chart-2": "oklch(0.685 0.169 237.323)", + "chart-3": "oklch(0.588 0.158 241.966)", + "chart-4": "oklch(0.5 0.134 242.749)", + "chart-5": "oklch(0.443 0.11 240.79)", + "sidebar-primary": "oklch(0.685 0.169 237.323)", + "sidebar-primary-foreground": "oklch(0.293 0.066 243.157)", + }, + }, + }, + { + name: "teal", + title: "蓝绿色", + cssVars: { + light: { + primary: "oklch(0.511 0.096 186.391)", + "primary-foreground": "oklch(0.984 0.014 180.72)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.855 0.138 181.071)", + "chart-2": "oklch(0.704 0.14 182.503)", + "chart-3": "oklch(0.6 0.118 184.704)", + "chart-4": "oklch(0.511 0.096 186.391)", + "chart-5": "oklch(0.437 0.078 188.216)", + "sidebar-primary": "oklch(0.6 0.118 184.704)", + "sidebar-primary-foreground": "oklch(0.984 0.014 180.72)", + }, + dark: { + primary: "oklch(0.437 0.078 188.216)", + "primary-foreground": "oklch(0.984 0.014 180.72)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.855 0.138 181.071)", + "chart-2": "oklch(0.704 0.14 182.503)", + "chart-3": "oklch(0.6 0.118 184.704)", + "chart-4": "oklch(0.511 0.096 186.391)", + "chart-5": "oklch(0.437 0.078 188.216)", + "sidebar-primary": "oklch(0.704 0.14 182.503)", + "sidebar-primary-foreground": "oklch(0.277 0.046 192.524)", + }, + }, + }, + { + name: "violet", + title: "紫罗兰色", + cssVars: { + light: { + primary: "oklch(0.491 0.27 292.581)", + "primary-foreground": "oklch(0.969 0.016 293.756)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.811 0.111 293.571)", + "chart-2": "oklch(0.606 0.25 292.717)", + "chart-3": "oklch(0.541 0.281 293.009)", + "chart-4": "oklch(0.491 0.27 292.581)", + "chart-5": "oklch(0.432 0.232 292.759)", + "sidebar-primary": "oklch(0.541 0.281 293.009)", + "sidebar-primary-foreground": "oklch(0.969 0.016 293.756)", + }, + dark: { + primary: "oklch(0.432 0.232 292.759)", + "primary-foreground": "oklch(0.969 0.016 293.756)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.811 0.111 293.571)", + "chart-2": "oklch(0.606 0.25 292.717)", + "chart-3": "oklch(0.541 0.281 293.009)", + "chart-4": "oklch(0.491 0.27 292.581)", + "chart-5": "oklch(0.432 0.232 292.759)", + "sidebar-primary": "oklch(0.606 0.25 292.717)", + "sidebar-primary-foreground": "oklch(0.969 0.016 293.756)", + }, + }, + }, + { + name: "yellow", + title: "黄色", + cssVars: { + light: { + primary: "oklch(0.852 0.199 91.936)", + "primary-foreground": "oklch(0.421 0.095 57.708)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.905 0.182 98.111)", + "chart-2": "oklch(0.795 0.184 86.047)", + "chart-3": "oklch(0.681 0.162 75.834)", + "chart-4": "oklch(0.554 0.135 66.442)", + "chart-5": "oklch(0.476 0.114 61.907)", + "sidebar-primary": "oklch(0.681 0.162 75.834)", + "sidebar-primary-foreground": "oklch(0.987 0.026 102.212)", + }, + dark: { + primary: "oklch(0.795 0.184 86.047)", + "primary-foreground": "oklch(0.421 0.095 57.708)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.905 0.182 98.111)", + "chart-2": "oklch(0.795 0.184 86.047)", + "chart-3": "oklch(0.681 0.162 75.834)", + "chart-4": "oklch(0.554 0.135 66.442)", + "chart-5": "oklch(0.476 0.114 61.907)", + "sidebar-primary": "oklch(0.795 0.184 86.047)", + "sidebar-primary-foreground": "oklch(0.987 0.026 102.212)", + }, + }, + }, +] diff --git a/packages/blocks/src/blocks/appearance/ui-state.tsx b/packages/blocks/src/blocks/appearance/ui-state.tsx new file mode 100644 index 0000000..abef254 --- /dev/null +++ b/packages/blocks/src/blocks/appearance/ui-state.tsx @@ -0,0 +1,213 @@ +import React from "react" +import { + type UiState, + type UiStateKey, + type UiStateUpdate, + type UiStateValue, +} from "./state" +import { + buildThemeStyleSheet, + THEME_STYLE_ELEMENT_ID, + type ThemeScheme, +} from "./theme" + +const systemThemeQuery = "(prefers-color-scheme: dark)" +const themeStateKeys = new Set([ + "theme-mode", + "theme-light", + "theme-dark", +]) +let restoreThemeTransitionFrame: number | undefined + +function disableThemeTransitionsTemporarily() { + if (typeof document === "undefined") return + + const root = document.documentElement + root.classList.add("ui:theme-changing") + + if (restoreThemeTransitionFrame !== undefined) { + cancelAnimationFrame(restoreThemeTransitionFrame) + } + + restoreThemeTransitionFrame = requestAnimationFrame(() => { + restoreThemeTransitionFrame = requestAnimationFrame(() => { + root.classList.remove("ui:theme-changing") + restoreThemeTransitionFrame = undefined + }) + }) +} + +function applyUiState(state: UiState) { + if (typeof document === "undefined") return + + const root = document.documentElement + const resolvedTheme = + state["theme-mode"] === "system" ? getSystemTheme() : state["theme-mode"] + const themeStyle = document.getElementById(THEME_STYLE_ELEMENT_ID) + + if (themeStyle) { + themeStyle.textContent = buildThemeStyleSheet( + state["theme-light"], + state["theme-dark"] + ) + } + + root.classList.toggle("ui:compacted", state["main-compacted"]) + root.classList.toggle("ui:collapsed", state["sidebar-collapsed"]) + root.classList.toggle("ui:dark", resolvedTheme === "dark") + root.classList.toggle("ui:light", resolvedTheme === "light") + root.style.colorScheme = resolvedTheme +} + +function subscribeToSystemTheme(listener: VoidFunction) { + const mediaQuery = window.matchMedia(systemThemeQuery) + mediaQuery.addEventListener("change", listener) + return () => mediaQuery.removeEventListener("change", listener) +} + +function getSystemTheme(): ThemeScheme { + return window.matchMedia(systemThemeQuery).matches ? "dark" : "light" +} + +function getServerSystemTheme(): ThemeScheme { + return "light" +} + +type UiStateStore = { + getSnapshot: () => UiState + getServerSnapshot: () => UiState + setValue: (key: K, value: UiState[K]) => void + subscribe: (listener: VoidFunction) => VoidFunction +} + +function createUiStateStore(initialState: UiState): UiStateStore { + let snapshot = initialState + const listeners = new Set() + + return { + getSnapshot: () => snapshot, + getServerSnapshot: () => initialState, + setValue: (key, value) => { + if (snapshot[key] === value) return + snapshot = { ...snapshot, [key]: value } + listeners.forEach((listener) => listener()) + }, + subscribe: (listener) => { + listeners.add(listener) + return () => listeners.delete(listener) + }, + } +} + +export type UiStateChangeHandler = ( + update: UiStateUpdate +) => Promise | void + +interface UiStateContextValue { + onStateChange?: UiStateChangeHandler + store: UiStateStore +} + +const UiStateContext = React.createContext(null) + +export interface UiStateProviderProps { + children: React.ReactNode + initialState: UiState + onStateChange?: UiStateChangeHandler +} + +export function UiStateProvider({ + children, + initialState, + onStateChange, +}: UiStateProviderProps) { + const storeRef = React.useRef(null) + if (!storeRef.current) { + storeRef.current = createUiStateStore(initialState) + } + + const state = React.useSyncExternalStore( + storeRef.current.subscribe, + storeRef.current.getSnapshot, + storeRef.current.getServerSnapshot + ) + + React.useLayoutEffect(() => { + applyUiState(state) + }, [state]) + + React.useEffect(() => { + if (state["theme-mode"] !== "system") return + + const mediaQuery = window.matchMedia(systemThemeQuery) + const applySystemTheme = () => { + disableThemeTransitionsTemporarily() + applyUiState(storeRef.current!.getSnapshot()) + } + + mediaQuery.addEventListener("change", applySystemTheme) + return () => mediaQuery.removeEventListener("change", applySystemTheme) + }, [state["theme-mode"]]) + + const contextValue = React.useMemo( + () => ({ + onStateChange, + store: storeRef.current!, + }), + [onStateChange] + ) + + return ( + + {children} + + ) +} + +export function useUiState( + key: K +): [UiStateValue, React.Dispatch>>] { + const context = React.useContext(UiStateContext) + if (!context) { + throw new Error("useUiState must be used within UiStateProvider") + } + const { onStateChange, store } = context + + const state = React.useSyncExternalStore( + store.subscribe, + store.getSnapshot, + store.getServerSnapshot + ) + const value = state[key] as UiStateValue + + const setValue = React.useCallback< + React.Dispatch>> + >( + (action) => { + const currentValue = store.getSnapshot()[key] as UiStateValue + const nextValue = + typeof action === "function" ? action(currentValue) : action + + if (themeStateKeys.has(key)) { + disableThemeTransitionsTemporarily() + } + store.setValue(key, nextValue) + applyUiState(store.getSnapshot()) + void onStateChange?.({ key, value: nextValue } as UiStateUpdate) + }, + [key, onStateChange, store] + ) + + return [value, setValue] as const +} + +export function useResolvedTheme(): ThemeScheme { + const [themeMode] = useUiState("theme-mode") + const systemTheme = React.useSyncExternalStore( + subscribeToSystemTheme, + getSystemTheme, + getServerSystemTheme + ) + + return themeMode === "system" ? systemTheme : themeMode +} diff --git a/packages/blocks/src/blocks/chats/chat-popover-handle.ts b/packages/blocks/src/blocks/chats/chat-popover-handle.ts new file mode 100644 index 0000000..38b9183 --- /dev/null +++ b/packages/blocks/src/blocks/chats/chat-popover-handle.ts @@ -0,0 +1,3 @@ +import { createPopoverHandle } from "@workspace/ui/components/popover" + +export const chatPopoverHandle = createPopoverHandle() diff --git a/packages/blocks/src/blocks/chats/chat-popover-trigger.tsx b/packages/blocks/src/blocks/chats/chat-popover-trigger.tsx new file mode 100644 index 0000000..f15c5ec --- /dev/null +++ b/packages/blocks/src/blocks/chats/chat-popover-trigger.tsx @@ -0,0 +1,13 @@ +import * as React from "react" +import { PopoverTrigger } from "@workspace/ui/components/popover" + +import { chatPopoverHandle } from "./chat-popover-handle" + +type ChatPopoverTriggerProps = Omit< + React.ComponentProps, + "handle" +> + +export function ChatPopoverTrigger(props: ChatPopoverTriggerProps) { + return +} diff --git a/packages/blocks/src/blocks/chats/chat-popover.tsx b/packages/blocks/src/blocks/chats/chat-popover.tsx new file mode 100644 index 0000000..0908fd4 --- /dev/null +++ b/packages/blocks/src/blocks/chats/chat-popover.tsx @@ -0,0 +1,43 @@ +import { ItemGroup } from "@workspace/ui/components/item" +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, +} from "@workspace/ui/components/popover" + +import { useOnScroll } from "../../hooks/use-on-scroll" + +import { chatPopoverHandle } from "./chat-popover-handle" +import { ChatThreadItem } from "./chat-thread-item" +import type { ChatThread } from "./types" + +export interface ChatPopoverProps { + threads: readonly ChatThread[] + title?: string +} + +export function ChatPopover({ threads, title = "Chats" }: ChatPopoverProps) { + useOnScroll(() => { + chatPopoverHandle.close() + }) + + return ( + + + + + {title}({threads.length}) + + + + + {threads.map((thread) => ( + + ))} + + + + ) +} diff --git a/packages/blocks/src/blocks/chats/chat-thread-item.tsx b/packages/blocks/src/blocks/chats/chat-thread-item.tsx new file mode 100644 index 0000000..f46a071 --- /dev/null +++ b/packages/blocks/src/blocks/chats/chat-thread-item.tsx @@ -0,0 +1,45 @@ +import * as React from "react" +import { Avatar, AvatarImage } from "@workspace/ui/components/avatar" +import { + Item, + ItemContent, + ItemMedia, + ItemTitle, +} from "@workspace/ui/components/item" +import { useRippleRef } from "@workspace/ui/hooks/use-ripple" +import { cn } from "@workspace/ui/lib/utils" + +import type { ChatThread } from "./types" + +interface ChatThreadItemProps extends Omit< + React.ComponentProps, + "children" +> { + thread: ChatThread +} + +export function ChatThreadItem({ + thread, + ref, + className, + ...props +}: ChatThreadItemProps) { + const rippleRef = useRippleRef(ref) + + return ( + + + + + + + + {thread.name} + + + ) +} diff --git a/packages/blocks/src/blocks/chats/index.ts b/packages/blocks/src/blocks/chats/index.ts new file mode 100644 index 0000000..8f4a53a --- /dev/null +++ b/packages/blocks/src/blocks/chats/index.ts @@ -0,0 +1,4 @@ +export { ChatPopover } from "./chat-popover" +export type { ChatPopoverProps } from "./chat-popover" +export { ChatPopoverTrigger } from "./chat-popover-trigger" +export type { ChatThread } from "./types" diff --git a/packages/blocks/src/blocks/chats/types.ts b/packages/blocks/src/blocks/chats/types.ts new file mode 100644 index 0000000..b1d53c0 --- /dev/null +++ b/packages/blocks/src/blocks/chats/types.ts @@ -0,0 +1,5 @@ +export interface ChatThread { + avatarUrl: string + name: string + presence?: "offline" | "online" +} diff --git a/packages/blocks/src/blocks/layout/app-breadcrumb.tsx b/packages/blocks/src/blocks/layout/app-breadcrumb.tsx new file mode 100644 index 0000000..5fd2b3f --- /dev/null +++ b/packages/blocks/src/blocks/layout/app-breadcrumb.tsx @@ -0,0 +1,450 @@ +import { Link } from "@tanstack/react-router" +import * as React from "react" +import { + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@workspace/ui/components/breadcrumb" +import { Button } from "@workspace/ui/components/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@workspace/ui/components/dropdown-menu" + +import { type NavigationInfo, useNavigationStack } from "../navigation" +import { Icon } from "../../components/icon" + +const MAX_VISIBLE_ENTRIES = 3 + +type BreadcrumbEntry = NavigationInfo + +type BreadcrumbDisplayItem = + | { + entry: BreadcrumbEntry + kind: "entry" + } + | { + entries: readonly BreadcrumbEntry[] + kind: "menu" + } + +function getDisplayItems( + entries: readonly BreadcrumbEntry[], + requestedVisibleCount: number +): BreadcrumbDisplayItem[] { + if (entries.length === 0) { + return [] + } + + const visibleCount = Math.max( + 1, + Math.min(MAX_VISIBLE_ENTRIES, requestedVisibleCount, entries.length) + ) + + if (entries.length <= visibleCount) { + return entries.map((entry) => ({ entry, kind: "entry" })) + } + + if (visibleCount === 1) { + return [ + { entries: entries.slice(0, -1), kind: "menu" }, + { entry: entries.at(-1)!, kind: "entry" }, + ] + } + + const trailingEntries = entries.slice(-(visibleCount - 1)) + const hiddenEntries = entries.slice(1, -(visibleCount - 1)) + + return [ + { entry: entries[0], kind: "entry" }, + ...(hiddenEntries.length > 0 + ? [{ entries: hiddenEntries, kind: "menu" as const }] + : []), + ...trailingEntries.map((entry) => ({ + entry, + kind: "entry" as const, + })), + ] +} + +function useVisibleEntryCount(entries: readonly BreadcrumbEntry[]) { + const breadcrumbRef = React.useRef(null) + const listRef = React.useRef(null) + const measurementRef = React.useRef(null) + const entriesKey = entries.map((entry) => entry.id).join("\u0000") + const maximumVisibleCount = Math.min(MAX_VISIBLE_ENTRIES, entries.length) + const [measurement, setMeasurement] = React.useState(() => ({ + entriesKey, + visibleCount: maximumVisibleCount, + })) + + React.useLayoutEffect(() => { + if (maximumVisibleCount === 0) { + setMeasurement({ entriesKey, visibleCount: 0 }) + return + } + + const breadcrumb = breadcrumbRef.current + const list = listRef.current + const measurement = measurementRef.current + + if (!breadcrumb || !list || !measurement) { + return + } + + const updateVisibleCount = () => { + const entryWidths = Array.from( + measurement.querySelectorAll( + "[data-breadcrumb-measure-entry]" + ) + ).map((element) => element.getBoundingClientRect().width) + const menuWidth = + measurement + .querySelector("[data-breadcrumb-measure-menu]") + ?.getBoundingClientRect().width ?? 0 + const separatorWidth = + measurement + .querySelector("[data-breadcrumb-measure-separator]") + ?.getBoundingClientRect().width ?? 0 + const listStyle = window.getComputedStyle(list) + const gap = Number.parseFloat(listStyle.columnGap || listStyle.gap) || 0 + const availableWidth = breadcrumb.getBoundingClientRect().width + + let nextVisibleCount = 1 + + for ( + let candidateCount = maximumVisibleCount; + candidateCount >= 1; + candidateCount -= 1 + ) { + const candidateItems = getDisplayItems(entries, candidateCount) + const itemWidth = candidateItems.reduce((total, item) => { + if (item.kind === "menu") { + return total + menuWidth + } + + const entryIndex = entries.indexOf(item.entry) + + return total + (entryWidths[entryIndex] ?? 0) + }, 0) + const separatorCount = Math.max(0, candidateItems.length - 1) + const naturalWidth = + itemWidth + separatorWidth * separatorCount + gap * separatorCount * 2 + + if (naturalWidth <= availableWidth) { + nextVisibleCount = candidateCount + break + } + } + + setMeasurement((current) => { + if ( + current.entriesKey === entriesKey && + current.visibleCount === nextVisibleCount + ) { + return current + } + + return { entriesKey, visibleCount: nextVisibleCount } + }) + } + + updateVisibleCount() + + const observer = new ResizeObserver(updateVisibleCount) + observer.observe(breadcrumb) + observer.observe(measurement) + + return () => observer.disconnect() + }, [entries, entriesKey, maximumVisibleCount]) + + const visibleCount = + measurement.entriesKey === entriesKey + ? Math.min(measurement.visibleCount, maximumVisibleCount) + : maximumVisibleCount + + return { + breadcrumbRef, + listRef, + measurementRef, + visibleCount, + } +} + +function BreadcrumbEntryContent({ entry }: { entry: BreadcrumbEntry }) { + return ( + <> + {entry.icon && } + {entry.label} + + ) +} + +function BreadcrumbNavigationMenuItem({ + currentId, + entry, +}: { + currentId?: string + entry: BreadcrumbEntry +}) { + const isCurrent = entry.id === currentId + + if (entry.items?.length) { + return ( + + + + + + {entry.items.map((childEntry) => ( + + ))} + + + ) + } + + if (!entry.to) { + return ( + + + + ) + } + + return ( + + ) : ( + + ) + } + > + + + ) +} + +function BreadcrumbRouteLink({ + currentId, + entry, +}: { + currentId?: string + entry: BreadcrumbEntry +}) { + if (entry.items?.length) { + return ( + + + + + + + {entry.items.map((childEntry) => ( + + ))} + + + + ) + } + + if (!entry.to) { + return ( + + + + ) + } + + const link = + entry.to === "/" ? ( + + ) : ( + + ) + + return ( + + + + ) +} + +function BreadcrumbMenu({ entries }: { entries: readonly BreadcrumbEntry[] }) { + return ( + + + } + > + + + + + {entries.map((entry) => { + const content = ( + <> + {entry.icon && ( + + )} + {entry.label} + + ) + + if (!entry.to) { + return ( + + {content} + + ) + } + + return ( + + ) : ( + + ) + } + > + {content} + + ) + })} + + + + ) +} + +export function AppBreadcrumb() { + const navigationStack = useNavigationStack() + const currentId = navigationStack.at(-1)?.id + const entries = React.useMemo( + () => navigationStack.filter((entry) => entry.to !== "/"), + [navigationStack] + ) + const { breadcrumbRef, listRef, measurementRef, visibleCount } = + useVisibleEntryCount(entries) + const displayItems = getDisplayItems(entries, visibleCount) + + if (entries.length === 0) { + return null + } + + return ( + + + {displayItems.map((item, index) => { + const isCurrent = index === displayItems.length - 1 + const key = + item.kind === "entry" + ? item.entry.id + : `menu-${item.entries.map((entry) => entry.id).join("-")}` + + return ( + + {index > 0 && } + + {item.kind === "menu" ? ( + + ) : isCurrent ? ( + + {item.entry.icon && ( + + )} + {item.entry.label} + + ) : ( + + )} + + + ) + })} + + + + ) +} diff --git a/packages/blocks/src/blocks/layout/app-layout.tsx b/packages/blocks/src/blocks/layout/app-layout.tsx new file mode 100644 index 0000000..52d4efc --- /dev/null +++ b/packages/blocks/src/blocks/layout/app-layout.tsx @@ -0,0 +1,246 @@ +import * as React from "react" +import { Button } from "@workspace/ui/components/button" +import { Kbd } from "@workspace/ui/components/kbd" +import { useIsTablet } from "@workspace/ui/hooks/use-breakpoint" +import { cn } from "@workspace/ui/lib/utils" +import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react" + +import { ChatPopover, ChatPopoverTrigger, type ChatThread } from "../chats" +import { Icon } from "../../components/icon" +import { + MobileNavigationSheet, + MobileNavigationSheetTrigger, + NavigationProvider, + type NavigationGroup, +} from "../navigation" +import { + NotificationCenterSheet, + NotificationCenterTrigger, + type UseNotificationsOptions, +} from "../notifications" +import { + BellIcon, + MessageMultiple01Icon, + Search01Icon, +} from "@hugeicons/core-free-icons" + +import { AppSidebar } from "./app-sidebar" +import { + headerActionIconClassName, + HeaderActionButton, +} from "./header-action-button" +import { useSidebarState } from "./sidebar-state" +import { UserMenu, UserMenuTrigger } from "./user-menu" +import { AppBreadcrumb } from "./app-breadcrumb" +import { Separator } from "@workspace/ui/components/separator" +import { AppQueryIndicator } from "./app-query-indicator" + +export interface AppLayoutProps { + chatThreads: readonly ChatThread[] + children: React.ReactNode + navigationGroups: readonly NavigationGroup[] + notifications: UseNotificationsOptions +} + +export function AppLayout({ + chatThreads, + children, + navigationGroups, + notifications, +}: AppLayoutProps) { + return ( + + + + + +
+ +
+ + + {children} +
+
+
+ ) +} + +function AppHeader() { + return ( +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+ + + +
+
+ + } + /> + + } + /> + +
+
+
+
+ ) +} + +function AppHeaderBackground() { + return ( +