-
Component Showcases
+
Components
All {componentFamilies.length} public @pathscale/ui component families,
with focused demonstrations and a complete coverage lab.
diff --git a/src/pages/Theming.tsx b/src/pages/Theming.tsx
index 7961768..2d51a93 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,
@@ -81,6 +82,7 @@ import {
DEFAULT_STRENGTH,
DEFAULT_SURFACE,
DEFAULT_TEXT_BRIGHTNESS,
+ enforceThemeInkContrast,
SOFTNESS_STOPS,
STRENGTH_STOPS,
surfaceColors,
@@ -96,6 +98,11 @@ const withIdentity = (theme: Theme): Theme => ({
_id: theme._id || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
});
+const normalizeTheme = (theme: Theme): Theme =>
+ withIdentity(
+ withThemeAliases(withGlassThemeDefaults(enforceThemeInkContrast(theme))),
+ );
+
const randomItem = (values: readonly T[]): T =>
values[Math.floor(Math.random() * values.length)];
@@ -150,15 +157,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 +172,7 @@ const restoreThemeLibrary = (
linkedCurrentTheme = true;
return currentTheme;
}
- return withIdentity(withGlassThemeDefaults(theme));
+ return normalizeTheme(theme);
});
return { currentTheme, themes };
@@ -217,7 +220,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 +295,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 +323,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 +332,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
deleted file mode 100644
index e1285c7..0000000
--- a/src/pages/docs/Components.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { Component, For } from "solid-js";
-import { ContentContainer } from "../../components/content/ContentContainer";
-import { Card, Flex, Grid } from "@pathscale/ui";
-import { routes } from "../../routes";
-import { ROUTES } from "../../config/routes";
-
-const Components: Component = () => {
- // Filter out non-component routes
- const componentRoutes = routes.filter(
- (route) =>
- route.path !== ROUTES.HOME &&
- route.path !== ROUTES.DOCS &&
- route.path !== ROUTES.THEMING &&
- !route.path.startsWith("/docs/")
- );
-
- // Group components by category
- const categories = {
- Layout: ["Grid", "Flex", "Stack", "Divider"],
- Navigation: [
- "Navbar",
- "Breadcrumb",
- "Sidenav",
- "Steps",
- "Tabs",
- "Menu",
- "Pagination",
- ],
- "Data Display": [
- "Card",
- "Table",
- "Stats",
- "Timeline",
- "Badge",
- "Avatar",
- "Indicator",
- ],
- Feedback: [
- "Alert",
- "Spinner",
- "Progress",
- "Radial Progress",
- "Toast",
- "Skeleton",
- ],
- "Forms & Inputs": [
- "Button",
- "Input",
- "Textarea",
- "Select",
- "Checkbox",
- "Radio",
- "Range",
- "Switch",
- "File Input",
- "Rating",
- ],
- "Overlays & Popups": ["Dialog", "Dropdown", "Tooltip", "Drawer"],
- "Media & Display": [
- "Carousel",
- "Artboard",
- "Browser Mockup",
- "Phone Mockup",
- "Window Mockup",
- "Code Mockup",
- "Mask",
- ],
- "Utilities & Helpers": [
- "Kbd",
- "Join",
- "Collapse",
- "Accordion",
- "Swap",
- "Countdown",
- "Diff",
- "Copy Button",
- ],
- };
-
- const categoryDescriptions = {
- Layout:
- "Core components for structuring your application's layout with flexible grids and spacing utilities.",
- Navigation:
- "Components for building intuitive navigation systems and user flows.",
- "Data Display":
- "Elements for presenting data and information in clear, organized formats.",
- Feedback:
- "Interactive components that provide visual feedback and status updates to users.",
- "Forms & Inputs":
- "Form components designed for optimal user input and data collection.",
- "Overlays & Popups":
- "Modal and overlay components for additional content and interactions.",
- "Media & Display":
- "Components for showcasing media content and creating visual mockups.",
- "Utilities & Helpers":
- "Utility components that enhance functionality and user experience.",
- };
-
- const getComponentsByCategory = (category: string) => {
- return componentRoutes.filter((route) =>
- categories[category as keyof typeof categories]?.some((comp) =>
- route.name.toLowerCase().includes(comp.toLowerCase())
- )
- );
- };
-
- return (
-
-
-
Component Library
-
- Explore our extensive collection of UI components, thoughtfully
- organized by category. Each component is designed with accessibility,
- performance, and developer experience in mind.
-
-
-
-
-
- {(category) => {
- const components = getComponentsByCategory(category);
- return (
-
- );
- }}
-
-
-
- );
-};
-
-export default Components;
diff --git a/src/routes.ts b/src/routes.ts
index 1d44485..f60ca00 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."],
@@ -203,6 +211,6 @@ export const routes: RouteConfig[] = [
{ name: "Installation", path: ROUTES.DOCS_INSTALLATION, component: DocsInstallation, description: "Install UI and Solid Layouts." },
{ name: "Solid Layouts", path: ROUTES.DOCS_LAYOUTS, component: DocsLayouts, description: "Understand the two compiler passes." },
{ name: "Usage Cheatsheet", path: ROUTES.DOCS_USAGE, component: DocsUsage, description: "Common component patterns." },
- { name: "Showcases", path: ROUTES.SHOWCASES, component: Showcases, description: "Current interactive examples." },
+ { name: "Components", path: ROUTES.SHOWCASES, component: Showcases, description: "Current interactive examples." },
...componentRoutes,
];
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/docs.ron b/tests/ps-qa/checks/docs.ron
index c9376f5..9f51776 100644
--- a/tests/ps-qa/checks/docs.ron
+++ b/tests/ps-qa/checks/docs.ron
@@ -268,7 +268,7 @@
open: Some("Layouts"),
hover: None,
click: Some("link:Component showcases"),
- subject: "heading:Component Showcases",
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
diff --git a/tests/ps-qa/checks/showcase.ron b/tests/ps-qa/checks/showcase.ron
index afa2284..c0bcbc1 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,11 +115,11 @@
(
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,
- subject: "heading:Component Showcases",
+ click: Some("link:Explore Components"),
+ subject: "heading:Components",
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,
),
@@ -260,7 +260,7 @@
open: Some("Home"),
hover: None,
click: Some("link:Explore Components"),
- subject: "heading:Component Showcases",
+ subject: "heading:Components",
expect: Present,
),
(
@@ -280,7 +280,7 @@
open: Some("Home"),
hover: None,
click: Some("link:Explore all components"),
- subject: "heading:Component Showcases",
+ subject: "heading:Components",
expect: Present,
),
(
@@ -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..0b87003 100644
--- a/tests/ps-qa/checks/theming.ron
+++ b/tests/ps-qa/checks/theming.ron
@@ -66,6 +66,7 @@
click: Some("button:Light mode"),
subject: "button:Light mode",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-theme-flower-updates-the-surface",
@@ -77,6 +78,7 @@
click: Some("radio:Theme color #"),
subject: "#theme-preview-glass-card",
expect: PixelsChange,
+ outcome_timeout_ms: 8000,
),
(
id: "the-theme-composer-offers-five-strength-stops",
@@ -99,6 +101,7 @@
click: Some("button:Colour strength 50%"),
subject: "button:Colour strength 50%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "theme-strength-repaints-the-preview",
@@ -133,6 +136,7 @@
click: Some("button:Control accent 1"),
subject: "button:Control accent 1",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-theme-composer-offers-artwork-accent-friends",
@@ -155,6 +159,7 @@
click: Some("button:Artwork accent 1"),
subject: "button:Artwork accent 1",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "and-a-swatch-per-token",
@@ -259,6 +264,7 @@
click: Some("radio:Boxes radius 4 pixels"),
subject: "radio:Boxes radius 4 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-field-size-slider-updates-the-token",
@@ -273,6 +279,7 @@
key_on: Some("slider:Fields base size"),
subject: "slider:Fields base size",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-selector-size-slider-updates-the-token",
@@ -287,6 +294,7 @@
key_on: Some("slider:Selectors base size"),
subject: "slider:Selectors base size",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-border-slider-updates-the-token",
@@ -301,6 +309,7 @@
key_on: Some("slider:Border width"),
subject: "slider:Border width",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-glass-depth-slider-responds-to-a-real-pointer",
@@ -314,6 +323,7 @@
pointer_drag: Some((from: "slider:Depth", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Depth",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-glass-depth-slider-visibly-moves",
@@ -327,6 +337,7 @@
pointer_drag: Some((from: "slider:Depth", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Depth",
expect: PixelsChange,
+ outcome_timeout_ms: 8000,
),
(
id: "the-field-size-slider-responds-to-a-real-pointer",
@@ -340,6 +351,7 @@
pointer_drag: Some((from: "slider:Fields base size", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Fields base size",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-field-size-slider-visibly-moves",
@@ -353,6 +365,7 @@
pointer_drag: Some((from: "slider:Fields base size", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Fields base size",
expect: PixelsChange,
+ outcome_timeout_ms: 8000,
),
(
id: "the-selector-size-slider-responds-to-a-real-pointer",
@@ -366,6 +379,7 @@
pointer_drag: Some((from: "slider:Selectors base size", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Selectors base size",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-selector-size-slider-visibly-moves",
@@ -379,6 +393,7 @@
pointer_drag: Some((from: "slider:Selectors base size", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Selectors base size",
expect: PixelsChange,
+ outcome_timeout_ms: 8000,
),
(
id: "the-border-slider-responds-to-a-real-pointer",
@@ -392,6 +407,7 @@
pointer_drag: Some((from: "slider:Border width", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Border width",
expect: ValueChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "the-border-slider-visibly-moves",
@@ -405,6 +421,7 @@
pointer_drag: Some((from: "slider:Border width", dx: 100.0, dy: 0.0, steps: 4)),
subject: "slider:Border width",
expect: PixelsChange,
+ outcome_timeout_ms: 8000,
),
(
id: "the-preview-price-slider-is-labelled-and-interactive",
@@ -1386,6 +1403,7 @@
click: Some("radio:Boxes radius square"),
subject: "radio:Boxes radius square",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "boxes_4_pixels_can_be_selected",
@@ -1397,6 +1415,7 @@
click: Some("radio:Boxes radius 4 pixels"),
subject: "radio:Boxes radius 4 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "boxes_8_pixels_can_be_selected",
@@ -1408,6 +1427,7 @@
click: Some("radio:Boxes radius 8 pixels"),
subject: "radio:Boxes radius 8 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "boxes_16_pixels_can_be_selected",
@@ -1419,6 +1439,7 @@
click: Some("radio:Boxes radius 16 pixels"),
subject: "radio:Boxes radius 16 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "boxes_32_pixels_can_be_selected",
@@ -1430,6 +1451,7 @@
click: Some("radio:Boxes radius 32 pixels"),
subject: "radio:Boxes radius 32 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "fields_square_can_be_selected",
@@ -1441,6 +1463,7 @@
click: Some("radio:Fields radius square"),
subject: "radio:Fields radius square",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "fields_4_pixels_can_be_selected",
@@ -1452,6 +1475,7 @@
click: Some("radio:Fields radius 4 pixels"),
subject: "radio:Fields radius 4 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "fields_8_pixels_can_be_selected",
@@ -1463,6 +1487,7 @@
click: Some("radio:Fields radius 8 pixels"),
subject: "radio:Fields radius 8 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "fields_16_pixels_can_be_selected",
@@ -1474,6 +1499,7 @@
click: Some("radio:Fields radius 16 pixels"),
subject: "radio:Fields radius 16 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "fields_32_pixels_can_be_selected",
@@ -1485,6 +1511,7 @@
click: Some("radio:Fields radius 32 pixels"),
subject: "radio:Fields radius 32 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "selectors_square_can_be_selected",
@@ -1496,6 +1523,7 @@
click: Some("radio:Selectors radius square"),
subject: "radio:Selectors radius square",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "selectors_4_pixels_can_be_selected",
@@ -1507,6 +1535,7 @@
click: Some("radio:Selectors radius 4 pixels"),
subject: "radio:Selectors radius 4 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "selectors_8_pixels_can_be_selected",
@@ -1518,6 +1547,7 @@
click: Some("radio:Selectors radius 8 pixels"),
subject: "radio:Selectors radius 8 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "selectors_16_pixels_can_be_selected",
@@ -1529,6 +1559,7 @@
click: Some("radio:Selectors radius 16 pixels"),
subject: "radio:Selectors radius 16 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "selectors_32_pixels_can_be_selected",
@@ -1540,6 +1571,7 @@
click: Some("radio:Selectors radius 32 pixels"),
subject: "radio:Selectors radius 32 pixels",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "glass_defaults_execute_their_reset_callback",
@@ -1572,6 +1604,29 @@
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_header_keeps_readable_contrast",
+ group: "theming",
+ what: "the generated header text remains readable against the applied site theme",
+ open: None,
+ hover: None,
+ click: None,
+ subject: "link:Home",
+ expect: Contrast,
+ outcome_timeout_ms: 8000,
+ ),
(
id: "whole_site_application_can_be_removed",
group: "theming",
@@ -1592,6 +1647,7 @@
click: Some("button:Colour strength 20%"),
subject: "button:Colour strength 20%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "remembering_a_theme_writes_persistence",
@@ -1634,6 +1690,7 @@
click: Some("button:Colour strength 10%"),
subject: "button:Colour strength 20%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "returning-to-the-editor-restores-the-page-top",
@@ -2235,6 +2292,7 @@
click: Some("button:Colour strength 20%"),
subject: "button:Colour strength 20%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "colour_strength_30_is_selectable",
@@ -2246,6 +2304,7 @@
click: Some("button:Colour strength 30%"),
subject: "button:Colour strength 30%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "colour_strength_40_is_selectable",
@@ -2257,6 +2316,7 @@
click: Some("button:Colour strength 40%"),
subject: "button:Colour strength 40%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "softness_0_is_selectable",
@@ -2268,6 +2328,7 @@
click: Some("button:Softness 0%"),
subject: "button:Softness 0%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "softness_25_is_selectable",
@@ -2279,6 +2340,7 @@
click: Some("button:Softness 25%"),
subject: "button:Softness 25%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "softness_50_is_selectable",
@@ -2290,6 +2352,7 @@
click: Some("button:Softness 50%"),
subject: "button:Softness 50%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "softness_75_is_selectable",
@@ -2301,6 +2364,7 @@
click: Some("button:Softness 75%"),
subject: "button:Softness 75%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "softness_100_is_selectable",
@@ -2312,6 +2376,7 @@
click: Some("button:Softness 100%"),
subject: "button:Softness 100%",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "text_brightness_minus_4_is_selectable",
@@ -2323,6 +2388,7 @@
click: Some("button:Text brightness -4"),
subject: "button:Text brightness -4",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "text_brightness_minus_2_is_selectable",
@@ -2334,6 +2400,7 @@
click: Some("button:Text brightness -2"),
subject: "button:Text brightness -2",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "text_brightness_0_is_selectable",
@@ -2345,6 +2412,7 @@
click: Some("button:Text brightness 0"),
subject: "button:Text brightness 0",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "text_brightness_3_is_selectable",
@@ -2356,6 +2424,7 @@
click: Some("button:Text brightness +3"),
subject: "button:Text brightness +3",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "text_brightness_6_is_selectable",
@@ -2367,6 +2436,7 @@
click: Some("button:Text brightness +6"),
subject: "button:Text brightness +6",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "control_accent_3_is_selectable",
@@ -2378,6 +2448,7 @@
click: Some("button:Control accent 3"),
subject: "button:Control accent 3",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "control_accent_4_is_selectable",
@@ -2389,6 +2460,7 @@
click: Some("button:Control accent 4"),
subject: "button:Control accent 4",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "control_accent_5_is_selectable",
@@ -2400,6 +2472,7 @@
click: Some("button:Control accent 5"),
subject: "button:Control accent 5",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "control_accent_6_is_selectable",
@@ -2411,6 +2484,7 @@
click: Some("button:Control accent 6"),
subject: "button:Control accent 6",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "control_accent_7_is_selectable",
@@ -2422,6 +2496,7 @@
click: Some("button:Control accent 7"),
subject: "button:Control accent 7",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "artwork_accent_3_is_selectable",
@@ -2433,6 +2508,7 @@
click: Some("button:Artwork accent 3"),
subject: "button:Artwork accent 3",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "artwork_accent_4_is_selectable",
@@ -2444,6 +2520,7 @@
click: Some("button:Artwork accent 4"),
subject: "button:Artwork accent 4",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "artwork_accent_5_is_selectable",
@@ -2455,6 +2532,7 @@
click: Some("button:Artwork accent 5"),
subject: "button:Artwork accent 5",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "artwork_accent_6_is_selectable",
@@ -2466,6 +2544,7 @@
click: Some("button:Artwork accent 6"),
subject: "button:Artwork accent 6",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "artwork_accent_7_is_selectable",
@@ -2477,6 +2556,7 @@
click: Some("button:Artwork accent 7"),
subject: "button:Artwork accent 7",
expect: SelectionChanges,
+ outcome_timeout_ms: 8000,
),
(
id: "theme-softness-repaints-the-preview",
@@ -2512,19 +2592,7 @@
click: Some("button:Reset"),
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,
+ outcome_timeout_ms: 8000,
),
(
id: "the-glass-blur-slider-repaints-the-preview",
@@ -2571,46 +2639,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..79b6d82 100644
--- a/tests/ps-qa/checks/unlisted.ron
+++ b/tests/ps-qa/checks/unlisted.ron
@@ -27,10 +27,10 @@
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,
- subject: "heading:Component Showcases",
+ click: Some("link:Explore Components"),
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
@@ -97,8 +97,8 @@
what: "the header returns to the index, which is how this group moves between pages",
open: None,
hover: None,
- click: Some("link:Showcases"),
- subject: "heading:Component Showcases",
+ click: Some("link:All components"),
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
@@ -142,8 +142,8 @@
what: "the header returns to the index",
open: None,
hover: None,
- click: Some("link:Showcases"),
- subject: "heading:Component Showcases",
+ click: Some("link:All components"),
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
@@ -186,8 +186,8 @@
what: "the header returns to the index",
open: None,
hover: None,
- click: Some("link:Showcases"),
- subject: "heading:Component Showcases",
+ click: Some("link:All components"),
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
@@ -219,8 +219,8 @@
what: "the header returns to the index",
open: None,
hover: None,
- click: Some("link:Showcases"),
- subject: "heading:Component Showcases",
+ click: Some("link:All components"),
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
@@ -273,8 +273,8 @@
what: "the header returns to the index",
open: None,
hover: None,
- click: Some("link:Showcases"),
- subject: "heading:Component Showcases",
+ click: Some("link:All components"),
+ subject: "heading:Components",
expect: Present,
outcome_timeout_ms: 8000,
),
diff --git a/tests/ps-qa/ps-qa.ron b/tests/ps-qa/ps-qa.ron
index 3373246..d8534f2 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")),
+ (name: "coverage", opener: "Complete Coverage", via: ["Components", "Surfaces"], marker: Some("Load LiveChatPanel")),
],
- navigation_controls: ["Home", "Theming", "Docs", "Layouts", "Showcases", "Components", "Complete Coverage"],
+ navigation_controls: ["Home", "Theming", "Docs", "Layouts", "Explore Components", "Components", "Surfaces", "Complete Coverage"],
inert_controls: ["Bottom with arrow"],
home_opener: Some("Home"),
)
diff --git a/tests/themeCSSModal.test.ts b/tests/themeCSSModal.test.ts
new file mode 100644
index 0000000..f5676bf
--- /dev/null
+++ b/tests/themeCSSModal.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from "bun:test";
+import { generateThemeCSS } from "../src/lib/themeCSS";
+import { withThemeAliases } from "../src/lib/themeAliases";
+import {
+ applyThemeComposition,
+ enforceThemeInkContrast,
+} from "../src/lib/themeComposer";
+import type { Theme } from "../src/types/theme";
+
+const seed: Theme = {
+ name: 'QA "theme"',
+ _themeType: "light",
+ "--color-primary": "#3366cc",
+ "--color-secondary": "#6633cc",
+ "--color-accent": "#0088aa",
+ "--color-neutral": "#46505a",
+ "--color-info": "#1769aa",
+ "--color-success": "#16834a",
+ "--color-warning": "#8a5b00",
+ "--color-error": "#b42335",
+};
+
+describe("theme CSS export", () => {
+ it("exports every live UI color token with the generated theme", () => {
+ const theme = withThemeAliases(
+ enforceThemeInkContrast(
+ applyThemeComposition(seed, {
+ surface: "#d9e8ff",
+ strength: 40,
+ softness: 6,
+ textBrightness: -2,
+ }),
+ ),
+ );
+ const css = generateThemeCSS(theme, {
+ isDefault: true,
+ isPrefersDark: true,
+ colorScheme: "light",
+ });
+
+ for (const [key, value] of Object.entries(theme)) {
+ if (key.startsWith("--color-")) {
+ expect(css).toContain(`${key}: ${value};`);
+ }
+ }
+ expect(css).toContain(":root,");
+ expect(css).toContain('@media (prefers-color-scheme: dark)');
+ expect(css).toContain('[data-theme="QA \\22 theme\\22 "]');
+ });
+});
diff --git a/tests/themeComposer.test.ts b/tests/themeComposer.test.ts
new file mode 100644
index 0000000..173af34
--- /dev/null
+++ b/tests/themeComposer.test.ts
@@ -0,0 +1,133 @@
+import { describe, expect, it, setDefaultTimeout } from "bun:test";
+import chroma from "chroma-js";
+import { calculateContrastRatio } from "../src/utils/theme/contrastCalculation";
+import {
+ accentOptions,
+ applyThemeComposition,
+ artworkAccentOptions,
+ enforceThemeInkContrast,
+ SOFTNESS_STOPS,
+ STRENGTH_STOPS,
+ surfaceColors,
+} from "../src/lib/themeComposer";
+import type { Theme } from "../src/types/theme";
+
+setDefaultTimeout(15_000);
+
+const seed = (mode: "light" | "dark"): Theme => ({
+ name: `${mode} test`,
+ _themeType: mode,
+} as Theme);
+
+describe("theme composition contrast", () => {
+ for (const mode of ["light", "dark"] as const) {
+ for (const surface of ["#000000", "#ffffff", "#7f00ff"]) {
+ it(`keeps shared ${mode} text readable on every ${surface} surface tier`, () => {
+ const theme = applyThemeComposition(seed(mode), {
+ surface,
+ strength: 50,
+ softness: 12,
+ textBrightness: -4,
+ });
+
+ for (const key of [
+ "--color-base-100",
+ "--color-base-200",
+ "--color-base-300",
+ ]) {
+ expect(
+ calculateContrastRatio(theme[key], theme["--color-base-content"]),
+ ).toBeGreaterThanOrEqual(4.5);
+ const background = chroma(theme[key]);
+ const muted = chroma.mix(
+ background,
+ theme["--color-base-content"],
+ 0.6,
+ "rgb",
+ );
+ expect(chroma.contrast(background, muted)).toBeGreaterThanOrEqual(4.5);
+ }
+ });
+ }
+ }
+
+ it("keeps every generated accent readable as ink on every surface tier", () => {
+ for (const mode of ["light", "dark"] as const) {
+ for (const surface of surfaceColors(mode)) {
+ for (const strength of STRENGTH_STOPS) {
+ for (const softness of SOFTNESS_STOPS) {
+ const theme = applyThemeComposition(seed(mode), {
+ surface,
+ strength,
+ softness,
+ });
+ const surfaces = [
+ theme["--color-base-100"],
+ theme["--color-base-200"],
+ theme["--color-base-300"],
+ ];
+ for (const options of [
+ accentOptions(surface, mode, strength, softness),
+ artworkAccentOptions(surface, mode, strength, softness),
+ ]) {
+ expect(options).toHaveLength(7);
+ expect(new Set(options).size).toBe(options.length);
+ for (const accent of options) {
+ for (const background of surfaces) {
+ expect(calculateContrastRatio(background, accent)).toBeGreaterThanOrEqual(4.5);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ it("repairs every theme ink token against the composed surfaces", () => {
+ const composed = applyThemeComposition(seed("light"), {
+ surface: "#ffffff",
+ strength: 50,
+ softness: 12,
+ });
+ const unsafe = {
+ ...composed,
+ "--color-primary": composed["--color-base-100"],
+ "--color-secondary": composed["--color-base-100"],
+ "--color-accent": composed["--color-base-100"],
+ "--color-neutral": composed["--color-base-100"],
+ "--color-info": composed["--color-base-100"],
+ "--color-success": composed["--color-base-100"],
+ "--color-warning": composed["--color-base-100"],
+ "--color-error": composed["--color-base-100"],
+ };
+ const repaired = enforceThemeInkContrast(unsafe);
+ for (const key of [
+ "--color-primary",
+ "--color-secondary",
+ "--color-accent",
+ "--color-neutral",
+ "--color-info",
+ "--color-success",
+ "--color-warning",
+ "--color-error",
+ ]) {
+ for (const background of [
+ repaired["--color-base-100"],
+ repaired["--color-base-200"],
+ repaired["--color-base-300"],
+ ]) {
+ expect(calculateContrastRatio(background, repaired[key])).toBeGreaterThanOrEqual(4.5);
+ }
+ expect(
+ calculateContrastRatio(repaired[key], repaired[`${key}-content`]),
+ ).toBeGreaterThanOrEqual(4.5);
+ expect(
+ calculateContrastRatio(
+ repaired[`${key}-soft`],
+ repaired[`${key}-soft-foreground`],
+ ),
+ ).toBeGreaterThanOrEqual(4.5);
+ }
+ });
+});