feat: integrate latest HMDesign design system updates
This commit is contained in:
1 parent
99faec189e
commit
1c4e0bb9a1
209 files changed
+8845
-552
No files matched your search
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from "react"
|
||||
import { flushSync } from "react-dom"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { defaultRuntimeTheme, runtimeThemeContract, fetchRuntimeTheme, type RuntimeThemeToken } from "@hm-design/ui/theme"
|
||||
import { designerThemeStore } from "./designer-preview"
|
||||
import { Specimens } from "./specimens"
|
||||
import { DesignerExtras } from "./designer-extras"
|
||||
import { StoryPage } from "@/components/story-page"
|
||||
import { VerificationStyles, waitForThemeState } from "./verification-state"
|
||||
import "./designer-theme.css"
|
||||
|
||||
const roles = [
|
||||
@@ -12,7 +14,7 @@ const roles = [
|
||||
["color.state.active", "Active / Selected", "Checkbox · Radio · Switch · Calendar"],
|
||||
["color.on.state.active", "On Active", "선택된 컨트롤의 아이콘과 글자"],
|
||||
["color.stroke.input.focus", "Focus", "입력 테두리 · Checkbox / Calendar 포커스 링"],
|
||||
["color.accent", "Accent", "메뉴 · Calendar · Table · Pagination의 45% 강조 면"],
|
||||
["color.accent", "Accent", "메뉴 · Calendar · Pagination 45%, Table 행 20% 강조 면"],
|
||||
["color.on.accent", "On Accent", "강조 면의 글자"],
|
||||
["color.action.destructive", "Destructive", "오류 · Invalid · Destructive Button / Badge"],
|
||||
["color.bg.surface", "Surface", "입력 · Card · Popup"],
|
||||
@@ -110,7 +112,7 @@ function ThemeEditor() {
|
||||
return <StoryPage><main className="hm-designer">
|
||||
<header className="hm-designer-header">
|
||||
<div><p>HM DESIGN SYSTEM / COLOR THEME</p><h1>디자인팀 Theme Playground</h1>
|
||||
<p>컬러를 바꾸면 이 화면과 다른 Story의 UI에 즉시 반영됩니다. 현재 탭의 세션 동안 유지됩니다.</p></div>
|
||||
<p>임시 컬러는 이 편집기와 Live Preview에만 적용됩니다. 편집값은 현재 탭의 세션 동안 보관하며 다른 Story는 HM Default로 검수합니다.</p></div>
|
||||
<button type="button" onClick={reset}>Reset to HM Default</button>
|
||||
</header>
|
||||
<div className="hm-designer-layout">
|
||||
@@ -156,21 +158,20 @@ function ThemeEditor() {
|
||||
</main></StoryPage>
|
||||
}
|
||||
|
||||
const meta = { title: "Theme/Designer Playground", parameters: { layout: "fullscreen" } } satisfies Meta
|
||||
const meta = { title: "Theme/Designer Playground", tags: ["internal"], parameters: { layout: "fullscreen" } } satisfies Meta
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
export const Editor: Story = { name: "Designer Playground", render: () => <ThemeEditor /> }
|
||||
|
||||
export const LiveVerification: Story = {
|
||||
tags: ["verification"],
|
||||
render: () => <><output data-testid="designer-result">Ready</output><ThemeEditor /></>,
|
||||
render: () => <div data-hm-theme-verification><VerificationStyles /><output data-testid="designer-result">Ready</output><ThemeEditor /></div>,
|
||||
play: async ({ canvasElement }) => {
|
||||
const result = canvasElement.querySelector<HTMLOutputElement>('[data-testid="designer-result"]')!
|
||||
const preview = canvasElement.querySelector<HTMLElement>('[aria-label="Live component preview"]')!
|
||||
const origin = performance.timeOrigin
|
||||
const checks: string[] = []
|
||||
const assert = (ok: boolean, label: string) => { if (!ok) throw Error(label); checks.push(label) }
|
||||
const settle = () => new Promise(resolve => setTimeout(resolve, 250))
|
||||
const variable = (property: string) => getComputedStyle(document.documentElement).getPropertyValue(property).trim().toLowerCase()
|
||||
const style = (selector: string, property: "backgroundColor" | "color" | "borderTopColor") => getComputedStyle(preview.querySelector<HTMLElement>(selector)!)[property]
|
||||
const geometry = () => Array.from(preview.querySelectorAll<HTMLElement>("[data-slot]")).map(element => {
|
||||
@@ -180,13 +181,14 @@ export const LiveVerification: Story = {
|
||||
const setInput = (label: string, value: string) => {
|
||||
const input = canvasElement.querySelector<HTMLInputElement>(`[aria-label="${label}"]`)!
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, value)
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
flushSync(() => input.dispatchEvent(new Event("input", { bubbles: true })))
|
||||
}
|
||||
const click = (label: string) => Array.from(canvasElement.querySelectorAll<HTMLButtonElement>("button")).find(button => button.textContent === label)!.click()
|
||||
const click = (label: string) => flushSync(() => Array.from(canvasElement.querySelectorAll<HTMLButtonElement>("button")).find(button => button.textContent === label)!.click())
|
||||
try {
|
||||
result.textContent = "Running designer workflow verification…"
|
||||
await document.fonts.ready
|
||||
click("Reset to HM Default"); await settle()
|
||||
click("Reset to HM Default")
|
||||
await waitForThemeState(() => roles.every(([token, label]) => canvasElement.querySelector<HTMLInputElement>(`[aria-label="${label} HEX"]`)?.value === defaultRuntimeTheme.tokens[token]), "Designer default inputs")
|
||||
const initial = JSON.stringify(geometry())
|
||||
for (const [token, label] of roles) {
|
||||
assert(canvasElement.querySelector<HTMLInputElement>(`[aria-label="${label} HEX"]`)!.value === defaultRuntimeTheme.tokens[token], `${label}: auto-loaded HM Default`)
|
||||
@@ -200,14 +202,17 @@ export const LiveVerification: Story = {
|
||||
] as const
|
||||
for (const [label, property, value] of core) {
|
||||
const others = core.filter(row => row[1] !== property).map(row => [row[1], variable(row[1])])
|
||||
setInput(`${label} HEX`, value); await settle()
|
||||
setInput(`${label} HEX`, value)
|
||||
await waitForThemeState(() => variable(property) === value, `${label}: live input`)
|
||||
assert(variable(property) === value, `${label}: input applies live`)
|
||||
if (label === "Primary / Brand") assert(canvasElement.querySelector<HTMLInputElement>('[aria-label="On Accent HEX"]')!.value === variable("--hm-sem-on-accent"), "On Accent displays its actual resolved role")
|
||||
for (const [key, previous] of others) assert(variable(key) === previous, `${label}: leaves ${key} independent`)
|
||||
assert(JSON.stringify(geometry()) === initial, `${label}: component geometry unchanged`)
|
||||
assert(performance.timeOrigin === origin && preview.isConnected, `${label}: same document, reload 0`)
|
||||
}
|
||||
assert(style('[data-slot="checkbox"][aria-label="Checked"]', "backgroundColor") === "rgb(0, 127, 115)", "Checkbox fill uses Active")
|
||||
const checked = preview.querySelector<HTMLElement>('[data-slot="checkbox"][aria-label="Checked"]')!
|
||||
await waitForThemeState(() => checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && style('[data-slot="checkbox"][aria-label="Checked"]', "backgroundColor") === "rgb(0, 127, 115)", "Designer checked Checkbox fill")
|
||||
assert(checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && style('[data-slot="checkbox"][aria-label="Checked"]', "backgroundColor") === "rgb(0, 127, 115)", "Checkbox fill uses Active")
|
||||
assert(style('[data-slot="radio-group-item"]', "borderTopColor") === "rgb(0, 127, 115)", "Radio uses Active")
|
||||
assert(style('[data-slot="switch"]', "backgroundColor") === "rgb(0, 127, 115)", "Switch uses Active")
|
||||
assert(style('[data-slot="slider-range"]', "backgroundColor") === "rgb(0, 127, 115)", "Slider uses Active")
|
||||
@@ -215,29 +220,31 @@ export const LiveVerification: Story = {
|
||||
assert(style('[data-slot="badge"]', "backgroundColor") === "rgb(104, 55, 168)", "Badge uses Primary")
|
||||
assert(style('[data-slot="pagination-link"][data-active="true"]', "backgroundColor") === "rgb(104, 55, 168)", "Pagination current uses Primary")
|
||||
for (const property of ["--hm-select-option-state-background", "--hm-calendar-day-hover-background", "--hm-calendar-range-middle-background", "--hm-pagination-hover-background", "--hm-table-row-hover-background", "--hm-table-row-selected-background", "--hm-command-highlight-background", "--hm-dropdown-menu-highlight-background", "--hm-context-menu-highlight-background", "--hm-menubar-highlight-background"])
|
||||
assert(variable(property).includes("#ffd36d 45%"), `${property}: live Accent 45%`)
|
||||
assert(variable(property).includes(`#ffd36d ${property.startsWith("--hm-table-row") ? 20 : 45}%`), `${property}: live Accent at its component opacity`)
|
||||
click("Show JSON")
|
||||
await settle()
|
||||
const transfer = canvasElement.querySelector<HTMLTextAreaElement>('[aria-label="Theme JSON"]')!
|
||||
const exported = transfer.value
|
||||
assert(JSON.parse(exported).tokens["color.state.active"] === "#007f73", "Export is official partial contract JSON")
|
||||
click("Reset to HM Default"); await settle()
|
||||
click("Reset to HM Default")
|
||||
assert(JSON.stringify(geometry()) === initial && variable("--hm-sem-state-active") === "#266651", "Reset restores HM Default immediately")
|
||||
const fillJson = (value: string) => {
|
||||
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")!.set!.call(transfer, value)
|
||||
transfer.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
flushSync(() => transfer.dispatchEvent(new Event("input", { bubbles: true })))
|
||||
}
|
||||
fillJson(exported); await settle(); click("Import JSON"); await settle()
|
||||
fillJson(exported); click("Import JSON")
|
||||
await waitForThemeState(() => variable("--hm-sem-state-active") === "#007f73", "Designer JSON import")
|
||||
assert(variable("--hm-sem-state-active") === "#007f73" && variable("--hm-sem-stroke-input-focus") === "#c75b00", "Export -> Import round trip applies immediately")
|
||||
for (const invalid of ["not json", '{"schemaVersion":1,"tokens":{"color.action.primary":"red"}}', '{"schemaVersion":1,"tokens":{"radius.input.default":"7px"}}', '{"schemaVersion":1,"tokens":{"--hm-checkbox-checked-background":"#123456"}}']) {
|
||||
fillJson(invalid); await settle(); click("Import JSON"); await settle()
|
||||
fillJson(invalid); click("Import JSON")
|
||||
assert(variable("--hm-sem-state-active") === "#007f73", "Invalid/color-only import preserves last valid Theme")
|
||||
}
|
||||
fillJson('{"schemaVersion":1,"tokens":{"color.state.active":"#345678"}}'); await settle(); click("Import JSON"); await settle()
|
||||
fillJson('{"schemaVersion":1,"tokens":{"color.state.active":"#345678"}}'); click("Import JSON")
|
||||
await waitForThemeState(() => variable("--hm-sem-state-active") === "#345678", "Designer partial import")
|
||||
assert(variable("--hm-sem-state-active") === "#345678" && variable("--hm-sem-action-primary") === "#123328" && variable("--hm-sem-stroke-input-focus") === "#266651", "Partial import replaces previous keys; unspecified roles use Default")
|
||||
for (const [index, [token, label]] of roles.entries()) {
|
||||
const value = `#3456${(120 + index).toString(16)}`
|
||||
setInput(`${label} HEX`, value); await settle()
|
||||
setInput(`${label} HEX`, value)
|
||||
await waitForThemeState(() => designerThemeStore.getSnapshot().tokens[token] === value, `${label}: editor role`)
|
||||
assert(designerThemeStore.getSnapshot().tokens[token] === value, `${label}: editor role works`)
|
||||
}
|
||||
assert(JSON.stringify(geometry()) === initial, "All color roles leave component layout unchanged")
|
||||
|
||||
@@ -2,12 +2,14 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { applyRuntimeTheme, resetRuntimeTheme, fetchRuntimeTheme, loadRuntimeTheme, resolveRuntimeTheme, defaultRuntimeTheme, runtimeThemeContract } from "@hm-design/ui/theme"
|
||||
import { Specimens } from "./specimens"
|
||||
import { DesignerExtras } from "./designer-extras"
|
||||
import { VerificationStyles, waitForThemeState } from "./verification-state"
|
||||
|
||||
const meta = { title: "Theme/Runtime Contract/Loader", tags: ["verification"], parameters: { layout: "fullscreen" } } satisfies Meta
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
export const CdnVerification: Story = {
|
||||
render: () => <main>
|
||||
render: () => <main data-hm-theme-verification>
|
||||
<VerificationStyles />
|
||||
<h1>Runtime JSON Loader verification</h1>
|
||||
<button onClick={event => void verifyLoader(event.currentTarget.parentElement!).catch(() => undefined)}>Run loader verification</button>
|
||||
<output data-testid="loader-result">Ready</output>
|
||||
@@ -28,25 +30,24 @@ async function verifyLoader(container: HTMLElement) {
|
||||
const style = getComputedStyle(element)
|
||||
return [element.dataset.slot, ...["width", "height", "padding", "margin", "borderRadius", "gap", "fontSize", "lineHeight", "display"].map(key => style[key as keyof CSSStyleDeclaration])]
|
||||
})
|
||||
const settle = () => new Promise(resolve => setTimeout(resolve, 250))
|
||||
const dataUrl = (value: unknown) => `data:application/json,${encodeURIComponent(JSON.stringify(value))}`
|
||||
const originalFetch = globalThis.fetch
|
||||
try {
|
||||
result.textContent = "Running JSON Loader verification…"
|
||||
await document.fonts.ready
|
||||
resetRuntimeTheme(root); await settle()
|
||||
resetRuntimeTheme(root)
|
||||
const initial = JSON.stringify(geometry())
|
||||
const origin = performance.timeOrigin
|
||||
const defaults = await fetchRuntimeTheme("/themes/default.json", { cache: "no-store" })
|
||||
assert(Object.keys(defaults.tokens).length === 71, "Default JSON exposes 71 color contract entries")
|
||||
for (const [key, value] of Object.entries(defaults.tokens)) assert(defaultRuntimeTheme.tokens[key as keyof typeof runtimeThemeContract] === value, `${key}: JSON equals generated package fallback`)
|
||||
assert(await loadRuntimeTheme(root, "/themes/default.json", { request: { cache: "no-store" } }), "Default JSON uses official URL Loader")
|
||||
await settle()
|
||||
assert(JSON.stringify(geometry()) === initial, "Default JSON preserves component geometry")
|
||||
assert(await loadRuntimeTheme(root, "/themes/service-example.json", { request: { cache: "no-store" } }), "External partial JSON uses same URL Loader")
|
||||
await settle()
|
||||
for (const [property, value] of [["--hm-sem-action-primary", "#6837a8"], ["--hm-sem-state-active", "#007f73"], ["--hm-sem-stroke-input-focus", "#c75b00"], ["--hm-sem-accent", "#ffd36d"], ["--hm-sem-action-destructive", "#d41466"]]) assert(variable(property) === value, `${property}: independent external role`)
|
||||
assert(getComputedStyle(container.querySelector<HTMLElement>('[data-slot="checkbox"][aria-label="Checked"]')!).backgroundColor === "rgb(0, 127, 115)", "HTTP JSON reaches rendered Checkbox")
|
||||
const checked = container.querySelector<HTMLElement>('[data-slot="checkbox"][aria-label="Checked"]')!
|
||||
await waitForThemeState(() => checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && getComputedStyle(checked).backgroundColor === "rgb(0, 127, 115)", "HTTP JSON reaches checked Checkbox")
|
||||
assert(checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && getComputedStyle(checked).backgroundColor === "rgb(0, 127, 115)", "HTTP JSON reaches rendered Checkbox")
|
||||
assert(JSON.stringify(geometry()) === initial, "External JSON preserves component geometry")
|
||||
const resolved = resolveRuntimeTheme({ schemaVersion: 1, tokens: { "color.state.active": "#345678" } }, defaults)
|
||||
assert(resolved.tokens["color.action.primary"] === "#123328" && resolved.tokens["color.state.active"] === "#345678", "Default JSON + partial service override merge")
|
||||
|
||||
@@ -66,7 +66,8 @@ const shadowHasColor = (shadow: string, hex: string) => {
|
||||
const subscribeViewport = (notify: () => void) => { window.addEventListener("resize", notify); return () => window.removeEventListener("resize", notify) }
|
||||
function BaselineSpecimens() {
|
||||
const mobile = useSyncExternalStore(subscribeViewport, () => window.matchMedia("(max-width: 1023px)").matches)
|
||||
const values = mobile ? { ...baseline, "--hm-gnb-height": "56px", "--hm-gnb-padding-x": "16px", "--hm-lnb-width": "0px", "--hm-dashboard-padding": "20px" } : baseline
|
||||
const reviewed = { ...baseline, "--hm-table-row-hover-background": "color-mix(in srgb, var(--hm-sem-accent) 20%, transparent)", "--hm-table-row-selected-background": "color-mix(in srgb, var(--hm-sem-accent) 20%, transparent)", "--hm-lnb-group-open-background": "color-mix(in srgb, var(--hm-sem-palette-black) 8%, transparent)" }
|
||||
const values = mobile ? { ...reviewed, "--hm-gnb-height": "56px", "--hm-gnb-padding-x": "16px", "--hm-lnb-width": "0px", "--hm-dashboard-padding": "20px" } : reviewed
|
||||
return <div data-testid="before" data-hm-theme="baseline" style={values as CSSProperties}><Specimens /></div>
|
||||
}
|
||||
|
||||
@@ -75,7 +76,8 @@ export const Verification: Story = {
|
||||
{/* Compare settled state targets, independent of background-tab animation clocks. */}
|
||||
<style>{'[data-testid="before"] *, [data-testid="after"] * { transition-duration: 0s !important; }'}</style>
|
||||
<h1 style={{ padding: 20 }}>Runtime Theme integration verification</h1>
|
||||
<button style={{ margin: "0 20px" }} onClick={event => { void verifyRuntime(event.currentTarget.parentElement!).catch(() => undefined) }}>Run verification</button>
|
||||
<p style={{ padding: "0 20px" }}>키보드 Focus는 Tab으로 아래 버튼에 이동한 뒤 Enter 또는 Space로 실행하세요. 포인터·자동 실행에서는 KEYBOARD_FOCUS: NOT VERIFIED로 표시합니다.</p>
|
||||
<button style={{ margin: "0 20px" }} onClick={event => { void verifyRuntime(event.currentTarget.parentElement!, event.detail === 0 && event.currentTarget.matches(":focus-visible")).catch(() => undefined) }}>Run verification</button>
|
||||
<output data-testid="result" style={{ display: "block", padding: 20 }}>Ready — Run verification</output>
|
||||
<BaselineSpecimens />
|
||||
<div data-testid="after" data-hm-theme="preset"><Specimens /></div>
|
||||
@@ -83,12 +85,17 @@ export const Verification: Story = {
|
||||
play: async ({ canvasElement }) => verifyRuntime(canvasElement),
|
||||
}
|
||||
|
||||
async function verifyRuntime(canvasElement: HTMLElement) {
|
||||
async function verifyRuntime(canvasElement: HTMLElement, keyboardFocus = false) {
|
||||
const before = canvasElement.querySelector<HTMLElement>('[data-testid="before"]')!
|
||||
const after = canvasElement.querySelector<HTMLElement>('[data-testid="after"]')!
|
||||
const result = canvasElement.querySelector<HTMLOutputElement>('output')!
|
||||
if (result.dataset.running === "true") return
|
||||
result.dataset.running = "true"
|
||||
// Compare neutral specimens regardless of the user's previous focus or Tab
|
||||
// navigation during asynchronous measurement. Focus is tested separately.
|
||||
const previousInert = [before.inert, after.inert]
|
||||
before.inert = true
|
||||
after.inert = true
|
||||
const checks: string[] = []
|
||||
const assert = (condition: boolean, label: string) => { if (!condition) throw Error(label); checks.push(label) }
|
||||
const variable = (name: string) => {
|
||||
@@ -128,14 +135,24 @@ async function verifyRuntime(canvasElement: HTMLElement) {
|
||||
assert(getComputedStyle(checked).backgroundColor === "rgb(0, 127, 115)", "Rendered Checkbox uses Active")
|
||||
assert(getComputedStyle(badge).backgroundColor === "rgb(104, 55, 168)", "Rendered Badge uses Primary")
|
||||
assert(getComputedStyle(selectedDay).backgroundColor === "rgb(0, 127, 115)", "Rendered Calendar selected uses Active")
|
||||
checked.focus()
|
||||
assert(shadowHasColor(getComputedStyle(checked).boxShadow, "#c75b00"), "Actual Checkbox focus uses independent Focus")
|
||||
selectedDay.focus()
|
||||
assert(shadowHasColor(getComputedStyle(selectedDay).boxShadow, "#c75b00"), "Actual Calendar focus uses independent Focus")
|
||||
selectedDay.blur()
|
||||
if (keyboardFocus) {
|
||||
after.inert = false
|
||||
checked.focus()
|
||||
assert(checked.matches(":focus-visible") && shadowHasColor(getComputedStyle(checked).boxShadow, "#c75b00"), "Keyboard Checkbox focus uses independent Focus")
|
||||
selectedDay.focus()
|
||||
assert(selectedDay.matches(":focus-visible") && shadowHasColor(getComputedStyle(selectedDay).boxShadow, "#c75b00"), "Keyboard Calendar focus uses independent Focus")
|
||||
selectedDay.blur()
|
||||
after.inert = true
|
||||
}
|
||||
await settle(after)
|
||||
assert(variable("--hm-sem-bg-surface") === "#ffffff", "Partial override preserves unspecified Default")
|
||||
for (const key of ["--hm-select-option-state-background", "--hm-calendar-range-middle-background", "--hm-table-row-selected-background", "--hm-pagination-hover-background", "--hm-command-highlight-background"])
|
||||
assert(variable("--hm-table-row-selected-background").includes("#ffd36d 20%"), "Table receives Accent 20% independently")
|
||||
applyRuntimeTheme(after, { schemaVersion: 1, tokens: { "color.action.cancel": "#345678" } })
|
||||
assert(variable("--hm-button-cancel-runtime-ink") === "#345678", "Custom public cancel role retains Button mapping")
|
||||
resetRuntimeTheme(after)
|
||||
assert(after.style.getPropertyValue("--hm-button-cancel-runtime-ink") === "", "Cancel mapping cleaned on Reset")
|
||||
applyRuntimeTheme(after, external)
|
||||
for (const key of ["--hm-select-option-state-background", "--hm-calendar-range-middle-background", "--hm-pagination-hover-background", "--hm-command-highlight-background"])
|
||||
assert(variable(key).includes("#ffd36d 45%"), `${key} receives Accent 45%`)
|
||||
// Exhaustively inject every public entry, independently, through the same DOM scope.
|
||||
for (const [key,def] of Object.entries(runtimeThemeContract)) {
|
||||
@@ -179,11 +196,11 @@ async function verifyRuntime(canvasElement: HTMLElement) {
|
||||
applyRuntimeTheme(after, external); resetRuntimeTheme(after)
|
||||
assert(after.style.getPropertyValue("--hm-sem-state-active") === "#112233" && after.style.getPropertyPriority("--hm-sem-state-active") === "important", "Cleanup restores previous inline value and priority")
|
||||
after.style.removeProperty("--hm-sem-state-active")
|
||||
result.textContent = `PASS — ${checks.length} checks; ${initial.length} rendered parts; default parity, all public tokens, independent roles, partial/empty/invalid, switching, cleanup, global/scoped, geometry.`
|
||||
result.textContent = `PASS — ${checks.length} checks; ${initial.length} rendered parts; default parity, all public tokens, independent roles, partial/empty/invalid, switching, cleanup, global/scoped, geometry. KEYBOARD_FOCUS: ${keyboardFocus ? "PASS" : "NOT VERIFIED"}.`
|
||||
} catch (error) {
|
||||
result.textContent = `FAIL after ${checks.length} checks — ${String(error)}`
|
||||
throw error
|
||||
} finally { resetRuntimeTheme(after); result.dataset.running = "false" }
|
||||
} finally { resetRuntimeTheme(after); before.inert = previousInert[0]; after.inert = previousInert[1]; result.dataset.running = "false" }
|
||||
}
|
||||
|
||||
export const Dashboard: Story = { render: () => <DashboardLayout
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Verification-only: compare settled CSS targets even when an inactive tab's
|
||||
// animation clock stops. Editor/production transitions keep their normal timing.
|
||||
export function VerificationStyles() {
|
||||
return <style>{'[data-hm-theme-verification] *, [data-hm-theme-verification] *::before, [data-hm-theme-verification] *::after { transition-duration: 0s !important; transition-delay: 0s !important; }'}</style>
|
||||
}
|
||||
|
||||
export async function waitForThemeState(ready: () => boolean, label: string) {
|
||||
const deadline = performance.now() + 5000
|
||||
while (!ready()) {
|
||||
if (performance.now() >= deadline) throw Error(`Theme state did not settle: ${label}`)
|
||||
// Retry a concrete condition; elapsed time alone never makes a check pass.
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 25))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user