From c8e3cffc4b62e94b7f7e57ef1cea902b5329229e Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 23:04:36 +0700 Subject: [PATCH] fix(site): restore the UI showcase interactions --- README.md | 17 +- index.html | 1 - package.json | 3 +- rsbuild.config.ts | 4 + scripts/verify-production-policy.mjs | 76 ++ src/App.tsx | 20 +- src/components/ComponentsDemo.tsx | 49 +- src/components/Preview.tsx | 22 +- src/components/content/Search.tsx | 412 ++++++---- .../layout/Header/MarketingHeader.tsx | 2 + .../Header/components/MobileSidebar.tsx | 6 +- .../layout/Navigation/CompactNavigation.tsx | 11 +- .../navbar-showcase/BasicSections.tsx | 2 +- src/components/theming/EffectsSection.tsx | 14 +- src/components/theming/GlassSection.tsx | 83 +- src/components/theming/ThemeCSSModal.tsx | 29 +- src/components/theming/ThemeComposer.tsx | 223 +++++ src/components/theming/ThemeEditor.tsx | 82 +- src/components/theming/ThemeList.tsx | 2 +- src/index.css | 22 + src/lib/glassFormulas.ts | 147 +--- src/lib/glassTokens.ts | 166 ++-- src/lib/themeComposer.ts | 223 +++++ src/lib/themeEditorPersistence.ts | 12 + src/lib/themeGenerator.ts | 11 +- src/pages/Theming.tsx | 220 ++++- src/pages/docs/Index.tsx | 2 +- src/utils/theme/colorConversion.ts | 2 +- src/utils/theme/colorSelection.ts | 54 +- src/utils/theme/contrastCalculation.ts | 43 +- src/utils/validateThemeGeneration.ts | 4 +- tests/ps-qa/checks/navigation.ron | 96 ++- tests/ps-qa/checks/theming.ron | 768 +++++++++++++++++- 33 files changed, 2268 insertions(+), 560 deletions(-) create mode 100644 scripts/verify-production-policy.mjs create mode 100644 src/components/theming/ThemeComposer.tsx create mode 100644 src/lib/themeComposer.ts diff --git a/README.md b/README.md index a33d679..482b773 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,31 @@ The kitchen sink demo and documentation for our UI components. ## Native QA -Build the site, then run every declared outcome with the font-enabled chuzz host +Build an uncompressed QA bundle, then run every declared outcome with the font-enabled chuzz host and ps-qa 0.7.2 or newer: ```sh -bun run build +bun run build:apps ps-qa --app tests/ps-qa/ps-qa.ron qa-hosted \ --host ../chuzz/target/release/chuzz-headless --page dist \ --checks tests/ps-qa/checks ``` -The UI 3.2 registry build passes all 600 native checks across 13 groups, +The UI 3.2.2 registry build passes all 669 native checks across 13 groups, including Calendar selection, month navigation, and keyboard and pointer-driven -Slider and Color Picker outcomes. CI includes every group. +Slider and Color Picker outcomes. The release gate includes every group. The Layouts route uses its own document marker, and decorative cards are articles rather than controls that promise an action. +The production response policy is a separate release check because chuzz does +not emulate browser CSP enforcement. It rejects a policy that blocks UI's +dynamic Slider and theme-preview styles, and it rejects stale Google Fonts +sources: + +```sh +bun run qa:production-policy +``` + ## Code Style - Keep code clean and self-documenting through clear variable/function names diff --git a/index.html b/index.html index 3a60bc0..61b0acc 100644 --- a/index.html +++ b/index.html @@ -45,6 +45,5 @@
- diff --git a/package.json b/package.json index b5d3e65..024a7f9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "typecheck": "tsc --noEmit", "build": "bun run typecheck && rsbuild build && bun cleanup.js", "build:apps": "rsbuild build", + "qa:production-policy": "bun scripts/verify-production-policy.mjs", "preview": "rsbuild preview", "prepare": "husky", "lint": "biome check .", @@ -35,7 +36,7 @@ "@iconify-json/lucide": "^1.2.127", "@iconify-json/mdi": "^1.2.3", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "^3.2.1", + "@pathscale/ui": "^3.2.2", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@solidjs/router": "2.0.0-next.19", diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 3b144cf..01528af 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -5,6 +5,10 @@ import CompressionPlugin from "compression-webpack-plugin"; import { pluginSolid2LayoutsApplication } from "rsbuild-plugin-solid-layouts"; export default defineConfig({ + html: { + template: "./index.html", + title: "JS.Software - SolidJS Component Library", + }, plugins: [ pluginSolid2LayoutsApplication({ layouts: ["@pathscale/ui"] }), /* diff --git a/scripts/verify-production-policy.mjs b/scripts/verify-production-policy.mjs new file mode 100644 index 0000000..2d7d1d8 --- /dev/null +++ b/scripts/verify-production-policy.mjs @@ -0,0 +1,76 @@ +const targetUrl = process.env.PRODUCTION_POLICY_URL || "https://js.software/theming"; + +const response = await fetch(targetUrl, { + redirect: "follow", + headers: { Accept: "text/html" }, +}); + +if (!response.ok) { + throw new Error(`Production policy check failed for ${targetUrl}: HTTP ${response.status}`); +} + +const policy = response.headers.get("content-security-policy"); +const html = await response.text(); +const stylesheetUrls = [...html.matchAll(/]*\brel=["'][^"']*stylesheet[^"']*["'][^>]*>/gi)] + .map(([tag]) => tag.match(/\bhref=["']([^"']+)["']/i)?.[1]) + .filter(Boolean) + .map((href) => new URL(href, response.url).href); +const stylesheetBodies = await Promise.all( + stylesheetUrls.map(async (url) => { + const stylesheet = await fetch(url, { redirect: "follow" }); + if (!stylesheet.ok) { + throw new Error(`Production stylesheet check failed for ${url}: HTTP ${stylesheet.status}`); + } + return stylesheet.text(); + }), +); +const productionSource = [html, ...stylesheetBodies].join("\n"); + +if (/fonts\.(googleapis|gstatic)\.com/i.test(productionSource)) { + throw new Error("Production HTML or CSS still loads Google Fonts."); +} + +if (!policy) { + console.log(`Production policy accepts runtime theme styles and has no Google Fonts source: ${targetUrl} has no CSP header.`); + process.exit(0); +} + +const directives = new Map( + policy + .split(";") + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => { + const [name, ...sources] = entry.split(/\s+/); + return [name.toLowerCase(), sources]; + }), +); + +const allSources = [...directives.values()].flat(); +const googleFontSources = allSources.filter((source) => + /(^|\.)fonts\.(googleapis|gstatic)\.com$/i.test( + source.replace(/^https?:\/\//, ""), + ), +); + +if (googleFontSources.length > 0) { + throw new Error( + `Production CSP still allows Google Fonts: ${googleFontSources.join(", ")}`, + ); +} + +const styleAttributeSources = + directives.get("style-src-attr") ?? + directives.get("style-src") ?? + directives.get("default-src") ?? + []; + +if (!styleAttributeSources.includes("'unsafe-inline'")) { + throw new Error( + "Production CSP blocks the dynamic style attributes used by UI sliders and the theme preview.", + ); +} + +console.log( + `Production policy accepts runtime theme styles and has no Google Fonts source: ${targetUrl}`, +); diff --git a/src/App.tsx b/src/App.tsx index 1af6c26..37a50bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,34 +2,28 @@ import { createRouter, useLocation } from "@solidjs/router"; import { ParentComponent, createEffect } from "solid-js"; import { routes } from "./routes"; -import { BaseLayout } from "./layouts/BaseLayout"; import { MarketingHeader } from "./components/layout/Header/MarketingHeader"; +import { BaseLayout } from "./layouts/BaseLayout"; const Layout: ParentComponent = (props) => { const location = useLocation(); createEffect( () => location.pathname, - () => window.scrollTo(0, 0), + () => { + // Chrome 152 returns a Promise from scrollTo(). Solid 2 treats an + // effect return value as cleanup, so return nothing explicitly. + window.scrollTo(0, 0); + }, ); return ( - + {props.children} ); }; -/* - * Routes are configuration, not JSX children. - * - * `@solidjs/router` 2.x replaced / with a factory: the tree is - * declared once as plain objects and `createRouter` returns the provider - * component. The old `root` prop becomes the outermost route's `component`. - */ const Routes = createRouter({ routes: [ { diff --git a/src/components/ComponentsDemo.tsx b/src/components/ComponentsDemo.tsx index e25b355..d4e90d7 100644 --- a/src/components/ComponentsDemo.tsx +++ b/src/components/ComponentsDemo.tsx @@ -4,7 +4,7 @@ import { Menu, Join } from "@pathscale/ui/lab"; import { ROUTES } from "../config/routes"; import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; -export default function ComponentsDemo() { +export default function ComponentsDemo(props: { glassEnabled?: boolean }) { const [modalOpen, setModalOpen] = createSignal(false); const [page, setPage] = createSignal(2); const [price, setPrice] = createSignal(25); @@ -20,20 +20,34 @@ export default function ComponentsDemo() { const [themeSwitch, setThemeSwitch] = createSignal(false); const [previewSearch, setPreviewSearch] = createSignal(""); const [recommendation, setRecommendation] = createSignal("yes"); + const [referralSource, setReferralSource] = createSignal(null); const actionStatus = createActionStatus("Preview ready"); return ( -
-
+
+
- + Filters + + {props.glassEnabled === false ? "Solid" : "Glass"} + + ); + return ( <> - - - -
+ {trigger} + + +
{ + if (event.target === event.currentTarget) closeSearch(); + }} > -
- - setQuery(e.currentTarget.value)} - onKeyDown={handleSearchKeyDown} - class="flex-1 ml-3 bg-transparent placeholder-base-content/70 outline-none" - /> - ESC -
+ - + + ); }; diff --git a/src/components/layout/Header/MarketingHeader.tsx b/src/components/layout/Header/MarketingHeader.tsx index 2d03b92..8a6ee53 100644 --- a/src/components/layout/Header/MarketingHeader.tsx +++ b/src/components/layout/Header/MarketingHeader.tsx @@ -7,6 +7,7 @@ import { MobileSidebar } from "./components/MobileSidebar"; import { GitHubIcon } from "./components/GitHubIcon"; import { MarketingHeaderProps } from "./types"; import { ROUTES, EXTERNAL_ROUTES } from "../../../config/routes"; +import Search from "../../content/Search"; export const MarketingHeader: Component = (props) => { const navigation = useNavigation(); @@ -66,6 +67,7 @@ export const MarketingHeader: Component = (props) => {
- + setIsOpen(false)} + />
diff --git a/src/components/layout/Navigation/CompactNavigation.tsx b/src/components/layout/Navigation/CompactNavigation.tsx index c730501..1b4b2ff 100644 --- a/src/components/layout/Navigation/CompactNavigation.tsx +++ b/src/components/layout/Navigation/CompactNavigation.tsx @@ -15,7 +15,9 @@ export const CompactNavigation: Component = (props) => { { title: "Overview", href: ROUTES.DOCS }, { title: "Components", href: ROUTES.SHOWCASES }, { title: "Theming", href: ROUTES.THEMING }, - { title: "Guides", href: "/docs/guides" }, + { title: "Installation", href: ROUTES.DOCS_INSTALLATION }, + { title: "Solid Layouts", href: ROUTES.DOCS_LAYOUTS }, + { title: "Usage", href: ROUTES.DOCS_USAGE }, ]; const getCurrentPageTitle = () => { @@ -68,13 +70,6 @@ export const CompactNavigation: Component = (props) => {
- setIsOpen(false)} - > - Resources - ( - +
diff --git a/src/components/theming/EffectsSection.tsx b/src/components/theming/EffectsSection.tsx index 24d4e58..77f9b5d 100644 --- a/src/components/theming/EffectsSection.tsx +++ b/src/components/theming/EffectsSection.tsx @@ -11,14 +11,14 @@ const EFFECTS = [ { key: "--depth", label: "Depth Effect", - description: "3D depth on fields & selectors", + description: "Depth on fields and selectors", }, { key: "--noise", - label: "Noise Effect", - description: "Noise pattern on fields & selectors", + label: "Noise Effect", + description: "Texture on fields and selectors", }, -]; +] as const; export default function EffectsSection(props: EffectsSectionProps) { return ( @@ -26,13 +26,13 @@ export default function EffectsSection(props: EffectsSectionProps) {

Effects -

- + +
{(effect) => ( ) => void; + enabled: boolean; + onEnabledChange: (enabled: boolean) => void; } export default function GlassSection(props: GlassSectionProps) { - const tuning = () => tuningFromTheme(props.theme); + const mode = () => (props.theme._themeType === "light" ? "light" : "dark"); + const tuning = () => tuningFromTheme(props.theme, mode()); const applyTuning = ( - next: Partial<{ blur: number; refraction: number; depth: number }>, + next: Partial>, ) => { - const current = tuning(); - const merged = { - blur: next.blur ?? current.blur, - refraction: next.refraction ?? current.refraction, - depth: next.depth ?? current.depth, - }; - const cssVars = resolveGlassCssVariables(merged); - props.onThemeUpdate(cssVars); + props.onThemeUpdate(resolveGlassThemeValues({ ...tuning(), ...next }, mode())); }; - const resetToHype4 = () => applyTuning(HYPE4_DEFAULTS); + const resetGlass = () => props.onThemeUpdate(glassThemeDefaults(mode())); return (
@@ -42,21 +38,32 @@ export default function GlassSection(props: GlassSectionProps) { size="sm" variant="ghost" class="ml-auto" - onClick={resetToHype4} - title="Reset to Hype4 defaults" + onClick={resetGlass} + title="Reset glass settings" > - Hype4 + Reset
+ + Glass material + +
`${value}px`} @@ -70,7 +77,7 @@ export default function GlassSection(props: GlassSectionProps) { label="Refraction" size="sm" min={0} - max={REFRACTION_MAX} + max={GLASS_LIMITS.refraction.max} step={0.01} value={tuning().refraction} formatValue={(value) => value.toFixed(2)} @@ -84,12 +91,40 @@ export default function GlassSection(props: GlassSectionProps) { label="Depth" size="sm" min={0} - max={DEPTH_MAX} + max={GLASS_LIMITS.depth.max} step={1} value={tuning().depth} onChange={(value) => applyTuning({ depth: value })} />
+ +
+ `${value}%`} + onChange={(value) => applyTuning({ opacity: value })} + /> +
+ +
+ `${value}%`} + onChange={(value) => applyTuning({ scrim: value })} + /> +
); diff --git a/src/components/theming/ThemeCSSModal.tsx b/src/components/theming/ThemeCSSModal.tsx index e6c6733..2cd89db 100644 --- a/src/components/theming/ThemeCSSModal.tsx +++ b/src/components/theming/ThemeCSSModal.tsx @@ -1,5 +1,6 @@ import { Button, Dialog, Icon, Textarea } from "@pathscale/ui"; import { createEffect, createSignal } from "solid-js"; +import { ActionStatus } from "../showcase/ActionStatus"; import { GLASS_THEME_DEFAULTS, GLASS_THEME_TOKEN_ORDER, @@ -15,9 +16,16 @@ interface ThemeCSSModalProps { colorScheme?: "light" | "dark"; } +const escapeCssString = (value: string) => + value.replace(/[\0-\x1f\x7f"\\]/g, (character) => { + const codePoint = character.codePointAt(0) || 0; + return codePoint === 0 ? "\uFFFD" : `\\${codePoint.toString(16)} `; + }); + export default function ThemeCSSModal(props: ThemeCSSModalProps) { const [cssText, setCssText] = createSignal(""); const [isClipboardButtonPressed, setIsClipboardButtonPressed] = createSignal(false); + const [copyStatus, setCopyStatus] = createSignal("Copy generated CSS"); const generateCSS = (theme: Theme) => { /* @@ -35,7 +43,7 @@ export default function ThemeCSSModal(props: ThemeCSSModalProps) { */ const selectors = [ props.isDefault ? ":root" : null, - `[data-theme="${theme.name}"]`, + `[data-theme="${escapeCssString(theme.name)}"]`, ].filter(Boolean).join(",\n"); const baseProps = [` color-scheme: ${props.colorScheme || "light"};`]; @@ -111,18 +119,27 @@ export default function ThemeCSSModal(props: ThemeCSSModalProps) { createEffect( () => ({ open: props.open, theme: props.theme }), ({ open, theme }) => { - if (open) setCssText(generateCSS(theme)); + if (open) { + setCssText(generateCSS(theme)); + setCopyStatus("Copy generated CSS"); + setIsClipboardButtonPressed(false); + } }, ); const copyThemeCSSToClipboard = () => { - navigator.clipboard - .writeText(cssText()) + const write = navigator.clipboard?.writeText(cssText()); + if (!write) { + setCopyStatus("CSS copy unavailable"); + return; + } + write .then(() => { setIsClipboardButtonPressed(true); + setCopyStatus("CSS copied"); setTimeout(() => setIsClipboardButtonPressed(false), 2000); }) - .catch((err) => console.error("Failed to copy:", err)); + .catch(() => setCopyStatus("CSS copy unavailable")); }; return ( @@ -159,6 +176,7 @@ export default function ThemeCSSModal(props: ThemeCSSModalProps) {
+ diff --git a/src/components/theming/ThemeComposer.tsx b/src/components/theming/ThemeComposer.tsx new file mode 100644 index 0000000..2d578ec --- /dev/null +++ b/src/components/theming/ThemeComposer.tsx @@ -0,0 +1,223 @@ +import { Button, ComplexColorWheel, Flex } from "@pathscale/ui"; +import { createMemo, For } from "solid-js"; +import { hexToOklch, Theme, updateThemeColor } from "../../utils/themeUtils"; +import { + accentOptions, + artworkAccentOptions, + applyCompositionWithAccentHarmony, + applyThemeComposition, + compositionFromTheme, + SOFTNESS_STOPS, + STRENGTH_STOPS, + surfaceColors, + surfaceTone, + TEXT_BRIGHTNESS_STOPS, + type ThemeComposition, +} from "../../lib/themeComposer"; + +interface ThemeComposerProps { + theme: Theme; + onThemeChange: (theme: Theme, message: string) => void; + onReset: () => void; +} + +const parseOklch = (value: string) => { + const canonical = value.trim().startsWith("#") ? hexToOklch(value) : value; + const match = canonical.match( + /^oklch\(\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))%\s+([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s+([+-]?(?:\d+(?:\.\d*)?|\.\d+))/i, + ); + if (!match) return null; + return { + lightness: Number(match[1]), + chroma: Number(match[2]), + hue: Number(match[3]), + }; +}; + +const colorsMatch = (stored: string, swatch: string) => { + const left = parseOklch(stored); + const right = parseOklch(swatch); + if (!left || !right) return stored.trim().toLowerCase() === swatch.trim().toLowerCase(); + + // updateThemeColor stores whole lightness/hue values and three chroma + // decimals. Compare within that quantization instead of round-tripping to a + // hex value that can move by several RGB channels. + const hueDelta = Math.abs(left.hue - right.hue) % 360; + const hueDistance = Math.min(hueDelta, 360 - hueDelta); + return ( + Math.abs(left.lightness - right.lightness) <= 0.51 && + Math.abs(left.chroma - right.chroma) <= 0.001 && + hueDistance <= 1 + ); +}; + +function AccentSelector(props: { + id: string; + label: string; + hint: string; + value: string; + options: readonly string[]; + onPick: (value: string) => void; +}) { + return ( +
+
+ + {props.label} + + {props.hint} +
+ + + {(option, index) => { + const selected = () => colorsMatch(props.value, option); + return ( + + ); + }} + + +
+ ); +} + +export default function ThemeComposer(props: ThemeComposerProps) { + const mode = () => (props.theme._themeType === "dark" ? "dark" : "light"); + const composition = () => compositionFromTheme(props.theme); + const palette = () => surfaceColors(mode()); + const controlFriends = () => + accentOptions( + composition().surface, + mode(), + composition().strength, + composition().softness, + ); + const artworkFriends = () => + artworkAccentOptions( + composition().surface, + mode(), + composition().strength, + composition().softness, + ); + + const commitComposition = (patch: Partial, message: string) => + props.onThemeChange(applyCompositionWithAccentHarmony(props.theme, patch), message); + + const adjustments = createMemo(() => [ + { + id: "strength", + label: "Colour strength", + hint: "How far the picked colour reaches into the surfaces", + stops: STRENGTH_STOPS, + value: composition().strength, + onChange: (value: number) => commitComposition({ strength: value }, "Surface strength updated"), + preview: (value: number) => + surfaceTone({ ...composition(), strength: value }, mode(), 1), + formatValue: (value: number) => `${value}%`, + }, + { + id: "softness", + label: "Softness", + hint: "Lifts surfaces away from the light or dark edge", + stops: SOFTNESS_STOPS, + value: composition().softness, + onChange: (value: number) => commitComposition({ softness: value }, "Surface softness updated"), + preview: (value: number) => + surfaceTone({ ...composition(), softness: value }, mode(), 1), + formatValue: (value: number) => `${Math.round((value / 12) * 100)}%`, + }, + { + id: "text-brightness", + label: "Text brightness", + hint: "Moves body text toward its strongest contrast", + stops: TEXT_BRIGHTNESS_STOPS, + value: composition().textBrightness, + onChange: (value: number) => + commitComposition({ textBrightness: value }, "Text brightness updated"), + preview: () => props.theme["--color-base-200"], + ink: (value: number) => + applyThemeComposition(props.theme, { textBrightness: value })["--color-base-content"], + formatValue: (value: number) => `${value > 0 ? "+" : ""}${value}`, + }, + ]); + + return ( +
+ commitComposition({ surface }, "Surface colour updated")} + mode={mode()} + palette={palette()} + aria-label="Surface colour" + adjustments={adjustments()} + layout="stacked" + material="solid" + action={ + + } + /> + +
+ + props.onThemeChange( + { + ...updateThemeColor(props.theme, "--color-primary", value), + _controlAccentIndex: `${controlFriends().indexOf(value)}`, + }, + "Control accent updated", + ) + } + /> + + props.onThemeChange( + { + ...updateThemeColor(props.theme, "--color-accent", value), + _artAccentIndex: `${artworkFriends().indexOf(value)}`, + }, + "Artwork accent updated", + ) + } + /> +
+
+ ); +} diff --git a/src/components/theming/ThemeEditor.tsx b/src/components/theming/ThemeEditor.tsx index 9f6974a..af7f58b 100644 --- a/src/components/theming/ThemeEditor.tsx +++ b/src/components/theming/ThemeEditor.tsx @@ -1,4 +1,4 @@ -import { For, createSignal } from "solid-js"; +import { For, createEffect, createSignal } from "solid-js"; import { Button, Grid, Icon, Input, Separator, Switch } from "@pathscale/ui"; import { Theme, COLOR_GROUPS } from "../../utils/themeUtils"; import ColorGroup from "./ColorGroup"; @@ -6,15 +6,19 @@ import RadiusSection from "./RadiusSection"; import EffectsSection from "./EffectsSection"; import GlassSection from "./GlassSection"; import SizesSection from "./SizesSection"; +import ThemeComposer from "./ThemeComposer"; +import { applyCompositionWithAccentHarmony } from "../../lib/themeComposer"; import { ActionStatus } from "../showcase/ActionStatus"; interface ThemeEditorProps { theme: Theme; onThemeNameChange: (name: string) => void; onColorClick: (colorKey: string, event: MouseEvent) => void; + onThemeChange: (theme: Theme, message: string) => void; onThemePropertyUpdate: (key: string, value: string) => void; onGlassThemeUpdate: (values: Record) => void; onRandomizeTheme: () => void; + onResetTheme: () => void; onExportCSS: (isDefault: boolean, isPrefersDark: boolean, colorScheme: "light" | "dark") => void; dockActiveItem: string; applyToWholeSite: boolean; @@ -27,11 +31,20 @@ interface ThemeEditorProps { export default function ThemeEditor(props: ThemeEditorProps) { const [isDefault, setIsDefault] = createSignal(false); const [isPrefersDark, setIsPrefersDark] = createSignal(false); - const [colorScheme, setColorScheme] = createSignal<"light" | "dark">("light"); + const [colorScheme, setColorScheme] = createSignal<"light" | "dark">( + props.theme._themeType === "dark" ? "dark" : "light", + ); + + createEffect( + () => props.theme._themeType, + (themeType) => { + setColorScheme(themeType === "dark" ? "dark" : "light"); + }, + ); return (
- Change Colors + Compose Theme + + + + + + + + + +

+ + Individual Tokens

@@ -104,6 +169,13 @@ export default function ThemeEditor(props: ThemeEditorProps) { + props.onThemeChange( + { ...props.theme, _glassEnabled: enabled ? "1" : "0" }, + enabled ? "Glass preview enabled" : "Glass preview disabled", + ) + } /> setColorScheme(checked ? "dark" : "light")} > - Dark color scheme + Export dark color scheme
diff --git a/src/components/theming/ThemeList.tsx b/src/components/theming/ThemeList.tsx index cf6dc99..97415c5 100644 --- a/src/components/theming/ThemeList.tsx +++ b/src/components/theming/ThemeList.tsx @@ -17,7 +17,7 @@ const themeDomId = (theme: Theme) => export default function ThemeList(props: ThemeListProps) { return ( -