From 126beebafb6270eecd61284848d6e51910943c53 Mon Sep 17 00:00:00 2001 From: Mike Long Date: Tue, 1 Sep 2026 23:28:07 -0700 Subject: [PATCH 1/2] Render real pattern surfaces --- package.json | 4 +- site/build-site.mjs | 2020 ++++++++++++++++- site/component-specimen-runtime.mjs | 7 +- src/react/native-components.mjs | 76 +- src/react/styles.css | 49 + tests/components/component-browser.test.mjs | 127 +- .../component-site-integration.test.mjs | 103 +- tests/components/native-components.test.mjs | 25 + tests/patterns-browser.test.mjs | 216 ++ tests/site.test.mjs | 201 +- 10 files changed, 2646 insertions(+), 182 deletions(-) create mode 100644 tests/patterns-browser.test.mjs diff --git a/package.json b/package.json index 38fa65f..6474725 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "site:build": "node site/build-site.mjs", "site:dev": "npm run site:build && node scripts/site-local-server.mjs", "site:serve": "node scripts/site-local-server.mjs", - "test:site": "node tests/site.test.mjs && node tests/site-local-server.test.mjs", + "test:site": "node tests/site.test.mjs && node tests/site-local-server.test.mjs && node tests/patterns-browser.test.mjs", "test:components:inventory": "node tests/components/component-reference-inventory.test.mjs", "test:components:contracts": "node tests/components/component-contract-registry.test.mjs", "test:components:unit": "node tests/components/component-state-model.test.mjs && node tests/components/component-specimen-runtime.test.mjs && node tests/components/native-components.test.mjs && node tests/components/interactive-components.test.mjs", @@ -73,7 +73,7 @@ "smoke:icons": "node scripts/lucide-icon-catalog-smoke.mjs", "smoke:openai-ui-workflow": "node scripts/smoke-openai-ui-workflow.mjs", "test:presentation-theme": "node tests/presentation-theme.test.mjs && npm run presentation-theme:actual:evidence && npm run presentation-theme:pack:smoke", - "test": "node tests/lucide-icon-catalog.test.mjs && node tests/lucide-icon-catalog-smoke.test.mjs && node tests/kernel-contract.test.mjs && node tests/analyze-implementation-brief.test.mjs && node tests/activity-model-review.test.mjs && node tests/surface-types.test.mjs && node tests/artifact-inspector-routing.test.mjs && node tests/artifact-inspector-workflow.test.mjs && node tests/workbench-surface-profile.test.mjs && node tests/artifact-inspector-surface-profile.test.mjs && node tests/artifact-inspector-authority.test.mjs && node tests/ui-generation-handoff.test.mjs && node tests/visual-composition-integration.test.mjs && node tests/visual-composition-browser-runtime.test.mjs && node tests/cognitive-dimensions-review.test.mjs && node tests/openai-responses-provider.test.mjs && node tests/cli.test.mjs && node tests/demo-one-shot.test.mjs && node tests/demo-modal-actions.test.mjs && node tests/demo-comparison.test.mjs && node tests/demo-music-comparison.test.mjs && node tests/demo-model-ui-matrix.test.mjs && node tests/ui-generation-evals.test.mjs && node tests/mcp-pilot-evals.test.mjs && node tests/mcp-pilot-llm-judge.test.mjs && node tests/eval-rerun-guardrails.test.mjs && node tests/mcp.test.mjs && node tests/mcp-stdio.test.mjs && node tests/mcp-http.test.mjs && node tests/install-mcp.test.mjs && node tests/codex-plugin.test.mjs && node tests/agent-plugin-adapters.test.mjs && npm run test:presentation-theme && npm run test:components && node tests/ai-native-design-system-release.test.mjs && node tests/er-flow-dashboard.test.mjs && node tests/site.test.mjs && node tests/site-local-server.test.mjs && node tests/skills.test.mjs && node evals/run-evals.mjs" + "test": "node tests/lucide-icon-catalog.test.mjs && node tests/lucide-icon-catalog-smoke.test.mjs && node tests/kernel-contract.test.mjs && node tests/analyze-implementation-brief.test.mjs && node tests/activity-model-review.test.mjs && node tests/surface-types.test.mjs && node tests/artifact-inspector-routing.test.mjs && node tests/artifact-inspector-workflow.test.mjs && node tests/workbench-surface-profile.test.mjs && node tests/artifact-inspector-surface-profile.test.mjs && node tests/artifact-inspector-authority.test.mjs && node tests/ui-generation-handoff.test.mjs && node tests/visual-composition-integration.test.mjs && node tests/visual-composition-browser-runtime.test.mjs && node tests/cognitive-dimensions-review.test.mjs && node tests/openai-responses-provider.test.mjs && node tests/cli.test.mjs && node tests/demo-one-shot.test.mjs && node tests/demo-modal-actions.test.mjs && node tests/demo-comparison.test.mjs && node tests/demo-music-comparison.test.mjs && node tests/demo-model-ui-matrix.test.mjs && node tests/ui-generation-evals.test.mjs && node tests/mcp-pilot-evals.test.mjs && node tests/mcp-pilot-llm-judge.test.mjs && node tests/eval-rerun-guardrails.test.mjs && node tests/mcp.test.mjs && node tests/mcp-stdio.test.mjs && node tests/mcp-http.test.mjs && node tests/install-mcp.test.mjs && node tests/codex-plugin.test.mjs && node tests/agent-plugin-adapters.test.mjs && npm run test:presentation-theme && npm run test:components && node tests/ai-native-design-system-release.test.mjs && node tests/er-flow-dashboard.test.mjs && npm run test:site && node tests/skills.test.mjs && node evals/run-evals.mjs" }, "dependencies": { "@emotion/cache": "^11.14.0", diff --git a/site/build-site.mjs b/site/build-site.mjs index beb78cf..122a7c2 100755 --- a/site/build-site.mjs +++ b/site/build-site.mjs @@ -9,7 +9,13 @@ import { fileURLToPath } from "node:url"; import { build as buildWithEsbuild } from "esbuild"; import React, { createElement } from "react"; import { renderToString } from "react-dom/server"; -import { ActionButton } from "../src/react/index.mjs"; +import { + ActionButton, + ActionGroup, + SelectField, + TextArea, + TextField, +} from "../src/react/index.mjs"; import { JUDGMENTKIT_MCP_TOOL_NAMES, @@ -74,7 +80,7 @@ const VISUAL_COMPOSITION_DARK_POSTER_PATH = const HOMEPAGE_FILM_ENABLED = false; const DESIGN_SYSTEM_SPECIMEN_RENDERER = { id: "judgmentkit-static-specimens", - version: "0.1.0", + version: "0.2.0", }; const COMPONENT_SPECIMEN_RENDERER = { id: COMPONENT_RUNTIME_ADAPTER.id, @@ -2717,10 +2723,12 @@ pre { line-height: 1.05; overflow-wrap: anywhere; } -.design-system-metrics p { +.design-system-metrics .design-system-metric-detail { margin: 6px 0 0; color: var(--muted); font-size: 13px; + font-weight: 400; + line-height: 1.4; } .design-system-section { padding-top: clamp(28px, 5vw, 48px); @@ -3271,7 +3279,6 @@ pre { overflow-wrap: anywhere; } .jk-state-label, -.jk-pattern-header span, .jk-specimen-map-row > span { color: var(--jk-color-muted, var(--muted)); font-size: 11px; @@ -3279,49 +3286,981 @@ pre { text-transform: uppercase; } .jk-pattern-controls { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; + min-width: 0; } .jk-specimen-map-row { display: grid; gap: 6px; min-width: 0; } -.jk-pattern-header { +.jk-specimen-preview.jk-pattern-preview { + gap: 0; + padding: 0; + overflow: hidden; + background: var(--jk-color-surface, var(--panel)); +} +.jk-pattern-surface { + min-width: 0; + overflow: hidden; + background: var(--jk-color-canvas, var(--bg)); + color: var(--jk-color-text, var(--ink)); +} +.jk-pattern-surface *, +.jk-pattern-surface *::before, +.jk-pattern-surface *::after { + box-sizing: border-box; +} +.jk-pattern-surface h4, +.jk-pattern-surface h5, +.jk-pattern-surface h6, +.jk-pattern-surface p, +.jk-pattern-surface dl, +.jk-pattern-surface dd, +.jk-pattern-surface ol, +.jk-pattern-surface ul { + margin: 0; +} +.jk-pattern-surface ol, +.jk-pattern-surface ul { + padding: 0; +} +.jk-pattern-surface button, +.jk-pattern-surface input, +.jk-pattern-surface textarea { + font: inherit; +} +.jk-pattern-surface [aria-disabled="true"] { + pointer-events: none; +} +.jk-pattern-surface [data-static-control="true"] { + pointer-events: none; +} +.jk-pattern-static-label { + display: flex; + gap: 7px; + align-items: center; + min-height: 32px; + padding: 7px 14px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-soft-surface, var(--soft-surface)); + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; + font-weight: 850; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.jk-pattern-static-label span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--jk-color-receipt, var(--receipt)); +} +.jk-surface-kicker { + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; + font-weight: 850; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.jk-surface-toolbar { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + min-width: 0; + padding: 14px 16px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-toolbar > div:first-child { + display: grid; + gap: 2px; + min-width: 0; +} +.jk-surface-toolbar h4 { + font-size: 18px; +} +.jk-surface-text-link { + color: var(--jk-color-focus, var(--accent)); + font-weight: 800; + text-underline-offset: 3px; +} +.jk-surface-status-pill { + display: inline-flex; + min-height: 28px; + align-items: center; + padding: 5px 8px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: 999px; + background: var(--jk-color-soft-surface, var(--soft-surface)); + color: var(--jk-color-text, var(--ink)); + font-size: 11px; + font-weight: 850; + white-space: nowrap; +} +.jk-surface-status-pill.is-review, +.jk-surface-status-pill.is-warning { + border-color: color-mix(in srgb, var(--jk-color-warning, var(--warn)) 60%, var(--jk-color-border, var(--line))); + background: color-mix(in srgb, var(--jk-color-warning, var(--warn)) 10%, var(--jk-color-surface, var(--panel))); +} +.jk-surface-status-pill.is-good { + border-color: color-mix(in srgb, var(--jk-color-receipt, var(--receipt)) 60%, var(--jk-color-border, var(--line))); + background: color-mix(in srgb, var(--jk-color-receipt, var(--receipt)) 10%, var(--jk-color-surface, var(--panel))); +} +.jk-pattern-completion { + margin: 0; + padding: 9px 10px; + border-left: 3px solid var(--jk-color-receipt, var(--receipt)); + background: color-mix(in srgb, var(--jk-color-receipt, var(--receipt)) 8%, transparent); + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-marketing-nav { + display: flex; + gap: 20px; + align-items: center; + min-height: 52px; + padding: 12px 20px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); + color: var(--jk-color-muted, var(--muted)); + font-size: 12px; +} +.jk-surface-marketing-nav strong { + margin-right: auto; + color: var(--jk-color-text, var(--ink)); + font-size: 17px; +} +.jk-surface-marketing-hero { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(260px, 0.9fr); + gap: 28px; + align-items: center; + padding: clamp(24px, 5vw, 56px); + background: var(--jk-color-canvas, var(--bg)); +} +.jk-surface-marketing-offer { + display: grid; + gap: 14px; + align-content: center; + min-width: 0; +} +.jk-surface-marketing-offer h4 { + max-width: 15ch; + font-size: clamp(28px, 5vw, 48px); + line-height: 0.98; + letter-spacing: -0.035em; +} +.jk-surface-marketing-offer p:not(.jk-surface-kicker) { + max-width: 52ch; + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-marketing-offer .jk-action-button { + width: max-content; +} +.jk-surface-marketing-offer small { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-marketing-proof { + display: grid; + gap: 14px; + min-width: 0; + padding: 18px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-panel, 8px); + background: var(--jk-color-surface, var(--panel)); + box-shadow: 0 16px 36px color-mix(in srgb, var(--jk-color-text, var(--ink)) 10%, transparent); +} +.jk-surface-plan-card { + display: grid; + gap: 2px; +} +.jk-surface-plan-card > div { + display: grid; + grid-template-columns: 40px minmax(0, 1fr); + gap: 2px 10px; + padding: 10px 0; + border-bottom: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-plan-card > div:last-child { + border-bottom: 0; +} +.jk-surface-plan-card span { + grid-row: 1 / 3; + color: var(--jk-color-focus, var(--accent)); + font-size: 11px; + font-weight: 900; +} +.jk-surface-plan-card small { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-proof-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + border-top: 1px solid var(--jk-color-border, var(--line)); + border-bottom: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-proof-stats div { + padding: 10px; +} +.jk-surface-proof-stats div + div { + border-left: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-proof-stats dt { + font-size: 18px; + font-weight: 900; +} +.jk-surface-proof-stats dd { + color: var(--jk-color-muted, var(--muted)); + font-size: 11px; +} +.jk-surface-marketing-next { + display: flex; + gap: 8px 24px; + align-items: center; + justify-content: center; + padding: 14px 20px; + border-top: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-soft-surface, var(--soft-surface)); + text-align: center; +} +.jk-surface-marketing-next span { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-workbench-layout { + display: grid; + grid-template-columns: minmax(220px, 0.7fr) minmax(0, 1.8fr); + min-width: 0; +} +.jk-surface-work-queue { + min-width: 0; + border-right: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-queue, +.jk-surface-exceptions { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} +.jk-surface-queue th, +.jk-surface-queue td, +.jk-surface-exceptions th, +.jk-surface-exceptions td { + padding: 10px 12px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + text-align: left; + overflow-wrap: anywhere; +} +.jk-surface-queue th, +.jk-surface-exceptions th { + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; + text-transform: uppercase; +} +.jk-surface-queue th:last-child, +.jk-surface-queue td:last-child, +.jk-surface-exceptions th:last-child, +.jk-surface-exceptions td:last-child { + width: 35%; + text-align: right; +} +.jk-surface-queue tr.is-selected > * { + background: color-mix(in srgb, var(--jk-color-focus, var(--accent)) 10%, var(--jk-color-surface, var(--panel))); +} +.jk-surface-queue-selection { + max-width: 100%; + min-block-size: 2rem; + padding-block: 0.35rem; + padding-inline: 0.55rem; + text-align: left; +} +.jk-surface-workspace { + display: grid; + gap: 14px; + min-width: 0; + padding: 16px; +} +.jk-surface-workspace > header, +.jk-surface-trend > header, +.jk-surface-diagnostic > header { + display: flex; + gap: 14px; + align-items: start; + justify-content: space-between; +} +.jk-surface-workspace > header h5 { + font-size: 20px; +} +.jk-surface-home-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-panel, 8px); + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-home-summary div { + display: grid; + gap: 3px; + min-width: 0; + padding: 12px; +} +.jk-surface-home-summary div + div { + border-left: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-home-summary span { + color: var(--jk-color-muted, var(--muted)); + font-size: 11px; +} +.jk-surface-home-summary strong { + overflow-wrap: anywhere; +} +.jk-surface-home-facts { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-control, 4px); +} +.jk-surface-home-facts div { + padding: 10px; +} +.jk-surface-home-facts div + div { + border-left: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-home-facts dt { + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; + text-transform: uppercase; +} +.jk-surface-home-facts dd { + margin-top: 2px; + font-weight: 850; +} +.jk-surface-evidence { + padding: 12px; + border-left: 3px solid var(--jk-color-focus, var(--accent)); + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-evidence h6 { + margin-bottom: 5px; + font-size: 12px; +} +.jk-surface-evidence ul { display: grid; gap: 3px; + padding-left: 18px; + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-decision-bar { + display: grid; + gap: 10px; + padding-top: 2px; +} +.jk-surface-review-layout { + display: grid; + grid-template-columns: minmax(0, 1.55fr) minmax(220px, 0.65fr); + min-width: 0; } -.jk-pattern-header strong { +.jk-surface-review-document { + min-width: 0; + padding: 20px; + border-right: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-review-document > header { + display: grid; + gap: 4px; + padding-bottom: 14px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-review-document > header span { + color: var(--jk-color-muted, var(--muted)); + font-size: 11px; +} +.jk-surface-review-document > header strong { font-size: 18px; } -.jk-pattern-region-grid { +.jk-surface-itinerary { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); - gap: var(--jk-space-3, 0.75rem); + list-style: none; +} +.jk-surface-itinerary li { + display: grid; + grid-template-columns: 52px minmax(0, 1fr); + gap: 12px; + padding: 13px 0; + border-bottom: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-itinerary time { + color: var(--jk-color-muted, var(--muted)); + font-size: 12px; +} +.jk-surface-itinerary li div { + display: grid; + gap: 2px; +} +.jk-surface-itinerary li span { + color: var(--jk-color-muted, var(--muted)); + font-size: 12px; +} +.jk-surface-itinerary li.is-flagged { + padding-inline: 10px; + border-left: 3px solid var(--jk-color-warning, var(--warn)); + background: color-mix(in srgb, var(--jk-color-warning, var(--warn)) 8%, transparent); +} +.jk-surface-review-rail { + display: grid; + align-content: start; + gap: 12px; + min-width: 0; + padding: 16px; + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-review-rail section { + padding: 12px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-control, 4px); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-review-rail h5 { + margin-bottom: 7px; +} +.jk-surface-review-rail ul { + display: grid; + gap: 5px; + padding-left: 18px; + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-review-rail .jk-surface-risk { + border-color: color-mix(in srgb, var(--jk-color-warning, var(--warn)) 55%, var(--jk-color-border, var(--line))); +} +.jk-surface-risk p { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-review-decision { + padding: 14px 16px; + border-top: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-review-receipt, +.jk-surface-confirmation, +.jk-surface-report-share { + display: flex; + gap: 8px 18px; + align-items: center; + justify-content: space-between; + padding: 11px 16px; + border-top: 1px solid var(--jk-color-border, var(--line)); + background: color-mix(in srgb, var(--jk-color-receipt, var(--receipt)) 8%, var(--jk-color-soft-surface, var(--soft-surface))); +} +.jk-surface-review-receipt span, +.jk-surface-confirmation span, +.jk-surface-report-share span { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-form { + display: grid; + gap: 16px; + padding: 18px; + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-form > header { + display: grid; + gap: 2px; +} +.jk-surface-form > header h4 { + font-size: 20px; +} +.jk-surface-steps { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + list-style: none; + counter-reset: steps; +} +.jk-surface-steps li { + position: relative; + padding: 28px 6px 7px; + border-bottom: 3px solid var(--jk-color-border, var(--line)); + color: var(--jk-color-muted, var(--muted)); + font-size: 11px; + font-weight: 800; + text-align: center; +} +.jk-surface-steps li::before { + position: absolute; + top: 2px; + left: 50%; + display: grid; + width: 22px; + height: 22px; + place-items: center; + transform: translateX(-50%); + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: 50%; + background: var(--jk-color-canvas, var(--bg)); + content: counter(steps); + counter-increment: steps; + font-size: 10px; +} +.jk-surface-steps li.is-complete, +.jk-surface-steps li.is-current { + border-bottom-color: var(--jk-color-focus, var(--accent)); + color: var(--jk-color-text, var(--ink)); +} +.jk-surface-steps li.is-complete::before, +.jk-surface-steps li.is-current::before { + border-color: var(--jk-color-focus, var(--accent)); +} +.jk-surface-field-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + min-width: 0; + padding: 14px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-panel, 8px); +} +.jk-surface-field-grid legend { + padding: 0 6px; + font-weight: 850; +} +.jk-surface-field-valid { + padding: 9px 10px; + border-left: 3px solid var(--jk-color-success, var(--receipt)); + background: color-mix(in srgb, var(--jk-color-success, var(--receipt)) 8%, transparent); + color: var(--jk-color-text, var(--ink)); +} +.jk-surface-order-review { + display: flex; + gap: 18px; + align-items: end; + justify-content: space-between; + min-width: 0; + padding-top: 2px; +} +.jk-surface-order-review dl { + display: grid; + gap: 4px; + width: min(320px, 100%); +} +.jk-surface-order-review dl div { + display: flex; + justify-content: space-between; +} +.jk-surface-order-review dl div:last-child { + padding-top: 5px; + border-top: 1px solid var(--jk-color-border, var(--line)); + font-weight: 900; +} +.jk-surface-filter-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.jk-surface-metric-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + padding: 14px 16px; +} +.jk-surface-metric-grid article { + display: grid; + gap: 2px; + min-width: 0; + padding: 12px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-control, 4px); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-metric-grid span, +.jk-surface-metric-grid small { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-metric-grid span { + font-size: 10px; + font-weight: 850; + text-transform: uppercase; +} +.jk-surface-metric-grid strong { + font-size: 22px; +} +.jk-surface-dashboard-layout { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(220px, 0.55fr); + gap: 12px; + padding: 0 16px 16px; +} +.jk-surface-trend, +.jk-surface-exception-panel { + min-width: 0; + padding: 14px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-panel, 8px); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-trend > header div { + display: grid; + gap: 2px; +} +.jk-surface-trend > header span { + color: var(--jk-color-muted, var(--muted)); + font-size: 11px; +} +.jk-surface-trend > header > strong { + color: var(--jk-color-receipt, var(--receipt)); +} +.jk-surface-trend-table { + width: 100%; + margin-top: 12px; + border-collapse: collapse; + table-layout: fixed; +} +.jk-surface-trend-table th, +.jk-surface-trend-table td { + padding: 7px 3px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + font-size: 10px; + text-align: center; +} +.jk-surface-trend-table th { + color: var(--jk-color-muted, var(--muted)); + font-weight: 750; +} +.jk-surface-trend-note { + margin-top: 7px !important; + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; +} +.jk-surface-exception-panel h5 { + margin-bottom: 8px; +} +.jk-surface-follow-up, +.jk-surface-next-fix { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-top: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-follow-up > div, +.jk-surface-next-fix > div { + display: grid; + gap: 2px; + min-width: 0; +} +.jk-surface-follow-up span, +.jk-surface-next-fix span { + color: var(--jk-color-muted, var(--muted)); + font-size: 12px; +} +.jk-surface-report-toolbar { + display: flex; + gap: 16px; + align-items: center; + justify-content: space-between; + padding: 18px 20px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-report-toolbar > div { + display: grid; + gap: 2px; +} +.jk-surface-report-toolbar h4 { + font-size: 22px; +} +.jk-surface-report-layout { + display: grid; + grid-template-columns: minmax(160px, 0.45fr) minmax(0, 1.55fr); + min-width: 0; +} +.jk-surface-report-toc { + display: grid; + align-content: start; + gap: 4px; + min-width: 0; + padding: 18px; + border-right: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-report-toc strong { + margin-bottom: 6px; + font-size: 11px; + text-transform: uppercase; +} +.jk-surface-report-toc a { + padding: 7px 0; + color: var(--jk-color-text, var(--ink)); + text-decoration: none; +} +.jk-surface-report-body { + display: grid; + gap: 18px; + min-width: 0; + padding: 22px; + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-report-body > section { + min-width: 0; +} +.jk-surface-report-body h5 { + margin: 8px 0 5px; + font-size: 20px; +} +.jk-surface-report-body p { + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-budget-categories { + min-width: 0; +} +.jk-surface-budget-categories table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} +.jk-surface-budget-categories th, +.jk-surface-budget-categories td { + padding: 8px 6px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + font-size: 11px; + text-align: right; + overflow-wrap: anywhere; +} +.jk-surface-budget-categories th:first-child, +.jk-surface-budget-categories td:first-child { + text-align: left; +} +.jk-surface-budget-categories thead th { + color: var(--jk-color-muted, var(--muted)); + font-size: 9px; + text-transform: uppercase; +} +.jk-surface-report-evidence { + padding: 14px; + border-left: 3px solid var(--jk-color-receipt, var(--receipt)); + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-report-evidence h5 { + margin-top: 0; + font-size: 15px; +} +.jk-surface-report-evidence .jk-surface-text-link { + display: inline-block; + margin-top: 8px; +} +.jk-surface-debug-layout { + display: grid; + grid-template-columns: minmax(220px, 0.7fr) minmax(0, 1.3fr); + gap: 12px; + padding: 16px; +} +.jk-surface-debug-config, +.jk-surface-test-result, +.jk-surface-diagnostic { + min-width: 0; + padding: 14px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-panel, 8px); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-debug-config { + grid-row: 1 / 3; + display: grid; + align-content: start; + gap: 12px; +} +.jk-surface-debug-config .jk-action-button { + margin-top: 4px; +} +.jk-surface-test-result { + display: flex; + gap: 12px; + align-items: center; +} +.jk-surface-test-result p { + margin-top: 3px; + color: var(--jk-color-muted, var(--muted)); +} +.jk-surface-result-icon { + display: grid; + width: 34px; + height: 34px; + flex: 0 0 34px; + place-items: center; + border-radius: 50%; + background: color-mix(in srgb, var(--jk-color-warning, var(--warn)) 14%, var(--jk-color-soft-surface, var(--soft-surface))); + color: var(--jk-color-warning, var(--warn)); + font-weight: 950; +} +.jk-surface-diagnostic > header { + align-items: center; +} +.jk-surface-diagnostic .jk-action-button { + min-block-size: 2.25rem; + padding: 0.45rem 0.7rem; + font-size: 11px; +} +.jk-surface-console { + margin: 12px 0 0; + padding: 12px; + overflow-x: auto; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: var(--jk-radius-control, 4px); + background: var(--code-surface); + color: var(--jk-color-text, var(--ink)); + font-size: 12px; + line-height: 1.7; +} +.jk-surface-conversation-header { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 14px 16px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-conversation-header > div:nth-child(2) { + min-width: 0; +} +.jk-surface-conversation-header h4 { + font-size: 17px; +} +.jk-surface-conversation-header div span { + color: var(--jk-color-muted, var(--muted)); + font-size: 11px; +} +.jk-surface-avatar { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border-radius: 50%; + background: var(--jk-color-focus, var(--accent)); + color: var(--jk-color-canvas, var(--bg)); + font-weight: 900; +} +.jk-surface-status-dot { + color: var(--jk-color-receipt, var(--receipt)); + font-size: 11px; + font-weight: 850; +} +.jk-surface-conversation-layout { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(210px, 0.5fr); + min-width: 0; +} +.jk-surface-thread { + display: grid; + grid-template-rows: 1fr auto; + min-width: 0; + border-right: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-messages { + display: grid; + gap: 11px; + align-content: start; + padding: 16px; + list-style: none; + background: var(--jk-color-canvas, var(--bg)); +} +.jk-surface-messages li { + display: grid; + gap: 4px; + max-width: 78%; + padding: 10px 12px; + border: 1px solid var(--jk-color-border, var(--line)); + border-radius: 10px; + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-messages li.is-outgoing { + justify-self: end; + background: color-mix(in srgb, var(--jk-color-focus, var(--accent)) 10%, var(--jk-color-surface, var(--panel))); +} +.jk-surface-messages li.is-recovered { + max-width: 100%; + border-style: dashed; + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-messages li > span, +.jk-surface-messages li > small { + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; +} +.jk-surface-messages .jk-surface-retry { + justify-self: start; + min-block-size: 2.1rem; + padding: 0.4rem 0.65rem; + font-size: 11px; +} +.jk-surface-composer { + display: grid; + gap: 9px; + padding: 12px 16px; + border-top: 1px solid var(--jk-color-border, var(--line)); + background: var(--jk-color-surface, var(--panel)); +} +.jk-surface-composer textarea { + min-height: 68px; + resize: none; +} +.jk-surface-composer .jk-pattern-controls .jk-action-group__actions { + justify-content: flex-end; +} +.jk-surface-context { + min-width: 0; + padding: 16px; + background: var(--jk-color-soft-surface, var(--soft-surface)); +} +.jk-surface-context h5 { + margin-bottom: 12px; +} +.jk-surface-context dl { + display: grid; + gap: 10px; +} +.jk-surface-context dl div { + padding-bottom: 9px; + border-bottom: 1px solid var(--jk-color-border, var(--line)); +} +.jk-surface-context dt { + color: var(--jk-color-muted, var(--muted)); + font-size: 10px; + text-transform: uppercase; +} +.jk-surface-context dd { + margin-top: 2px; + font-weight: 800; } -.jk-pattern-region-grid section { - min-width: 0; - min-height: 92px; - padding: var(--jk-space-3, 0.75rem); +.jk-surface-attachment { + display: flex; + gap: 9px; + align-items: center; + margin-top: 14px; + padding: 9px; border: 1px solid var(--jk-color-border, var(--line)); border-radius: var(--jk-radius-control, 4px); background: var(--jk-color-surface, var(--panel)); } -.jk-pattern-region-grid strong, -.jk-pattern-region-grid p, -.jk-pattern-completion { - margin: 0; +.jk-surface-attachment > span { + padding: 5px; + border-radius: 3px; + background: var(--jk-color-focus, var(--accent)); + color: var(--jk-color-canvas, var(--bg)); + font-size: 9px; + font-weight: 900; } -.jk-pattern-region-grid p, -.jk-pattern-completion { - color: var(--jk-color-muted, var(--muted)); +.jk-surface-attachment div { + display: grid; + min-width: 0; } -.jk-pattern-completion { - padding: 9px 10px; - border-left: 3px solid var(--jk-color-receipt, var(--receipt)); - background: color-mix(in srgb, var(--jk-color-receipt, var(--receipt)) 8%, transparent); +.jk-surface-attachment strong, +.jk-surface-attachment small { + overflow-wrap: anywhere; +} +.jk-surface-attachment small { + color: var(--jk-color-muted, var(--muted)); } .token-value-with-swatch { display: inline-flex; @@ -4625,7 +5564,6 @@ pre { .design-system-role-grid, .design-system-specimen-body, .jk-component-state-grid, - .jk-pattern-region-grid, .design-icon-scenario-grid, .design-icon-index-list { grid-template-columns: 1fr; @@ -4647,6 +5585,190 @@ pre { .design-system-specimen-details-grid .design-system-specimen-details-wide { grid-column: auto; } + .jk-pattern-static-label { + padding-inline: 12px; + } + .jk-surface-toolbar, + .jk-surface-report-toolbar, + .jk-surface-marketing-next, + .jk-surface-review-receipt, + .jk-surface-confirmation, + .jk-surface-report-share, + .jk-surface-follow-up, + .jk-surface-next-fix, + .jk-surface-order-review { + align-items: stretch; + flex-direction: column; + } + .jk-surface-toolbar, + .jk-surface-report-toolbar { + flex-wrap: wrap; + } + .jk-surface-toolbar > .jk-surface-filter-row { + width: 100%; + } + .jk-surface-filter-row > * { + flex: 1 1 120px; + } + .jk-surface-marketing-nav { + min-height: 46px; + padding-inline: 14px; + } + .jk-surface-marketing-nav span { + display: none; + } + .jk-surface-marketing-hero, + .jk-surface-workbench-layout, + .jk-surface-review-layout, + .jk-surface-dashboard-layout, + .jk-surface-report-layout, + .jk-surface-debug-layout, + .jk-surface-conversation-layout { + grid-template-columns: minmax(0, 1fr); + } + .jk-surface-marketing-hero { + gap: 20px; + padding: 22px 16px; + } + .jk-surface-marketing-offer h4 { + max-width: 18ch; + font-size: clamp(27px, 10vw, 40px); + } + .jk-surface-marketing-proof { + padding: 14px; + } + .jk-surface-marketing-next, + .jk-surface-review-receipt, + .jk-surface-confirmation, + .jk-surface-report-share { + gap: 3px; + text-align: left; + } + .jk-surface-work-queue, + .jk-surface-review-document, + .jk-surface-thread, + .jk-surface-report-toc { + border-right: 0; + border-bottom: 1px solid var(--jk-color-border, var(--line)); + } + .jk-surface-home-facts { + grid-template-columns: minmax(0, 1fr); + } + .jk-surface-home-facts div + div { + border-top: 1px solid var(--jk-color-border, var(--line)); + border-left: 0; + } + .jk-surface-review-document, + .jk-surface-review-rail, + .jk-surface-report-body, + .jk-surface-context { + padding: 16px; + } + .jk-surface-field-grid, + .jk-surface-metric-grid { + grid-template-columns: minmax(0, 1fr); + } + .jk-surface-steps li { + padding-inline: 2px; + font-size: 10px; + } + .jk-surface-order-review .jk-pattern-controls, + .jk-surface-follow-up .jk-action-button, + .jk-surface-next-fix .jk-action-button { + width: 100%; + } + .jk-surface-dashboard-layout { + padding-inline: 12px; + } + .jk-surface-report-toc { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 4px 10px; + } + .jk-surface-report-toc strong { + grid-column: 1 / -1; + } + .jk-surface-report-toc a { + font-size: 12px; + } + .jk-surface-debug-config { + grid-row: auto; + } + .jk-surface-debug-layout { + padding: 12px; + } + .jk-surface-console { + font-size: 11px; + } + .jk-surface-conversation-header { + grid-template-columns: auto minmax(0, 1fr); + } + .jk-surface-conversation-header > .jk-surface-status-dot { + grid-column: 2; + } + .jk-surface-messages { + padding: 12px; + } + .jk-surface-messages li { + max-width: 92%; + } + .jk-surface-composer { + padding: 12px; + } + [data-pattern-index] .design-system-table-wrap { + overflow: visible; + border: 0; + background: transparent; + } + [data-pattern-index] .design-system-table { + display: block; + min-width: 0; + } + [data-pattern-index] .design-system-table caption, + [data-pattern-index] .design-system-table thead { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; + } + [data-pattern-index] .design-system-table tbody { + display: grid; + gap: 10px; + } + [data-pattern-index] .design-system-table tr { + display: grid; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + } + [data-pattern-index] .design-system-table td { + display: grid; + grid-template-columns: minmax(84px, 0.35fr) minmax(0, 1fr); + gap: 12px; + padding: 10px 12px; + overflow-wrap: anywhere; + } + [data-pattern-index] .design-system-table td:first-child { + border-top: 0; + } + [data-pattern-index] .design-system-table td::before { + content: attr(data-label); + color: var(--muted); + font-size: 11px; + font-weight: 900; + text-transform: uppercase; + } + [data-pattern-index] .design-system-table td a { + display: inline-flex; + min-height: 44px; + align-items: center; + } .design-system-metrics div { border-right: 0; border-bottom: 1px solid var(--line); @@ -5553,55 +6675,706 @@ function renderComponentSpecimenPreview(contract, context, scenarios) { return `
${runtimeHtml}
`; } -function renderPatternControl(control, index) { +const PATTERN_EXAMPLES = { + marketing: { + label: "Weeknight meal planning", + participant: "Busy household comparing meal-planning memberships", + activity: + "Understand the offer, inspect a sample plan and trust signals, and choose whether to try it.", + surface_layout: "landing-page", + regions: { + offer: "Get five flexible dinners planned around the food your household already enjoys.", + proof: "Preview this week's recipes, the average grocery savings, and how easy it is to swap a meal.", + "primary next step": "Try a free week or inspect the full sample plan before joining.", + }, + controls: { + "primary call to action": "Try a free week", + "secondary information path": "See a sample plan", + }, + completion: + "The shopper can start a trial or inspect the recipes, grocery list, and cancellation terms first.", + }, + workbench: { + label: "Apartment shortlist", + participant: "Renter comparing saved apartments", + activity: + "Move through saved homes, compare practical evidence, and decide which listing to advance or share.", + surface_layout: "queue-workspace", + regions: { + "work queue": "Four saved apartments remain after filtering for the move-in date and budget.", + "detail workspace": "184 Oak Street is a two-bedroom apartment available September 15 for $2,420 per month.", + evidence: "The commute is 24 minutes, laundry is in-unit, and the estimated monthly total is $2,585.", + "decision or handoff": "Request a tour or share the listing with a partner before deciding.", + }, + controls: { + selection: "Open Oak Street", + "filter or sort": "Sort by total cost", + "decision action": "Request a tour", + "handoff action": "Share with partner", + }, + completion: "Tour requested for Friday at 4:30 PM, with the cost and commute comparison preserved.", + }, + operator_review: { + label: "Family trip review", + participant: "Parent reviewing an AI-generated family itinerary", + activity: + "Review the proposed days, reservation evidence, and practical risks before approving or revising the trip.", + surface_layout: "document-review", + regions: { + "produced work": "A four-day Chicago itinerary balances the aquarium, architecture cruise, parks, and free time.", + evidence: "Opening hours, reservation windows, walking times, and cancellation terms are linked to each day.", + risk: "The Saturday plan has a 35-minute transfer with a stroller and no weather backup.", + decision: "Approve the itinerary, request a slower Saturday, or share the draft for another review.", + receipt: "Record which version was approved and which concern still needs a change.", + }, + controls: { + "approve or accept": "Approve itinerary", + "return or request changes": "Request changes", + "handoff action": "Share with family", + }, + completion: "Changes requested for Saturday; Version 3 records the concern and the family reviewer.", + }, + form_flow: { + label: "Grocery delivery checkout", + participant: "Household shopper scheduling a grocery delivery", + activity: + "Enter delivery details, resolve validation, review the basket, and place the order.", + surface_layout: "staged-form", + regions: { + inputs: "Confirm the address, delivery window, phone number, and substitution preference.", + validation: "The phone number is verified for live substitution approval.", + "review or submit": "Review 18 items, the delivery fee, and the final estimated total before ordering.", + confirmation: "Order G-204 is scheduled for Tuesday between 6:00 and 7:00 PM.", + }, + controls: { + "field controls": "Delivery details", + "submit action": "Place order", + "cancel or back action": "Back to basket", + }, + completion: "Order G-204 is placed with its delivery window, total, and substitution preference confirmed.", + }, + dashboard_monitor: { + label: "Home energy monitor", + participant: "Homeowner monitoring household energy use", + activity: + "Understand current energy use, find unusual devices, compare the weekly trend, and decide whether to investigate.", + surface_layout: "monitoring-dashboard", + regions: { + "status summary": "The home has used 18.4 kWh today, 7% below the usual Tuesday level.", + exceptions: "The water heater and basement dehumidifier used more energy than expected overnight.", + "trend or comparison": "Daily use declined across the week after the thermostat schedule changed.", + "follow-up path": "The water heater accounts for the largest unusual load and is ready for inspection.", + }, + controls: { + filter: "Upstairs + kitchen", + "time range": "Last 7 days", + "drill in": "Inspect water heater", + }, + completion: "The homeowner knows current usage and which appliance is most likely to need follow-up.", + }, + content_report: { + label: "Household budget report", + participant: "Household member reviewing the monthly budget", + activity: + "Read the monthly summary, inspect category details and source transactions, and share the report.", + surface_layout: "reading-report", + regions: { + summary: "August spending was $4,280, which is $190 below the household plan.", + sections: "Review housing, food, transport, subscriptions, and flexible spending in reading order.", + "evidence or references": "Each category links to the transactions and notes that make up its total.", + "share or export": "Copy a stable report link or export a PDF for the monthly household check-in.", + }, + controls: { + "table of contents": "Jump to category", + "copy or export": "Export PDF", + "reference navigation": "View transactions", + }, + completion: "The household understands the month and can share a stable, transaction-backed report.", + }, + setup_debug_tool: { + label: "Home Wi-Fi connection test", + participant: "Resident troubleshooting a slow home connection", + activity: + "Choose the network and room, run a test, inspect the result, and follow a specific repair step.", + surface_layout: "developer-console", + regions: { + configuration: "Test the Juniper Home network from the living room on a laptop.", + "test result": "The internet connection is available, but the living-room signal is weak.", + "diagnostic detail": "Download is 38 Mbps, latency is 42 ms, and signal strength is -72 dBm.", + "next fix": "Move the mesh point away from the television, then rerun the room test.", + }, + controls: { + "run test": "Run Wi-Fi test", + "copy diagnostic": "Copy diagnostic", + "retry or repair": "Rerun room test", + }, + completion: "The resident sees the likely signal problem and has one concrete repair to try next.", + }, + conversation: { + label: "Pet-care handoff", + participant: "Pet owner coordinating with a weekend sitter", + activity: + "Continue a care exchange with the feeding plan, medication note, attachments, and delivery status intact.", + surface_layout: "message-thread", + regions: { + "message history": "The sitter confirms Luna ate breakfast and asks whether the evening tablet should be given with food.", + composer: "Draft the next care instruction without losing the active weekend context.", + "context or attachments": "The feeding plan, medication note, vet number, and Luna's photo stay attached.", + status: "The latest care update is delivered and the medication question is awaiting the owner's reply.", + }, + controls: { + send: "Send reply", + "attach or reference": "Attach care note", + "recover or retry": "Retry last update", + }, + completion: "The conversation continues or recovers with Luna's care plan and message status intact.", + }, +}; + +function patternExample(contract) { + const example = PATTERN_EXAMPLES[contract.id]; + if (!example) { + throw new Error(`Missing pattern example for ${contract.id}`); + } + + for (const region of contract.required_regions ?? []) { + if (!example.regions?.[region]) { + throw new Error(`Missing ${contract.id} example region: ${region}`); + } + } + for (const control of contract.expected_controls ?? []) { + if (!example.controls?.[control]) { + throw new Error(`Missing ${contract.id} example control: ${control}`); + } + } + + return example; +} + +function renderPatternControl( + control, + index, + label = control, + className = "", + type = "button", +) { return renderToString( createElement( ActionButton, { + "aria-label": `${label} (static example)`, "data-pattern-control": slugId(control), + className: className || undefined, + disabled: true, tone: index === 0 ? "decision" : "secondary", + type, + }, + label, + ), + ); +} + +function renderPatternActionGroup(label, actions) { + return renderToString( + createElement( + ActionGroup, + { + "aria-label": `${label} (static example)`, + className: "jk-pattern-controls", + disabled: true, + id: `pattern-${slugId(label)}-actions`, }, - control, + actions.map((action, index) => + createElement( + ActionButton, + { + "aria-label": `${action.label} (static example)`, + "data-pattern-control": slugId(action.control), + key: action.control, + tone: action.tone ?? (index === 0 ? "decision" : "secondary"), + type: action.type ?? "button", + }, + action.label, + ), + ), ), ); } +function renderPatternStaticLink(control, label, href = "#", className = "jk-surface-text-link") { + return `${escapeHtml(label)}`; +} + +function renderPatternTextField({ + id, + label, + value = "", + placeholder, + errorMessage, + ...inputProps +}) { + return renderToString( + createElement(TextField, { + id, + label, + value, + placeholder, + errorMessage, + readOnly: true, + tabIndex: -1, + "aria-disabled": "true", + ...inputProps, + }), + ); +} + +function renderPatternTextArea({ id, label, placeholder, value = "" }) { + return renderToString( + createElement(TextArea, { + id, + label, + value, + placeholder, + readOnly: true, + rows: 3, + tabIndex: -1, + "aria-disabled": "true", + }), + ); +} + +function renderPatternSelectField({ control, id, label, value, options }) { + return renderToString( + createElement(SelectField, { + "data-pattern-control": control ? slugId(control) : undefined, + id, + label, + value, + options, + tabIndex: -1, + "aria-disabled": "true", + }), + ); +} + +function renderPatternSurfaceShell(contract, example, content) { + return `
+
Fictional static UI example
+ ${content} +
`; +} + +function renderMarketingSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
+ Savor + How it works + Recipes + Pricing +
+
+
+

Five dinners. One calm week.

+

${escapeHtml(example.regions.offer)}

+

Flexible recipes, one organized grocery list, and simple swaps for nights when plans change.

+ ${renderPatternControl("primary call to action", 0, example.controls["primary call to action"])} + No card required · Cancel any time +
+ +
+
+ See the whole week before joining. + Recipes, grocery list, swaps, and cancellation terms are included. +
+
`, + ); +} + +function renderWorkbenchSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
+
4 saved homes

Apartment shortlist

+ ${renderPatternSelectField({ + control: "filter or sort", + id: "apartment-sort", + label: "Sort homes", + value: "total-cost", + options: [ + { value: "total-cost", label: "Total monthly cost" }, + { value: "commute", label: "Commute time" }, + { value: "move-in", label: "Move-in date" }, + ], + })} +
+
+ +
+
Top match
184 Oak Street
$2,420/mo
+
+
Budget fit$165 under max
+
Commute rank2nd of 4
+
Move-inSeptember 15
+
+
+
Commute
24 min
+
Monthly total
$2,585
+
Laundry
In unit
+
+
+
What stands out
+
  • Quiet street after 8 PM
  • Groceries within a 6-minute walk
  • $45 pet fee included in total
+
+
+ ${renderPatternActionGroup("Apartment decision actions", [ + { control: "decision action", label: example.controls["decision action"] }, + { control: "handoff action", label: example.controls["handoff action"] }, + ])} +

${escapeHtml(example.completion)}

+
+
+
+
`, + ); +} + +function renderOperatorReviewSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
+
Family trip · Version 3

Chicago · 4 days

+ Changes requested +
+
+
+
Saturday · Museum CampusAquarium, lakefront, and an easy dinner
+
    +
  1. Shedd AquariumTimed entry · confirmation held
  2. +
  3. Lakefront lunchIndoor backup nearby
  4. +
  5. Architecture cruise35-minute transfer with stroller
  6. +
  7. Pizza near the hotelReservation optional
  8. +
+
+ +
+
+ ${renderPatternActionGroup("Itinerary review actions", [ + { control: "approve or accept", label: example.controls["approve or accept"] }, + { control: "return or request changes", label: example.controls["return or request changes"] }, + { control: "handoff action", label: example.controls["handoff action"] }, + ])} +
+
+ Changes requested${escapeHtml(example.completion)} +
+
`, + ); +} + +function renderFormFlowSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
FreshCart · 18 items

Delivery checkout

+
    +
  1. Completed step: Basket
  2. Completed step: Delivery
  3. Completed step: Review
  4. Confirmation
  5. +
+
+ Delivery details + ${renderPatternTextField({ id: "grocery-address", label: "Address", value: "42 Maple Avenue", autoComplete: "street-address" })} + ${renderPatternSelectField({ + id: "grocery-window", + label: "Delivery window", + value: "tue-6", + options: [ + { value: "tue-6", label: "Tue · 6–7 PM" }, + { value: "tue-7", label: "Tue · 7–8 PM" }, + { value: "wed-5", label: "Wed · 5–6 PM" }, + ], + })} + ${renderPatternTextField({ id: "grocery-phone", label: "Phone for substitutions", value: "(555) 014-2068", type: "tel", inputMode: "tel", autoComplete: "tel", required: true })} + ${renderPatternSelectField({ + id: "grocery-substitutions", + label: "Substitutions", + value: "ask-first", + options: [ + { value: "ask-first", label: "Ask me before replacing" }, + { value: "best-match", label: "Choose the closest match" }, + { value: "refund", label: "Refund unavailable items" }, + ], + })} +
+

Validation complete. ${escapeHtml(example.regions.validation)}

+
+
Items
$82.40
Delivery
$4.99
Estimated total
$87.39
+ ${renderPatternActionGroup("Checkout actions", [ + { control: "cancel or back action", label: example.controls["cancel or back action"], tone: "secondary" }, + { control: "submit action", label: example.controls["submit action"], tone: "decision", type: "submit" }, + ])} +
+
+ Order G-204 confirmed${escapeHtml(example.completion)} +
+
`, + ); +} + +function renderDashboardSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
+
Maple Avenue

Home energy

+
+ ${renderPatternSelectField({ + control: "filter", + id: "energy-area", + label: "Area", + value: "upstairs-kitchen", + options: [ + { value: "whole-home", label: "Whole home" }, + { value: "upstairs-kitchen", label: "Upstairs + kitchen" }, + { value: "basement", label: "Basement" }, + ], + })} + ${renderPatternSelectField({ + control: "time range", + id: "energy-range", + label: "Time range", + value: "7-days", + options: [ + { value: "today", label: "Today" }, + { value: "7-days", label: "Last 7 days" }, + { value: "30-days", label: "Last 30 days" }, + ], + })} +
+
+
+
Today18.4 kWh7% below usual
+
Current draw1.2 kWMostly heating
+
Estimated month$142$11 under plan
+
+
+
+
Daily use
Down after the thermostat change
−9%
+ + + +
WedThuFriSatSunMonTue
23.121.422.220.119.418.918.4
+

kWh per day · Tuesday is 9% below last Wednesday

+
+
+
Needs attention
+
DeviceChange
Water heater+18%
Dehumidifier+9%
+
+
+
+
Water heater used 3.8 kWh overnightLargest unusual load · compare schedule and temperature
+ ${renderPatternControl("drill in", 0, example.controls["drill in"])} +
+
`, + ); +} + +function renderContentReportSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
+
August 2026

Household budget

+ ${renderPatternControl("copy or export", 1, example.controls["copy or export"])} +
+
+ +
+
+ $190 below plan +
August stayed on track
+

Total spending was $4,280. Grocery costs rose, while transport and flexible spending came in lower than planned.

+
+
+ + + + + + + + +
CategoryActualPlanResult
Housing$2,120$2,120On plan
Food$760$650$110 over
Transport$410$520$110 under
Everything else$990$1,180$190 under
+
+
+
Why food was higher

Two hosted dinners added $96. Every category total is backed by its source transactions.

+ Budget source transactions + ${renderPatternStaticLink("reference navigation", example.controls["reference navigation"], "#budget-transactions")} +
+
+
+
+ Report ready to shareStable link · transaction-backed totals · PDF available +
+
`, + ); +} + +function renderSetupDebugSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
Juniper Home

Wi-Fi connection test

Weak signal
+
+
+ ${renderPatternSelectField({ + id: "wifi-network", + label: "Network", + value: "juniper-home", + options: [ + { value: "juniper-home", label: "Juniper Home" }, + { value: "juniper-guest", label: "Juniper Guest" }, + ], + })} + ${renderPatternSelectField({ + id: "wifi-room", + label: "Room", + value: "living-room", + options: [ + { value: "living-room", label: "Living room" }, + { value: "bedroom", label: "Bedroom" }, + { value: "home-office", label: "Home office" }, + ], + })} + ${renderPatternSelectField({ + id: "wifi-device", + label: "Device", + value: "laptop", + options: [ + { value: "laptop", label: "Laptop" }, + { value: "phone", label: "Phone" }, + { value: "television", label: "Television" }, + ], + })} + ${renderPatternControl("run test", 0, example.controls["run test"], "", "submit")} +
+
+ +
Connected, but the signal is weak

Browsing should work; video calls may become unstable.

+
+
+
Room diagnostic
${renderPatternControl("copy diagnostic", 1, example.controls["copy diagnostic"])}
+
download   38 Mbps
+latency    42 ms
+signal    -72 dBm
+channel     149
+mesh hop      2
+
+
+
+
Move the mesh point away from the televisionThen rerun this room test to compare the signal.
+ ${renderPatternControl("retry or repair", 1, example.controls["retry or repair"])} +
+
`, + ); +} + +function renderConversationSurface(contract, example) { + return renderPatternSurfaceShell( + contract, + example, + `
+
+ +

Luna · Weekend care

Latest update delivered · medication question waiting
+ Delivered +
+
+
+
    +
  1. Sam · 8:12 AM

    Luna ate all of breakfast and had her morning walk.

  2. +
  3. You · 8:18 AM

    Perfect—thank you. Her blue tablet is due with dinner.

    Delivered
  4. +
  5. Sam · 5:42 PM

    Should I hide the tablet in food, or give it just after she eats?

  6. +
  7. Earlier update · recovered

    The photo upload finished after reconnecting.

    ${renderPatternControl("recover or retry", 1, example.controls["recover or retry"], "jk-surface-retry")}
  8. +
+
+ ${renderPatternTextArea({ id: "pet-care-reply", label: "Reply to Sam", placeholder: "Write a care update" })} + ${renderPatternActionGroup("Pet-care message actions", [ + { control: "attach or reference", label: example.controls["attach or reference"], tone: "secondary" }, + { control: "send", label: example.controls.send, tone: "decision", type: "submit" }, + ])} +
+
+ +
+
`, + ); +} + +const PATTERN_SURFACE_RENDERERS = { + marketing: renderMarketingSurface, + workbench: renderWorkbenchSurface, + operator_review: renderOperatorReviewSurface, + form_flow: renderFormFlowSurface, + dashboard_monitor: renderDashboardSurface, + content_report: renderContentReportSurface, + setup_debug_tool: renderSetupDebugSurface, + conversation: renderConversationSurface, +}; + function renderPatternSpecimenPreview(contract, context) { const id = specimenId("pattern", contract.id); - return `
-
- ${escapeHtml(contract.surface_type.replaceAll("_", " "))} - ${escapeHtml(contract.label)} -
-
- ${(contract.required_regions ?? []) - .map( - (region, index) => `
- ${escapeHtml(region)} -

${escapeHtml(patternRegionCopy(contract, region, index))}

-
`, - ) - .join("\n ")} -
-
- ${(contract.expected_controls ?? []) - .map(renderPatternControl) - .join("\n ")} -
-

${escapeHtml(contract.completion_or_handoff)}

-
`; -} + const example = patternExample(contract); + const renderSurface = PATTERN_SURFACE_RENDERERS[contract.id]; + if (!renderSurface) { + throw new Error(`Missing pattern surface renderer for ${contract.id}`); + } -function patternRegionCopy(contract, region, index) { - const lowerRegion = region.toLowerCase(); - if (lowerRegion.includes("evidence")) return "Visible support for the decision stays adjacent to the work."; - if (lowerRegion.includes("risk")) return "Risk is named in domain terms before action."; - if (lowerRegion.includes("decision")) return "The next action is bounded by the review context."; - if (lowerRegion.includes("status")) return "Current state is summarized without hiding exceptions."; - if (lowerRegion.includes("configuration")) return "Settings are visible because setup is the activity."; - if (lowerRegion.includes("composer")) return "Message creation stays close to context and status."; - if (lowerRegion.includes("offer")) return "The main promise is stated before supporting proof."; - return `${contract.label} region ${index + 1} supports ${contract.purpose.toLowerCase()}`; + return `
+ ${renderSurface(contract, example)} +
`; } function buildSpecimenContext(adapter, system) { @@ -5720,6 +7493,7 @@ function buildPatternSpecimens(contracts, context) { return contracts.map((contract) => { const id = specimenId("pattern", contract.id); const contractHash = hashCanonical(contract); + const example = patternExample(contract); const renderedHtml = renderPatternSpecimenPreview(contract, { ...context, contract_hash: contractHash, @@ -5761,6 +7535,7 @@ function buildPatternSpecimens(contracts, context) { completion_or_handoff: contract.completion_or_handoff, disclosure_boundary: contract.disclosure_boundary, accessibility_expectations: contract.accessibility_expectations ?? [], + example, rendered_html: renderedHtml, contract, }; @@ -6105,25 +7880,13 @@ function buildDesignSystemContentModel() { summary: "Surface patterns that connect activity purpose to required regions, controls, and completion behavior.", sections: [ - "Usage", - "Specimens", "Surface patterns", + "Pattern examples", "Presentation profiles", "Review checks", "Accessibility", ], - examples: [ - { - title: "Workbench activity", - use: "Use the workbench pattern when the user repeatedly inspects, compares, decides, and hands off work items.", - caution: "Do not turn repeated review work into a numbered wizard without staged-flow evidence.", - }, - { - title: "Setup or debugging", - use: "Use the setup pattern when implementation details are the work material.", - caution: "Do not leak diagnostics into product surfaces where the activity is not setup, auditing, or debugging.", - }, - ], + examples: [], }, { id: "accessibility", @@ -6372,7 +8135,7 @@ function renderDesignSystemHero(pageEntry) { } function renderDesignSystemMetric(label, value, detail = "") { - return `
${escapeHtml(label)}
${escapeHtml(value)}
${detail ? `

${escapeHtml(detail)}

` : ""}
`; + return `
${escapeHtml(label)}
${escapeHtml(value)}
${detail ? `
${escapeHtml(detail)}
` : ""}
`; } function renderDesignSystemMetrics(metrics) { @@ -6517,8 +8280,9 @@ function renderPatternSpecimenList(specimens) { return `
+

Fictional screen example · ${escapeHtml(specimen.example.label)}

${escapeHtml(specimen.label)}

-

${escapeHtml(specimen.purpose)}

+

${escapeHtml(specimen.example.activity)}

@@ -6557,8 +8321,14 @@ function renderPatternSpecimenList(specimens) {
`; } -function renderDesignSystemTable({ caption, columns, rows, rowAttributes = () => "" }) { - return `
+function renderDesignSystemTable({ + caption, + columns, + rows, + rowAttributes = () => "", + responsiveLabels = false, +}) { + return `
@@ -6573,7 +8343,10 @@ function renderDesignSystemTable({ caption, columns, rows, rowAttributes = () => ${columns .map((column) => { const value = column.render ? column.render(row) : escapeHtml(row[column.key] ?? ""); - return ``; + const labelAttribute = responsiveLabels + ? ` data-label="${escapeHtml(column.label)}"` + : ""; + return `${value}`; }) .join("")} `, @@ -7234,61 +9007,59 @@ function renderDesignSystemPatternsPage(model) { "patterns", ` ${renderDesignSystemHero(pageEntry)} - ${renderDesignSystemMetrics([ - { - label: "Specimens", - value: specimens.length, - detail: "Rendered from current contracts.", - }, - { - label: "Regions", - value: regionCount, - detail: "Required regions across patterns.", - }, - { - label: "Controls", - value: controlCount, - detail: "Expected control families across patterns.", - }, - ])} -
-

Usage

-

Use surface patterns after the activity and surface type are clear. A pattern names the regions, controls, completion behavior, and disclosure boundary the interface must support.

-
-
-

Specimens

-

Each specimen pairs a rendered miniature surface with required regions, expected controls, completion behavior, and source evidence from the current contract.

- ${renderPatternSpecimenList(specimens)} -
-
+

Surface patterns

+

Choose after the activity and surface type are clear: use what the participant is trying to accomplish and what completion needs to mean. Every public pattern links to one fictional consumer-domain screen below; the examples show required regions and controls through distinct UI compositions.

${renderDesignSystemTable({ caption: "Surface pattern contracts", columns: [ { key: "id", label: "Pattern", - render: (row) => `${escapeHtml(row.id)}
${escapeHtml(row.label)}`, + render: (row) => `${escapeHtml(row.id)}
${escapeHtml(row.label)}
`, }, { key: "purpose", - label: "Purpose", + label: "Use when", }, { - key: "required_regions", - label: "Regions", - render: (row) => escapeHtml((row.required_regions ?? []).join(", ")), + key: "completion_or_handoff", + label: "Done means", }, { - key: "expected_controls", - label: "Controls", - render: (row) => escapeHtml((row.expected_controls ?? []).join(", ")), + key: "example", + label: "Example", + render: (row) => + `View ${escapeHtml(row.label)} example`, }, ], rows: contracts, + responsiveLabels: true, rowAttributes: (row) => `data-pattern-contract="${escapeHtml(row.id)}" data-surface-type="${escapeHtml(row.surface_type)}"`, })}
+ ${renderDesignSystemMetrics([ + { + label: "Examples", + value: specimens.length, + detail: "One for every public pattern.", + }, + { + label: "Regions", + value: regionCount, + detail: "Required regions across patterns.", + }, + { + label: "Controls", + value: controlCount, + detail: "Expected control families across patterns.", + }, + ])} +
+

Pattern examples

+

Each fictional composition renders a recognizable surface UI with consumer-domain content, contextual controls, and a visible completion or handoff. The scenarios are illustrative rather than customer evidence; contract and source evidence stays available in the collapsed details.

+ ${renderPatternSpecimenList(specimens)} +

Presentation profiles

Presentation profiles specialize a surface pattern after the activity, interaction contract, and active design-system source are established. Their status identifies whether the profile is supported or still proposed; they do not reclassify the activity or select a runtime renderer.

@@ -7354,7 +9125,6 @@ function renderDesignSystemPatternsPage(model) { "Dense workbenches and dashboards need responsive no-overflow evidence.", ])}
- ${renderDesignSystemExamples(pageEntry)} `, ), { @@ -7637,19 +9407,19 @@ function renderDesignSystemPageMarkdown(model, pageEntry) { if (pageEntry.id === "patterns") { lines.push( - "## Specimens", + "## Surface Pattern Contracts", markdownList( - model.pattern_specimens.map( + model.pattern_contracts.map( (entry) => - `\`${entry.id}\`: rendered from \`${entry.contract_id}\`; regions: ${entry.covered_regions.join(", ")}; controls: ${entry.covered_controls.join(", ")}; output: \`${entry.output_hash}\``, + `\`${entry.id}\` (${entry.surface_type}): ${entry.purpose}; regions: ${(entry.required_regions ?? []).join(", ")}; controls: ${(entry.expected_controls ?? []).join(", ")}`, ), ), "", - "## Surface Pattern Contracts", + "## Pattern Examples", markdownList( - model.pattern_contracts.map( + model.pattern_specimens.map( (entry) => - `\`${entry.id}\` (${entry.surface_type}): ${entry.purpose}; regions: ${(entry.required_regions ?? []).join(", ")}; controls: ${(entry.expected_controls ?? []).join(", ")}`, + `\`${entry.id}\`: ${entry.example.label} for ${entry.example.participant}; activity: ${entry.example.activity}; regions: ${entry.covered_regions.join(", ")}; controls: ${entry.covered_controls.join(", ")}; completion: ${entry.example.completion}; output: \`${entry.output_hash}\``, ), ), "", diff --git a/site/component-specimen-runtime.mjs b/site/component-specimen-runtime.mjs index 4c6a049..621ac6e 100644 --- a/site/component-specimen-runtime.mjs +++ b/site/component-specimen-runtime.mjs @@ -252,6 +252,7 @@ export const RUNTIME_COMPONENT_SCENARIOS = Object.freeze([ help_text: "Assign the team responsible for the next decision.", value: "policy", placeholder: "Choose a review owner", + direction: "rtl", counter_label: "Changes", control_mode: "controlled_update", }), @@ -715,7 +716,10 @@ function renderTextAreaScenario(entry, runtime) { const REVIEW_OWNER_OPTIONS = Object.freeze([ Object.freeze({ value: "operations", label: "Operations" }), - Object.freeze({ value: "policy", label: "Policy" }), + Object.freeze({ + value: "policy", + label: "Policy, compliance, and customer care escalation", + }), Object.freeze({ value: "support", label: "Customer support" }), ]); @@ -734,6 +738,7 @@ function renderSelectFieldScenario(entry, runtime) { disabled: state === "disabled", placeholder: fixture.placeholder, options: REVIEW_OWNER_OPTIONS, + dir: fixture.direction, ...valueProps, "data-scenario-focus-target": state === "focus-visible" ? "true" : undefined, diff --git a/src/react/native-components.mjs b/src/react/native-components.mjs index f1ca545..c02e22e 100644 --- a/src/react/native-components.mjs +++ b/src/react/native-components.mjs @@ -295,7 +295,7 @@ function collectSelectOptionValues(children) { return values; } -/** Choose one bounded value through a browser-owned native select. */ +/** Choose one bounded value through a native select with governed field chrome. */ export function SelectField({ id, label, @@ -313,6 +313,7 @@ export function SelectField({ children, className, selectClassName, + dir, ...selectProps }) { const controlled = value !== undefined; @@ -388,29 +389,60 @@ export function SelectField({ }, (accessibilityProps) => h( - "select", + "div", { - ...selectProps, - ...accessibilityProps, - className: joinClassNames("jk-select-field", selectClassName), - value: empty ? "" : currentValue, - "aria-describedby": mergeIdRefs( - selectProps["aria-describedby"], - accessibilityProps["aria-describedby"], - ), - "data-jk-component": "select_field", - "data-jk-state": baseState, - "data-jk-base-state": baseState, - onChange: handleChange, + className: "jk-select-field__control", + dir, + "data-jk-presentation-owner": "design_system", + "data-jk-composition-variant": + "field_value_trailing_indicator_slot", }, - placeholder !== undefined || (empty && !hasExplicitEmptyOption) - ? h( - "option", - { value: "", disabled: required || undefined }, - placeholder ?? "Select an option", - ) - : null, - optionNodes, + h( + "select", + { + ...selectProps, + ...accessibilityProps, + dir, + className: joinClassNames("jk-select-field", selectClassName), + value: empty ? "" : currentValue, + "aria-describedby": mergeIdRefs( + selectProps["aria-describedby"], + accessibilityProps["aria-describedby"], + ), + "data-jk-component": "select_field", + "data-jk-state": baseState, + "data-jk-base-state": baseState, + "data-part": "value", + onChange: handleChange, + }, + placeholder !== undefined || (empty && !hasExplicitEmptyOption) + ? h( + "option", + { value: "", disabled: required || undefined }, + placeholder ?? "Select an option", + ) + : null, + optionNodes, + ), + h( + "span", + { + className: "jk-select-field__indicator-slot", + "data-part": "indicator-slot", + "aria-hidden": "true", + }, + h( + "svg", + { + className: "jk-select-field__indicator", + "data-part": "indicator", + viewBox: "0 0 16 16", + focusable: "false", + "aria-hidden": "true", + }, + h("path", { d: "m4 6 4 4 4-4" }), + ), + ), ), ); } diff --git a/src/react/styles.css b/src/react/styles.css index 182279a..b9e856d 100644 --- a/src/react/styles.css +++ b/src/react/styles.css @@ -131,9 +131,44 @@ } .jk-select-field { + -webkit-appearance: none; + appearance: none; + padding-inline-start: var(--jk-select-value-start-space, 1rem); + padding-inline-end: var(--jk-select-indicator-slot-width, 3rem); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; cursor: pointer; } +.jk-select-field__control { + position: relative; + display: grid; + min-inline-size: 0; +} + +.jk-select-field__indicator-slot { + position: absolute; + inset-block: 0; + inset-inline-end: 0; + display: grid; + inline-size: var(--jk-select-indicator-slot-width, 3rem); + place-items: center; + color: var(--jk-component-text); + pointer-events: none; +} + +.jk-select-field__indicator { + display: block; + inline-size: var(--jk-select-indicator-size, 1rem); + block-size: var(--jk-select-indicator-size, 1rem); + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + .jk-text-field::placeholder, .jk-text-area::placeholder { color: var(--jk-component-muted); @@ -156,6 +191,10 @@ opacity: 1; } +.jk-select-field:disabled + .jk-select-field__indicator-slot { + color: var(--jk-component-disabled); +} + .jk-text-field:focus-visible, .jk-text-area:focus-visible, .jk-select-field:focus-visible { @@ -1203,6 +1242,16 @@ } @media (forced-colors: active) { + .jk-select-field { + -webkit-appearance: auto; + appearance: auto; + padding-inline: 0.75rem; + } + + .jk-select-field__indicator-slot { + display: none; + } + .jk-text-field:focus-visible, .jk-text-area:focus-visible, .jk-select-field:focus-visible, diff --git a/tests/components/component-browser.test.mjs b/tests/components/component-browser.test.mjs index 31153f5..ffce788 100644 --- a/tests/components/component-browser.test.mjs +++ b/tests/components/component-browser.test.mjs @@ -438,7 +438,71 @@ async function inspectScenario(client, sessionId, scenario) { ); } -async function assertScenarioSemantics(client, sessionId, scenario) { +async function inspectSelectComposition(client, sessionId, scenario) { + return evaluate( + client, + sessionId, + `(() => { + const scenarioRoot = document.querySelector(${JSON.stringify(scenarioSelector(scenario.id))}); + const container = scenarioRoot?.querySelector(".jk-select-field__control"); + const select = container?.querySelector("select.jk-select-field"); + const value = container?.querySelector("[data-part='value']"); + const slot = container?.querySelector("[data-part='indicator-slot']"); + const indicator = slot?.querySelector("[data-part='indicator']"); + if (!container || !select || !value || !slot || !indicator) return null; + const containerRect = container.getBoundingClientRect(); + const selectRect = select.getBoundingClientRect(); + const slotRect = slot.getBoundingClientRect(); + const indicatorRect = indicator.getBoundingClientRect(); + const direction = getComputedStyle(container).direction; + const selectStyle = getComputedStyle(select); + return { + appearance: selectStyle.appearance || selectStyle.webkitAppearance, + direction, + valueText: select.selectedOptions[0]?.textContent?.trim() || "", + valueStartInset: Number.parseFloat(selectStyle.paddingInlineStart), + valueEndReserve: Number.parseFloat(selectStyle.paddingInlineEnd), + slotWidth: slotRect.width, + slotEndInset: direction === "rtl" + ? slotRect.left - containerRect.left + : containerRect.right - slotRect.right, + indicatorSize: { width: indicatorRect.width, height: indicatorRect.height }, + slotCenterDelta: Math.abs( + (slotRect.left + slotRect.right) / 2 - + (indicatorRect.left + indicatorRect.right) / 2 + ), + indicatorEndInset: direction === "rtl" + ? indicatorRect.left - containerRect.left + : containerRect.right - indicatorRect.right, + nativeControlMatchesContainer: + Math.abs(selectRect.left - containerRect.left) <= 0.5 && + Math.abs(selectRect.right - containerRect.right) <= 0.5 && + Math.abs(selectRect.top - containerRect.top) <= 0.5 && + Math.abs(selectRect.bottom - containerRect.bottom) <= 0.5, + slotPointerEvents: getComputedStyle(slot).pointerEvents, + overflowX: selectStyle.overflowX, + textOverflow: selectStyle.textOverflow, + whiteSpace: selectStyle.whiteSpace, + selectedTextWidth: (() => { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (!context) return null; + context.font = selectStyle.font; + return context.measureText(select.selectedOptions[0]?.textContent?.trim() || "").width; + })(), + availableValueWidth: + selectRect.width - + Number.parseFloat(selectStyle.paddingInlineStart) - + Number.parseFloat(selectStyle.paddingInlineEnd), + valueRegionToIndicatorGap: direction === "rtl" + ? selectRect.left + Number.parseFloat(selectStyle.paddingInlineEnd) - indicatorRect.right + : indicatorRect.left - (selectRect.right - Number.parseFloat(selectStyle.paddingInlineEnd)), + }; + })()`, + ); +} + +async function assertScenarioSemantics(client, sessionId, scenario, viewport) { const observed = await inspectScenario(client, sessionId, scenario); assert.ok(observed, `${scenario.id}: required DOM target is missing`); assert.equal(observed.scenarioId, scenario.id, `${scenario.id}: scenario id drifted`); @@ -561,6 +625,41 @@ async function assertScenarioSemantics(client, sessionId, scenario) { assert.equal(ax.properties.disabled, true, `${scenario.id}: AX disabled missing`); } if (scenario.contract_id === "select_field") { + const composition = await inspectSelectComposition(client, sessionId, scenario); + assert.ok(composition, `${scenario.id}: select composition parts are missing`); + assert.equal(composition.appearance, "none", `${scenario.id}: browser-owned indicator remains active`); + const expectedValueText = { + operations: "Operations", + policy: "Policy, compliance, and customer care escalation", + support: "Customer support", + }[scenario.fixture.value] ?? scenario.fixture.placeholder; + assert.equal(composition.valueText, expectedValueText, `${scenario.id}: visible selected value drifted`); + assert.ok(Math.abs(composition.valueStartInset - 16) <= 0.5, `${scenario.id}: selected value start inset drifted`); + assert.ok(Math.abs(composition.valueEndReserve - 48) <= 0.5, `${scenario.id}: selected value trailing reserve drifted`); + assert.ok(Math.abs(composition.slotWidth - 48) <= 0.5, `${scenario.id}: trailing indicator slot width drifted`); + assert.ok(Math.abs(composition.slotEndInset) <= 0.5, `${scenario.id}: trailing indicator slot is not edge anchored`); + assert.ok(Math.abs(composition.indicatorSize.width - 16) <= 0.5, `${scenario.id}: indicator width drifted`); + assert.ok(Math.abs(composition.indicatorSize.height - 16) <= 0.5, `${scenario.id}: indicator height drifted`); + assert.ok(composition.slotCenterDelta <= 0.5, `${scenario.id}: indicator is not centered in its slot`); + assert.ok(Math.abs(composition.indicatorEndInset - 16) <= 0.5, `${scenario.id}: indicator end inset drifted`); + assert.equal(composition.nativeControlMatchesContainer, true, `${scenario.id}: native hit target drifted from the visible field`); + assert.equal(composition.slotPointerEvents, "none", `${scenario.id}: indicator slot blocks the native control`); + assert.ok( + ["clip", "hidden"].includes(composition.overflowX), + `${scenario.id}: selected value overflow is not clipped`, + ); + assert.equal(composition.textOverflow, "ellipsis", `${scenario.id}: selected value ellipsis is missing`); + assert.equal(composition.whiteSpace, "nowrap", `${scenario.id}: selected value may wrap into the indicator slot`); + assert.ok(composition.valueRegionToIndicatorGap >= 15.5, `${scenario.id}: selected value region reaches the indicator`); + if (scenario.fixture.direction) { + assert.equal(composition.direction, scenario.fixture.direction, `${scenario.id}: public direction prop did not reach the composition`); + } + if (viewport.mobile && scenario.id === "select_field.ready") { + assert.ok( + composition.selectedTextWidth > composition.availableValueWidth, + `${scenario.id}: mobile fixture does not exercise selected-value truncation`, + ); + } const options = await evaluate( client, sessionId, @@ -582,7 +681,11 @@ async function assertScenarioSemantics(client, sessionId, scenario) { ); assert.deepEqual( options.slice(1).map((option) => option.label), - ["Operations", "Policy", "Customer support"], + [ + "Operations", + "Policy, compliance, and customer care escalation", + "Customer support", + ], `${scenario.id}: visible option labels drifted`, ); assert.deepEqual( @@ -2700,6 +2803,9 @@ async function verifyForcedColorsRules(client, baseUrl) { const scenario = RUNTIME_COMPONENT_SCENARIOS.find( (entry) => entry.id === "action_button.focus-visible", ); + const selectScenario = RUNTIME_COMPONENT_SCENARIOS.find( + (entry) => entry.id === "select_field.ready", + ); await assertFocusVisible(client, page.sessionId, scenario); const observed = await evaluate( client, @@ -2707,11 +2813,22 @@ async function verifyForcedColorsRules(client, baseUrl) { `(() => { const target = document.querySelector(${JSON.stringify(focusTargetSelector(scenario))}); const style = target ? getComputedStyle(target) : null; + const select = document.querySelector(${JSON.stringify(semanticSelector(selectScenario))}); + const selectStyle = select ? getComputedStyle(select) : null; + const selectSlot = document.querySelector(${JSON.stringify( + `${scenarioSelector(selectScenario.id)} .jk-select-field__indicator-slot`, + )}); return { matches: matchMedia("(forced-colors: active)").matches, boxShadow: style?.boxShadow ?? null, outlineStyle: style?.outlineStyle ?? null, outlineWidth: style?.outlineWidth ?? null, + select: { + appearance: selectStyle?.appearance || selectStyle?.webkitAppearance || null, + paddingInlineStart: selectStyle?.paddingInlineStart ?? null, + paddingInlineEnd: selectStyle?.paddingInlineEnd ?? null, + slotDisplay: selectSlot ? getComputedStyle(selectSlot).display : null, + }, specimenErrors: document.querySelectorAll("[data-component-specimen-error]").length }; })()`, @@ -2720,6 +2837,10 @@ async function verifyForcedColorsRules(client, baseUrl) { assert.equal(observed.boxShadow, "none", "forced-colors focus kept a shadow-only cue"); assert.notEqual(observed.outlineStyle, "none", "forced-colors focus outline missing"); assert.ok(Number.parseFloat(observed.outlineWidth) >= 2, "forced-colors focus outline is too thin"); + assert.notEqual(observed.select.appearance, "none", "forced-colors select kept the custom appearance"); + assert.equal(observed.select.paddingInlineStart, "12px", "forced-colors select start padding drifted"); + assert.equal(observed.select.paddingInlineEnd, "12px", "forced-colors select end padding drifted"); + assert.equal(observed.select.slotDisplay, "none", "forced-colors custom indicator remained visible"); assert.equal(observed.specimenErrors, 0); await assertPageClean(page, "forced-colors page"); return observed; @@ -3453,7 +3574,7 @@ async function main() { }); for (const scenario of RUNTIME_COMPONENT_SCENARIOS) { - await assertScenarioSemantics(client, page.sessionId, scenario); + await assertScenarioSemantics(client, page.sessionId, scenario, viewport); const axe = await runAxeForScenario( client, page.sessionId, diff --git a/tests/components/component-site-integration.test.mjs b/tests/components/component-site-integration.test.mjs index 0b53d19..3e4c4e2 100644 --- a/tests/components/component-site-integration.test.mjs +++ b/tests/components/component-site-integration.test.mjs @@ -355,18 +355,27 @@ try { (count, specimen) => count + specimen.covered_controls.length, 0, ); - const renderedPatternControls = patternPage.match( - /]*data-pattern-control="[^"]+")[^>]*>[\s\S]*?<\/button>/g, + const renderedPatternControlMarkers = patternPage.match( + /data-pattern-control="[^"]+"/g, ) ?? []; assert.equal( - renderedPatternControls.length, + renderedPatternControlMarkers.length, expectedPatternControlCount, - "Every visible pattern control must render exactly once.", + "Every expected control must appear exactly once in its rendered surface.", ); - for (const controlMarkup of renderedPatternControls) { - assert.match(controlMarkup, /class="jk-action-button"/); + const renderedPatternActionButtons = patternPage.match( + /]*data-pattern-control="[^"]+")(?=[^>]*data-jk-component="action_button")[^>]*>[\s\S]*?<\/button>/g, + ) ?? []; + assert.ok( + renderedPatternActionButtons.length > 0, + "Action-oriented surfaces should still use the real ActionButton component.", + ); + for (const controlMarkup of renderedPatternActionButtons) { + assert.match(controlMarkup, /class="[^"]*\bjk-action-button\b[^"]*"/); assert.match(controlMarkup, /data-jk-component="action_button"/); - assert.match(controlMarkup, /data-jk-state="ready"/); + assert.match(controlMarkup, /data-jk-state="disabled"/); + assert.match(controlMarkup, /data-jk-base-state="disabled"/); + assert.match(controlMarkup, /\sdisabled=""/); assert.match(controlMarkup, /data-jk-tone="(?:decision|secondary)"/); assert.match( controlMarkup, @@ -374,23 +383,68 @@ try { ); assert.doesNotMatch(controlMarkup, /class="is-primary"/); } + assert.doesNotMatch( + patternPage, + /]*data-pattern-control=/, + "Pattern control evidence must be attached to a semantic control, not a styled span.", + ); + assert.doesNotMatch( + patternPage, + /]*href=)(?=[^>]*aria-disabled="true")[^>]*>/, + "Static links must resolve safely instead of combining href with aria-disabled.", + ); + const renderedPatternActionGroups = patternPage.match( + /]*data-jk-component="action_group")(?=[^>]*aria-label="[^"]+")[^>]*>/g, + ) ?? []; + assert.ok( + renderedPatternActionGroups.length >= 4, + "Related pattern actions should use labeled ActionGroup components.", + ); + const patternPageIds = [...patternPage.matchAll(/\sid="([^"]+)"/g)].map( + (match) => match[1], + ); + assert.equal( + new Set(patternPageIds).size, + patternPageIds.length, + "The pattern page must not repeat generated component IDs.", + ); for (const specimen of patternSpecimens.specimens) { + assert.match(specimen.rendered_html, /data-pattern-surface="[^"]+"/); + assert.doesNotMatch(specimen.rendered_html, /jk-pattern-region-grid/); + const staticInteractiveTags = specimen.rendered_html.match( + /<(?:a|button|input|select|textarea)\b[^>]*>/g, + ) ?? []; + assert.ok( + staticInteractiveTags.length > 0, + `${specimen.contract_id} should render recognizable surface controls`, + ); + for (const tag of staticInteractiveTags) { + assert.match( + tag, + /tabindex="-1"|\sdisabled(?:="")?/, + `${specimen.contract_id} static preview controls must stay out of the tab order`, + ); + } + const fieldTags = specimen.rendered_html.match( + /<(?:input|select|textarea)\b[^>]*>/g, + ) ?? []; + for (const tag of fieldTags) { + assert.match( + tag, + /data-jk-component="(?:text_field|select_field|text_area)"/, + `${specimen.contract_id} fields should use JudgmentKit field components`, + ); + } for (const control of specimen.covered_controls) { const controlId = control .replace(/[^a-z0-9]+/gi, "-") .replace(/^-+|-+$/g, "") .toLowerCase(); - const controlMarkup = specimen.rendered_html.match( - new RegExp( - `]*data-pattern-control="${controlId}")[^>]*>[\\s\\S]*?<\\/button>`, - ), - )?.[0]; - assert.ok( - controlMarkup, - `${specimen.contract_id}.${controlId} should render a real ActionButton`, + assert.match( + specimen.rendered_html, + new RegExp(`data-pattern-control="${controlId}"`), + `${specimen.contract_id}.${controlId} should render in the surface UI`, ); - assert.match(controlMarkup, /data-jk-component="action_button"/); - assert.match(controlMarkup, /class="jk-action-button"/); } } assert.doesNotMatch( @@ -403,6 +457,21 @@ try { /\.design-system-specimen\[data-pattern-specimen\] \.design-system-specimen-body \{\s*display: block;/, "Pattern specimens must retain the full-width preview layout.", ); + assert.match( + siteCss, + /\.jk-pattern-surface \[aria-disabled="true"\] \{\s*pointer-events: none;/, + "Static surface controls must not expose no-op pointer interactions.", + ); + assert.doesNotMatch( + siteCss, + /\.jk-pattern-surface \.jk-action-button\[data-jk-tone=/, + "Pattern specimens must not override component-owned disabled button colors.", + ); + assert.match( + patternPage, + /]*id="grocery-phone")(?=[^>]*type="tel")(?=[^>]*required="")[^>]*>/, + "The grocery phone field should expose telephone input purpose and required state.", + ); assert.equal( (patternPage.match(/
/g) ?? []) .length, diff --git a/tests/components/native-components.test.mjs b/tests/components/native-components.test.mjs index 5ec7cae..08fdda2 100644 --- a/tests/components/native-components.test.mjs +++ b/tests/components/native-components.test.mjs @@ -142,6 +142,31 @@ const defaultSelectMarkup = render(SelectField, { assert.match(defaultSelectMarkup, /]*id="review-owner"/); assert.match(defaultSelectMarkup, /
${escapeHtml(caption)}
${value}