From a12610724f8f689b5a832bf03715f7f9ccb23de1 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 07:25:50 +0700 Subject: [PATCH 1/7] chore(ui): pin the showcase to UI 3.2 --- package.json | 2 +- tests/ps-qa/checks/theming.ron | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b54bcb5..2c30ba9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@iconify-json/lucide": "^1.2.127", "@iconify-json/mdi": "^1.2.3", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "^3.1.0", + "@pathscale/ui": "^3.2.0", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@solidjs/router": "2.0.0-next.19", diff --git a/tests/ps-qa/checks/theming.ron b/tests/ps-qa/checks/theming.ron index e4d9a49..60cde5f 100644 --- a/tests/ps-qa/checks/theming.ron +++ b/tests/ps-qa/checks/theming.ron @@ -210,6 +210,16 @@ subject: "switch:Apply to whole site", expect: SelectionChanges, ), + ( + id: "and-the-whole-site-option-is-restored", + group: "theming", + what: "the editor leaves the showcase theme scoped to its preview", + open: None, + hover: None, + click: Some("switch:Apply to whole site"), + subject: "switch:Apply to whole site", + expect: SelectionChanges, + ), ( id: "the-remember-option-is-a-named-switch", group: "theming", @@ -220,6 +230,16 @@ subject: "switch:Remember this theme", expect: SelectionChanges, ), + ( + id: "and-the-remember-option-is-restored", + group: "theming", + what: "the suite does not persist its generated theme into later runs", + open: None, + hover: None, + click: Some("switch:Remember this theme"), + subject: "switch:Remember this theme", + expect: SelectionChanges, + ), ( id: "the-default-theme-option-is-a-named-switch", group: "theming", @@ -422,6 +442,7 @@ hover: None, click: None, subject: "Generated theme CSS", + reveal_before_capture: Some("textbox:Generated theme CSS"), expect: InteriorInk, outcome_timeout_ms: 8000, ), From ec9325008739eda71d1d809a79e004d93fc58d76 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 11:13:44 +0700 Subject: [PATCH 2/7] fix(showcase): prove sliders through native input --- README.md | 5 +- src/components/ComponentsDemo.tsx | 11 ++- src/components/CoverageShowcase.tsx | 39 ++++++++--- src/components/theming/GlassSection.tsx | 70 +++++++------------ src/components/theming/SizesSection.tsx | 2 +- src/components/theming/ThemeEditor.tsx | 3 +- src/pages/Theming.tsx | 5 ++ tests/ps-qa/checks/input.ron | 92 +++++++++++++++++++++---- tests/ps-qa/checks/surfaces.ron | 29 +++++++- tests/ps-qa/checks/theming.ron | 91 ++++++++++++++++++++++++ 10 files changed, 268 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index fbb0302..a7670fd 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ ps-qa --app tests/ps-qa/ps-qa.ron qa-hosted \ --checks tests/ps-qa/checks ``` -The UI 3.2 registry build passes all 330 native checks across 12 groups, -including Calendar selection and month navigation. CI includes every group. +The UI 3.2 registry build passes all 347 native checks across 12 groups, +including Calendar selection, month navigation, and keyboard and pointer-driven +Slider and Color Picker outcomes. CI includes every group. The Layouts route uses its own document marker, and decorative cards are articles rather than controls that promise an action. diff --git a/src/components/ComponentsDemo.tsx b/src/components/ComponentsDemo.tsx index 9d039c8..330c08c 100644 --- a/src/components/ComponentsDemo.tsx +++ b/src/components/ComponentsDemo.tsx @@ -6,6 +6,7 @@ import { ROUTES } from "../config/routes"; export default function ComponentsDemo() { const [modalOpen, setModalOpen] = createSignal(false); const [page, setPage] = createSignal(2); + const [price, setPrice] = createSignal(25); return (
@@ -166,7 +167,15 @@ export default function ComponentsDemo() { Price Range - {}} /> + `$${value}`} + onChange={setPrice} + /> $0 $25 diff --git a/src/components/CoverageShowcase.tsx b/src/components/CoverageShowcase.tsx index 802d231..176c3d6 100644 --- a/src/components/CoverageShowcase.tsx +++ b/src/components/CoverageShowcase.tsx @@ -1,5 +1,5 @@ import type { JSX } from "@solidjs/web"; -import { createErrorBoundary, For } from "solid-js"; +import { createErrorBoundary, createSignal, For } from "solid-js"; import { Accordion, Button, Card, Checkbox, Chip, Collapsible, createI18n, Empty, Flex, Header, Input, InputOTP, LanguageSwitcher, ListBox, LiveChatBubble, MetalBorder, Popover, Progress, ScrollArea, Separator, Slider, Spinner, Text, Textarea, ThemeColorPicker } from "@pathscale/ui"; import { ButtonGroup, CheckboxGroup, CloseButton, Meter, RadialProgress, SizePicker, TimeField, DateField, ColorPicker, ColorArea, ColorField, ColorSlider, ComboBox, DatePicker, DateRangePicker, RangeCalendar, Toolbar, Kbd } from "@pathscale/ui/lab"; import ShowcaseLayout from "./ShowcaseLayout"; @@ -131,7 +131,10 @@ const EXAMPLES: Example[] = [ { name: "SizePicker", render: () => }, { name: "Slider", - render: () => {}} />, + render: () => { + const [value, setValue] = createSignal(40); + return ; + }, }, { name: "Spinner", render: () => }, { name: "Card (surface)", render: () => On a surface. }, @@ -164,20 +167,36 @@ const EXAMPLES: Example[] = [ const COMPOSED: Example[] = [ { name: "ColorPicker", - render: () => ( - - - - - - ), + render: () => { + const [value, setValue] = createSignal("#6366f1"); + return ( + + + + + + ); + }, }, { name: "ColorArea", render: () => , }, { name: "ColorField", render: () => }, - { name: "ColorSlider", render: () => }, + { + name: "ColorSlider", + render: () => { + const [value, setValue] = createSignal(250); + return ( + + ); + }, + }, { name: "ComboBox", render: () => ( diff --git a/src/components/theming/GlassSection.tsx b/src/components/theming/GlassSection.tsx index 6c4670c..4457f1b 100644 --- a/src/components/theming/GlassSection.tsx +++ b/src/components/theming/GlassSection.tsx @@ -1,4 +1,4 @@ -import { Button, Icon, Separator } from "@pathscale/ui"; +import { Button, Icon, Separator, Slider } from "@pathscale/ui"; import { BLUR_MAX, DEPTH_MAX, @@ -11,7 +11,7 @@ import { Theme } from "../../utils/themeUtils"; interface GlassSectionProps { theme: Theme; - onThemeUpdate: (key: string, value: string) => void; + onThemeUpdate: (values: Record) => void; } export default function GlassSection(props: GlassSectionProps) { @@ -27,9 +27,7 @@ export default function GlassSection(props: GlassSectionProps) { depth: next.depth ?? current.depth, }; const cssVars = resolveGlassCssVariables(merged); - for (const [key, value] of Object.entries(cssVars)) { - props.onThemeUpdate(key, value); - } + props.onThemeUpdate(cssVars); }; const resetToHype4 = () => applyTuning(HYPE4_DEFAULTS); @@ -51,65 +49,43 @@ export default function GlassSection(props: GlassSectionProps) {
- +
- +
- + ); diff --git a/src/components/theming/SizesSection.tsx b/src/components/theming/SizesSection.tsx index c63ad5d..7a944e6 100644 --- a/src/components/theming/SizesSection.tsx +++ b/src/components/theming/SizesSection.tsx @@ -92,7 +92,7 @@ export default function SizesSection(props: SizesSectionProps) {
void; onThemePropertyUpdate: (key: string, value: string) => void; + onGlassThemeUpdate: (values: Record) => void; onRandomizeTheme: () => void; onExportCSS: (isDefault: boolean, isPrefersDark: boolean, colorScheme: "light" | "dark") => void; dockActiveItem: string; @@ -87,7 +88,7 @@ export default function ThemeEditor(props: ThemeEditorProps) { ) => { + setCurrentTheme((theme) => ({ ...theme, ...values })); + }; + const exportCSS = ( isDefault: boolean, isPrefersDark: boolean, @@ -169,6 +173,7 @@ export default function Theming() { theme={currentTheme()} onColorClick={openColorPicker} onThemePropertyUpdate={updateThemePropertyValue} + onGlassThemeUpdate={updateGlassThemeValues} onRandomizeTheme={randomizeTheme} onExportCSS={exportCSS} dockActiveItem={dockActiveItem()} diff --git a/tests/ps-qa/checks/input.ron b/tests/ps-qa/checks/input.ron index 566f169..80f3ef1 100644 --- a/tests/ps-qa/checks/input.ron +++ b/tests/ps-qa/checks/input.ron @@ -4,11 +4,9 @@ // The Input category: Checkbox, File Input, Form, Input, Radio Group, Select, // Slider, Textarea. // -// Two of the ten pages in this category are reached but never opened, and the -// reason is written into the first two checks. `/calendar` and `/color-picker` -// both take the whole application down when they mount, and every group runs -// against one shared browser, so opening either would end the run there and -// report every later group as a failure of its own. +// Calendar is reached but never opened. It takes the application down when it +// mounts, and every group runs against one shared browser, so opening it would +// end the run there and report every later group as a failure of its own. // // What takes them down: // @@ -16,16 +14,8 @@ // `Intl`, and the date formatting reaches for it during the // first render. Not the site's doing. // -// `/color-picker` `useColorPickerContext must be used within a -// ColorPickerContext.Provider`. The page renders -// `` on its own, and the prose beside it -// says that is supported; it is not. The library's own -// `ColorWheel` wraps the same component in the provider. -// -// Both escape every boundary, and Solid 2 answers an escaped error by halting -// its reactive system for good, so the page paints once and then answers -// nothing. What is asserted here is that a person can reach them, which is -// true, and nothing about what they render, which is not. +// The Color Picker provider failure was fixed in UI 3.2. Its page is now opened +// and its two-dimensional, hue and alpha controls are driven below. [ ( id: "the-input-group-starts-from-a-page-that-clears-the-menu", @@ -442,6 +432,78 @@ subject: "slider:Volume", expect: Paints, ), + ( + id: "the-slider-responds-to-the-keyboard", + group: "input", + what: "the public Slider example changes its controlled value with the keyboard", + open: None, + prepare: Some("slider:Volume"), + prepare_key: Some("Home"), + hover: None, + click: None, + key: Some("End"), + key_on: Some("slider:Volume"), + subject: "slider:Volume", + expect: ValueChanges, + ), + ( + id: "the-slider-responds-to-a-real-pointer-drag", + group: "input", + what: "dragging the public Slider example changes the value retained by its caller", + open: None, + prepare: Some("slider:Volume"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Volume", dx: 160.0, dy: 0.0, steps: 6)), + subject: "slider:Volume", + expect: ValueChanges, + ), + // Color Picker. + ( + id: "the-colour-picker-page-opens", + group: "input", + what: "the menu reaches a live Color Picker showcase", + open: None, + hover: None, + click: Some("link:Color Picker"), + subject: "heading:Color Picker", + expect: Present, + outcome_timeout_ms: 8000, + ), + ( + id: "the-colour-picker-area-responds-to-a-real-pointer", + group: "input", + what: "dragging the default colour area changes its controlled value", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Color area", dx: 40.0, dy: 30.0, steps: 4)), + subject: "slider:Color area", + expect: ValueChanges, + ), + ( + id: "the-colour-picker-hue-responds-to-a-real-pointer", + group: "input", + what: "dragging the default hue rail changes its controlled value", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Hue", dx: 60.0, dy: 0.0, steps: 4)), + subject: "slider:Hue", + expect: ValueChanges, + ), + ( + id: "the-colour-picker-alpha-responds-to-a-real-pointer", + group: "input", + what: "dragging the alpha rail changes the RGBA value retained by its caller", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Alpha", dx: 60.0, dy: 0.0, steps: 4)), + subject: "slider:Alpha", + expect: ValueChanges, + ), // Textarea. ( id: "the-textarea-page-opens", diff --git a/tests/ps-qa/checks/surfaces.ron b/tests/ps-qa/checks/surfaces.ron index 5fcf92f..7827f64 100644 --- a/tests/ps-qa/checks/surfaces.ron +++ b/tests/ps-qa/checks/surfaces.ron @@ -201,12 +201,37 @@ ( id: "and-a-hue-slider-beside-it", group: "surfaces", - what: "the one-dimensional half of the same picker is there too", + what: "the one-dimensional half of the same picker responds to a real pointer", open: None, hover: None, click: None, + pointer_drag: Some((from: "slider:Hue", dx: 60.0, dy: 0.0, steps: 4)), subject: "slider:Hue", - expect: Paints, + expect: ValueChanges, + ), + ( + id: "the-coverage-slider-retains-a-pointer-change", + group: "surfaces", + what: "the coverage Slider is a controlled example rather than a frozen specimen", + open: None, + prepare: Some("slider:Volume"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Volume", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Volume", + expect: ValueChanges, + ), + ( + id: "the-standalone-colour-slider-retains-a-pointer-change", + group: "surfaces", + what: "the standalone ColorSlider is separately named and interactive", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Standalone hue", dx: 60.0, dy: 0.0, steps: 4)), + subject: "slider:Standalone hue", + expect: ValueChanges, ), ( id: "the-coverage-page-builds-a-combobox", diff --git a/tests/ps-qa/checks/theming.ron b/tests/ps-qa/checks/theming.ron index 60cde5f..492f72d 100644 --- a/tests/ps-qa/checks/theming.ron +++ b/tests/ps-qa/checks/theming.ron @@ -180,6 +180,97 @@ subject: "slider:Border width", expect: ValueChanges, ), + ( + id: "the-glass-blur-slider-responds-to-a-real-pointer", + group: "theming", + what: "the UI blur slider stays changed after a real pointer drag", + open: None, + prepare: Some("slider:Blur"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Blur", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Blur", + expect: ValueChanges, + ), + ( + id: "the-glass-refraction-slider-responds-to-a-real-pointer", + group: "theming", + what: "the UI refraction slider stays changed after a real pointer drag", + open: None, + prepare: Some("slider:Refraction"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Refraction", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Refraction", + expect: ValueChanges, + ), + ( + id: "the-glass-depth-slider-responds-to-a-real-pointer", + group: "theming", + what: "the UI depth slider stays changed after a real pointer drag", + open: None, + prepare: Some("slider:Depth"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Depth", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Depth", + expect: ValueChanges, + ), + ( + id: "the-field-size-slider-responds-to-a-real-pointer", + group: "theming", + what: "the field size token changes from the Slider pointer contract", + open: None, + prepare: Some("slider:Fields base size"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Fields base size", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Fields base size", + expect: ValueChanges, + ), + ( + id: "the-selector-size-slider-responds-to-a-real-pointer", + group: "theming", + what: "the selector size token changes from the Slider pointer contract", + open: None, + prepare: Some("slider:Selectors base size"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Selectors base size", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Selectors base size", + expect: ValueChanges, + ), + ( + id: "the-border-slider-responds-to-a-real-pointer", + group: "theming", + what: "the border token changes from the Slider pointer contract", + open: None, + prepare: Some("slider:Border width"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Border width", dx: 100.0, dy: 0.0, steps: 4)), + subject: "slider:Border width", + expect: ValueChanges, + ), + ( + id: "the-preview-price-slider-is-labelled-and-interactive", + group: "theming", + what: "the live component preview retains a price changed by a real pointer drag", + open: None, + prepare: Some("slider:Price range"), + prepare_key: Some("Home"), + hover: None, + click: None, + pointer_drag: Some((from: "slider:Price range", dx: 80.0, dy: 0.0, steps: 4)), + subject: "slider:Price range", + expect: ValueChanges, + ), ( id: "the-depth-effect-switch-updates-the-token", group: "theming", From 28a6b2ce9b488d92c887db83555c4e8f9b5644af Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 13:18:34 +0700 Subject: [PATCH 3/7] fix(showcase): restore theming and prove every action --- src/App.tsx | 11 +- src/ThemeToggle.tsx | 12 +- src/components/AlertShowcase.tsx | 31 +- src/components/BadgeShowcase.tsx | 27 +- src/components/ButtonShowcase.tsx | 43 +- src/components/CalendarShowcase.tsx | 14 +- src/components/CardShowcase.tsx | 19 +- src/components/ComponentVariants.tsx | 30 +- src/components/ComponentsDemo.tsx | 104 +- src/components/CoverageShowcase.tsx | 65 +- src/components/DataGridShowcase.tsx | 12 +- src/components/DrawerShowcase.tsx | 18 +- src/components/DropdownShowcase.tsx | 38 +- src/components/FormActionsShowcase.tsx | 7 +- src/components/JoinShowcase.tsx | 15 +- src/components/Preview.tsx | 6 +- src/components/TableShowcase.tsx | 14 +- src/components/VideoPreviewShowcase.tsx | 11 + src/components/content/Search.tsx | 30 +- .../navbar-showcase/MultilevelSection.tsx | 30 +- src/components/showcase/ActionStatus.tsx | 27 + src/components/theming/ColorPickerPopover.tsx | 15 +- src/components/theming/ThemeEditor.tsx | 26 +- src/components/theming/ThemeList.tsx | 74 + src/index.tsx | 3 + src/lib/themeEditorPersistence.ts | 77 ++ src/pages/Theming.tsx | 191 ++- src/pages/docs/Layouts.tsx | 7 +- tests/ps-qa/checks/calendar.ron | 101 +- tests/ps-qa/checks/navigation.ron | 220 +++ tests/ps-qa/checks/theming.ron | 1207 +++++++++++++++++ tests/ps-qa/ps-qa.ron | 1 + 32 files changed, 2283 insertions(+), 203 deletions(-) create mode 100644 src/components/showcase/ActionStatus.tsx create mode 100644 src/components/theming/ThemeList.tsx create mode 100644 src/lib/themeEditorPersistence.ts diff --git a/src/App.tsx b/src/App.tsx index 701edd6..1af6c26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,18 @@ -import { createRouter } from "@solidjs/router"; -import { ParentComponent } from "solid-js"; +import { createRouter, useLocation } from "@solidjs/router"; +import { ParentComponent, createEffect } from "solid-js"; import { routes } from "./routes"; import { BaseLayout } from "./layouts/BaseLayout"; import { MarketingHeader } from "./components/layout/Header/MarketingHeader"; const Layout: ParentComponent = (props) => { + const location = useLocation(); + + createEffect( + () => location.pathname, + () => window.scrollTo(0, 0), + ); + return ( {theme() === "dark" ? : } - + ); } diff --git a/src/components/AlertShowcase.tsx b/src/components/AlertShowcase.tsx index 9edc4f6..0fedd3a 100644 --- a/src/components/AlertShowcase.tsx +++ b/src/components/AlertShowcase.tsx @@ -1,11 +1,14 @@ -import { Component } from "solid-js"; +import { Component, createSignal } from "solid-js"; import ShowcaseLayout from "./ShowcaseLayout"; import { Alert, Button, Flex } from "@pathscale/ui"; import { PropsTable } from "./showcase/PropsTable"; import { CodeBlock } from "./showcase/CodeBlock"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; const AlertShowcase: Component = () => { + const [cookieChoice, setCookieChoice] = createSignal<"accept" | "deny" | null>(null); + const actions = createActionStatus(); const sections = [ { id: "contents", title: "Contents" }, { id: "default", title: "Default" }, @@ -154,15 +157,20 @@ const AlertShowcase: Component = () => { justify="start" gap="md" > - - we use cookies for no reason. -
- - -
-
+
+ + we use cookies for no reason. +
+ + +
+
+

