From 0c80019179f3805bca4397c9439713e3642a5f1d Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Wed, 2 Sep 2026 13:17:49 +0800 Subject: [PATCH 1/3] feat(theme): unlock light/dark with animated theme toggler (#139) Unlock the next-themes provider (light+dark, default dark, persisted), port magicui's AnimatedThemeToggler into @workspace/ui as a controlled component (View Transitions circular reveal + motion icon crossfade, graceful fallback without VT), and mount it in the app sidebar footer above the account row. Shell color literals are paired so dark resolves pixel-identical: neutral-50/400 become the semantic foreground/muted-foreground (exact dark matches), blues get light values with dark: originals. The project-chrome-surface base moved into .dark with a light value in :root. Canvas surfaces and registry previews keep their intentional dark appearance. Refs: labring/sealos-private#138 #139 --- .../features/shell/app-sidebar-account.tsx | 16 +- .../shell/app-sidebar-notifications.tsx | 22 +- apps/ui/src/features/shell/app-sidebar.tsx | 85 +++- bun.lock | 9 + packages/ui/package.json | 1 + .../animated-theme-toggler.test.tsx | 266 ++++++++++ .../src/components/animated-theme-toggler.tsx | 460 ++++++++++++++++++ packages/ui/src/components/theme-provider.tsx | 3 +- packages/ui/src/components/theme-toggle.tsx | 29 ++ packages/ui/src/styles/globals.css | 29 +- 10 files changed, 888 insertions(+), 32 deletions(-) create mode 100644 packages/ui/src/components/animated-theme-toggler.test.tsx create mode 100644 packages/ui/src/components/animated-theme-toggler.tsx create mode 100644 packages/ui/src/components/theme-toggle.tsx diff --git a/apps/ui/src/features/shell/app-sidebar-account.tsx b/apps/ui/src/features/shell/app-sidebar-account.tsx index 8d745a72..6426351f 100644 --- a/apps/ui/src/features/shell/app-sidebar-account.tsx +++ b/apps/ui/src/features/shell/app-sidebar-account.tsx @@ -141,7 +141,7 @@ function AppSidebarQuotaBar({ percent }: { percent: number | null }) { @@ -469,7 +469,7 @@ function AppSidebarAccountMenuRow({ rel={rel} target={target} > - + {icon} {label} @@ -554,8 +554,8 @@ function AppSidebarUsageAccordion({ > @@ -694,7 +694,7 @@ function AppSidebarAccountMenuView({ {userId === "" ? null : ( + ); +}; diff --git a/packages/ui/src/components/theme-provider.tsx b/packages/ui/src/components/theme-provider.tsx index 16d2eb3d..7c353e29 100644 --- a/packages/ui/src/components/theme-provider.tsx +++ b/packages/ui/src/components/theme-provider.tsx @@ -13,8 +13,7 @@ function ThemeProvider({ defaultTheme="dark" disableTransitionOnChange enableSystem={false} - forcedTheme="dark" - themes={["dark"]} + themes={["light", "dark"]} {...props} > {children} diff --git a/packages/ui/src/components/theme-toggle.tsx b/packages/ui/src/components/theme-toggle.tsx new file mode 100644 index 00000000..ea0ab337 --- /dev/null +++ b/packages/ui/src/components/theme-toggle.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { + AnimatedThemeToggler, + type AnimatedThemeTogglerProps, +} from "@workspace/ui/components/animated-theme-toggler"; +import { useTheme } from "next-themes"; + +/** + * next-themes-backed {@link AnimatedThemeToggler}. The provider owns + * persistence; the toggler only reports the requested theme and paints the + * View Transition reveal. Kept in @workspace/ui because app packages do not + * resolve `next-themes` directly (same boundary as sonner.tsx). + */ +function ThemeToggle( + props: Omit +) { + const { resolvedTheme, setTheme } = useTheme(); + + return ( + + ); +} + +export { ThemeToggle }; diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 211ab9a6..44e95cdb 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -163,8 +163,9 @@ /** Shared glass-sheet blur radius; mirrors CANVAS_GLASS_BLUR_RADIUS in JS. */ --canvas-glass-blur-radius: 40px; - /** Project chrome surface tokens. */ - --project-chrome-surface-base: #080a11; + /** Project chrome surface tokens. The dark base (#080a11) moved into + `.dark` so light mode follows the theme; dark resolves identically. */ + --project-chrome-surface-base: oklch(0.985 0 0); --project-chrome-surface: color-mix( in oklab, var(--input) 30%, @@ -255,6 +256,10 @@ ); --sealai-brand-primary-hover: var(--color-blue-500); --sealai-brand-primary-foreground: oklch(0.922 0 0); + + /** Project chrome surface base (was :root-only dark; paired here so dark + resolves exactly as before while light follows the theme). */ + --project-chrome-surface-base: #080a11; } @layer base { @@ -746,6 +751,26 @@ } } +/* AnimatedThemeToggler View Transitions (ported from magicui): disable the + browser's default crossfade so the JS clip-path animation owns the reveal. + Scoped via the data-attribute the toggler sets only while a toggle is in + flight, so other view transitions in the app (e.g. Next.js navigation) + keep default behavior. The clip-from pin keeps Firefox from painting the + new theme unclipped between snapshot and the JS animation. */ +::view-transition-old(root), +::view-transition-new(root) { + mix-blend-mode: normal; + animation: none; +} + +html[data-sealai-theme-vt="active"]::view-transition-group(root) { + animation-duration: var(--sealai-theme-toggle-vt-duration); +} + +html[data-sealai-theme-vt="active"]::view-transition-new(root) { + clip-path: var(--sealai-theme-vt-clip-from); +} + /* Registered via @utility (not a plain class in @layer utilities) so it can compose with variants; plain layered classes silently produce no CSS when used behind a variant prefix. */ From f163515d4efb7783a44135996943616aa88a6a4b Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Wed, 2 Sep 2026 13:39:22 +0800 Subject: [PATCH 2/3] feat(theme): light-mode adaptation for DB Access / DataBrowser (#140) Drop the hard-coded `dark` wrappers from DataBrowserPane and the main action surface frame so the DB Access experience follows the theme. Pair the dark-only component tokens so dark resolves pixel-identical while light gets readable values: - --db-access-sticky-header-surface / new --db-access-row-selector- surface: navy originals moved to .dark, light uses --muted. - --main-action-surface-bg / --main-action-surface-glow-color: the overlay paints the canvas navy + blue glow in dark, a plain sheet with no glow in light. - White-overlay chips and zinc text in export/backup dialogs get light semantic bases with dark: originals. Test contract updated: the surface no longer carries a literal bg-neutral-950 class (background is token-driven now). Refs: labring/sealos-private#138 #140 --- .../features/db-browser/DataBrowserPane.tsx | 2 +- .../backups/BackupServiceSurface.tsx | 8 ++++--- .../shared/SingleObjectExportModal.tsx | 4 ++-- .../sql/TableView/TableView.DataGrid.tsx | 2 +- .../actions/canvas-action-surface.test.tsx | 5 ++--- .../actions/canvas-action-surface.tsx | 4 ++-- packages/ui/src/styles/globals.css | 22 ++++++++++++++++--- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/apps/ui/src/features/db-browser/DataBrowserPane.tsx b/apps/ui/src/features/db-browser/DataBrowserPane.tsx index bf440a33..d8176df4 100644 --- a/apps/ui/src/features/db-browser/DataBrowserPane.tsx +++ b/apps/ui/src/features/db-browser/DataBrowserPane.tsx @@ -42,7 +42,7 @@ export function DataBrowserPane({ refreshProjectCanvas={refreshProjectCanvas} selectedDatabaseData={selectedDatabaseData} > -
+
diff --git a/apps/ui/src/features/db-browser/backups/BackupServiceSurface.tsx b/apps/ui/src/features/db-browser/backups/BackupServiceSurface.tsx index cc5bb6c0..c69ad12f 100644 --- a/apps/ui/src/features/db-browser/backups/BackupServiceSurface.tsx +++ b/apps/ui/src/features/db-browser/backups/BackupServiceSurface.tsx @@ -295,7 +295,7 @@ function BackupRowsList({ const canDelete = backup.deletable && !isDeleting; return (
-

+

{"Type "} - {backup.name} + + {backup.name} + {" to confirm."}

orders-db<\/p>/; const DB_ACCESS_TITLE_RE = /

DB Access<\/h2>/; -const ACTION_SURFACE_BACKGROUND_RE = /bg-neutral-950/; +// The surface background is theme-paired (light sheet / dark canvas navy) +// via --main-action-surface-bg, so no literal bg-neutral-950 remains. const MAIN_ACTION_SURFACE_BACKGROUND_RE = /main-action-surface-background/; const MAIN_ACTION_BODY_BACKGROUND_RE = /main-action-surface-body-background/; const DB_ACCESS_RE = /text-foreground/; @@ -76,7 +77,6 @@ test("main action surface renders shared chrome and empty body slot", () => { assert.match(html, DB_ACCESS_TITLE_RE); assert.doesNotMatch(html, ENGINE_SUBTITLE_RE); assert.match(html, CLOSE_LABEL_RE); - assert.match(html, ACTION_SURFACE_BACKGROUND_RE); assert.match(html, MAIN_ACTION_SURFACE_BACKGROUND_RE); assert.match(html, MAIN_ACTION_BODY_BACKGROUND_RE); assert.match(html, DB_ACCESS_RE); @@ -101,7 +101,6 @@ test("main action surface frame renders custom surface content", () => { assert.match(html, CUSTOM_TITLE_RE); assert.match(html, CUSTOM_SUBTITLE_RE); assert.match(html, CUSTOM_BODY_RE); - assert.match(html, ACTION_SURFACE_BACKGROUND_RE); assert.match(html, MAIN_ACTION_SURFACE_BACKGROUND_RE); assert.match(html, MAIN_ACTION_BODY_BACKGROUND_RE); }); diff --git a/apps/ui/src/features/project-canvas/actions/canvas-action-surface.tsx b/apps/ui/src/features/project-canvas/actions/canvas-action-surface.tsx index 83067760..2c78ae4b 100644 --- a/apps/ui/src/features/project-canvas/actions/canvas-action-surface.tsx +++ b/apps/ui/src/features/project-canvas/actions/canvas-action-surface.tsx @@ -62,7 +62,7 @@ export function MainActionSurfaceFrame({ return (
{icon == null ? null : ( - + {icon} )} diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 44e95cdb..aef6dde6 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -80,6 +80,7 @@ --color-db-access-sticky-header-surface: var( --db-access-sticky-header-surface ); + --color-db-access-row-selector-surface: var(--db-access-row-selector-surface); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); @@ -153,7 +154,14 @@ /** Canvas component tokens. */ --color-canvas-surface: var(--color-neutral-950); --canvas-minimap-node-radius: 0.625rem; - --db-access-sticky-header-surface: #161e33; + /* DB Access sticky surfaces: light values here; the dark navy originals + moved to `.dark` (dark resolves pixel-identical). */ + --db-access-sticky-header-surface: var(--muted); + --db-access-row-selector-surface: var(--muted); + /* Main action surface (DB Access / logs overlay): light is a plain chrome + sheet; `.dark` swaps in the canvas navy base + blue glow. */ + --main-action-surface-bg: var(--background); + --main-action-surface-glow-color: transparent; --color-canvas-glow: var(--color-blue-700); --color-canvas-dot: color-mix( in oklab, @@ -260,6 +268,14 @@ /** Project chrome surface base (was :root-only dark; paired here so dark resolves exactly as before while light follows the theme). */ --project-chrome-surface-base: #080a11; + + /** DB Access sticky surfaces (dark navy originals, now paired). */ + --db-access-sticky-header-surface: #161e33; + --db-access-row-selector-surface: #0c1120; + + /** Main action surface: dark keeps the canvas navy base + blue glow. */ + --main-action-surface-bg: var(--color-canvas-surface); + --main-action-surface-glow-color: var(--color-canvas-glow); } @layer base { @@ -798,7 +814,7 @@ html[data-sealai-theme-vt="active"]::view-transition-new(root) { .main-action-surface-background { overflow: hidden; - background-color: var(--color-canvas-surface); + background-color: var(--main-action-surface-bg); isolation: isolate; } @@ -813,7 +829,7 @@ html[data-sealai-theme-vt="active"]::view-transition-new(root) { content: ""; background: radial-gradient( ellipse closest-side at center, - var(--color-canvas-glow) 0%, + var(--main-action-surface-glow-color) 0%, transparent 100% ); mix-blend-mode: screen; From f69011c5e7d9737106e2c71e01804e47fbefdf25 Mon Sep 17 00:00:00 2001 From: zjy365 <3161362058@qq.com> Date: Wed, 2 Sep 2026 13:51:23 +0800 Subject: [PATCH 3/3] feat(theme): light-mode adaptation for Billing (#141) The Billing tab shell / cost drawer / plan-change dialog use the canvas-glow material; it now shares the theme-paired --main-action-surface-bg / --main-action-surface-glow-color tokens (light sheet, no glow; dark keeps the canvas navy + blue glow pixel-identical). - Drop the forced `dark` class from the checkout prorated-charge tooltip (it followed the page theme in dark anyway). - Pair white-overlay skeletons/rows and blue-400 accents across billing surfaces (light: bg-muted / blue-600; dark: originals). - Cost-chart legend dots fill with the paired surface token so the ring reads correctly in both themes. Refs: labring/sealos-private#138 #141 --- .../features/billing/billing-app-cost-drawer.tsx | 2 +- .../src/features/billing/billing-cost-charts.tsx | 6 +++--- .../ui/src/features/billing/billing-cost-tree.tsx | 2 +- .../features/billing/billing-costs-surface.tsx | 2 +- .../billing/billing-plan-checkout-dialog.tsx | 15 ++++++--------- .../src/features/billing/billing-plan-surface.tsx | 4 ++-- apps/ui/src/features/billing/billing-pricing.tsx | 4 ++-- .../ui/src/features/billing/billing-tab-shell.tsx | 4 ++-- apps/ui/src/features/billing/billing-usage.tsx | 4 ++-- packages/ui/src/styles/globals.css | 9 +++++---- 10 files changed, 25 insertions(+), 27 deletions(-) diff --git a/apps/ui/src/features/billing/billing-app-cost-drawer.tsx b/apps/ui/src/features/billing/billing-app-cost-drawer.tsx index 9c7ce93c..fe949ef0 100644 --- a/apps/ui/src/features/billing/billing-app-cost-drawer.tsx +++ b/apps/ui/src/features/billing/billing-app-cost-drawer.tsx @@ -349,7 +349,7 @@ export function BillingAppCostDrawer({ key={`day-${tableRow.day}`} > diff --git a/apps/ui/src/features/billing/billing-cost-charts.tsx b/apps/ui/src/features/billing/billing-cost-charts.tsx index 89ca3586..96472dcb 100644 --- a/apps/ui/src/features/billing/billing-cost-charts.tsx +++ b/apps/ui/src/features/billing/billing-cost-charts.tsx @@ -178,7 +178,7 @@ function LineLegendContent({ ; + return ; } if (error != null) { return ( @@ -349,7 +349,7 @@ export function BillingCostCharts({ {daily.series.map((series) => ( {formatBillingAmount(cost, currency)} diff --git a/apps/ui/src/features/billing/billing-costs-surface.tsx b/apps/ui/src/features/billing/billing-costs-surface.tsx index 2cf87a4c..56e4b28b 100644 --- a/apps/ui/src/features/billing/billing-costs-surface.tsx +++ b/apps/ui/src/features/billing/billing-costs-surface.tsx @@ -624,7 +624,7 @@ export function BillingCostsSurface({ >
{bannerTitle}: - + {isLoading ? "…" : formatBillingAmount(bannerCostMicroUnits, currency)} diff --git a/apps/ui/src/features/billing/billing-plan-checkout-dialog.tsx b/apps/ui/src/features/billing/billing-plan-checkout-dialog.tsx index e5a8e8fc..41965dd2 100644 --- a/apps/ui/src/features/billing/billing-plan-checkout-dialog.tsx +++ b/apps/ui/src/features/billing/billing-plan-checkout-dialog.tsx @@ -246,7 +246,7 @@ function paymentStatusCopy(status: PaymentWaitStatus): PaymentStatusCopy { icon: ( ), @@ -411,7 +411,7 @@ function DowngradeStage({ return ( <> - + Downgrade to {plan.name} @@ -454,7 +454,7 @@ function DowngradeStage({

You are now in the process of changing your subscription, and the change will{" "} - + {"take effect on the following month's subscription date."}

@@ -569,7 +569,7 @@ function PromotionCodeField({ if (!open) { return ( setOpen(true)} variant="link" @@ -782,10 +782,7 @@ function QuoteStage({ strokeWidth={1.75} /> - + Prorated charge for the remainder of the current billing cycle. @@ -814,7 +811,7 @@ function QuoteStage({ )} {card != null && onManageCard != null ? ( diff --git a/apps/ui/src/features/billing/billing-plan-surface.tsx b/apps/ui/src/features/billing/billing-plan-surface.tsx index a46adaf9..b60bbb96 100644 --- a/apps/ui/src/features/billing/billing-plan-surface.tsx +++ b/apps/ui/src/features/billing/billing-plan-surface.tsx @@ -841,7 +841,7 @@ export function BillingBalanceValue({

{giftMicroUnits > 0 ? ( Gift {formatBillingAmount(giftMicroUnits, currency)} @@ -1221,7 +1221,7 @@ export function BillingPlanSurface({
diff --git a/apps/ui/src/features/billing/billing-pricing.tsx b/apps/ui/src/features/billing/billing-pricing.tsx index ac243eec..7bd847df 100644 --- a/apps/ui/src/features/billing/billing-pricing.tsx +++ b/apps/ui/src/features/billing/billing-pricing.tsx @@ -228,7 +228,7 @@ export function BillingPriceTable({ ) : ( sections.map((section) => ( - + {section.title} @@ -439,7 +439,7 @@ function CalculatorRow({ function CalculatorSectionBar({ title }: { title: string }) { return ( -

+

{title}

); diff --git a/apps/ui/src/features/billing/billing-tab-shell.tsx b/apps/ui/src/features/billing/billing-tab-shell.tsx index caee7277..41ae8a4c 100644 --- a/apps/ui/src/features/billing/billing-tab-shell.tsx +++ b/apps/ui/src/features/billing/billing-tab-shell.tsx @@ -83,7 +83,7 @@ export function BillingNavigationFrame({

@@ -108,7 +108,7 @@ export function BillingNavigationFrame({ className={cn( "flex h-9 shrink-0 items-center gap-2 whitespace-nowrap rounded-md p-2 font-medium text-primary text-sm leading-none transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70", active - ? "bg-input text-foreground [&_svg]:text-blue-400" + ? "bg-input text-foreground [&_svg]:text-blue-600 dark:[&_svg]:text-blue-400" : "hover:bg-input/30 hover:text-foreground" )} href={tab.href} diff --git a/apps/ui/src/features/billing/billing-usage.tsx b/apps/ui/src/features/billing/billing-usage.tsx index 8c3d9904..f256249d 100644 --- a/apps/ui/src/features/billing/billing-usage.tsx +++ b/apps/ui/src/features/billing/billing-usage.tsx @@ -67,7 +67,7 @@ function UsageProgress({ value }: { value: number }) { aria-valuemax={100} aria-valuemin={0} aria-valuenow={value} - className="h-1 w-full max-w-40 overflow-hidden rounded-full bg-white/8" + className="h-1 w-full max-w-40 overflow-hidden rounded-full bg-muted dark:bg-white/8" role="progressbar" >
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index aef6dde6..073d9761 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -842,11 +842,12 @@ html[data-sealai-theme-vt="active"]::view-transition-new(root) { } /* Canvas Glow material (see CONTEXT.md): same canvas base + glow as - .main-action-surface-background, but the glow renders above the surface's - content: content layers must stay below z-20. */ + .main-action-surface-background, but the glow renders above the + surface's content: content layers must stay below z-20. Shares the + theme-paired surface/glow tokens (light sheet, dark canvas navy). */ .canvas-glow-overlay { overflow: hidden; - background-color: var(--color-canvas-surface); + background-color: var(--main-action-surface-bg); isolation: isolate; } @@ -861,7 +862,7 @@ html[data-sealai-theme-vt="active"]::view-transition-new(root) { content: ""; background: radial-gradient( ellipse closest-side at center, - var(--color-canvas-glow) 0%, + var(--main-action-surface-glow-color) 0%, transparent 100% ); mix-blend-mode: screen;