diff --git a/README.md b/README.md index 482b773..27203b4 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,16 @@ 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.3 candidate build passes all 675 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 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: +not emulate browser CSP enforcement. When CSP is enabled, it rejects a policy +that blocks UI's dynamic Slider and theme-preview styles. It always rejects +stale Google Fonts sources: ```sh bun run qa:production-policy diff --git a/package.json b/package.json index 024a7f9..1abc92d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@iconify-json/lucide": "^1.2.127", "@iconify-json/mdi": "^1.2.3", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "^3.2.2", + "@pathscale/ui": "^3.2.3", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@solidjs/router": "2.0.0-next.19", diff --git a/scripts/verify-production-policy.mjs b/scripts/verify-production-policy.mjs index 2d7d1d8..7439d1b 100644 --- a/scripts/verify-production-policy.mjs +++ b/scripts/verify-production-policy.mjs @@ -31,7 +31,9 @@ if (/fonts\.(googleapis|gstatic)\.com/i.test(productionSource)) { } if (!policy) { - console.log(`Production policy accepts runtime theme styles and has no Google Fonts source: ${targetUrl} has no CSP header.`); + console.log( + `Production policy has no enforced CSP and no Google Fonts source: ${targetUrl}`, + ); process.exit(0); } @@ -71,6 +73,18 @@ if (!styleAttributeSources.includes("'unsafe-inline'")) { ); } +const styleElementSources = + directives.get("style-src-elem") ?? + directives.get("style-src") ?? + directives.get("default-src") ?? + []; + +if (!styleElementSources.includes("'unsafe-inline'")) { + throw new Error( + "Production CSP blocks the style elements used by the UI runtime.", + ); +} + console.log( `Production policy accepts runtime theme styles and has no Google Fonts source: ${targetUrl}`, ); 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/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/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..a20adc0 100644 --- a/src/components/layout/Header/components/ComponentsMenu.tsx +++ b/src/components/layout/Header/components/ComponentsMenu.tsx @@ -1,6 +1,9 @@ import { Component, For, createMemo } from "solid-js"; -import { Button, Flex, Navbar } from "@pathscale/ui"; -import { navigationItems } from "../navigationData"; +import { Button, Flex, Link, Navbar } from "@pathscale/ui"; +import { + DEFAULT_COMPONENT_CATEGORY, + navigationItems, +} from "../navigationData"; import { useNavigation } from "../hooks/useNavigation"; import clsx from "clsx"; @@ -33,9 +36,10 @@ export const ComponentsMenu: Component = (props) => { class={clsx( "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" + (activeCategory() === "Components" && + subcategory.title === DEFAULT_COMPONENT_CATEGORY) + ? "bg-primary text-primary-content" + : "text-base-content hover:bg-base-300" )} > {subcategory.title} @@ -49,15 +53,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..2a98878 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/hooks/useNavigation.ts b/src/components/layout/Header/hooks/useNavigation.ts index b26fc83..9d44c66 100644 --- a/src/components/layout/Header/hooks/useNavigation.ts +++ b/src/components/layout/Header/hooks/useNavigation.ts @@ -1,7 +1,10 @@ import { createSignal, createEffect, createMemo } from "solid-js"; import { useLocation } from "@solidjs/router"; import { setIsNavbarExpanded } from "../../LayoutGrid"; -import { navigationItems } from "../navigationData"; +import { + DEFAULT_COMPONENT_CATEGORY, + navigationItems, +} from "../navigationData"; import { ROUTES } from "../../../../config/routes"; export const useNavigation = () => { @@ -24,7 +27,9 @@ export const useNavigation = () => { (cat) => cat.title === activeCategory() ); } else if (activeCategory() === "Components") { - return componentsItem?.subcategories?.[0]; + return componentsItem?.subcategories?.find( + (category) => category.title === DEFAULT_COMPONENT_CATEGORY, + ); } return null; }; @@ -65,4 +70,4 @@ export const useNavigation = () => { getSelectedSubcategory, shouldShowComponentsMenu, }; -}; \ No newline at end of file +}; diff --git a/src/components/layout/Header/navigationData.ts b/src/components/layout/Header/navigationData.ts index 118340c..27455de 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,6 @@ export const navigationItems: NavigationItem[] = [ }, { title: "Docs", href: ROUTES.DOCS }, { title: "Layouts", href: ROUTES.DOCS_LAYOUTS }, - { title: "Showcases", href: ROUTES.SHOWCASES }, ]; + +export const DEFAULT_COMPONENT_CATEGORY = "Surfaces"; 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/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/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 2cd89db..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,103 +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]};`); - - // 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, - ...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. @@ -120,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 (
-

- 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/Showcases.tsx b/src/pages/Showcases.tsx index a952e40..36a55f5 100644 --- a/src/pages/Showcases.tsx +++ b/src/pages/Showcases.tsx @@ -10,6 +10,7 @@ const Showcases: Component = () => { route.path !== ROUTES.HOME && route.path !== ROUTES.DOCS && route.path !== ROUTES.THEMING && + route.path !== ROUTES.SHOWCASES && !route.path.startsWith("/docs/") ); @@ -41,7 +42,7 @@ const Showcases: Component = () => {

-

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 ( -
-

{category}

-

- { - categoryDescriptions[ - category as keyof typeof categoryDescriptions - ] - } -

- - - {(component) => ( - - - -

- {component.name} -

-

- {component.description} -

-
-
-
- )} -
-
-
- ); - }} -
-
- - ); -}; - -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); + } + }); +});