+ {cookieChoice() ? `Cookie preference saved: ${cookieChoice()}.` : "Choose a cookie preference."} +

+
@@ -182,8 +190,9 @@ const AlertShowcase: Component = () => { > You have 1 unread message - + + diff --git a/src/components/BadgeShowcase.tsx b/src/components/BadgeShowcase.tsx index 21380ab..ccbd711 100644 --- a/src/components/BadgeShowcase.tsx +++ b/src/components/BadgeShowcase.tsx @@ -1,8 +1,10 @@ import { Badge, Button, Chip, Flex, Icon } from "@pathscale/ui"; +import { createSignal } from "solid-js"; import ShowcaseLayout from "./ShowcaseLayout"; import { CodeBlock } from "./showcase/CodeBlock"; import { PropsTable } from "./showcase/PropsTable"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; const badgeProps = [ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Indicator size." }, @@ -13,19 +15,21 @@ const badgeProps = [ ]; export default function BadgeShowcase() { + const [selectedPlacement, setSelectedPlacement] = createSignal<"top-right" | "top-left" | "bottom-right" | "bottom-left">("top-right"); + const actions = createActionStatus(); return ( - 3 - + 12 @@ -38,12 +42,23 @@ export default function BadgeShowcase() { {(["top-left", "top-right", "bottom-left", "bottom-right"] as const).map((placement) => ( - - - - + ))} + + Preview + + + diff --git a/src/components/ButtonShowcase.tsx b/src/components/ButtonShowcase.tsx index aa673bd..35a33fa 100644 --- a/src/components/ButtonShowcase.tsx +++ b/src/components/ButtonShowcase.tsx @@ -1,8 +1,10 @@ import { Button, Flex, Icon } from "@pathscale/ui"; +import { createSignal } from "solid-js"; import ShowcaseLayout from "./ShowcaseLayout"; import { CodeBlock } from "./showcase/CodeBlock"; import { PropsTable } from "./showcase/PropsTable"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; const buttonProps = [ { name: "variant", type: '"primary" | "secondary" | "tertiary" | "outline" | "ghost" | "danger" | "danger-soft"', default: '"primary"', description: "Semantic visual treatment." }, @@ -16,6 +18,8 @@ const buttonProps = [ ]; export default function ButtonShowcase() { + const actions = createActionStatus(); + const [pressed, setPressed] = createSignal(true); const startIcon = ; const endIcon = ; @@ -24,13 +28,13 @@ export default function ButtonShowcase() { - - - - - - - + + + + + + + Primary @@ -39,9 +43,9 @@ export default function ButtonShowcase() { - - - + + + Small @@ -50,9 +54,9 @@ export default function ButtonShowcase() { - - - + + @@ -65,7 +69,14 @@ export default function ButtonShowcase() { - + Saving @@ -73,10 +84,12 @@ export default function ButtonShowcase() { - + Continue`} /> + +

Button deliberately renders a button. Use a native anchor or the Link component for diff --git a/src/components/CalendarShowcase.tsx b/src/components/CalendarShowcase.tsx index af16cb9..e7b1b0f 100644 --- a/src/components/CalendarShowcase.tsx +++ b/src/components/CalendarShowcase.tsx @@ -142,9 +142,8 @@ export default function CalendarShowcase() { setRangePreview(undefined); }; - const today = new Date(); - const minDate = new Date(today.getFullYear(), today.getMonth(), 1); - const maxDate = new Date(today.getFullYear(), today.getMonth() + 2, 0); + const minDate = new Date(2025, 5, 1); + const maxDate = new Date(2025, 6, 31); return ( @@ -164,7 +163,7 @@ export default function CalendarShowcase() { - + `} /> @@ -172,6 +171,7 @@ export default function CalendarShowcase() { setSelectedDate(value)} /> @@ -191,6 +191,8 @@ export default function CalendarShowcase() { - + `} /> - + Shoes!

If a dog chews shoes whose shoes does he choose?

- + @@ -131,8 +135,8 @@ export default function CardShowcase() { - - + + @@ -167,7 +171,7 @@ export default function CardShowcase() {

Shoes!

If a dog chews shoes whose shoes does he choose?

- + @@ -250,13 +254,18 @@ export default function CardShowcase() { console.log("card pressed")} + aria-pressed={cardPressed() ? "true" : "false"} + onClick={() => { + setCardPressed((value) => !value); + actions.announce(`Pressable card ${cardPressed() ? "selected" : "cleared"}`); + }} >

Press me

Acts like a button; Enter/Space activates.

+ +
Buttons
- - - - - - - - + {variants.map(([label, props]) => ( + + ))}
diff --git a/src/components/ComponentsDemo.tsx b/src/components/ComponentsDemo.tsx index 330c08c..e0aa9a4 100644 --- a/src/components/ComponentsDemo.tsx +++ b/src/components/ComponentsDemo.tsx @@ -2,14 +2,20 @@ import { createSignal } from "solid-js"; import { Alert, Avatar, Breadcrumb, BreadcrumbItem, Button, Card, ChatBubble, Checkbox, Chip, Dialog, Drawer, Dropdown, Flex, Form, Icon, Input, Navbar, Pagination, Progress, Radio, Select, Skeleton, Slider, Spinner, Switch, Table, Tabs, Tooltip, TooltipContent, TooltipTrigger } from "@pathscale/ui"; import { Menu, Join } from "@pathscale/ui/lab"; import { ROUTES } from "../config/routes"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; export default function ComponentsDemo() { const [modalOpen, setModalOpen] = createSignal(false); const [page, setPage] = createSignal(2); const [price, setPrice] = createSignal(25); + const [selectedDay, setSelectedDay] = createSignal(5); + const actionStatus = createActionStatus("Preview ready"); return (
+
+ +
@@ -18,7 +24,7 @@ export default function ComponentsDemo() { Filters - @@ -29,7 +35,7 @@ export default function ComponentsDemo() { - + Hoodies @@ -38,7 +44,7 @@ export default function ComponentsDemo() { - + Bags @@ -47,7 +53,7 @@ export default function ComponentsDemo() { - + Shoes @@ -56,7 +62,7 @@ export default function ComponentsDemo() { - + Accessories @@ -96,27 +102,21 @@ export default function ComponentsDemo() { S - - - - - - - + {[1, 2, 3, 4, 5, 6, 7].map((day) => ( + + ))} Show all day events - + @@ -189,7 +189,7 @@ export default function ComponentsDemo() { to - @@ -303,7 +303,7 @@ export default function ComponentsDemo() { - {" "} to verify email @@ -312,7 +312,7 @@ export default function ComponentsDemo() { Access denied - @@ -377,7 +377,7 @@ export default function ComponentsDemo() { Advanced analytics - @@ -461,8 +461,8 @@ export default function ComponentsDemo() { Are you sure you want to continue? - - + @@ -489,7 +489,7 @@ export default function ComponentsDemo() { 3 - @@ -527,13 +527,13 @@ export default function ComponentsDemo() { - - - @@ -542,7 +542,7 @@ export default function ComponentsDemo() { - + actionStatus.announce(`Menu opened: ${String(key)}`)}> @@ -603,17 +603,17 @@ export default function ComponentsDemo() { - - - - @@ -643,10 +643,8 @@ export default function ComponentsDemo() { - - + + Dashboard @@ -659,7 +657,7 @@ export default function ComponentsDemo() { Brand - @@ -694,13 +692,13 @@ export default function ComponentsDemo() { - - - @@ -710,7 +708,7 @@ export default function ComponentsDemo() { class="flex-1" size="sm" /> - @@ -741,10 +739,10 @@ export default function ComponentsDemo() { - + Would recommend - diff --git a/src/components/CoverageShowcase.tsx b/src/components/CoverageShowcase.tsx index 176c3d6..fc31925 100644 --- a/src/components/CoverageShowcase.tsx +++ b/src/components/CoverageShowcase.tsx @@ -1,7 +1,8 @@ import type { JSX } from "@solidjs/web"; import { createErrorBoundary, createSignal, For } from "solid-js"; import { Accordion, Button, Card, Checkbox, Chip, Collapsible, createI18n, Empty, Flex, Header, Input, InputOTP, LanguageSwitcher, ListBox, LiveChatBubble, MetalBorder, Popover, Progress, ScrollArea, Separator, Slider, Spinner, Text, Textarea, ThemeColorPicker } from "@pathscale/ui"; -import { ButtonGroup, CheckboxGroup, CloseButton, Meter, RadialProgress, SizePicker, TimeField, DateField, ColorPicker, ColorArea, ColorField, ColorSlider, ComboBox, DatePicker, DateRangePicker, RangeCalendar, Toolbar, Kbd } from "@pathscale/ui/lab"; +import CloseButton from "@pathscale/ui/components/close-button"; +import { ButtonGroup, CheckboxGroup, Meter, RadialProgress, SizePicker, TimeField, DateField, ColorPicker, ColorArea, ColorField, ColorSlider, ComboBox, DatePicker, DateRangePicker, RangeCalendar, Toolbar, Kbd } from "@pathscale/ui/lab"; import ShowcaseLayout from "./ShowcaseLayout"; /** @@ -22,15 +23,35 @@ type Example = { render: () => JSX.Element; }; +function CloseButtonExample() { + const [closed, setClosed] = createSignal(false); + return ( + <> + setClosed(true)} + /> +

{closed() ? "Dismissed" : "Coverage notice open"}

+ + ); +} + const EXAMPLES: Example[] = [ { name: "ButtonGroup", - render: () => ( - - - - - ), + render: () => { + const [selected, setSelected] = createSignal(""); + return ( + <> + + + + +

{selected() ? `Selected ${selected()}` : "No button selected"}

+ + ); + }, }, { name: "CheckboxGroup", @@ -41,7 +62,10 @@ const EXAMPLES: Example[] = [ ), }, - { name: "CloseButton", render: () => }, + { + name: "CloseButton", + render: () => , + }, { // Kbd itself is shown on its own page, but its Abbr is used nowhere: it // renders a glyph for a named key and titles itself with that key's name. @@ -87,12 +111,25 @@ const EXAMPLES: Example[] = [ { name: "Input (invalid)", render: () => }, { name: "Input (grouped)", - render: () => ( - - - - - ), + render: () => { + const [value, setValue] = createSignal(""); + const [submitted, setSubmitted] = createSignal("Nothing submitted"); + let submissions = 0; + return ( + <> + + setValue(event.currentTarget.value)} + /> + + +

{submitted()}

+ + ); + }, }, { name: "InputOTP", render: () => }, { diff --git a/src/components/DataGridShowcase.tsx b/src/components/DataGridShowcase.tsx index 034f7e8..606a5f2 100644 --- a/src/components/DataGridShowcase.tsx +++ b/src/components/DataGridShowcase.tsx @@ -5,6 +5,7 @@ import ShowcaseLayout from "./ShowcaseLayout"; import { CodeBlock } from "./showcase/CodeBlock"; import { PropsTable } from "./showcase/PropsTable"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; type Person = { id: number; @@ -35,6 +36,7 @@ function seed(grid: ReturnType>) { } export default function DataGridShowcase() { + const actions = createActionStatus("Select rows, then inspect the selection."); const sections = [ { id: "contents", title: "Contents" }, { id: "default", title: "Default" }, @@ -309,10 +311,18 @@ grid.addColumn("lastName", "Last Name", "string", { searchable: true }); +
({ selection: "multiple" }); // or "single" diff --git a/src/components/DrawerShowcase.tsx b/src/components/DrawerShowcase.tsx index 4ab8f5b..415b950 100644 --- a/src/components/DrawerShowcase.tsx +++ b/src/components/DrawerShowcase.tsx @@ -92,9 +92,7 @@ export default function DrawerShowcase() { - - - + Open drawer @@ -107,9 +105,7 @@ export default function DrawerShowcase() { - - - + Open drawer @@ -125,9 +121,7 @@ export default function DrawerShowcase() { - - - + Open from right @@ -145,9 +139,7 @@ export default function DrawerShowcase() { - - - + Open with header @@ -166,7 +158,7 @@ export default function DrawerShowcase() { - + Open diff --git a/src/components/DropdownShowcase.tsx b/src/components/DropdownShowcase.tsx index 6771286..cb67fed 100644 --- a/src/components/DropdownShowcase.tsx +++ b/src/components/DropdownShowcase.tsx @@ -1,10 +1,15 @@ import ShowcaseLayout from "./ShowcaseLayout"; import { Button, Card, Dropdown, Flex, Navbar } from "@pathscale/ui"; +import { createSignal } from "solid-js"; import { PropsTable } from "./showcase/PropsTable"; import { CodeBlock } from "./showcase/CodeBlock"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus } from "./showcase/ActionStatus"; export default function DropdownShowcase() { + const [placementStatus, setPlacementStatus] = createSignal( + "No placement menu opened", + ); const sections = [ { id: "default", title: "Default" }, { id: "as-card", title: "As Card" }, @@ -220,7 +225,13 @@ export default function DropdownShowcase() { - + + setPlacementStatus(`Left menu ${open ? "opened" : "closed"}`) + } + > Left Item 1 @@ -228,7 +239,13 @@ export default function DropdownShowcase() { - + + setPlacementStatus(`Right menu ${open ? "opened" : "closed"}`) + } + > Right Item 1 @@ -236,7 +253,13 @@ export default function DropdownShowcase() { - + + setPlacementStatus(`Top menu ${open ? "opened" : "closed"}`) + } + > Top Item 1 @@ -244,7 +267,13 @@ export default function DropdownShowcase() { - + + setPlacementStatus(`Bottom menu ${open ? "opened" : "closed"}`) + } + > Bottom Item 1 @@ -252,6 +281,7 @@ export default function DropdownShowcase() { + ... ... diff --git a/src/components/FormActionsShowcase.tsx b/src/components/FormActionsShowcase.tsx index 675e361..7fb8510 100644 --- a/src/components/FormActionsShowcase.tsx +++ b/src/components/FormActionsShowcase.tsx @@ -2,18 +2,21 @@ import { Button, Fieldset } from "@pathscale/ui"; import ShowcaseLayout from "./ShowcaseLayout"; import { CodeBlock } from "./showcase/CodeBlock"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; export default function FormActionsShowcase() { + const actions = createActionStatus(); return (
Profile - - + +
+ Profile diff --git a/src/components/JoinShowcase.tsx b/src/components/JoinShowcase.tsx index fb3fdcf..0064917 100644 --- a/src/components/JoinShowcase.tsx +++ b/src/components/JoinShowcase.tsx @@ -3,16 +3,25 @@ import { Join } from "@pathscale/ui/lab"; import ShowcaseLayout from "./ShowcaseLayout"; import { CodeBlock } from "./showcase/CodeBlock"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { createSignal } from "solid-js"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; export default function JoinShowcase() { + const [page, setPage] = createSignal(2); + const actions = createActionStatus(); + const choose = (next: number) => { + setPage(next); + actions.announce(`Page ${next} selected`); + }; return ( - - - + + + + diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx index d2d7415..f19b225 100644 --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -34,13 +34,13 @@ export default function Preview(props: PreviewProps) { class="bg-base-300" > - + - + - + diff --git a/src/components/TableShowcase.tsx b/src/components/TableShowcase.tsx index b03f29e..961297a 100644 --- a/src/components/TableShowcase.tsx +++ b/src/components/TableShowcase.tsx @@ -5,10 +5,15 @@ import ShowcaseLayout from "./ShowcaseLayout"; import { CodeBlock } from "./showcase/CodeBlock"; import { PropsTable } from "./showcase/PropsTable"; import { ShowcaseSection } from "./showcase/ShowcaseSection"; +import { ActionStatus, createActionStatus } from "./showcase/ActionStatus"; // Mask avatars, Tag "ghost" color, Button size "xs") are no longer part of the public API. // The new compound: Table -> ScrollContainer -> Content -> Header/Column + Body/Row/Cell. export default function TableShowcase() { + const actions = createActionStatus("Choose a row to inspect its details."); + const showDetails = (name: string) => { + actions.announce(`Showing details for ${name}`); + }; const sections = [ { id: "contents", title: "Contents" }, { id: "default", title: "Default" }, @@ -212,7 +217,7 @@ export default function TableShowcase() { Purple - @@ -244,7 +249,7 @@ export default function TableShowcase() { Red - @@ -276,7 +281,7 @@ export default function TableShowcase() { Crimson - @@ -308,7 +313,7 @@ export default function TableShowcase() { Indigo - @@ -318,6 +323,7 @@ export default function TableShowcase() { Footer content area + diff --git a/src/components/VideoPreviewShowcase.tsx b/src/components/VideoPreviewShowcase.tsx index e4b05d2..6327b4c 100644 --- a/src/components/VideoPreviewShowcase.tsx +++ b/src/components/VideoPreviewShowcase.tsx @@ -14,13 +14,20 @@ export default function VideoPreviewShowcase() { ] as const; const [stream, setStream] = createSignal(null); + const [cameraStatus, setCameraStatus] = createSignal("Camera is off."); + let cameraAttempts = 0; const startCamera = async () => { + cameraAttempts += 1; + const attempt = cameraAttempts; + setCameraStatus(`Requesting camera access · attempt ${attempt}`); try { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }); setStream(mediaStream); + setCameraStatus(`Camera is active · attempt ${attempt}`); } catch (err) { console.error("Failed to access camera:", err); + setCameraStatus(`Camera unavailable · attempt ${attempt}`); } }; @@ -31,6 +38,7 @@ export default function VideoPreviewShowcase() { track.stop(); } setStream(null); + setCameraStatus("Camera stopped."); } }; @@ -102,6 +110,9 @@ export default function VideoPreviewShowcase() { )}
+

+ {cameraStatus()} +

= (props) => { }); const SearchIcon = () => ( - - - - + ); return ( <> - +
@@ -255,7 +249,7 @@ export const Search: Component = (props) => { >
- = (props) => { > {(result, index) => ( -
- + )}
diff --git a/src/components/navbar-showcase/MultilevelSection.tsx b/src/components/navbar-showcase/MultilevelSection.tsx index 7dd2731..05e0198 100644 --- a/src/components/navbar-showcase/MultilevelSection.tsx +++ b/src/components/navbar-showcase/MultilevelSection.tsx @@ -31,7 +31,15 @@ export const MultilevelSection = () => { - @@ -45,15 +53,18 @@ export const MultilevelSection = () => { "Performance", "Funding Comparison", ].map((item) => ( - + ))}
@@ -63,12 +74,15 @@ export const MultilevelSection = () => { {strategies.map((s) => ( - + ))} @@ -98,7 +112,7 @@ export const MultilevelSection = () => { {["Dashboard", "Strategies", ...].map((item) => ( - + ))} @@ -108,7 +122,7 @@ export const MultilevelSection = () => { {strategies.map((s) => ( - + ))} diff --git a/src/components/showcase/ActionStatus.tsx b/src/components/showcase/ActionStatus.tsx new file mode 100644 index 0000000..0736393 --- /dev/null +++ b/src/components/showcase/ActionStatus.tsx @@ -0,0 +1,27 @@ +import { createSignal, type Component } from "solid-js"; + +export function createActionStatus(initial = "Ready") { + const [message, setMessage] = createSignal(initial); + let sequence = 0; + + const announce = (next: string) => { + sequence += 1; + setMessage(`${next} · action ${sequence}`); + }; + + return { + message, + announce, + handler: (next: string) => () => announce(next), + }; +} + +export const ActionStatus: Component<{ message: string }> = (props) => ( +

+ {props.message} +

+); diff --git a/src/components/theming/ColorPickerPopover.tsx b/src/components/theming/ColorPickerPopover.tsx index e42ed0d..fa4f606 100644 --- a/src/components/theming/ColorPickerPopover.tsx +++ b/src/components/theming/ColorPickerPopover.tsx @@ -41,13 +41,18 @@ export default function ColorPickerPopover(props: ColorPickerPopoverProps) { > -
+
{([colorName, colorValue]) => ( - + )}
diff --git a/src/components/theming/ThemeEditor.tsx b/src/components/theming/ThemeEditor.tsx index 499693f..e3d843f 100644 --- a/src/components/theming/ThemeEditor.tsx +++ b/src/components/theming/ThemeEditor.tsx @@ -1,33 +1,45 @@ import { For, createSignal } from "solid-js"; -import { Button, Dropdown, Grid, Icon, Separator, Switch } from "@pathscale/ui"; +import { Button, Grid, Icon, Input, Separator, Switch } from "@pathscale/ui"; import { Theme, COLOR_GROUPS } from "../../utils/themeUtils"; import ColorGroup from "./ColorGroup"; import RadiusSection from "./RadiusSection"; import EffectsSection from "./EffectsSection"; import GlassSection from "./GlassSection"; import SizesSection from "./SizesSection"; +import { ActionStatus } from "../showcase/ActionStatus"; interface ThemeEditorProps { theme: Theme; + onThemeNameChange: (name: string) => void; onColorClick: (colorKey: string, event: MouseEvent) => void; onThemePropertyUpdate: (key: string, value: string) => void; onGlassThemeUpdate: (values: Record) => void; onRandomizeTheme: () => void; onExportCSS: (isDefault: boolean, isPrefersDark: boolean, colorScheme: "light" | "dark") => void; dockActiveItem: string; + applyToWholeSite: boolean; + rememberTheme: boolean; + onApplyToWholeSiteChange: (checked: boolean) => void; + onRememberThemeChange: (checked: boolean) => void; + status: string; } export default function ThemeEditor(props: ThemeEditorProps) { const [isDefault, setIsDefault] = createSignal(false); const [isPrefersDark, setIsPrefersDark] = createSignal(false); const [colorScheme, setColorScheme] = createSignal<"light" | "dark">("light"); - const [applyToWholeSite, setApplyToWholeSite] = createSignal(false); - const [rememberTheme, setRememberTheme] = createSignal(false); return (
+ props.onThemeNameChange(event.currentTarget.value)} + /> + + +
+ +
+ + {(theme) => { + const selected = () => + themeIdentity(theme) === themeIdentity(props.currentTheme); + return ( +
+ + +
+ ); + }} +
+
+ + 1}> + + + + ); +} diff --git a/src/index.tsx b/src/index.tsx index 1f6b72a..6b8b112 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ // else mounted it, so the site was rendering with no theme attribute at all // and falling through to whatever `:root` happened to declare. import "./lib/theme"; +import { restoreAppliedTheme } from "./lib/themeEditorPersistence"; /* @refresh reload */ import "./index.css"; import { render } from "@solidjs/web"; @@ -10,6 +11,8 @@ import App from "./App"; const root = document.getElementById("root"); +restoreAppliedTheme(); + if (import.meta.env.DEV && !(root instanceof HTMLElement)) { throw new Error( "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?" diff --git a/src/lib/themeEditorPersistence.ts b/src/lib/themeEditorPersistence.ts new file mode 100644 index 0000000..4af24c2 --- /dev/null +++ b/src/lib/themeEditorPersistence.ts @@ -0,0 +1,77 @@ +import type { Theme } from "../types/theme"; + +const STORAGE_KEY = "js-software-theme-editor"; +const appliedProperties = new Set(); + +export interface PersistedThemeEditorState { + currentTheme: Theme; + themes: Theme[]; + applyToWholeSite: boolean; +} + +const isTheme = (value: unknown): value is Theme => { + if (!value || typeof value !== "object") return false; + const candidate = value as Record; + return ( + typeof candidate.name === "string" && + Object.keys(candidate).some( + (key) => key.startsWith("--") && typeof candidate[key] === "string", + ) + ); +}; + +export const readThemeEditorState = (): PersistedThemeEditorState | null => { + if (typeof window === "undefined") return null; + + try { + const value = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null") as { + currentTheme?: unknown; + themes?: unknown; + applyToWholeSite?: unknown; + } | null; + + if (!value || !isTheme(value.currentTheme)) return null; + const themes = Array.isArray(value.themes) + ? value.themes.filter(isTheme) + : []; + + return { + currentTheme: value.currentTheme, + themes, + applyToWholeSite: value.applyToWholeSite === true, + }; + } catch { + return null; + } +}; + +export const writeThemeEditorState = (state: PersistedThemeEditorState) => { + if (typeof window === "undefined") return; + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); +}; + +export const clearThemeEditorState = () => { + if (typeof window === "undefined") return; + localStorage.removeItem(STORAGE_KEY); +}; + +export const applyThemeToDocument = (theme: Theme | null) => { + if (typeof document === "undefined") return; + + const root = document.documentElement; + for (const property of appliedProperties) root.style.removeProperty(property); + appliedProperties.clear(); + + if (!theme) return; + for (const [property, value] of Object.entries(theme)) { + if (property.startsWith("--")) { + root.style.setProperty(property, value); + appliedProperties.add(property); + } + } +}; + +export const restoreAppliedTheme = () => { + const state = readThemeEditorState(); + if (state?.applyToWholeSite) applyThemeToDocument(state.currentTheme); +}; diff --git a/src/pages/Theming.tsx b/src/pages/Theming.tsx index 3804de4..d99ae82 100644 --- a/src/pages/Theming.tsx +++ b/src/pages/Theming.tsx @@ -59,17 +59,69 @@ import { withGlassThemeDefaults, } from "../utils/themeUtils"; import ThemeEditor from "../components/theming/ThemeEditor"; +import ThemeList from "../components/theming/ThemeList"; import ColorPickerPopover from "../components/theming/ColorPickerPopover"; import ThemeCSSModal from "../components/theming/ThemeCSSModal"; import { ContentContainer } from "../components/content/ContentContainer"; +import { + applyThemeToDocument, + clearThemeEditorState, + readThemeEditorState, + writeThemeEditorState, +} from "../lib/themeEditorPersistence"; +import { createActionStatus } from "../components/showcase/ActionStatus"; + +const withIdentity = (theme: Theme): Theme => ({ + ...theme, + _id: theme._id || `${Date.now()}-${Math.random().toString(36).slice(2)}`, +}); + +const restoreThemeLibrary = ( + persisted: ReturnType, +): { currentTheme: Theme; themes: Theme[] } => { + if (!persisted) { + const currentTheme = withIdentity( + withGlassThemeDefaults({ ...generateRandomTheme(), name: "Theme 1" }), + ); + return { currentTheme, themes: [currentTheme] }; + } + + const currentTheme = withIdentity( + withGlassThemeDefaults(persisted.currentTheme), + ); + let linkedCurrentTheme = false; + const themes = persisted.themes.map((theme) => { + const isCurrentTheme = + !linkedCurrentTheme && + (theme._id === currentTheme._id || + (!persisted.currentTheme._id && theme.name === currentTheme.name)); + if (isCurrentTheme) { + linkedCurrentTheme = true; + return currentTheme; + } + return withIdentity(withGlassThemeDefaults(theme)); + }); + + return { currentTheme, themes }; +}; export default function Theming() { - const initialTheme = withGlassThemeDefaults(generateRandomTheme()); + const persisted = readThemeEditorState(); + const restored = restoreThemeLibrary(persisted); + const initialTheme = restored.currentTheme; const [currentTheme, setCurrentTheme] = createSignal(initialTheme); + const [customThemes, setCustomThemes] = createSignal(restored.themes); const [showColorPicker, setShowColorPicker] = createSignal(false); const [selectedColorKey, setSelectedColorKey] = createSignal(""); const [pickerPosition, setPickerPosition] = createSignal({ x: 0, y: 0 }); const [showCSSModal, setShowCSSModal] = createSignal(false); + const [applyToWholeSite, setApplyToWholeSite] = createSignal( + persisted?.applyToWholeSite || false, + ); + const [rememberTheme, setRememberTheme] = createSignal(Boolean(persisted)); + const actionStatus = createActionStatus( + persisted ? "Remembered theme restored" : "Theme editor ready", + ); // Detect initial theme type automatically const initialIsDark = (initialTheme as any)._themeType === "dark"; @@ -80,6 +132,29 @@ export default function Theming() { }); const [dockActiveItem] = createSignal("editor"); + const identity = (theme: Theme) => theme._id || theme.name; + + const persist = ( + theme = currentTheme(), + themes = customThemes(), + apply = applyToWholeSite(), + ) => { + if (rememberTheme()) { + writeThemeEditorState({ currentTheme: theme, themes, applyToWholeSite: apply }); + } + }; + + const commitTheme = (theme: Theme) => { + const next = withIdentity(withGlassThemeDefaults(theme)); + setCurrentTheme(next); + const themes = customThemes().map((saved) => + identity(saved) === identity(next) ? next : saved, + ); + setCustomThemes(themes); + if (applyToWholeSite()) applyThemeToDocument(next); + persist(next, themes); + }; + /** * RANDOM THEME GENERATION PROCESS * @@ -110,7 +185,13 @@ export default function Theming() { */ const randomizeTheme = () => { const newTheme = withGlassThemeDefaults(generateRandomTheme()); - setCurrentTheme(newTheme); + const replacement = { + ...newTheme, + name: currentTheme().name, + _id: currentTheme()._id, + }; + commitTheme(replacement); + actionStatus.announce("Theme randomized"); const isDark = (newTheme as any)._themeType === "dark"; setThemeOptions((prev) => ({ @@ -119,6 +200,87 @@ export default function Theming() { })); }; + const createNewTheme = () => { + const usedNames = new Set(customThemes().map((theme) => theme.name)); + let nextThemeNumber = 1; + while (usedNames.has(`Theme ${nextThemeNumber}`)) nextThemeNumber += 1; + const theme = withIdentity( + withGlassThemeDefaults({ + ...generateRandomTheme(), + name: `Theme ${nextThemeNumber}`, + }), + ); + const themes = [theme, ...customThemes()]; + setCurrentTheme(theme); + setCustomThemes(themes); + if (applyToWholeSite()) applyThemeToDocument(theme); + persist(theme, themes); + actionStatus.announce(`Theme added: ${theme.name}`); + }; + + const loadTheme = (theme: Theme) => { + commitTheme(theme); + actionStatus.announce(`Theme selected: ${theme.name}`); + }; + + const removeTheme = (theme: Theme) => { + const remaining = customThemes().filter( + (saved) => identity(saved) !== identity(theme), + ); + if (identity(currentTheme()) !== identity(theme)) { + setCustomThemes(remaining); + persist(currentTheme(), remaining); + actionStatus.announce(`Theme deleted: ${theme.name}`); + return; + } + + const next = remaining[0] || withIdentity(withGlassThemeDefaults(generateRandomTheme())); + setCurrentTheme(next); + setCustomThemes(remaining); + if (applyToWholeSite()) applyThemeToDocument(next); + persist(next, remaining); + actionStatus.announce(`Theme deleted: ${theme.name}`); + }; + + const clearAllThemes = () => { + const next = withIdentity(withGlassThemeDefaults(generateRandomTheme())); + setCurrentTheme(next); + setCustomThemes([]); + if (applyToWholeSite()) applyThemeToDocument(next); + persist(next, []); + actionStatus.announce("Saved themes cleared"); + }; + + const updateThemeName = (name: string) => { + commitTheme({ ...currentTheme(), name }); + actionStatus.announce(`Theme renamed: ${name}`); + }; + + const changeApplyToWholeSite = (checked: boolean) => { + setApplyToWholeSite(checked); + applyThemeToDocument(checked ? currentTheme() : null); + persist(currentTheme(), customThemes(), checked); + actionStatus.announce( + checked ? "Theme applied to whole site" : "Theme scoped to preview", + ); + }; + + const changeRememberTheme = (checked: boolean) => { + setRememberTheme(checked); + if (checked) { + writeThemeEditorState({ + currentTheme: currentTheme(), + themes: customThemes(), + applyToWholeSite: applyToWholeSite(), + }); + } else { + clearThemeEditorState(); + } + actionStatus.announce( + checked ? "Theme will be remembered" : "Theme will not be remembered", + ); + }; + const openColorPicker = (colorKey: string, event: MouseEvent) => { const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); setPickerPosition({ @@ -134,17 +296,20 @@ export default function Theming() { if (key) { const newTheme = updateThemeColor(currentTheme(), key, colorValue); - setCurrentTheme(newTheme); + commitTheme(newTheme); + actionStatus.announce(`Color updated: ${key}`); } }; const updateThemePropertyValue = (key: string, value: string) => { const newTheme = updateThemeProperty(currentTheme(), key, value); - setCurrentTheme(newTheme); + commitTheme(newTheme); + actionStatus.announce(`Theme token updated: ${key}`); }; const updateGlassThemeValues = (values: Record) => { - setCurrentTheme((theme) => ({ ...theme, ...values })); + commitTheme({ ...currentTheme(), ...values }); + actionStatus.announce("Glass settings updated"); }; const exportCSS = ( @@ -168,15 +333,29 @@ export default function Theming() {

-
+
+
diff --git a/src/pages/docs/Layouts.tsx b/src/pages/docs/Layouts.tsx index 9b1a6af..f2560b2 100644 --- a/src/pages/docs/Layouts.tsx +++ b/src/pages/docs/Layouts.tsx @@ -3,8 +3,10 @@ import { ContentContainer } from "../../components/content/ContentContainer"; import { Callout } from "../../components/content/Callout"; import { CodeBlock } from "../../components/showcase/CodeBlock"; import { ROUTES } from "../../config/routes"; +import { ActionStatus, createActionStatus } from "../../components/showcase/ActionStatus"; export default function Layouts() { + const actions = createActionStatus("Choose how to continue the Layouts example."); return ( @@ -99,9 +101,10 @@ export function Actions() { }`} /> - - + + + diff --git a/tests/ps-qa/checks/calendar.ron b/tests/ps-qa/checks/calendar.ron index a46645c..9dcf671 100644 --- a/tests/ps-qa/checks/calendar.ron +++ b/tests/ps-qa/checks/calendar.ron @@ -165,8 +165,8 @@ open: None, prepare: Some("link:Controlled Value"), hover: None, - click: Some("gridcell:Tuesday, June 24, 2025"), - subject: "gridcell:Tuesday, June 24, 2025", + click: Some("#controlled-calendar--2025-06-24"), + subject: "#controlled-calendar--2025-06-24", expect: SelectionChanges, outcome_timeout_ms: 8000, ), @@ -185,8 +185,8 @@ prepare: Some("link:Controlled Value"), hover: None, // The July grid runs Sunday 29 June to Saturday 9 August 2025. - click: Some("gridcell:Saturday, July 12, 2025"), - subject: "gridcell:Friday, August 1, 2025", + click: Some("#controlled-calendar--2025-07-12"), + subject: "#controlled-calendar--2025-08-01", expect: Paints, outcome_timeout_ms: 8000, ), @@ -206,7 +206,98 @@ // accumulates months instead of replacing them -- but it is only // evidence at all once every check above it is green. Do not read a // pass here on its own. - subject: "gridcell:Sunday, June 1, 2025", + subject: "#controlled-calendar--2025-06-01", expect: Absent, ), + + // Each calendar navigation button is addressed by a stable authored id. + ( + id: "default-calendar-previous-calendar-month", + group: "calendar", + what: "default-calendar Previous month changes its own visible heading", + open: None, + hover: None, + click: Some("#default-calendar--previous-month"), + subject: "#default-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "default-calendar-next-calendar-month", + group: "calendar", + what: "default-calendar Next month changes its own visible heading back", + open: None, + hover: None, + click: Some("#default-calendar--next-month"), + subject: "#default-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "controlled-calendar-previous-calendar-month", + group: "calendar", + what: "controlled-calendar Previous month changes its own visible heading", + open: None, + hover: None, + click: Some("#controlled-calendar--previous-month"), + subject: "#controlled-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "controlled-calendar-next-calendar-month", + group: "calendar", + what: "controlled-calendar Next month changes its own visible heading back", + open: None, + hover: None, + click: Some("#controlled-calendar--next-month"), + subject: "#controlled-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "range-calendar-previous-calendar-month", + group: "calendar", + what: "range-calendar Previous month changes its own visible heading", + open: None, + hover: None, + click: Some("#range-calendar--previous-month"), + subject: "#range-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "range-calendar-next-calendar-month", + group: "calendar", + what: "range-calendar Next month changes its own visible heading back", + open: None, + hover: None, + click: Some("#range-calendar--next-month"), + subject: "#range-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "bounded-calendar-previous-calendar-month", + group: "calendar", + what: "the bounded calendar moves backward within its allowed range", + open: None, + hover: None, + click: Some("#bounded-calendar--previous-month"), + subject: "#bounded-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "bounded-calendar-next-calendar-month", + group: "calendar", + what: "the bounded calendar returns forward within its allowed range", + open: None, + hover: None, + click: Some("#bounded-calendar--next-month"), + subject: "#bounded-calendar--heading", + expect: NameChanges, + outcome_timeout_ms: 8000, + ), + ] diff --git a/tests/ps-qa/checks/navigation.ron b/tests/ps-qa/checks/navigation.ron index 5bf9f69..c273cea 100644 --- a/tests/ps-qa/checks/navigation.ron +++ b/tests/ps-qa/checks/navigation.ron @@ -316,4 +316,224 @@ subject: "tab:Vertical", expect: Paints, ), + + ( + id: "the-dropdown-outcomes-reset-navigation", + group: "navigation", + what: "the placement checks start from a closed global component menu", + open: Some("Docs"), + hover: None, + click: None, + subject: "heading:Quick Start", + expect: Present, + outcome_timeout_ms: 8000, + ), + ( + id: "the-dropdown-outcomes-open-components", + group: "navigation", + what: "the global component menu opens for the dropdown checks", + open: None, + hover: None, + click: Some("Components"), + subject: "button:Actions", + expect: PaintsNamed, + ), + ( + id: "the-dropdown-outcomes-select-actions", + group: "navigation", + what: "the Actions category exposes Dropdown", + open: None, + hover: None, + click: Some("button:Actions"), + subject: "link:Dropdown", + expect: PaintsNamed, + ), + ( + id: "the-dropdown-outcomes-open-the-page", + group: "navigation", + what: "the dropdown placement checks run on the Dropdown showcase", + open: None, + hover: None, + click: Some("link:Dropdown"), + subject: "heading:Dropdown", + expect: Present, + outcome_timeout_ms: 8000, + ), + // Placement menus and multilevel navigation get direct outcomes. + ( + id: "dropdown-left-placement-opens", + group: "navigation", + what: "the left placement trigger opens a visible menu", + open: None, + hover: None, + click: Some("#left-placement--trigger"), + subject: "menuitem:Item 1", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-left-placement-closes", + group: "navigation", + what: "Escape closes the left placement menu", + open: None, + prepare: Some("#left-placement--trigger"), + hover: None, + click: None, + key: Some("Escape"), + key_on: Some("#left-placement--trigger"), + subject: "menuitem:Item 1", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-right-placement-opens", + group: "navigation", + what: "the right placement trigger opens a visible menu", + open: None, + hover: None, + click: Some("#right-placement--trigger"), + subject: "menuitem:Item 1", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-right-placement-closes", + group: "navigation", + what: "Escape closes the right placement menu", + open: None, + prepare: Some("#right-placement--trigger"), + hover: None, + click: None, + key: Some("Escape"), + key_on: Some("#right-placement--trigger"), + subject: "menuitem:Item 1", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-top-placement-opens", + group: "navigation", + what: "the top placement trigger opens a visible menu", + open: None, + hover: None, + click: Some("#top-placement--trigger"), + subject: "menuitem:Item 1", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-top-placement-closes", + group: "navigation", + what: "Escape closes the top placement menu", + open: None, + prepare: Some("#top-placement--trigger"), + hover: None, + click: None, + key: Some("Escape"), + key_on: Some("#top-placement--trigger"), + subject: "menuitem:Item 1", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-bottom-placement-opens", + group: "navigation", + what: "the bottom placement trigger opens a visible menu", + open: None, + hover: None, + click: Some("#bottom-placement--trigger"), + subject: "menuitem:Item 1", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "dropdown-bottom-placement-closes", + group: "navigation", + what: "Escape closes the bottom placement menu", + open: None, + prepare: Some("#bottom-placement--trigger"), + hover: None, + click: None, + key: Some("Escape"), + key_on: Some("#bottom-placement--trigger"), + subject: "menuitem:Item 1", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the-navbar-outcomes-reset-navigation", + group: "navigation", + what: "the multilevel checks start from a closed global component menu", + open: Some("Docs"), + hover: None, + click: None, + subject: "heading:Quick Start", + expect: Present, + outcome_timeout_ms: 8000, + ), + ( + id: "the-navbar-outcomes-open-components", + group: "navigation", + what: "the global component menu opens for the navbar checks", + open: None, + hover: None, + click: Some("Components"), + subject: "button:Navigation", + expect: PaintsNamed, + ), + ( + id: "the-navbar-outcomes-select-navigation", + group: "navigation", + what: "the Navigation category exposes Navbar", + open: None, + hover: None, + click: Some("button:Navigation"), + subject: "link:Navbar", + expect: PaintsNamed, + ), + ( + id: "the-navbar-outcomes-open-the-page", + group: "navigation", + what: "the multilevel checks run on the Navbar showcase", + open: None, + hover: None, + click: Some("link:Navbar"), + subject: "heading:Navbar", + expect: Present, + outcome_timeout_ms: 8000, + ), + ( + id: "navbar-trading-terminal-selects", + group: "navigation", + what: "the Trading Terminal navigation choice reports its active state", + open: None, + hover: None, + click: Some("button:Trading Terminal"), + subject: "button:Trading Terminal", + expect: SelectionChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "navbar-performance-selects", + group: "navigation", + what: "the Performance navigation choice reports its active state", + open: None, + hover: None, + click: Some("button:Performance"), + subject: "button:Performance", + expect: SelectionChanges, + outcome_timeout_ms: 8000, + ), + ( + id: "navbar-funding-comparison-selects", + group: "navigation", + what: "the Funding Comparison navigation choice reports its active state", + open: None, + hover: None, + click: Some("button:Funding Comparison"), + subject: "button:Funding Comparison", + expect: SelectionChanges, + outcome_timeout_ms: 8000, + ), + ] diff --git a/tests/ps-qa/checks/theming.ron b/tests/ps-qa/checks/theming.ron index 492f72d..3e864b6 100644 --- a/tests/ps-qa/checks/theming.ron +++ b/tests/ps-qa/checks/theming.ron @@ -548,4 +548,1211 @@ expect: Vanishes, outcome_timeout_ms: 8000, ), + + // Restored workflows and complete editor-control coverage. + ( + id: "the-theme-library-starts-with-the-current-theme", + group: "theming", + what: "the editor exposes the current named theme as a selectable UI control", + open: None, + hover: None, + click: None, + subject: "button:Theme 1", + expect: PaintsNamed, + ), + ( + id: "a-new-theme-is-added", + group: "theming", + what: "Add creates and selects a second editable theme", + open: None, + hover: None, + click: Some("button:Add new theme"), + subject: "status:Theme added: Theme 2", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the-second-theme-is-reachable", + group: "theming", + what: "the added theme appears as a selected named control", + open: None, + hover: None, + click: None, + subject: "button:Theme 2", + expect: PaintsNamed, + ), + ( + id: "a-saved-theme-can-be-selected", + group: "theming", + what: "selecting an earlier theme loads it into the editor", + open: None, + hover: None, + click: Some("button:Theme 1"), + subject: "status:Theme selected: Theme 1", + expect: PaintsNamed, + ), + ( + id: "a-theme-can-be-renamed", + group: "theming", + what: "typing a new name updates the selected saved theme", + open: None, + hover: None, + click: None, + type_into: Some("textbox:Theme name"), + text: Some("Ocean"), + subject: "status:Theme renamed: Ocean", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the-renamed-theme-remains-selectable", + group: "theming", + what: "the saved-theme control carries the edited name", + open: None, + hover: None, + click: None, + subject: "button:Ocean", + expect: PaintsNamed, + ), + ( + id: "a-theme-can-be-deleted", + group: "theming", + what: "Delete removes the selected theme and loads a remaining one", + open: None, + hover: None, + click: Some("button:Delete theme Ocean"), + subject: "status:Theme deleted: Ocean", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "another-theme-can-be-added-before-clearing", + group: "theming", + what: "the library can grow again after a deletion", + open: None, + hover: None, + click: Some("button:Add new theme"), + subject: "status:Theme added: Theme 1", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "saved-themes-can-be-cleared", + group: "theming", + what: "Clear saved themes empties the saved library while retaining a working editor theme", + open: None, + hover: None, + click: Some("button:Clear saved themes"), + subject: "status:Saved themes cleared", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "clearing-removes-the-saved-theme-controls", + group: "theming", + what: "the saved theme controls are absent after the library is cleared", + open: None, + hover: None, + click: None, + subject: "button:Theme 2", + expect: Absent, + ), + ( + id: "the_base_100_swatch_opens_its_picker", + group: "theming", + what: "the --color-base-100 control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-base-100"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_100_picker_closes", + group: "theming", + what: "the --color-base-100 picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_200_swatch_opens_its_picker", + group: "theming", + what: "the --color-base-200 control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-base-200"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_200_picker_closes", + group: "theming", + what: "the --color-base-200 picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_300_swatch_opens_its_picker", + group: "theming", + what: "the --color-base-300 control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-base-300"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_300_picker_closes", + group: "theming", + what: "the --color-base-300 picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-base-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-base-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_base_content_picker_closes", + group: "theming", + what: "the --color-base-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_primary_swatch_opens_its_picker", + group: "theming", + what: "the --color-primary control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-primary"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "selecting_a_material_colour_updates_the_theme", + group: "theming", + what: "a picker choice writes the selected theme colour", + open: None, + hover: None, + click: Some("button:red-500"), + subject: "status:Color updated: --color-primary", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_primary_picker_closes", + group: "theming", + what: "the --color-primary picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_primary_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-primary-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-primary-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_primary_content_picker_closes", + group: "theming", + what: "the --color-primary-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_secondary_swatch_opens_its_picker", + group: "theming", + what: "the --color-secondary control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-secondary"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_secondary_picker_closes", + group: "theming", + what: "the --color-secondary picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_secondary_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-secondary-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-secondary-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_secondary_content_picker_closes", + group: "theming", + what: "the --color-secondary-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_accent_swatch_opens_its_picker", + group: "theming", + what: "the --color-accent control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-accent"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_accent_picker_closes", + group: "theming", + what: "the --color-accent picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_accent_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-accent-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-accent-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_accent_content_picker_closes", + group: "theming", + what: "the --color-accent-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_neutral_swatch_opens_its_picker", + group: "theming", + what: "the --color-neutral control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-neutral"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_neutral_picker_closes", + group: "theming", + what: "the --color-neutral picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_neutral_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-neutral-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-neutral-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_neutral_content_picker_closes", + group: "theming", + what: "the --color-neutral-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_info_swatch_opens_its_picker", + group: "theming", + what: "the --color-info control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-info"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_info_picker_closes", + group: "theming", + what: "the --color-info picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_info_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-info-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-info-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_info_content_picker_closes", + group: "theming", + what: "the --color-info-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_success_swatch_opens_its_picker", + group: "theming", + what: "the --color-success control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-success"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_success_picker_closes", + group: "theming", + what: "the --color-success picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_success_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-success-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-success-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_success_content_picker_closes", + group: "theming", + what: "the --color-success-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_warning_swatch_opens_its_picker", + group: "theming", + what: "the --color-warning control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-warning"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_warning_picker_closes", + group: "theming", + what: "the --color-warning picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_warning_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-warning-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-warning-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_warning_content_picker_closes", + group: "theming", + what: "the --color-warning-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_error_swatch_opens_its_picker", + group: "theming", + what: "the --color-error control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-error"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_error_picker_closes", + group: "theming", + what: "the --color-error picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "the_error_content_swatch_opens_its_picker", + group: "theming", + what: "the --color-error-content control opens the material colour picker", + open: None, + hover: None, + click: Some("button:--color-error-content"), + subject: "button:red-500", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_error_content_picker_closes", + group: "theming", + what: "the --color-error-content picker can be dismissed cleanly", + open: None, + hover: None, + click: Some("button:Done"), + subject: "button:red-500", + expect: Vanishes, + outcome_timeout_ms: 8000, + ), + ( + id: "boxes_square_can_be_selected", + group: "theming", + what: "boxes radius supports square", + open: None, + prepare: Some("radio:Boxes radius 4 pixels"), + hover: None, + click: Some("radio:Boxes radius square"), + subject: "radio:Boxes radius square", + expect: SelectionChanges, + ), + ( + id: "boxes_4_pixels_can_be_selected", + group: "theming", + what: "boxes radius supports 4 pixels", + open: None, + prepare: Some("radio:Boxes radius square"), + hover: None, + click: Some("radio:Boxes radius 4 pixels"), + subject: "radio:Boxes radius 4 pixels", + expect: SelectionChanges, + ), + ( + id: "boxes_8_pixels_can_be_selected", + group: "theming", + what: "boxes radius supports 8 pixels", + open: None, + prepare: Some("radio:Boxes radius square"), + hover: None, + click: Some("radio:Boxes radius 8 pixels"), + subject: "radio:Boxes radius 8 pixels", + expect: SelectionChanges, + ), + ( + id: "boxes_16_pixels_can_be_selected", + group: "theming", + what: "boxes radius supports 16 pixels", + open: None, + prepare: Some("radio:Boxes radius square"), + hover: None, + click: Some("radio:Boxes radius 16 pixels"), + subject: "radio:Boxes radius 16 pixels", + expect: SelectionChanges, + ), + ( + id: "boxes_32_pixels_can_be_selected", + group: "theming", + what: "boxes radius supports 32 pixels", + open: None, + prepare: Some("radio:Boxes radius square"), + hover: None, + click: Some("radio:Boxes radius 32 pixels"), + subject: "radio:Boxes radius 32 pixels", + expect: SelectionChanges, + ), + ( + id: "fields_square_can_be_selected", + group: "theming", + what: "fields radius supports square", + open: None, + prepare: Some("radio:Fields radius 4 pixels"), + hover: None, + click: Some("radio:Fields radius square"), + subject: "radio:Fields radius square", + expect: SelectionChanges, + ), + ( + id: "fields_4_pixels_can_be_selected", + group: "theming", + what: "fields radius supports 4 pixels", + open: None, + prepare: Some("radio:Fields radius square"), + hover: None, + click: Some("radio:Fields radius 4 pixels"), + subject: "radio:Fields radius 4 pixels", + expect: SelectionChanges, + ), + ( + id: "fields_8_pixels_can_be_selected", + group: "theming", + what: "fields radius supports 8 pixels", + open: None, + prepare: Some("radio:Fields radius square"), + hover: None, + click: Some("radio:Fields radius 8 pixels"), + subject: "radio:Fields radius 8 pixels", + expect: SelectionChanges, + ), + ( + id: "fields_16_pixels_can_be_selected", + group: "theming", + what: "fields radius supports 16 pixels", + open: None, + prepare: Some("radio:Fields radius square"), + hover: None, + click: Some("radio:Fields radius 16 pixels"), + subject: "radio:Fields radius 16 pixels", + expect: SelectionChanges, + ), + ( + id: "fields_32_pixels_can_be_selected", + group: "theming", + what: "fields radius supports 32 pixels", + open: None, + prepare: Some("radio:Fields radius square"), + hover: None, + click: Some("radio:Fields radius 32 pixels"), + subject: "radio:Fields radius 32 pixels", + expect: SelectionChanges, + ), + ( + id: "selectors_square_can_be_selected", + group: "theming", + what: "selectors radius supports square", + open: None, + prepare: Some("radio:Selectors radius 4 pixels"), + hover: None, + click: Some("radio:Selectors radius square"), + subject: "radio:Selectors radius square", + expect: SelectionChanges, + ), + ( + id: "selectors_4_pixels_can_be_selected", + group: "theming", + what: "selectors radius supports 4 pixels", + open: None, + prepare: Some("radio:Selectors radius square"), + hover: None, + click: Some("radio:Selectors radius 4 pixels"), + subject: "radio:Selectors radius 4 pixels", + expect: SelectionChanges, + ), + ( + id: "selectors_8_pixels_can_be_selected", + group: "theming", + what: "selectors radius supports 8 pixels", + open: None, + prepare: Some("radio:Selectors radius square"), + hover: None, + click: Some("radio:Selectors radius 8 pixels"), + subject: "radio:Selectors radius 8 pixels", + expect: SelectionChanges, + ), + ( + id: "selectors_16_pixels_can_be_selected", + group: "theming", + what: "selectors radius supports 16 pixels", + open: None, + prepare: Some("radio:Selectors radius square"), + hover: None, + click: Some("radio:Selectors radius 16 pixels"), + subject: "radio:Selectors radius 16 pixels", + expect: SelectionChanges, + ), + ( + id: "selectors_32_pixels_can_be_selected", + group: "theming", + what: "selectors radius supports 32 pixels", + open: None, + prepare: Some("radio:Selectors radius square"), + hover: None, + click: Some("radio:Selectors radius 32 pixels"), + subject: "radio:Selectors radius 32 pixels", + expect: SelectionChanges, + ), + ( + id: "glass_defaults_execute_their_reset_callback", + group: "theming", + what: "Reset runs the glass-token reset path", + open: None, + hover: None, + click: Some("button:Reset to Hype4 defaults"), + subject: "status:Glass settings updated", + expect: PaintsNamed, + ), + ( + id: "whole_site_application_changes_the_document", + group: "theming", + what: "Apply to whole site changes the surrounding page theme", + open: None, + hover: None, + click: Some("switch:Apply to whole site"), + subject: "heading:Theme Editor", + expect: PixelsChange, + outcome_timeout_ms: 8000, + ), + ( + id: "whole_site_application_reports_completion", + group: "theming", + what: "the site-scope callback reports its product outcome", + open: None, + hover: None, + click: None, + subject: "status:Theme applied to whole site", + expect: PaintsNamed, + ), + ( + id: "whole_site_application_can_be_removed", + group: "theming", + what: "turning site scope off restores preview-only styling", + open: None, + hover: None, + click: Some("switch:Apply to whole site"), + subject: "status:Theme scoped to preview", + expect: PaintsNamed, + ), + ( + id: "remembering_a_theme_writes_persistence", + group: "theming", + what: "Remember this theme executes the persistence path", + open: None, + hover: None, + click: Some("switch:Remember this theme"), + subject: "status:Theme will be remembered", + expect: PaintsNamed, + ), + ( + id: "the_remembered_theme_survives_route_teardown", + group: "theming", + what: "leaving the editor tears down the theming route while retaining storage", + open: Some("Home"), + hover: None, + click: None, + subject: "link:Start Building Today", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "the_remembered_theme_is_restored", + group: "theming", + what: "returning to the editor restores the persisted theme state", + open: Some("Theming"), + hover: None, + click: None, + subject: "status:Remembered theme restored", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "returning-to-the-editor-restores-the-page-top", + group: "theming", + what: "route navigation returns the editor to its heading rather than retaining a deep scroll position", + open: None, + hover: None, + click: None, + subject: "heading:Theme Editor", + expect: PaintsNamed, + ), + ( + id: "remembered_state_can_be_cleared", + group: "theming", + what: "turning Remember off clears the persisted editor state", + open: None, + hover: None, + click: Some("switch:Remember this theme"), + subject: "status:Theme will not be remembered", + expect: PaintsNamed, + ), + ( + id: "the_preview_variants_tab_is_named_and_operable", + group: "theming", + what: "the icon tab exposes a name and switches to variants", + open: None, + hover: None, + click: Some("tab:Component Variants"), + subject: "heading:Buttons", + expect: PaintsNamed, + ), + ( + id: "the_button_variant_button_acts", + group: "theming", + what: "the Button specimen reports its activation", + open: None, + hover: None, + click: Some("button:Button"), + subject: "status:Button variant activated", + expect: PaintsNamed, + ), + ( + id: "the_primary_variant_button_acts", + group: "theming", + what: "the Primary specimen reports its activation", + open: None, + hover: None, + click: Some("button:Primary"), + subject: "status:Primary variant activated", + expect: PaintsNamed, + ), + ( + id: "the_secondary_variant_button_acts", + group: "theming", + what: "the Secondary specimen reports its activation", + open: None, + hover: None, + click: Some("button:Secondary"), + subject: "status:Secondary variant activated", + expect: PaintsNamed, + ), + ( + id: "the_accent_variant_button_acts", + group: "theming", + what: "the Accent specimen reports its activation", + open: None, + hover: None, + click: Some("button:Accent"), + subject: "status:Accent variant activated", + expect: PaintsNamed, + ), + ( + id: "the_outline_variant_button_acts", + group: "theming", + what: "the Outline specimen reports its activation", + open: None, + hover: None, + click: Some("button:Outline"), + subject: "status:Outline variant activated", + expect: PaintsNamed, + ), + ( + id: "the_ghost_variant_button_acts", + group: "theming", + what: "the Ghost specimen reports its activation", + open: None, + hover: None, + click: Some("button:Ghost"), + subject: "status:Ghost variant activated", + expect: PaintsNamed, + ), + ( + id: "the_danger_variant_button_acts", + group: "theming", + what: "the Danger specimen reports its activation", + open: None, + hover: None, + click: Some("button:Danger"), + subject: "status:Danger variant activated", + expect: PaintsNamed, + ), + ( + id: "the_danger_soft_variant_button_acts", + group: "theming", + what: "the Danger Soft specimen reports its activation", + open: None, + hover: None, + click: Some("button:Danger Soft"), + subject: "status:Danger Soft variant activated", + expect: PaintsNamed, + ), + ( + id: "the_preview_palette_tab_is_named_and_operable", + group: "theming", + what: "the palette icon tab switches to the generated colours", + open: None, + hover: None, + click: Some("tab:Color Palette"), + subject: "heading:Color Palette", + expect: PaintsNamed, + ), + ( + id: "the_preview_demo_tab_is_named_and_operable", + group: "theming", + what: "the demo icon tab returns to interactive components", + open: None, + hover: None, + click: Some("tab:Components Demo"), + subject: "heading:Components Demo", + expect: PaintsNamed, + ), + ( + id: "preview_more_acts", + group: "theming", + what: "the preview more control reports its intended action", + open: None, + hover: None, + click: Some("button:more"), + subject: "status:More filters requested", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_apply_acts", + group: "theming", + what: "the preview Apply control reports its intended action", + open: None, + hover: None, + click: Some("button:Apply"), + subject: "status:Price range applied", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_click_acts", + group: "theming", + what: "the preview Click control reports its intended action", + open: None, + hover: None, + click: Some("button:Click"), + subject: "status:Email verification requested", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_support_acts", + group: "theming", + what: "the preview Support control reports its intended action", + open: None, + hover: None, + click: Some("button:Support"), + subject: "status:Support requested", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_choose_plan_acts", + group: "theming", + what: "the preview Choose Plan control reports its intended action", + open: None, + hover: None, + click: Some("button:Choose Plan"), + subject: "status:Plan selected", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_notifications_acts", + group: "theming", + what: "the preview Notifications control reports its intended action", + open: None, + hover: None, + click: Some("button:Notifications"), + subject: "status:Notifications opened", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_start_call_acts", + group: "theming", + what: "the preview Start call control reports its intended action", + open: None, + hover: None, + click: Some("button:Start call"), + subject: "status:Call started", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_open_messages_acts", + group: "theming", + what: "the preview Open messages control reports its intended action", + open: None, + hover: None, + click: Some("button:Open messages"), + subject: "status:Messages opened", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_conversation_settings_acts", + group: "theming", + what: "the preview Conversation settings control reports its intended action", + open: None, + hover: None, + click: Some("button:Conversation settings"), + subject: "status:Conversation settings opened", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_previous_track_acts", + group: "theming", + what: "the preview Previous track control reports its intended action", + open: None, + hover: None, + click: Some("button:Previous track"), + subject: "status:Previous track selected", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_play_track_acts", + group: "theming", + what: "the preview Play track control reports its intended action", + open: None, + hover: None, + click: Some("button:Play track"), + subject: "status:Playback started", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_next_track_acts", + group: "theming", + what: "the preview Next track control reports its intended action", + open: None, + hover: None, + click: Some("button:Next track"), + subject: "status:Next track selected", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_toggle_volume_acts", + group: "theming", + what: "the preview Toggle volume control reports its intended action", + open: None, + hover: None, + click: Some("button:Toggle volume"), + subject: "status:Volume toggled", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_open_account_acts", + group: "theming", + what: "the preview Open account control reports its intended action", + open: None, + hover: None, + click: Some("button:Open account"), + subject: "status:Account opened", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_button_1_acts", + group: "theming", + what: "the preview Button 1 control reports its intended action", + open: None, + hover: None, + click: Some("button:Button 1"), + subject: "status:Button 1 activated", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_button_2_acts", + group: "theming", + what: "the preview Button 2 control reports its intended action", + open: None, + hover: None, + click: Some("button:Button 2"), + subject: "status:Button 2 activated", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_button_3_acts", + group: "theming", + what: "the preview Button 3 control reports its intended action", + open: None, + hover: None, + click: Some("button:Button 3"), + subject: "status:Button 3 activated", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_search_preview_acts", + group: "theming", + what: "the preview Search preview control reports its intended action", + open: None, + hover: None, + click: Some("button:Search preview"), + subject: "status:Preview search submitted", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_submit_acts", + group: "theming", + what: "the preview Submit control reports its intended action", + open: None, + hover: None, + click: Some("button:Submit"), + subject: "status:Feedback submitted", + expect: PaintsNamed, + outcome_timeout_ms: 8000, + ), + ( + id: "preview_calendar_day_1_selects", + group: "theming", + what: "calendar day 1 reports its selected outcome", + open: None, + hover: None, + click: Some("button:1"), + subject: "status:Calendar day 1 selected", + expect: PaintsNamed, + ), + ( + id: "preview_calendar_day_2_selects", + group: "theming", + what: "calendar day 2 reports its selected outcome", + open: None, + hover: None, + click: Some("button:2"), + subject: "status:Calendar day 2 selected", + expect: PaintsNamed, + ), + ( + id: "preview_calendar_day_3_selects", + group: "theming", + what: "calendar day 3 reports its selected outcome", + open: None, + hover: None, + click: Some("button:3"), + subject: "status:Calendar day 3 selected", + expect: PaintsNamed, + ), + ( + id: "preview_calendar_day_4_selects", + group: "theming", + what: "calendar day 4 reports its selected outcome", + open: None, + hover: None, + click: Some("button:4"), + subject: "status:Calendar day 4 selected", + expect: PaintsNamed, + ), + ( + id: "preview_calendar_day_5_selects", + group: "theming", + what: "calendar day 5 reports its selected outcome", + open: None, + hover: None, + click: Some("button:5"), + subject: "status:Calendar day 5 selected", + expect: PaintsNamed, + ), + ( + id: "preview_calendar_day_6_selects", + group: "theming", + what: "calendar day 6 reports its selected outcome", + open: None, + hover: None, + click: Some("button:6"), + subject: "status:Calendar day 6 selected", + expect: PaintsNamed, + ), + ( + id: "preview_calendar_day_7_selects", + group: "theming", + what: "calendar day 7 reports its selected outcome", + open: None, + hover: None, + click: Some("button:7"), + subject: "status:Calendar day 7 selected", + expect: PaintsNamed, + ), ] diff --git a/tests/ps-qa/ps-qa.ron b/tests/ps-qa/ps-qa.ron index 36d726b..62186ca 100644 --- a/tests/ps-qa/ps-qa.ron +++ b/tests/ps-qa/ps-qa.ron @@ -9,5 +9,6 @@ AppProfile( (name: "showcases", opener: "Showcases", marker: Some("Theme Customization")), ], navigation_controls: ["Home", "Theming", "Docs", "Layouts", "Showcases", "Components"], + inert_controls: ["Bottom with arrow"], home_opener: Some("Home"), ) From 16ca0e90992e46644bc3431fa5ca5061de55fb0d Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 16:10:36 +0700 Subject: [PATCH 4/7] fix(showcase): cover every public UI family --- .github/workflows/qa.yml | 2 +- README.md | 4 +- src/components/CoverageShowcase.tsx | 710 ++++++++++++++++-- .../layout/Header/MarketingHeader.tsx | 4 + .../Header/components/ComponentsMenu.tsx | 3 +- .../Header/components/MobileSidebar.tsx | 3 +- src/pages/Showcases.tsx | 8 +- src/routes.ts | 72 +- tests/ps-qa/checks/actions.ron | 4 +- tests/ps-qa/checks/coverage.ron | 460 ++++++++++++ tests/ps-qa/checks/feedback.ron | 4 +- tests/ps-qa/checks/layout.ron | 4 +- tests/ps-qa/checks/navigation.ron | 12 +- tests/ps-qa/checks/showcase.ron | 86 ++- tests/ps-qa/checks/surfaces.ron | 4 +- tests/ps-qa/ps-qa.ron | 3 +- 16 files changed, 1298 insertions(+), 85 deletions(-) create mode 100644 tests/ps-qa/checks/coverage.ron diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index c7bcb6d..d27d5e2 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -39,7 +39,7 @@ jobs: QA_TIMEOUT_SCALE: 2 run: | for group in \ - actions calendar data-display docs feedback input \ + actions calendar coverage data-display docs feedback input \ layout navigation showcase surfaces theming unlisted do ps-qa --app tests/ps-qa/ps-qa.ron \ diff --git a/README.md b/README.md index a7670fd..daa378a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The kitchen sink demo and documentation for our UI components. ## Native QA Build the site, then run every declared outcome with the font-enabled chuzz host -and ps-qa 0.7.1 or newer: +and ps-qa 0.7.2 or newer: ```sh bun run build @@ -14,7 +14,7 @@ ps-qa --app tests/ps-qa/ps-qa.ron qa-hosted \ --checks tests/ps-qa/checks ``` -The UI 3.2 registry build passes all 347 native checks across 12 groups, +The UI 3.2 registry build passes all 555 native checks across 13 groups, including Calendar selection, month navigation, and keyboard and pointer-driven Slider and Color Picker outcomes. CI includes every group. The Layouts route uses its own document marker, and decorative cards are diff --git a/src/components/CoverageShowcase.tsx b/src/components/CoverageShowcase.tsx index fc31925..83eaa55 100644 --- a/src/components/CoverageShowcase.tsx +++ b/src/components/CoverageShowcase.tsx @@ -1,8 +1,60 @@ import type { JSX } from "@solidjs/web"; -import { createErrorBoundary, createSignal, For } from "solid-js"; -import { Accordion, Button, Card, Checkbox, Chip, Collapsible, createI18n, Empty, Flex, Header, Input, InputOTP, LanguageSwitcher, ListBox, LiveChatBubble, MetalBorder, Popover, Progress, ScrollArea, Separator, Slider, Spinner, Text, Textarea, ThemeColorPicker } from "@pathscale/ui"; +import { createErrorBoundary, createSignal, For, onCleanup, Show } from "solid-js"; +import { + Accordion, + Address, + AuthCard, + AuthFieldGroup, + AuthFooterLinks, + AuthMessage, + AuthPoweredBy, + AuthSubmitButton, + Button, + Card, + Checkbox, + Chip, + Collapsible, + ColorSwatch, + ColorWheel, + ComplexColorWheel, + Composer, + ConnectionSettings, + CookieConsent, + createConnectionSettings, + createI18n, + Empty, + FieldGroup, + FirefoxPWABanner, + Flex, + FlexGrid, + Header, + ImmersiveLanding, + InlineEdit, + Input, + InputOTP, + Label, + LanguageSwitcher, + ListBox, + LiveChatBubble, + LiveChatPanel, + MetalBorder, + PanelToggle, + PasswordField, + PasswordRequirements, + Popover, + Progress, + PWAInstallPrompt, + ScrollArea, + Separator, + Slider, + Spinner, + Text, + Textarea, + ThemeColorPicker, + type ComponentFamilyId, +} from "@pathscale/ui"; import CloseButton from "@pathscale/ui/components/close-button"; -import { ButtonGroup, CheckboxGroup, Meter, RadialProgress, SizePicker, TimeField, DateField, ColorPicker, ColorArea, ColorField, ColorSlider, ComboBox, DatePicker, DateRangePicker, RangeCalendar, Toolbar, Kbd } from "@pathscale/ui/lab"; +import { ButtonGroup, CheckboxGroup, Meter, RadialProgress, SizePicker, TimeField, DateField, ColorPicker, ColorArea, ColorField, ColorSlider, ColorSwatchPicker, ColorWheelFlower, ComboBox, DatePicker, DateRangePicker, RangeCalendar, Toolbar, Kbd } from "@pathscale/ui/lab"; import ShowcaseLayout from "./ShowcaseLayout"; /** @@ -37,7 +89,399 @@ function CloseButtonExample() { ); } +const [activeFullscreenExample, setActiveFullscreenExample] = createSignal(null); + +const savedDocumentStyle: Record = {}; + +function openFullscreenExample(name: string) { + const root = document.documentElement.style; + const body = document.body.style; + for (const [key, value] of [ + ["rootHeight", root.height], + ["rootMinHeight", root.minHeight], + ["rootOverflow", root.overflow], + ["bodyHeight", body.height], + ["bodyMinHeight", body.minHeight], + ["bodyOverflow", body.overflow], + ]) savedDocumentStyle[key] = value; + root.height = "100vh"; + root.minHeight = "0"; + root.overflow = "hidden"; + body.height = "100vh"; + body.minHeight = "0"; + body.overflow = "hidden"; + window.scrollTo({ top: 0, behavior: "instant" }); + setActiveFullscreenExample(name); +} + +function closeFullscreenExample() { + setActiveFullscreenExample(null); + const root = document.documentElement.style; + const body = document.body.style; + root.height = savedDocumentStyle.rootHeight ?? ""; + root.minHeight = savedDocumentStyle.rootMinHeight ?? ""; + root.overflow = savedDocumentStyle.rootOverflow ?? ""; + body.height = savedDocumentStyle.bodyHeight ?? ""; + body.minHeight = savedDocumentStyle.bodyMinHeight ?? ""; + body.overflow = savedDocumentStyle.bodyOverflow ?? ""; +} + +function DeferredExample(props: { name: string; render: () => JSX.Element }) { + return ( + <> + {activeFullscreenExample() === props.name + ? props.render() + : } + + ); +} + +function AddressExample() { + const [copied, setCopied] = createSignal("Nothing copied"); + return ( + <> +
setCopied(`Copied ${value}`)} /> +

{copied()}

+ + ); +} + +function AuthActionsExample() { + const [result, setResult] = createSignal("No auth action yet"); + return ( + + + + + + setResult("Sign in submitted")}>Sign in + + setResult("Privacy opened") }, + { key: "help", label: "Help", onClick: () => setResult("Help opened") }, + ]} + /> + +

{result()}

+
+ ); +} + +function ComposerExample() { + const [value, setValue] = createSignal(""); + const [sent, setSent] = createSignal("Nothing sent"); + return ( + <> + setSent(`Sent ${message}`)} + /> +

{sent()}

+ + ); +} + +function ConnectionSettingsExample() { + const [result, setResult] = createSignal("Default backend active"); + const store = createConnectionSettings({ + storageKey: "js-software-coverage-connection", + endpoints: [{ name: "api", fallback: "https://api.example.com" }], + onApply: ({ urls }) => { + setResult(`Connected to ${urls.api}`); + }, + }); + return ( + <> + { setResult(`Saved ${store.urls.api}`); }} + onResetDone={() => { setResult("Default backend restored"); }} + /> +

{result()}

+ + ); +} + +function InlineEditExample() { + const [value, setValue] = createSignal("Showcase title"); + return ( + <> + +

Saved title: {value()}

+ + ); +} + +function PanelToggleExample() { + const [expanded, setExpanded] = createSignal(true); + return ( +
+ setExpanded((value) => !value)} + /> + +
+ ); +} + +function PasswordFieldExample() { + const [value, setValue] = createSignal(""); + return ( + <> + + = 8 }, + { key: "number", message: "Contains a number", passed: /\d/.test(value()) }, + ]} + /> + + ); +} + +function LiveChatPanelExample() { + const [sent, setSent] = createSignal("Nothing sent"); + const [closed, setClosed] = createSignal(false); + return ( + Chat closed

}> + setClosed(true)} + onSendMessage={async ({ message }) => { + setSent(`Sent ${message}`); + return { messageId: "coverage-message", timestamp: Date.now() }; + }} + /> +

{sent()}

+
+ ); +} + +function ImmersiveLandingExample() { + const [page, setPage] = createSignal("intro"); + return ( + <> + setPage(to)} + > +

Introduction

+

Details

+
+

Landing page: {page()}

+ + ); +} + +function PWAExamples() { + const [result, setResult] = createSignal("No install choice"); + setTimeout(() => { + const installEvent = new Event("beforeinstallprompt", { cancelable: true }); + Object.defineProperties(installEvent, { + prompt: { value: () => undefined }, + userChoice: { value: Promise.resolve({ outcome: "accepted", platform: "web" }) }, + }); + window.dispatchEvent(installEvent); + }, 0); + return ( + <> + setResult("Install accepted")} + onDismiss={() => setResult("Install dismissed")} + /> +

{result()}

+ + ); +} + +function FirefoxPWAExample() { + const [result, setResult] = createSignal("Firefox prompt open"); + const descriptor = Object.getOwnPropertyDescriptor(navigator, "userAgent"); + Object.defineProperty(navigator, "userAgent", { + configurable: true, + value: "Mozilla/5.0 Firefox/130.0", + }); + const originalOpen = globalThis.open; + globalThis.open = (() => null) as typeof globalThis.open; + onCleanup(() => { + if (descriptor) Object.defineProperty(navigator, "userAgent", descriptor); + globalThis.open = originalOpen; + }); + return ( + <> + setResult("Firefox install opened")} + onDismiss={() => setResult("Firefox prompt dismissed")} + /> +

{result()}

+ + ); +} + +function CookieConsentExample() { + const [result, setResult] = createSignal("No cookie choice"); + return ( + <> + setResult(`Cookie consent ${type}`)} + /> +

{result()}

+ + ); +} + +function LanguageSwitcherExample() { + const [result, setResult] = createSignal("No language selected"); + const i18n = createI18n({ + languages: [ + { code: "en", name: "English" }, + { code: "es", name: "Espanol" }, + ], + storageKey: "coverage-locale", + }); + return ( + <> + setResult(`Language selected ${code}`)} + /> +

{result()}

+ + ); +} + const EXAMPLES: Example[] = [ + { name: "Address", render: () => }, + { name: "Auth composition", render: () => }, + { + name: "FieldGroup", + render: () => ( + + + + + ), + }, + { name: "Composer", render: () => }, + { name: "ConnectionSettings", render: () => }, + { + name: "ColorSwatch", + render: () => { + const [selected, setSelected] = createSignal("No swatch selected"); + return ( + <> + setSelected(`Selected ${value}`)} /> +

{selected()}

+ + ); + }, + }, + { + name: "ColorSwatchPicker", + render: () => { + const [value, setValue] = createSignal("#ef4444"); + return ( + + + + + ); + }, + }, + { + name: "ColorWheel", + render: () => { + const [value, setValue] = createSignal("#6366f1"); + return ; + }, + }, + { + name: "ColorWheelFlower", + render: () => { + const [value, setValue] = createSignal("#6366f1"); + return setValue(next.hex)} />; + }, + }, + { + name: "ComplexColorWheel", + render: () => { + const [value, setValue] = createSignal("#6366f1"); + const [strength, setStrength] = createSignal(10); + return ( + + ); + }, + }, + { name: "InlineEdit", render: () => }, + { name: "PanelToggle", render: () => }, + { name: "PasswordField and PasswordRequirements", render: () => }, + { + name: "FlexGrid", + render: () => ( + } + > + {(row) =>
{String(row)}
} +
+ ), + }, + { name: "LiveChatPanel", render: () => } /> }, + { name: "ImmersiveLanding", render: () => } /> }, + { name: "CookieConsent", render: () => } /> }, + { name: "PWAInstallPrompt", render: () => } /> }, + { name: "FirefoxPWABanner", render: () => } /> }, { name: "ButtonGroup", render: () => { @@ -131,7 +575,7 @@ const EXAMPLES: Example[] = [ ); }, }, - { name: "InputOTP", render: () => }, + { name: "InputOTP", render: () => }, { name: "ListBox", render: () => ( @@ -188,8 +632,20 @@ const EXAMPLES: Example[] = [ { name: "Text", render: () => Body text. }, { name: "Textarea", render: () =>