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..8f7f149 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,707 @@ 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,
+ disabled: true,
+ 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
+
+
+
+
Mon Lemon herb pasta 25 min · family favorite
+
Wed Crispy tofu bowls 30 min · easy swap
+
Fri Sheet-pan fajitas 35 min · one pan
+
+
+
4.8/5 member rating
+
$34 average weekly savings
+
+ ${renderPatternStaticLink("secondary information path", example.controls["secondary information path"], "#meal-plan")}
+
+
+
+ See the whole week before joining.
+ Recipes, grocery list, swaps, and cancellation terms are included.
+
+
`,
+ );
+}
+
+function renderWorkbenchSurface(contract, example) {
+ return renderPatternSurfaceShell(
+ contract,
+ example,
+ `
+
+
+
+
+ Home Total
+
+ Selected apartment: ${renderPatternControl("selection", 1, "184 Oak Street", "jk-surface-queue-selection")}$2,585
+ 32 Garden Lane $2,640
+ 8 Lakeview Court $2,710
+ 77 Mercer Avenue $2,760
+
+
+
+
+ Top match
184 Oak Street $2,420/mo
+
+ Budget fit $165 under max
+ Commute rank 2nd of 4
+ Move-in September 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
+
+
+
+
+
`,
+ );
+}
+
+function renderOperatorReviewSurface(contract, example) {
+ return renderPatternSurfaceShell(
+ contract,
+ example,
+ `
+
+
+
+ Saturday · Museum Campus Aquarium, lakefront, and an easy dinner
+
+ 9:00 Shedd Aquarium Timed entry · confirmation held
+ 1:15 Lakefront lunch Indoor backup nearby
+ 3:00 Architecture cruise 35-minute transfer with stroller
+ 6:30 Pizza near the hotel Reservation optional
+
+
+
+
+
+ ${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,
+ ``,
+ );
+}
+
+function renderDashboardSurface(contract, example) {
+ return renderPatternSurfaceShell(
+ contract,
+ example,
+ `
+
+
+ Today 18.4 kWh 7% below usual
+ Current draw 1.2 kW Mostly heating
+ Estimated month $142 $11 under plan
+
+
+
+
+
+ Wed Thu Fri Sat Sun Mon Tue
+ 23.1 21.4 22.2 20.1 19.4 18.9 18.4
+
+ kWh per day · Tuesday is 9% below last Wednesday
+
+
+ Needs attention
+ Device Change Water heater +18% Dehumidifier +9%
+
+
+
+
`,
+ );
+}
+
+function renderContentReportSurface(contract, example) {
+ return renderPatternSurfaceShell(
+ contract,
+ example,
+ `
+
+
+
+ In this report
+ Summary
+ Categories
+ Notes
+
+
+
+ $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.
+
+
+
+ Category Actual Plan Result
+
+ Housing $2,120 $2,120 On 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 share Stable link · transaction-backed totals · PDF available
+
+
`,
+ );
+}
+
+function renderSetupDebugSurface(contract, example) {
+ return renderPatternSurfaceShell(
+ contract,
+ example,
+ `
+
+
+
+
+ !
+ 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
+
+
+
+
`,
+ );
+}
+
+function renderConversationSurface(contract, example) {
+ return renderPatternSurfaceShell(
+ contract,
+ example,
+ `
+
+
+
+
+ Sam · 8:12 AM Luna ate all of breakfast and had her morning walk.
+ You · 8:18 AM Perfect—thank you. Her blue tablet is due with dinner.
Delivered
+ Sam · 5:42 PM Should I hide the tablet in food, or give it just after she eats?
+ Earlier update · recovered The photo upload finished after reconnecting.
${renderPatternControl("recover or retry", 1, example.controls["recover or retry"], "jk-surface-retry")}
+
+
+
+
+ Care plan
+
Dinner 6:00 PM · 1 cup
Medication Blue tablet with food
Vet Northside Animal Care
+ JPG Luna-medication.jpg Attached by you
+
+
+
`,
+ );
+}
+
+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 `
-
-
- ${(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 +7494,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 +7536,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 +7881,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 +8136,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 +8281,9 @@ function renderPatternSpecimenList(specimens) {
return `
@@ -6557,8 +8322,14 @@ function renderPatternSpecimenList(specimens) {
`;
}
-function renderDesignSystemTable({ caption, columns, rows, rowAttributes = () => "" }) {
- return `
+function renderDesignSystemTable({
+ caption,
+ columns,
+ rows,
+ rowAttributes = () => "",
+ responsiveLabels = false,
+}) {
+ return `
${escapeHtml(caption)}
@@ -6573,7 +8344,10 @@ function renderDesignSystemTable({ caption, columns, rows, rowAttributes = () =>
${columns
.map((column) => {
const value = column.render ? column.render(row) : escapeHtml(row[column.key] ?? "");
- return `${value} `;
+ const labelAttribute = responsiveLabels
+ ? ` data-label="${escapeHtml(column.label)}"`
+ : "";
+ return `${value} `;
})
.join("")}
`,
@@ -7234,61 +9008,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 +9126,6 @@ function renderDesignSystemPatternsPage(model) {
"Dense workbenches and dashboards need responsive no-overflow evidence.",
])}
- ${renderDesignSystemExamples(pageEntry)}
`,
),
{
@@ -7637,19 +9408,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..0730f4c 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,
@@ -312,7 +312,10 @@ export function SelectField({
placeholder,
children,
className,
+ controlClassName,
+ controlStyle,
selectClassName,
+ dir,
...selectProps
}) {
const controlled = value !== undefined;
@@ -388,29 +391,61 @@ 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: joinClassNames("jk-select-field__control", controlClassName),
+ style: controlStyle,
+ 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..9a81f22 100644
--- a/src/react/styles.css
+++ b/src/react/styles.css
@@ -131,9 +131,55 @@
}
.jk-select-field {
+ -webkit-appearance: none;
+ appearance: none;
+ /* The wrapper owns composition geometry; customize its size through
+ controlClassName/controlStyle so the indicator cannot detach. */
+ position: static !important;
+ inset: auto !important;
+ box-sizing: border-box !important;
+ inline-size: 100% !important;
+ max-inline-size: 100% !important;
+ min-inline-size: 0 !important;
+ margin: 0 !important;
+ padding-inline-start: var(--jk-select-value-start-space, 1rem);
+ padding-inline-end: var(--jk-select-indicator-slot-width, 3rem);
+ transform: none !important;
+ translate: none !important;
+ 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 +202,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 +1253,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..11cfb11 100644
--- a/tests/components/component-browser.test.mjs
+++ b/tests/components/component-browser.test.mjs
@@ -438,7 +438,72 @@ 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,
+ containerWidth: containerRect.width,
+ 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 +626,79 @@ 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`,
+ );
+ }
+ if (scenario.id === "select_field.ready") {
+ await evaluate(
+ client,
+ sessionId,
+ `(() => {
+ const root = document.querySelector(${JSON.stringify(scenarioSelector(scenario.id))});
+ const container = root?.querySelector(".jk-select-field__control");
+ const select = container?.querySelector("select.jk-select-field");
+ if (!container || !select) return false;
+ const probe = document.createElement("style");
+ probe.id = "jk-select-geometry-probe";
+ probe.textContent = ".jk-select-field.jk-select-field--geometry-probe { box-sizing: content-box; width: 50%; min-inline-size: 30rem; margin-inline-start: 2rem; position: relative; inset-inline-start: 1rem; transform: translateX(0.5rem); translate: 0.5rem 0; }";
+ document.head.append(probe);
+ container.style.maxInlineSize = "15rem";
+ select.classList.add("jk-select-field--geometry-probe");
+ select.setAttribute("style", "box-sizing: content-box; inline-size: 50%; min-inline-size: 30rem; margin-inline-start: 2rem; position: relative; inset-inline-start: 1rem; transform: translateX(0.5rem); translate: 0.5rem 0");
+ return true;
+ })()`,
+ );
+ const guardedComposition = await inspectSelectComposition(client, sessionId, scenario);
+ assert.ok(guardedComposition.containerWidth <= 240.5, `${scenario.id}: composition sizing hook was ignored`);
+ assert.equal(guardedComposition.nativeControlMatchesContainer, true, `${scenario.id}: select customization detached the native field`);
+ assert.ok(Math.abs(guardedComposition.slotEndInset) <= 0.5, `${scenario.id}: select customization detached the indicator slot`);
+ assert.ok(Math.abs(guardedComposition.indicatorEndInset - 16) <= 0.5, `${scenario.id}: select customization detached the indicator`);
+ await evaluate(
+ client,
+ sessionId,
+ `(() => {
+ const root = document.querySelector(${JSON.stringify(scenarioSelector(scenario.id))});
+ const container = root?.querySelector(".jk-select-field__control");
+ const select = container?.querySelector("select.jk-select-field");
+ container?.removeAttribute("style");
+ select?.classList.remove("jk-select-field--geometry-probe");
+ select?.removeAttribute("style");
+ document.querySelector("#jk-select-geometry-probe")?.remove();
+ })()`,
+ );
+ }
const options = await evaluate(
client,
sessionId,
@@ -582,7 +720,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 +2842,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 +2852,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 +2876,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 +3613,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..01045c9 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,75 @@ 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`,
+ );
+ if (/^
]*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 +464,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..12b487c 100644
--- a/tests/components/native-components.test.mjs
+++ b/tests/components/native-components.test.mjs
@@ -142,6 +142,49 @@ const defaultSelectMarkup = render(SelectField, {
assert.match(defaultSelectMarkup, /]*id="review-owner"/);
assert.match(defaultSelectMarkup, /Policy<\/option>/);
assert.match(defaultSelectMarkup, /data-jk-base-state="ready"/);
+assert.match(
+ defaultSelectMarkup,
+ /class="jk-select-field__control"[^>]*data-jk-presentation-owner="design_system"/,
+);
+assert.match(
+ defaultSelectMarkup,
+ /data-jk-composition-variant="field_value_trailing_indicator_slot"/,
+);
+assert.match(defaultSelectMarkup, /]*data-part="value"/);
+assert.match(defaultSelectMarkup, /data-part="indicator-slot"/);
+assert.match(defaultSelectMarkup, /data-part="indicator"/);
+assert.equal(count(defaultSelectMarkup, /]*focusable="false"/);
+assert.doesNotMatch(defaultSelectMarkup, /role="combobox"/);
+
+const rtlSelectMarkup = render(SelectField, {
+ id: "rtl-review-owner",
+ label: "Review owner",
+ value: "policy",
+ dir: "rtl",
+ options: [{ value: "policy", label: "Policy" }],
+});
+assert.match(rtlSelectMarkup, /class="jk-select-field__control" dir="rtl"/);
+assert.match(rtlSelectMarkup, /]*dir="rtl"/);
+
+const sizedSelectMarkup = render(SelectField, {
+ id: "sized-review-owner",
+ label: "Review owner",
+ value: "policy",
+ controlClassName: "review-owner-control",
+ controlStyle: { maxInlineSize: "18rem" },
+ selectClassName: "review-owner-select",
+ options: [{ value: "policy", label: "Policy" }],
+});
+assert.match(
+ sizedSelectMarkup,
+ /class="jk-select-field__control review-owner-control" style="max-inline-size:18rem"/,
+);
+assert.match(
+ sizedSelectMarkup,
+ /]*class="jk-select-field review-owner-select"/,
+);
const implicitEmptySelectMarkup = render(SelectField, {
id: "unassigned-owner",
diff --git a/tests/patterns-browser.test.mjs b/tests/patterns-browser.test.mjs
new file mode 100644
index 0000000..a14fd6c
--- /dev/null
+++ b/tests/patterns-browser.test.mjs
@@ -0,0 +1,226 @@
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { buildSite } from "../site/build-site.mjs";
+import { listenSiteLocalServer } from "../scripts/site-local-server.mjs";
+import {
+ evaluate,
+ openPage,
+ withChromium,
+} from "./components/support/chromium-harness.mjs";
+
+const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const AXE_PATH = path.join(REPO_ROOT, "node_modules", "axe-core", "axe.min.js");
+const VIEWPORTS = Object.freeze([
+ Object.freeze({ id: "desktop", width: 1280, height: 900, mobile: false }),
+ Object.freeze({ id: "mobile", width: 390, height: 844, mobile: true }),
+]);
+const EXPECTED_PATTERN_COUNT = 8;
+
+function closeServer(server) {
+ return new Promise((resolve, reject) => {
+ if (!server) {
+ resolve();
+ return;
+ }
+ server.close((error) => (error ? reject(error) : resolve()));
+ });
+}
+
+async function injectAxe(client, sessionId) {
+ assert.ok(fs.existsSync(AXE_PATH), "The local axe-core browser bundle is missing.");
+ await evaluate(client, sessionId, fs.readFileSync(AXE_PATH, "utf8"), {
+ awaitPromise: false,
+ });
+ assert.equal(
+ await evaluate(client, sessionId, `typeof globalThis.axe?.run === "function"`),
+ true,
+ "axe-core did not initialize on the Patterns page",
+ );
+}
+
+async function inspectPatternsPage(client, page, viewport) {
+ await injectAxe(client, page.sessionId);
+ const observed = await evaluate(
+ client,
+ page.sessionId,
+ `(async () => {
+ const index = document.querySelector("[data-pattern-index]");
+ const jumpLinks = [...(index?.querySelectorAll('a[href^="#"]') ?? [])];
+ const rows = [...(index?.querySelectorAll("tbody tr") ?? [])];
+ const specimens = [...document.querySelectorAll("[data-pattern-specimen]")];
+ const surfaces = [...document.querySelectorAll("[data-pattern-surface]")];
+ const axe = await globalThis.axe.run(document, {
+ reporter: "v2",
+ resultTypes: ["violations"]
+ });
+ return {
+ innerWidth,
+ innerHeight,
+ pageOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ jumpLinks: jumpLinks.map((link) => {
+ const target = document.querySelector(link.getAttribute("href"));
+ return {
+ href: link.getAttribute("href"),
+ text: link.textContent.trim(),
+ targetExists: Boolean(target),
+ };
+ }),
+ rows: rows.map((row) => ({
+ display: getComputedStyle(row).display,
+ overflow: row.scrollWidth - row.clientWidth,
+ cells: [...row.cells].map((cell) => ({
+ display: getComputedStyle(cell).display,
+ label: cell.getAttribute("data-label"),
+ generatedLabel: getComputedStyle(cell, "::before").content,
+ })),
+ })),
+ specimens: specimens.map((specimen) => ({
+ id: specimen.id,
+ contract: specimen.getAttribute("data-pattern-specimen"),
+ rect: (() => {
+ const rect = specimen.getBoundingClientRect();
+ return { left: rect.left, right: rect.right, width: rect.width, height: rect.height };
+ })(),
+ })),
+ surfaces: surfaces.map((surface) => {
+ const rect = surface.getBoundingClientRect();
+ return {
+ contract: surface.getAttribute("data-pattern-surface"),
+ layout: surface.getAttribute("data-surface-layout"),
+ rect: { left: rect.left, right: rect.right, width: rect.width, height: rect.height },
+ overflow: surface.scrollWidth - surface.clientWidth,
+ };
+ }),
+ staticTabStops: [...document.querySelectorAll(
+ "[data-pattern-surface] a, [data-pattern-surface] button, [data-pattern-surface] input, [data-pattern-surface] select, [data-pattern-surface] textarea"
+ )].filter((element) => !element.disabled && element.tabIndex >= 0).map((element) => element.outerHTML),
+ staticSelects: [...document.querySelectorAll("[data-pattern-surface] select")].map((select) => ({
+ disabled: select.disabled,
+ ariaDisabled: select.getAttribute("aria-disabled"),
+ })),
+ axeViolations: axe.violations.map((violation) => ({
+ id: violation.id,
+ impact: violation.impact,
+ targets: violation.nodes.flatMap((node) => node.target),
+ })),
+ };
+ })()`,
+ );
+
+ assert.equal(observed.innerWidth, viewport.width, `${viewport.id}: viewport width drifted`);
+ assert.equal(observed.innerHeight, viewport.height, `${viewport.id}: viewport height drifted`);
+ assert.ok(observed.pageOverflow <= 1, `${viewport.id}: page overflows by ${observed.pageOverflow}px`);
+ assert.equal(observed.jumpLinks.length, EXPECTED_PATTERN_COUNT, `${viewport.id}: jump-link count drifted`);
+ assert.equal(observed.jumpLinks.every((link) => link.targetExists), true, `${viewport.id}: a jump-link target is missing`);
+ assert.equal(new Set(observed.jumpLinks.map((link) => link.href)).size, EXPECTED_PATTERN_COUNT);
+ assert.equal(observed.specimens.length, EXPECTED_PATTERN_COUNT, `${viewport.id}: specimen count drifted`);
+ assert.equal(observed.surfaces.length, EXPECTED_PATTERN_COUNT, `${viewport.id}: surface count drifted`);
+ assert.equal(new Set(observed.surfaces.map((surface) => surface.layout)).size, EXPECTED_PATTERN_COUNT);
+ assert.equal(observed.staticTabStops.length, 0, `${viewport.id}: static examples entered the tab order`);
+ assert.ok(observed.staticSelects.length > 0, `${viewport.id}: static select coverage is missing`);
+ assert.equal(
+ observed.staticSelects.every((select) => select.disabled && select.ariaDisabled === "true"),
+ true,
+ `${viewport.id}: a static select remains label-focusable or changeable`,
+ );
+ assert.deepEqual(observed.axeViolations, [], `${viewport.id}: page-level axe violations`);
+
+ for (const specimen of observed.specimens) {
+ assert.ok(specimen.rect.width > 0 && specimen.rect.height > 0, `${viewport.id}: ${specimen.contract} specimen has no visible geometry`);
+ assert.ok(specimen.rect.left >= -1, `${viewport.id}: ${specimen.contract} specimen escapes the viewport start`);
+ assert.ok(specimen.rect.right <= viewport.width + 1, `${viewport.id}: ${specimen.contract} specimen escapes the viewport end`);
+ }
+ for (const surface of observed.surfaces) {
+ assert.ok(surface.rect.width > 0 && surface.rect.height > 0, `${viewport.id}: ${surface.contract} surface has no visible geometry`);
+ assert.ok(surface.overflow <= 1, `${viewport.id}: ${surface.contract} surface overflows by ${surface.overflow}px`);
+ }
+ if (viewport.mobile) {
+ assert.equal(observed.rows.length, EXPECTED_PATTERN_COUNT, "mobile: index row count drifted");
+ for (const row of observed.rows) {
+ assert.equal(row.display, "grid", "mobile: pattern index row did not stack");
+ assert.ok(row.overflow <= 1, `mobile: pattern index row overflows by ${row.overflow}px`);
+ assert.equal(row.cells.length, 4, "mobile: pattern index cell count drifted");
+ for (const cell of row.cells) {
+ assert.equal(cell.display, "grid", "mobile: pattern index cell did not stack its label");
+ assert.ok(cell.label, "mobile: responsive index label is missing");
+ assert.notEqual(cell.generatedLabel, "none", "mobile: responsive index label is not rendered");
+ }
+ }
+ }
+
+ for (const jumpLink of observed.jumpLinks) {
+ const activation = await evaluate(
+ client,
+ page.sessionId,
+ `(() => {
+ const link = document.querySelector(${JSON.stringify(`[data-pattern-index] a[href="${jumpLink.href}"]`)});
+ link?.click();
+ const target = document.querySelector(${JSON.stringify(jumpLink.href)});
+ const rect = target?.getBoundingClientRect();
+ return {
+ hash: location.hash,
+ targetVisible: Boolean(rect && rect.bottom > 0 && rect.top < innerHeight),
+ };
+ })()`,
+ );
+ assert.equal(activation.hash, jumpLink.href, `${viewport.id}: ${jumpLink.href} did not activate`);
+ assert.equal(activation.targetVisible, true, `${viewport.id}: ${jumpLink.href} target is not visible after activation`);
+ }
+
+ assert.deepEqual(page.runtimeExceptions, [], `${viewport.id}: uncaught page errors`);
+ assert.deepEqual(page.consoleErrors, [], `${viewport.id}: console errors`);
+ return observed;
+}
+
+const siteOutDir = fs.mkdtempSync(path.join(os.tmpdir(), "judgmentkit-patterns-browser-"));
+let localServer;
+
+try {
+ await buildSite(siteOutDir);
+ const listener = await listenSiteLocalServer({
+ host: "127.0.0.1",
+ port: 0,
+ siteDir: siteOutDir,
+ });
+ localServer = listener.server;
+
+ const receipts = await withChromium(async (client, browserVersion) => {
+ const presentations = [];
+ for (const viewport of VIEWPORTS) {
+ const page = await openPage(client, {
+ url: `${listener.url}/design-system/patterns/`,
+ viewport,
+ colorScheme: "light",
+ });
+ try {
+ presentations.push({
+ viewport: viewport.id,
+ observed: await inspectPatternsPage(client, page, viewport),
+ });
+ } finally {
+ await page.close();
+ }
+ }
+ return { browser: browserVersion.product, presentations };
+ });
+
+ process.stdout.write(`${JSON.stringify({
+ status: "pass",
+ scope: "patterns_browser",
+ browser: receipts.browser,
+ presentations: receipts.presentations.map((entry) => ({
+ viewport: entry.viewport,
+ patterns: entry.observed.surfaces.length,
+ jump_links: entry.observed.jumpLinks.length,
+ axe_violations: entry.observed.axeViolations.length,
+ page_overflow: entry.observed.pageOverflow,
+ })),
+ })}\n`);
+} finally {
+ await closeServer(localServer);
+ fs.rmSync(siteOutDir, { recursive: true, force: true });
+}
diff --git a/tests/site.test.mjs b/tests/site.test.mjs
index 1bc341e..4b3bbd5 100644
--- a/tests/site.test.mjs
+++ b/tests/site.test.mjs
@@ -2477,7 +2477,86 @@ for (const [label, html] of [
}
assert.ok(designSystemPatterns.includes("Patterns "));
assert.ok(designSystemPatterns.includes("Surface pattern contracts"));
-assert.ok(designSystemPatterns.includes("Specimens "));
+assert.ok(designSystemPatterns.includes("Pattern examples "));
+assert.equal(
+ designSystemPatterns.includes('Examples '),
+ false,
+ "The pattern page should not imply that only the two legacy usage cards are examples.",
+);
+const patternIndexStart = designSystemPatterns.indexOf('');
+const patternMetricsStart = designSystemPatterns.indexOf('');
+const patternExamplesStart = designSystemPatterns.indexOf('');
+assert.notEqual(patternIndexStart, -1, "The pattern page should expose a pattern index.");
+assert.notEqual(patternMetricsStart, -1, "The pattern page should retain its coverage summary.");
+assert.notEqual(patternExamplesStart, -1, "The pattern page should expose pattern examples.");
+assert.ok(
+ patternIndexStart < patternMetricsStart && patternMetricsStart < patternExamplesStart,
+ "The pattern index must be the first content section and appear before its coverage summary and examples.",
+);
+assert.ok(designSystemPatterns.includes("data-pattern-index"));
+assert.ok(
+ designSystemPatterns.includes('data-label="Pattern"'),
+ "The responsive pattern index should label its stacked cells.",
+);
+assert.equal(
+ designSystemComponents.includes('data-label="Component"'),
+ false,
+ "Responsive cell labels must not leak into unrelated design-system tables.",
+);
+const expectedPatternSurfaceStructures = {
+ marketing: {
+ layout: "landing-page",
+ markers: [' entry.contract_id === contract.id,
+ );
+ assert.ok(specimen, `${contract.id} should have one linked pattern example`);
+ assert.ok(
+ designSystemPatterns.includes(
+ `
View ${contract.label} example `,
+ ),
+ `${contract.id} should link from the pattern index to its example`,
+ );
+ assert.ok(
+ designSystemPatterns.includes(
+ `
Presentation profiles',
@@ -2529,10 +2608,10 @@ assert.ok(
"Pattern specimen bodies must reserve the full row for the live surface.",
);
assert.ok(
- siteCss.includes(
- "grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));",
- ),
- "Pattern regions should use the available specimen width before wrapping.",
+ siteCss.includes(".jk-pattern-surface {") &&
+ siteCss.includes(".jk-surface-workbench-layout {") &&
+ siteCss.includes(".jk-surface-conversation-layout {"),
+ "Pattern examples should use screen-level surface compositions instead of one shared region grid.",
);
assert.ok(siteCss.includes("--jk-color-surface: #181d1b;"));
assert.ok(
@@ -2574,7 +2653,7 @@ assert.ok(
);
assert.ok(designSystemAccessibilityMarkdown.includes("## Evidence Groups"));
assert.ok(designSystemComponentsMarkdown.includes("## Specimens"));
-assert.ok(designSystemPatternsMarkdown.includes("## Specimens"));
+assert.ok(designSystemPatternsMarkdown.includes("## Pattern Examples"));
const lucideSmokeProof = fs.readFileSync(
path.join(tempDir, "examples", "lucide-icon-catalog-smoke.html"),
"utf8",
@@ -2583,6 +2662,7 @@ assert.equal((lucideSmokeProof.match(/data-catalog-icon=/g) ?? []).length, 1737)
assert.ok(siteCss.includes(".design-system-page"));
assert.ok(siteCss.includes(".design-system-foundation-list"));
assert.ok(siteCss.includes(".design-system-table"));
+assert.ok(siteCss.includes("[data-pattern-index] .design-system-table tbody"));
assert.ok(siteCss.includes(".design-system-search"));
assert.ok(siteCss.includes(".design-system-example-grid"));
assert.ok(siteCss.includes(".design-system-specimen-list"));
@@ -3127,6 +3207,77 @@ for (const contract of patternContractsExport.contracts) {
specimen.selectors.root,
`[data-specimen-id="pattern.${contract.id}"]`,
);
+ assert.equal(specimen.example?.label?.length > 0, true, `${contract.id} should name its example`);
+ assert.equal(
+ specimen.example?.participant?.length > 0,
+ true,
+ `${contract.id} should name the participant in its example`,
+ );
+ assert.equal(
+ specimen.example?.activity?.length > 0,
+ true,
+ `${contract.id} should name the activity in its example`,
+ );
+ assert.deepEqual(
+ Object.keys(specimen.example?.regions ?? {}),
+ contract.required_regions,
+ `${contract.id} example copy should cover every required region`,
+ );
+ assert.deepEqual(
+ Object.keys(specimen.example?.controls ?? {}),
+ contract.expected_controls,
+ `${contract.id} example labels should cover every expected control`,
+ );
+ assert.equal(
+ specimen.example?.completion?.length > 0,
+ true,
+ `${contract.id} example should name a completion or handoff outcome`,
+ );
+ assert.ok(
+ specimen.rendered_html.includes(`data-pattern-example="${contract.id}"`),
+ `${contract.id} should identify its rendered example`,
+ );
+ const expectedSurface = expectedPatternSurfaceStructures[contract.id];
+ assert.ok(expectedSurface, `${contract.id} should declare a screen-level surface structure`);
+ assert.equal(
+ specimen.example?.surface_layout,
+ expectedSurface.layout,
+ `${contract.id} should name its distinct surface layout`,
+ );
+ renderedPatternLayouts.add(specimen.example.surface_layout);
+ assert.ok(
+ specimen.rendered_html.includes(`data-pattern-surface="${contract.id}"`),
+ `${contract.id} should render a recognizable surface screen`,
+ );
+ assert.ok(
+ specimen.rendered_html.includes('data-fictional-example="true"'),
+ `${contract.id} should identify its consumer scenario as fictional`,
+ );
+ assert.ok(
+ specimen.rendered_html.includes(`data-surface-layout="${expectedSurface.layout}"`),
+ `${contract.id} should expose its screen composition`,
+ );
+ assert.equal(
+ specimen.rendered_html.split("data-pattern-completion").length - 1,
+ 1,
+ `${contract.id} should render exactly one completion or handoff inside the screen`,
+ );
+ assert.doesNotMatch(
+ specimen.rendered_html,
+ /jk-pattern-region-grid/,
+ `${contract.id} must not fall back to the shared grid of region descriptions`,
+ );
+ for (const marker of expectedSurface.markers) {
+ assert.ok(
+ specimen.rendered_html.includes(marker),
+ `${contract.id} should render screen structure ${marker}`,
+ );
+ }
+ assert.doesNotMatch(
+ specimen.rendered_html,
+ new RegExp(`${contract.label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} region \\d+ supports`, "i"),
+ `${contract.id} should use concrete example copy instead of generic region filler`,
+ );
assert.ok(
designSystemPatterns.includes(`data-specimen-id="pattern.${contract.id}"`),
`${contract.id} specimen root should render`,
@@ -3155,20 +3306,46 @@ for (const contract of patternContractsExport.contracts) {
for (const region of contract.required_regions) {
assert.ok(specimen.covered_regions.includes(region), `${contract.id} should cover ${region}`);
assert.ok(specimen.selectors.regions[region], `${contract.id} should expose selector for ${region}`);
- assert.ok(
- specimen.rendered_html.includes(`data-pattern-region="${region.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}"`),
- `${contract.id} should render region ${region}`,
+ const regionMarker = `data-pattern-region="${region.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}"`;
+ assert.equal(
+ specimen.rendered_html.split(regionMarker).length - 1,
+ 1,
+ `${contract.id} should render region ${region} exactly once`,
);
}
for (const control of contract.expected_controls) {
assert.ok(specimen.covered_controls.includes(control), `${contract.id} should cover ${control}`);
assert.ok(specimen.selectors.controls[control], `${contract.id} should expose selector for ${control}`);
- assert.ok(
- specimen.rendered_html.includes(`data-pattern-control="${control.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}"`),
- `${contract.id} should render control ${control}`,
+ const controlMarker = `data-pattern-control="${control.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}"`;
+ assert.equal(
+ specimen.rendered_html.split(controlMarker).length - 1,
+ 1,
+ `${contract.id} should render control ${control} exactly once`,
);
}
}
+const marketingPatternHtml = patternSpecimensExport.specimens.find(
+ (specimen) => specimen.contract_id === "marketing",
+)?.rendered_html ?? "";
+assert.doesNotMatch(
+ marketingPatternHtml,
+ / specimen.contract_id === "form_flow",
+)?.rendered_html ?? "";
+assert.match(formFlowPatternHtml, /Confirmation<\/li>/);
+assert.equal(
+ (formFlowPatternHtml.match(/Completed step: <\/span>/g) ?? []).length,
+ 3,
+ "Every completed checkout step must expose its completed state to assistive technology.",
+);
+assert.equal(
+ renderedPatternLayouts.size,
+ patternContractsExport.contracts.length,
+ "Every public pattern should use a distinct screen composition.",
+);
assert.deepEqual(
specimenProvenanceExport.component_specimens.map((entry) => entry.id),