From a63af5d1c1c86f22ac8a5187fd38b6fe7e451da0 Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 13 Sep 2026 00:12:51 +0700 Subject: [PATCH 1/8] fix(showcase): align theme chrome and component catalog --- README.md | 2 +- src/components/AuthPoweredByShowcase.tsx | 151 ++++++++++++++++++ src/components/Preview.tsx | 4 - src/components/ShowcaseLayout.tsx | 13 +- .../layout/Header/MarketingHeader.tsx | 37 +++-- .../Header/components/ComponentsMenu.tsx | 14 +- .../Header/components/MainNavigation.tsx | 12 +- .../Header/components/MobileSidebar.tsx | 21 +-- .../layout/Header/navigationData.ts | 2 +- src/components/theming/GlassSection.tsx | 30 ---- src/components/theming/ThemeCSSModal.tsx | 14 ++ src/config/routes.ts | 1 + src/lib/glassTokens.ts | 49 ++---- src/lib/themeAliases.ts | 33 ++++ src/lib/themeEditorPersistence.ts | 4 +- src/pages/Home.tsx | 9 +- src/pages/Theming.tsx | 30 ++-- src/pages/docs/Components.tsx | 1 + src/routes.ts | 8 + src/styles/themes/theme.css | 40 +++++ tests/ps-qa/checks/coverage.ron | 2 +- tests/ps-qa/checks/showcase.ron | 95 ++++++++++- tests/ps-qa/checks/theming.ron | 65 ++------ tests/ps-qa/checks/unlisted.ron | 14 +- tests/ps-qa/ps-qa.ron | 3 +- 25 files changed, 447 insertions(+), 207 deletions(-) create mode 100644 src/components/AuthPoweredByShowcase.tsx create mode 100644 src/lib/themeAliases.ts diff --git a/README.md b/README.md index 482b773..85daed1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ps-qa --app tests/ps-qa/ps-qa.ron qa-hosted \ --checks tests/ps-qa/checks ``` -The UI 3.2.2 registry build passes all 669 native checks across 13 groups, +The UI 3.2.2 registry build passes all 674 native checks across 13 groups, including Calendar selection, month navigation, and keyboard and pointer-driven Slider and Color Picker outcomes. The release gate includes every group. The Layouts route uses its own document marker, and decorative cards are diff --git a/src/components/AuthPoweredByShowcase.tsx b/src/components/AuthPoweredByShowcase.tsx new file mode 100644 index 0000000..1e823bc --- /dev/null +++ b/src/components/AuthPoweredByShowcase.tsx @@ -0,0 +1,151 @@ +import { AuthPoweredBy, Flex, Icon, Link } from "@pathscale/ui"; +import ShowcaseLayout from "./ShowcaseLayout"; +import { CodeBlock } from "./showcase/CodeBlock"; +import { PropsTable } from "./showcase/PropsTable"; +import { ShowcaseSection } from "./showcase/ShowcaseSection"; + +const sections = [ + { id: "default", title: "Default Honey attribution" }, + { id: "variants", title: "Variants" }, + { id: "alignment", title: "Alignment" }, + { id: "custom", title: "Custom content" }, + { id: "props", title: "Props" }, +] as const; + +const props = [ + { + name: "label", + type: "string", + default: '"Secure Auth by Honey"', + description: "Accessible attribution text shown beside the provider mark.", + }, + { + name: "href", + type: "string", + default: '"https://honey.id/"', + description: "Provider destination opened in a separate browser tab.", + }, + { + name: "variant", + type: '"subtle" | "card" | "inline"', + default: '"subtle"', + description: "Visual treatment for the attribution.", + }, + { + name: "align", + type: '"left" | "center" | "right"', + default: '"center"', + description: "Horizontal placement inside the component root.", + }, + { + name: "logo", + type: "JSX.Element", + description: "Optional provider mark rendered before the label.", + }, + { + name: "id", + type: "string", + description: "Stable root identity; the link receives an --link suffix.", + }, +]; + +export default function AuthPoweredByShowcase() { + return ( + + + + + {sections.map((section) => ( + + {section.title} + + ))} + + + + + + + `} /> + + + + + + + + + + +`} + /> + + + + + + + + + + + + + + } + /> + } +/>`} + /> + + + + + + + + + ); +} diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx index e35469c..e81e064 100644 --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -27,10 +27,6 @@ export default function Preview(props: PreviewProps) {

{TAB_TITLES[selectedKey()]}

diff --git a/src/components/ShowcaseLayout.tsx b/src/components/ShowcaseLayout.tsx index a03f5b3..8160661 100644 --- a/src/components/ShowcaseLayout.tsx +++ b/src/components/ShowcaseLayout.tsx @@ -2,7 +2,8 @@ import { useLocation } from "@solidjs/router"; import { routes } from "../routes"; import type { JSX } from "@solidjs/web"; import type { ParentComponent } from "solid-js"; -import { Flex } from "@pathscale/ui"; +import { Flex, Link } from "@pathscale/ui"; +import { ROUTES } from "../config/routes"; interface ShowcaseLayoutProps { children: JSX.Element; @@ -31,8 +32,16 @@ const ShowcaseLayout: ParentComponent = (props) => {
+ + All components +

{current()?.name}

-

+

{current()?.description}

diff --git a/src/components/layout/Header/MarketingHeader.tsx b/src/components/layout/Header/MarketingHeader.tsx index 8a6ee53..8339864 100644 --- a/src/components/layout/Header/MarketingHeader.tsx +++ b/src/components/layout/Header/MarketingHeader.tsx @@ -1,5 +1,5 @@ import { Component, Show } from "solid-js"; -import { Button, Flex, Navbar } from "@pathscale/ui"; +import { Button, Flex, Link, Navbar } from "@pathscale/ui"; import { useNavigation } from "./hooks/useNavigation"; import { MainNavigation } from "./components/MainNavigation"; import { ComponentsMenu } from "./components/ComponentsMenu"; @@ -47,18 +47,18 @@ export const MarketingHeader: Component = (props) => { - + UI - +
diff --git a/src/components/layout/Header/components/ComponentsMenu.tsx b/src/components/layout/Header/components/ComponentsMenu.tsx index 33026ed..8261940 100644 --- a/src/components/layout/Header/components/ComponentsMenu.tsx +++ b/src/components/layout/Header/components/ComponentsMenu.tsx @@ -1,5 +1,5 @@ import { Component, For, createMemo } from "solid-js"; -import { Button, Flex, Navbar } from "@pathscale/ui"; +import { Button, Flex, Link, Navbar } from "@pathscale/ui"; import { navigationItems } from "../navigationData"; import { useNavigation } from "../hooks/useNavigation"; import clsx from "clsx"; @@ -34,8 +34,8 @@ export const ComponentsMenu: Component = (props) => { "px-3 py-1 rounded-md text-sm transition-colors", activeCategory() === subcategory.title || (activeCategory() === "Components" && subcategory === componentsItem?.subcategories?.[0]) - ? "bg-primary text-white" - : "hover:bg-base-300" + ? "bg-primary text-primary-content" + : "text-base-content hover:bg-base-300" )} > {subcategory.title} @@ -49,15 +49,17 @@ export const ComponentsMenu: Component = (props) => { {(item) => ( - {item.title} - + )} diff --git a/src/components/layout/Header/components/MainNavigation.tsx b/src/components/layout/Header/components/MainNavigation.tsx index f3c40f1..957ba10 100644 --- a/src/components/layout/Header/components/MainNavigation.tsx +++ b/src/components/layout/Header/components/MainNavigation.tsx @@ -1,5 +1,5 @@ import { Component, For } from "solid-js"; -import { Button, Flex } from "@pathscale/ui"; +import { Button, Flex, Link } from "@pathscale/ui"; import { navigationItems } from "../navigationData"; import { useNavigation } from "../hooks/useNavigation"; @@ -16,16 +16,16 @@ export const MainNavigation: Component = (props) => { {(item) => ( <> {item.href ? ( - {item.title} - + ) : (
diff --git a/src/components/layout/Header/navigationData.ts b/src/components/layout/Header/navigationData.ts index 118340c..34a1099 100644 --- a/src/components/layout/Header/navigationData.ts +++ b/src/components/layout/Header/navigationData.ts @@ -83,6 +83,7 @@ export const navigationItems: NavigationItem[] = [ { title: "Surfaces", items: [ + { title: "Auth Powered By", href: ROUTES.AUTH_POWERED_BY }, { title: "Glass Panel", href: ROUTES.GLASS_PANEL }, { title: "Glow Card", href: ROUTES.GLOW_CARD }, { title: "Noise Background", href: ROUTES.NOISE_BACKGROUND }, @@ -94,5 +95,4 @@ export const navigationItems: NavigationItem[] = [ }, { title: "Docs", href: ROUTES.DOCS }, { title: "Layouts", href: ROUTES.DOCS_LAYOUTS }, - { title: "Showcases", href: ROUTES.SHOWCASES }, ]; diff --git a/src/components/theming/GlassSection.tsx b/src/components/theming/GlassSection.tsx index 149af58..e46f93a 100644 --- a/src/components/theming/GlassSection.tsx +++ b/src/components/theming/GlassSection.tsx @@ -1,8 +1,6 @@ import { Button, Icon, Separator, Slider, Switch } from "@pathscale/ui"; import { GLASS_LIMITS, - GLASS_OPACITY_MAX, - GLASS_SCRIM_MAX, glassThemeDefaults, resolveGlassThemeValues, tuningFromTheme, @@ -97,34 +95,6 @@ export default function GlassSection(props: GlassSectionProps) { 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 2cd89db..1d9fa93 100644 --- a/src/components/theming/ThemeCSSModal.tsx +++ b/src/components/theming/ThemeCSSModal.tsx @@ -64,6 +64,19 @@ export default function ThemeCSSModal(props: ThemeCSSModalProps) { .filter(key => theme[key]) .map(key => ` ${key}: ${theme[key]};`); + const aliasOrder = [ + "--color-default", "--color-default-foreground", "--color-default-hover", + "--color-background", "--color-foreground", + "--color-bg-body", "--color-bg-inverse", "--color-bg-primary", + "--color-bg-secondary", "--color-bg-tertiary", + "--color-fg-body", "--color-fg-inverse", "--color-fg-primary", + "--color-fg-secondary", "--color-fg-tertiary", + "--b1", "--b2", "--b3", "--bc", "--shade", + ]; + const aliasProps = aliasOrder + .filter(key => theme[key]) + .map(key => ` ${key}: ${theme[key]};`); + // Add default radius, size and effect values if not present const defaultValues: Record = { "--radius-selector": "0.5rem", @@ -101,6 +114,7 @@ export default function ThemeCSSModal(props: ThemeCSSModalProps) { const allProps = [ ...baseProps, ...colorProps, + ...aliasProps, ...radiusProps, ...sizeProps, ...effectProps, diff --git a/src/config/routes.ts b/src/config/routes.ts index a9ea02d..f28e998 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -8,6 +8,7 @@ export const ROUTES = { SHOWCASES: "/showcases", ACCORDION: "/accordion", ALERT: "/alert", + AUTH_POWERED_BY: "/auth-powered-by", AVATAR: "/avatar", BADGE: "/badge", BREADCRUMB: "/breadcrumb", diff --git a/src/lib/glassTokens.ts b/src/lib/glassTokens.ts index a2c0766..95670fd 100644 --- a/src/lib/glassTokens.ts +++ b/src/lib/glassTokens.ts @@ -6,15 +6,7 @@ import { type GlassTuning, } from "@pathscale/ui/styles/glass.js"; -export const GLASS_OPACITY_DEFAULT = 55; -export const GLASS_OPACITY_MAX = 100; -export const GLASS_SCRIM_DEFAULT = 0; -export const GLASS_SCRIM_MAX = 70; - -export interface GlassThemeTuning extends Required { - opacity: number; - scrim: number; -} +export type GlassThemeTuning = Required; const finite = (value: string | undefined, fallback: number) => { const parsed = Number.parseFloat(value ?? ""); @@ -56,16 +48,6 @@ export const tuningFromTheme = ( GLASS_LIMITS.controlTint.min, GLASS_LIMITS.controlTint.max, ), - opacity: clamp( - finite(theme._glassOpacity, GLASS_OPACITY_DEFAULT), - 0, - GLASS_OPACITY_MAX, - ), - scrim: clamp( - finite(theme._glassScrim, GLASS_SCRIM_DEFAULT), - 0, - GLASS_SCRIM_MAX, - ), }; }; @@ -74,42 +56,37 @@ export const resolveGlassThemeValues = ( mode: GlassMode, ): Record => { const tokens = resolveGlassTokens(tuning, mode); - const opacity = clamp(tuning.opacity, 0, GLASS_OPACITY_MAX); - const scrim = clamp(tuning.scrim, 0, GLASS_SCRIM_MAX); return { ...tokens, - "--glass-background-opacity": `${opacity}%`, - "--theme-glass-scrim-opacity": `${scrim}%`, _glassBlur: `${tuning.blur}`, _glassRefraction: `${tuning.refraction}`, _glassDepth: `${tuning.depth}`, _glassControlTint: `${tuning.controlTint}`, - _glassOpacity: `${opacity}`, - _glassScrim: `${scrim}`, }; }; export const glassThemeDefaults = (mode: GlassMode): Record => - resolveGlassThemeValues( - { - ...GLASS_DEFAULTS[mode], - opacity: GLASS_OPACITY_DEFAULT, - scrim: GLASS_SCRIM_DEFAULT, - }, - mode, - ); + resolveGlassThemeValues(GLASS_DEFAULTS[mode], mode); export const GLASS_THEME_DEFAULTS = glassThemeDefaults("dark"); export const GLASS_THEME_TOKEN_ORDER = Object.keys( resolveGlassTokens(GLASS_DEFAULTS.dark, "dark"), -).concat("--theme-glass-scrim-opacity"); +); + +const withoutLegacyGlassOverrides = >(theme: T) => { + const next = { ...theme }; + delete next._glassOpacity; + delete next._glassScrim; + delete next["--theme-glass-scrim-opacity"]; + return next; +}; export function withGlassThemeDefaults>(theme: T) { const mode: GlassMode = theme._themeType === "light" ? "light" : "dark"; const tuning = tuningFromTheme(theme, mode); return { _glassEnabled: "1", - ...theme, + ...withoutLegacyGlassOverrides(theme), ...resolveGlassThemeValues(tuning, mode), }; } @@ -117,7 +94,7 @@ export function withGlassThemeDefaults>(theme: export function resetGlassTheme>(theme: T) { const mode: GlassMode = theme._themeType === "light" ? "light" : "dark"; return { - ...theme, + ...withoutLegacyGlassOverrides(theme), ...glassThemeDefaults(mode), _glassEnabled: "1", }; diff --git a/src/lib/themeAliases.ts b/src/lib/themeAliases.ts new file mode 100644 index 0000000..dd80cfd --- /dev/null +++ b/src/lib/themeAliases.ts @@ -0,0 +1,33 @@ +import type { Theme } from "../types/theme"; + +/** + * Keep the public UI token aliases attached to the generated surface ladder. + * + * A generated theme used to replace `--color-base-content` while leaving the + * imported light/dark theme's generic and compatibility foregrounds behind. + * Components that read those aliases could therefore retain dark ink on a dark + * generated theme, including chrome outside the preview scope. + */ +export const withThemeAliases = (theme: T): T => ({ + ...theme, + "--color-default": "var(--color-base-200)", + "--color-default-foreground": "var(--color-base-content)", + "--color-default-hover": "var(--color-base-300)", + "--color-background": "var(--color-base-100)", + "--color-foreground": "var(--color-base-content)", + "--color-bg-body": "var(--color-base-100)", + "--color-bg-inverse": "var(--color-base-content)", + "--color-bg-primary": "var(--color-base-200)", + "--color-bg-secondary": "var(--color-base-300)", + "--color-bg-tertiary": "var(--color-base-300)", + "--color-fg-body": "var(--color-base-content)", + "--color-fg-inverse": "var(--color-base-100)", + "--color-fg-primary": "var(--color-base-content)", + "--color-fg-secondary": "var(--color-base-content)", + "--color-fg-tertiary": "var(--color-base-content)", + "--b1": "var(--color-base-100)", + "--b2": "var(--color-base-200)", + "--b3": "var(--color-base-300)", + "--bc": "var(--color-base-content)", + "--shade": "var(--color-base-content)", +}) as T; diff --git a/src/lib/themeEditorPersistence.ts b/src/lib/themeEditorPersistence.ts index 11ee5f5..5f0f85d 100644 --- a/src/lib/themeEditorPersistence.ts +++ b/src/lib/themeEditorPersistence.ts @@ -1,4 +1,5 @@ import type { Theme } from "../types/theme"; +import { withThemeAliases } from "./themeAliases"; const STORAGE_KEY = "js-software-theme-editor"; const appliedProperties = new Set(); @@ -64,7 +65,8 @@ export const applyThemeToDocument = (theme: Theme | null) => { root.classList.remove("glass", "theme-glass-disabled"); if (!theme) return; - for (const [property, value] of Object.entries(theme)) { + const normalizedTheme = withThemeAliases(theme); + for (const [property, value] of Object.entries(normalizedTheme)) { if (property.startsWith("--")) { root.style.setProperty(property, value); appliedProperties.add(property); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 47ad21f..04dee1e 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,5 +1,5 @@ import { Component } from "solid-js"; -import { Button, Card, Chip, Flex, Icon } from "@pathscale/ui"; +import { Button, Card, Chip, Flex, Icon, Text } from "@pathscale/ui"; import { ContentContainer } from "../components/content/ContentContainer"; import { ROUTES } from "../config/routes"; @@ -22,10 +22,9 @@ const Home: Component = () => { -

- Build Exceptional UIs with{" "} - JS.Software UI -

+ + Build Exceptional UI/ +

A type-safe component library that keeps reusable presentation in diff --git a/src/pages/Theming.tsx b/src/pages/Theming.tsx index 7961768..f0a96a4 100644 --- a/src/pages/Theming.tsx +++ b/src/pages/Theming.tsx @@ -72,6 +72,7 @@ import { readThemeEditorState, writeThemeEditorState, } from "../lib/themeEditorPersistence"; +import { withThemeAliases } from "../lib/themeAliases"; import { createActionStatus } from "../components/showcase/ActionStatus"; import { accentOptions, @@ -96,6 +97,9 @@ const withIdentity = (theme: Theme): Theme => ({ _id: theme._id || `${Date.now()}-${Math.random().toString(36).slice(2)}`, }); +const normalizeTheme = (theme: Theme): Theme => + withIdentity(withThemeAliases(withGlassThemeDefaults(theme))); + const randomItem = (values: readonly T[]): T => values[Math.floor(Math.random() * values.length)]; @@ -150,15 +154,11 @@ const restoreThemeLibrary = ( persisted: ReturnType, ): { currentTheme: Theme; themes: Theme[] } => { if (!persisted) { - const currentTheme = withIdentity( - withGlassThemeDefaults({ ...createComposedTheme(), name: "Theme 1" }), - ); + const currentTheme = normalizeTheme({ ...createComposedTheme(), name: "Theme 1" }); return { currentTheme, themes: [currentTheme] }; } - const currentTheme = withIdentity( - withGlassThemeDefaults(persisted.currentTheme), - ); + const currentTheme = normalizeTheme(persisted.currentTheme); let linkedCurrentTheme = false; const themes = persisted.themes.map((theme) => { const isCurrentTheme = @@ -169,7 +169,7 @@ const restoreThemeLibrary = ( linkedCurrentTheme = true; return currentTheme; } - return withIdentity(withGlassThemeDefaults(theme)); + return normalizeTheme(theme); }); return { currentTheme, themes }; @@ -217,7 +217,7 @@ export default function Theming() { }; const commitTheme = (theme: Theme) => { - const next = withIdentity(withGlassThemeDefaults(theme)); + const next = normalizeTheme(theme); setCurrentTheme(next); const themes = customThemes().map((saved) => identity(saved) === identity(next) ? next : saved, @@ -292,12 +292,10 @@ export default function Theming() { const usedNames = new Set(customThemes().map((theme) => theme.name)); let nextThemeNumber = 1; while (usedNames.has(`Theme ${nextThemeNumber}`)) nextThemeNumber += 1; - const theme = withIdentity( - withGlassThemeDefaults({ - ...createComposedTheme(), - name: `Theme ${nextThemeNumber}`, - }), - ); + const theme = normalizeTheme({ + ...createComposedTheme(), + name: `Theme ${nextThemeNumber}`, + }); const themes = [theme, ...customThemes()]; setCurrentTheme(theme); setCustomThemes(themes); @@ -322,7 +320,7 @@ export default function Theming() { return; } - const next = remaining[0] || withIdentity(createComposedTheme()); + const next = remaining[0] || normalizeTheme(createComposedTheme()); setCurrentTheme(next); setCustomThemes(remaining); if (applyToWholeSite()) applyThemeToDocument(next); @@ -331,7 +329,7 @@ export default function Theming() { }; const clearAllThemes = () => { - const next = withIdentity(createComposedTheme()); + const next = normalizeTheme(createComposedTheme()); setCurrentTheme(next); setCustomThemes([]); if (applyToWholeSite()) applyThemeToDocument(next); diff --git a/src/pages/docs/Components.tsx b/src/pages/docs/Components.tsx index e1285c7..3340278 100644 --- a/src/pages/docs/Components.tsx +++ b/src/pages/docs/Components.tsx @@ -66,6 +66,7 @@ const Components: Component = () => { "Mask", ], "Utilities & Helpers": [ + "Auth Powered By", "Kbd", "Join", "Collapse", diff --git a/src/routes.ts b/src/routes.ts index 1d44485..ed593eb 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -10,6 +10,7 @@ import DocsLayouts from "./pages/docs/Layouts"; import DocsUsage from "./pages/docs/Usage"; import AccordionShowcase from "./components/AccordionShowcase"; import AlertShowcase from "./components/AlertShowcase"; +import AuthPoweredByShowcase from "./components/AuthPoweredByShowcase"; import AvatarShowcase from "./components/AvatarShowcase"; import BadgeShowcase from "./components/BadgeShowcase"; import BreadcrumbShowcase from "./components/BreadcrumbShowcase"; @@ -72,6 +73,7 @@ export interface RouteConfig { const dedicatedFamiliesByRoute = { Accordion: ["accordion"], Alert: ["alert"], + "Auth Powered By": ["auth-powered-by"], Avatar: ["avatar"], Badge: ["badge"], Breadcrumb: ["breadcrumb"], @@ -126,6 +128,12 @@ const dedicatedFamiliesByRoute = { const componentRoutes = ([ ["Accordion", ROUTES.ACCORDION, AccordionShowcase, "Expandable content sections."], ["Alert", ROUTES.ALERT, AlertShowcase, "Important status and feedback messages."], + [ + "Auth Powered By", + ROUTES.AUTH_POWERED_BY, + AuthPoweredByShowcase, + "Honey identity-provider attribution, variants, alignment, and custom marks.", + ], ["Avatar", ROUTES.AVATAR, AvatarShowcase, "Images, initials, and user identity."], ["Badge", ROUTES.BADGE, BadgeShowcase, "Compact labels, counts, and statuses."], ["Breadcrumb", ROUTES.BREADCRUMB, BreadcrumbShowcase, "Hierarchical navigation paths."], diff --git a/src/styles/themes/theme.css b/src/styles/themes/theme.css index 0dece37..7424b77 100644 --- a/src/styles/themes/theme.css +++ b/src/styles/themes/theme.css @@ -16,6 +16,26 @@ --color-base-200: oklch(95% 0.003 240); --color-base-300: oklch(91% 0.005 240); --color-base-content: oklch(12% 0.005 250); + --color-default: var(--color-base-200); + --color-default-foreground: var(--color-base-content); + --color-default-hover: var(--color-base-300); + --color-background: var(--color-base-100); + --color-foreground: var(--color-base-content); + --color-bg-body: var(--color-base-100); + --color-bg-inverse: var(--color-base-content); + --color-bg-primary: var(--color-base-200); + --color-bg-secondary: var(--color-base-300); + --color-bg-tertiary: var(--color-base-300); + --color-fg-body: var(--color-base-content); + --color-fg-inverse: var(--color-base-100); + --color-fg-primary: var(--color-base-content); + --color-fg-secondary: var(--color-base-content); + --color-fg-tertiary: var(--color-base-content); + --b1: var(--color-base-100); + --b2: var(--color-base-200); + --b3: var(--color-base-300); + --bc: var(--color-base-content); + --shade: var(--color-base-content); --color-primary: oklch(12% 0.005 250); --color-primary-content: oklch(98% 0.002 240); @@ -57,6 +77,26 @@ --color-base-200: oklch(8% 0.003 240); --color-base-300: oklch(20% 0.008 240); --color-base-content: oklch(100% 0 0); + --color-default: var(--color-base-200); + --color-default-foreground: var(--color-base-content); + --color-default-hover: var(--color-base-300); + --color-background: var(--color-base-100); + --color-foreground: var(--color-base-content); + --color-bg-body: var(--color-base-100); + --color-bg-inverse: var(--color-base-content); + --color-bg-primary: var(--color-base-200); + --color-bg-secondary: var(--color-base-300); + --color-bg-tertiary: var(--color-base-300); + --color-fg-body: var(--color-base-content); + --color-fg-inverse: var(--color-base-100); + --color-fg-primary: var(--color-base-content); + --color-fg-secondary: var(--color-base-content); + --color-fg-tertiary: var(--color-base-content); + --b1: var(--color-base-100); + --b2: var(--color-base-200); + --b3: var(--color-base-300); + --bc: var(--color-base-content); + --shade: var(--color-base-content); --color-primary: #c9a0ff; --color-primary-content: #111111; diff --git a/tests/ps-qa/checks/coverage.ron b/tests/ps-qa/checks/coverage.ron index 6e79d0d..6b4dd8d 100644 --- a/tests/ps-qa/checks/coverage.ron +++ b/tests/ps-qa/checks/coverage.ron @@ -435,7 +435,7 @@ group: "coverage", what: "normal navigation unmounts the fullscreen specimen cleanly", open: Some("Home"), hover: None, click: None, - subject: "heading:Build Exceptional UIs", expect: Present, + subject: "heading:Build Exceptional UI/", expect: Present, ), ( id: "coverage-reopens-the-component-menu-after-fullscreen-navigation", diff --git a/tests/ps-qa/checks/showcase.ron b/tests/ps-qa/checks/showcase.ron index afa2284..f087b60 100644 --- a/tests/ps-qa/checks/showcase.ron +++ b/tests/ps-qa/checks/showcase.ron @@ -17,7 +17,7 @@ open: Some("Home"), hover: None, click: None, - subject: "heading:Build Exceptional UIs", + subject: "heading:Build Exceptional UI/", expect: Present, ), ( @@ -115,10 +115,10 @@ ( id: "the-showcases-page-is-reachable", group: "showcase", - what: "the navigation reaches the component showcases", - open: Some("Showcases"), + what: "the landing page reaches the complete component catalog without a duplicate top-level destination", + open: Some("Home"), hover: None, - click: None, + click: Some("link:Explore Components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 3000, @@ -127,10 +127,10 @@ id: "the-logo-goes-home", group: "showcase", what: "the header's own name is a link back to the landing page", - open: Some("Showcases"), + open: Some("Docs"), hover: None, click: Some("link:UI"), - subject: "heading:Build Exceptional UIs", + subject: "heading:Build Exceptional UI/", expect: Present, outcome_timeout_ms: 3000, ), @@ -141,7 +141,7 @@ open: Some("Docs"), hover: None, click: Some("link:Home"), - subject: "heading:Build Exceptional UIs", + subject: "heading:Build Exceptional UI/", expect: Present, outcome_timeout_ms: 3000, ), @@ -303,6 +303,87 @@ subject: "heading:Quick Start", expect: Present, ), + ( + id: "the-component-menu-exposes-the-surfaces-category", + group: "showcase", + what: "the component menu exposes the category containing the Honey attribution", + open: Some("Home"), + hover: None, + click: Some("Components"), + subject: "button:Surfaces", + expect: Paints, + ), + ( + id: "the-surfaces-category-lists-the-honey-component", + group: "showcase", + what: "Auth Powered By is discoverable in the component navigation rather than buried in the coverage lab", + open: None, + hover: None, + click: Some("button:Surfaces"), + subject: "link:Auth Powered By", + expect: Paints, + ), + ( + id: "the-honey-component-has-a-dedicated-showcase", + group: "showcase", + what: "the component navigation opens a focused Honey attribution page", + open: None, + hover: None, + click: Some("link:Auth Powered By"), + subject: "heading:Auth Powered By", + expect: Present, + outcome_timeout_ms: 3000, + ), + ( + id: "the-honey-showcase-renders-the-real-default", + group: "showcase", + what: "the default Secure Auth by Honey component is rendered with its shipped label", + open: None, + hover: None, + click: None, + subject: "link:Secure Auth by Honey", + expect: Paints, + ), + ( + id: "the-honey-showcase-renders-the-card-variant", + group: "showcase", + what: "the card treatment is demonstrated as a live component", + open: None, + hover: None, + click: None, + subject: "link:Honey card attribution", + expect: Paints, + ), + ( + id: "the-honey-showcase-renders-the-inline-variant", + group: "showcase", + what: "the inline treatment is demonstrated separately from the card", + open: None, + hover: None, + click: None, + subject: "link:Honey inline attribution", + expect: Paints, + ), + ( + id: "the-honey-showcase-renders-all-alignments", + group: "showcase", + what: "the right-aligned example reaches the end of the alignment set", + open: None, + hover: None, + click: None, + subject: "link:Honey attribution aligned right", + expect: Paints, + ), + ( + id: "the-default-honey-attribution-is-an-enabled-link", + group: "showcase", + what: "the shipped Honey destination remains an enabled semantic link", + open: None, + hover: None, + click: None, + subject: "#honey-default--link", + expect: Enabled, + ), ( id: "the-github-link-keeps-the-showcase-open", group: "showcase", diff --git a/tests/ps-qa/checks/theming.ron b/tests/ps-qa/checks/theming.ron index 9f18312..a32994a 100644 --- a/tests/ps-qa/checks/theming.ron +++ b/tests/ps-qa/checks/theming.ron @@ -1572,6 +1572,18 @@ subject: "status:Theme applied to whole site", expect: PaintsNamed, ), + ( + id: "whole_site_foreground_updates_the_header", + group: "theming", + what: "the generated base-content foreground and its public aliases repaint the top navigation", + open: None, + prepare: Some("button:Dark mode"), + hover: None, + click: Some("button:Light mode"), + subject: "link:Home", + expect: PixelsChange, + outcome_timeout_ms: 8000, + ), ( id: "whole_site_application_can_be_removed", group: "theming", @@ -2513,19 +2525,6 @@ subject: "status:Theme builder reset", expect: PaintsNamed, ), - ( - id: "the-glass-opacity-slider-responds-to-a-real-pointer", - group: "theming", - what: "the UI opacity slider retains a real pointer change", - open: None, - prepare: Some("slider:Opacity"), - prepare_key: Some("Home"), - hover: None, - click: None, - pointer_drag: Some((from: "slider:Opacity", dx: 100.0, dy: 0.0, steps: 4)), - subject: "slider:Opacity", - expect: ValueChanges, - ), ( id: "the-glass-blur-slider-repaints-the-preview", group: "theming", @@ -2571,46 +2570,6 @@ expect: PixelsChange, outcome_timeout_ms: 2500, ), - ( - id: "the-glass-opacity-slider-repaints-the-preview", - group: "theming", - what: "glass opacity visibly changes the live material preview", - open: None, - prepare: Some("slider:Opacity"), - prepare_key: Some("Home"), - hover: None, - click: None, - pointer_drag: Some((from: "slider:Opacity", dx: 100.0, dy: 0.0, steps: 4)), - subject: "#theme-preview-glass-card", - expect: PixelsChange, - outcome_timeout_ms: 2500, - ), - ( - id: "the-glass-scrim-slider-responds-to-a-real-pointer", - group: "theming", - what: "the UI scrim slider retains a real pointer change", - open: None, - prepare: Some("slider:Scrim"), - prepare_key: Some("Home"), - hover: None, - click: None, - pointer_drag: Some((from: "slider:Scrim", dx: 100.0, dy: 0.0, steps: 4)), - subject: "slider:Scrim", - expect: ValueChanges, - ), - ( - id: "the-glass-scrim-slider-repaints-the-preview", - group: "theming", - what: "glass scrim visibly darkens the live preview backdrop", - open: None, - prepare: Some("slider:Scrim"), - prepare_key: Some("Home"), - hover: None, - click: None, - pointer_drag: Some((from: "slider:Scrim", dx: 100.0, dy: 0.0, steps: 4)), - subject: "#theme-preview", - expect: PixelsChange, - ), ( id: "the-preview-referral-select-opens", group: "theming", diff --git a/tests/ps-qa/checks/unlisted.ron b/tests/ps-qa/checks/unlisted.ron index 6877562..2ef9bb1 100644 --- a/tests/ps-qa/checks/unlisted.ron +++ b/tests/ps-qa/checks/unlisted.ron @@ -27,9 +27,9 @@ id: "the-unlisted-group-starts-from-the-showcase-index", group: "unlisted", what: "the index that lists every route, including the ones the menu omits, is reachable", - open: Some("Showcases"), + open: Some("Home"), hover: None, - click: None, + click: Some("link:Explore Components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 8000, @@ -97,7 +97,7 @@ what: "the header returns to the index, which is how this group moves between pages", open: None, hover: None, - click: Some("link:Showcases"), + click: Some("link:All components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 8000, @@ -142,7 +142,7 @@ what: "the header returns to the index", open: None, hover: None, - click: Some("link:Showcases"), + click: Some("link:All components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 8000, @@ -186,7 +186,7 @@ what: "the header returns to the index", open: None, hover: None, - click: Some("link:Showcases"), + click: Some("link:All components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 8000, @@ -219,7 +219,7 @@ what: "the header returns to the index", open: None, hover: None, - click: Some("link:Showcases"), + click: Some("link:All components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 8000, @@ -273,7 +273,7 @@ what: "the header returns to the index", open: None, hover: None, - click: Some("link:Showcases"), + click: Some("link:All components"), subject: "heading:Component Showcases", expect: Present, outcome_timeout_ms: 8000, diff --git a/tests/ps-qa/ps-qa.ron b/tests/ps-qa/ps-qa.ron index 3373246..1dbf828 100644 --- a/tests/ps-qa/ps-qa.ron +++ b/tests/ps-qa/ps-qa.ron @@ -6,10 +6,9 @@ AppProfile( (name: "theming", opener: "Theming", marker: Some("Random")), (name: "docs", opener: "Docs", marker: Some("Browse Components")), (name: "layouts", opener: "Layouts", marker: Some("#layouts-documentation")), - (name: "showcases", opener: "Showcases", marker: Some("Theme Customization")), (name: "coverage", opener: "Complete Coverage", marker: Some("Load LiveChatPanel")), ], - navigation_controls: ["Home", "Theming", "Docs", "Layouts", "Showcases", "Components", "Complete Coverage"], + navigation_controls: ["Home", "Theming", "Docs", "Layouts", "Explore Components", "Components", "Complete Coverage"], inert_controls: ["Bottom with arrow"], home_opener: Some("Home"), ) From 3da705389fb877f7d104d237cea3c60a091b06f4 Mon Sep 17 00:00:00 2001 From: meh Date: Sun, 13 Sep 2026 01:47:54 +0700 Subject: [PATCH 2/8] fix(theming): guarantee readable generated themes --- src/components/content/Search.tsx | 2 +- .../Header/components/MainNavigation.tsx | 4 +- src/components/theming/ColorGroup.tsx | 6 +- src/components/theming/SizesSection.tsx | 8 +- src/components/theming/ThemeCSSModal.tsx | 121 +----------- src/components/theming/ThemeComposer.tsx | 35 +++- src/lib/themeAliases.ts | 14 ++ src/lib/themeCSS.ts | 119 ++++++++++++ src/lib/themeComposer.ts | 176 +++++++++++++++--- src/pages/Showcases.tsx | 3 +- src/pages/Theming.tsx | 5 +- src/pages/docs/Components.tsx | 160 ---------------- src/routes.ts | 2 +- tests/ps-qa/checks/docs.ron | 2 +- tests/ps-qa/checks/showcase.ron | 6 +- tests/ps-qa/checks/theming.ron | 67 +++++++ tests/ps-qa/checks/unlisted.ron | 12 +- tests/themeCSSModal.test.ts | 50 +++++ tests/themeComposer.test.ts | 131 +++++++++++++ 19 files changed, 593 insertions(+), 330 deletions(-) create mode 100644 src/lib/themeCSS.ts delete mode 100644 src/pages/docs/Components.tsx create mode 100644 tests/themeCSSModal.test.ts create mode 100644 tests/themeComposer.test.ts diff --git a/src/components/content/Search.tsx b/src/components/content/Search.tsx index 03a9316..c54eb8e 100644 --- a/src/components/content/Search.tsx +++ b/src/components/content/Search.tsx @@ -50,7 +50,7 @@ export const Search: Component = (props) => { DOCS_LAYOUTS: "Solid Layouts", DOCS_USAGE: "Usage Cheatsheet", THEMING: "Theme Editor", - SHOWCASES: "Component Showcases", + SHOWCASES: "Components", CHAT_BUBBLE: "Chat Bubble", COLOR_PICKER: "Color Picker", FILE_INPUT: "File Input", diff --git a/src/components/layout/Header/components/MainNavigation.tsx b/src/components/layout/Header/components/MainNavigation.tsx index 957ba10..2a98878 100644 --- a/src/components/layout/Header/components/MainNavigation.tsx +++ b/src/components/layout/Header/components/MainNavigation.tsx @@ -20,7 +20,7 @@ export const MainNavigation: Component = (props) => { href={item.href} class={`px-3 py-2 rounded-lg transition-colors font-medium text-base ${ isActive(item.href) - ? "bg-primary/20 text-primary" + ? "bg-primary text-primary-content" : "text-base-content hover:bg-base-200" }`} > @@ -37,7 +37,7 @@ export const MainNavigation: Component = (props) => { }} class={`px-3 py-2 rounded-lg transition-colors font-medium text-base ${ activeCategory() === item.title - ? "bg-primary/20 text-primary" + ? "bg-primary text-primary-content" : "text-base-content hover:bg-base-200" }`} > diff --git a/src/components/theming/ColorGroup.tsx b/src/components/theming/ColorGroup.tsx index a94d0c4..19f1d4a 100644 --- a/src/components/theming/ColorGroup.tsx +++ b/src/components/theming/ColorGroup.tsx @@ -32,7 +32,10 @@ export default function ColorGroup(props: ColorGroupProps) { id={`theme-color-${colorKey.replace(/^--/, "").replace(/[^a-z0-9_-]+/gi, "-")}`} onClick={(event: MouseEvent) => props.onColorClick(colorKey, event)} class="w-8 h-8 rounded border border-gray-300 hover:border-gray-400 transition-colors relative group flex items-center justify-center" - style={{ background: backgroundColor() }} + style={{ + background: backgroundColor(), + color: textColor(), + }} title={colorKey} >

{label} diff --git a/src/components/theming/SizesSection.tsx b/src/components/theming/SizesSection.tsx index d8eabeb..edd97fa 100644 --- a/src/components/theming/SizesSection.tsx +++ b/src/components/theming/SizesSection.tsx @@ -77,7 +77,7 @@ export default function SizesSection(props: SizesSectionProps) {
{sizeType.label}
-
{sizeType.description}
+
{sizeType.description}
@@ -99,7 +99,7 @@ export default function SizesSection(props: SizesSectionProps) { style={{ height: `${heightPercent}%` }} />
-
+
{SIZE_LABELS[index()]} {pixelValue}
@@ -131,12 +131,12 @@ export default function SizesSection(props: SizesSectionProps) {
Border Width
-
All components
+
All components
-
+
0.5px 2px
diff --git a/src/components/theming/ThemeCSSModal.tsx b/src/components/theming/ThemeCSSModal.tsx index 1d9fa93..bc087af 100644 --- a/src/components/theming/ThemeCSSModal.tsx +++ b/src/components/theming/ThemeCSSModal.tsx @@ -1,11 +1,8 @@ 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, - Theme, -} from "../../utils/themeUtils"; +import type { Theme } from "../../utils/themeUtils"; +import { generateThemeCSS } from "../../lib/themeCSS"; interface ThemeCSSModalProps { open: boolean; @@ -16,117 +13,11 @@ 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) => { - /* - * Plain CSS. - * - * This used to export a theme in a format the site itself no longer uses: - * a theme in @pathscale/ui is a block of custom properties on a selector - * and nothing more. Anyone pasting the old output into this repository - * would have got a rule that never applied, and a plugin directive for a - * package that is not installed. - * - * `default` decides whether the theme also claims `:root`, and - * `prefersdark` whether it answers the OS preference - the two things the - * plugin's booleans meant, expressed as selectors and a media query. - */ - const selectors = [ - props.isDefault ? ":root" : null, - `[data-theme="${escapeCssString(theme.name)}"]`, - ].filter(Boolean).join(",\n"); - const baseProps = [` color-scheme: ${props.colorScheme || "light"};`]; - - // Colour properties in a fixed order, so a regenerated theme diffs cleanly. - const colorOrder = [ - "--color-base-100", "--color-base-content", "--color-base-200", "--color-base-300", - "--color-primary", "--color-primary-content", - "--color-secondary", "--color-secondary-content", - "--color-accent", "--color-accent-content", - "--color-neutral", "--color-neutral-content", - "--color-info", "--color-info-content", - "--color-success", "--color-success-content", - "--color-warning", "--color-warning-content", - "--color-error", "--color-error-content" - ]; - - const colorProps = colorOrder - .filter(key => theme[key]) - .map(key => ` ${key}: ${theme[key]};`); - - const aliasOrder = [ - "--color-default", "--color-default-foreground", "--color-default-hover", - "--color-background", "--color-foreground", - "--color-bg-body", "--color-bg-inverse", "--color-bg-primary", - "--color-bg-secondary", "--color-bg-tertiary", - "--color-fg-body", "--color-fg-inverse", "--color-fg-primary", - "--color-fg-secondary", "--color-fg-tertiary", - "--b1", "--b2", "--b3", "--bc", "--shade", - ]; - const aliasProps = aliasOrder - .filter(key => theme[key]) - .map(key => ` ${key}: ${theme[key]};`); - - // Add default radius, size and effect values if not present - const defaultValues: Record = { - "--radius-selector": "0.5rem", - "--radius-field": "0.25rem", - "--radius-box": "0.5rem", - "--size-selector": "0.25rem", - "--size-field": "0.25rem", - "--border": "1px", - "--depth": "1", - "--noise": "0", - ...GLASS_THEME_DEFAULTS, - }; - - const radiusProps = [ - ` --radius-selector: ${theme["--radius-selector"] || defaultValues["--radius-selector"]};`, - ` --radius-field: ${theme["--radius-field"] || defaultValues["--radius-field"]};`, - ` --radius-box: ${theme["--radius-box"] || defaultValues["--radius-box"]};` - ]; - - const sizeProps = [ - ` --size-selector: ${theme["--size-selector"] || defaultValues["--size-selector"]};`, - ` --size-field: ${theme["--size-field"] || defaultValues["--size-field"]};`, - ` --border: ${theme["--border"] || defaultValues["--border"]};` - ]; - - const effectProps = [ - ` --depth: ${theme["--depth"] || defaultValues["--depth"]};`, - ` --noise: ${theme["--noise"] || defaultValues["--noise"]};` - ]; - - const glassProps = GLASS_THEME_TOKEN_ORDER.map( - (key) => ` ${key}: ${theme[key] || defaultValues[key]};` - ); - - const allProps = [ - ...baseProps, - ...colorProps, - ...aliasProps, - ...radiusProps, - ...sizeProps, - ...effectProps, - ...glassProps - ]; - - const block = `${selectors} {\n${allProps.join("\n")}\n}`; - return props.isPrefersDark - ? `${block}\n\n@media (prefers-color-scheme: dark) {\n :root {\n${allProps.map((l) => ` ${l}`).join("\n")}\n }\n}` - : block; - }; - // Solid 2 splits an effect in two: the first function tracks and returns, // the second acts on that value. The one-argument form throws // MISSING_EFFECT_FN. @@ -134,7 +25,13 @@ export default function ThemeCSSModal(props: ThemeCSSModalProps) { () => ({ open: props.open, theme: props.theme }), ({ open, theme }) => { if (open) { - setCssText(generateCSS(theme)); + setCssText( + generateThemeCSS(theme, { + isDefault: props.isDefault, + isPrefersDark: props.isPrefersDark, + colorScheme: props.colorScheme, + }), + ); setCopyStatus("Copy generated CSS"); setIsClipboardButtonPressed(false); } diff --git a/src/components/theming/ThemeComposer.tsx b/src/components/theming/ThemeComposer.tsx index 2d578ec..4a1e8a2 100644 --- a/src/components/theming/ThemeComposer.tsx +++ b/src/components/theming/ThemeComposer.tsx @@ -51,12 +51,20 @@ const colorsMatch = (stored: string, swatch: string) => { ); }; +const storedSelection = (value: string | undefined) => { + const parsed = Number.parseInt(value ?? "", 10); + return Number.isInteger(parsed) && parsed >= 0 && parsed < 7 + ? parsed + : undefined; +}; + function AccentSelector(props: { id: string; label: string; hint: string; value: string; options: readonly string[]; + selectedIndex?: number; onPick: (value: string) => void; }) { return ( @@ -65,12 +73,15 @@ function AccentSelector(props: { {props.label} - {props.hint} + {props.hint}
{(option, index) => { - const selected = () => colorsMatch(props.value, option); + const selected = () => + props.selectedIndex === undefined + ? colorsMatch(props.value, option) + : props.selectedIndex === index(); return (