@aiaiai-pt/design-system 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ <!--
2
+ @component CaminhoStateChip
3
+
4
+ A Badge-variant chip rendering the four verification states as WORDS.
5
+
6
+ The four-state machine (H1 Slice 6 / verification surface):
7
+ to-verify — horizon not yet reached; awaiting observation
8
+ verified — observed and confirmed at the horizon
9
+ not-verified — horizon passed; observation explicitly absent
10
+ not-done — path discarded; verification not applicable
11
+
12
+ States are rendered as English words, not icons or abbreviations.
13
+ Screen readers announce the full word; no icon-only encoding.
14
+
15
+ Built over --badge-* component tokens. English-only canonical state names
16
+ (shared-by-default doctrine — no urban vocabulary here).
17
+
18
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
19
+
20
+ @example All four states
21
+ <CaminhoStateChip state="to-verify" />
22
+ <CaminhoStateChip state="verified" />
23
+ <CaminhoStateChip state="not-verified" />
24
+ <CaminhoStateChip state="not-done" />
25
+ -->
26
+ <script>
27
+ /**
28
+ * @typedef {'to-verify' | 'verified' | 'not-verified' | 'not-done'} CaminhoState
29
+ */
30
+
31
+ let {
32
+ /** @type {CaminhoState} */
33
+ state,
34
+ /** @type {string} */
35
+ class: className = '',
36
+ ...rest
37
+ } = $props();
38
+
39
+ /** @type {Record<CaminhoState, string>} */
40
+ const STATE_LABELS = {
41
+ 'to-verify': 'To verify',
42
+ 'verified': 'Verified',
43
+ 'not-verified': 'Not verified',
44
+ 'not-done': 'Not done',
45
+ };
46
+
47
+ const label = $derived(STATE_LABELS[state] ?? state);
48
+ </script>
49
+
50
+ <span class="caminho-chip caminho-chip-{state} {className}" {...rest}>
51
+ {label}
52
+ </span>
53
+
54
+ <style>
55
+ .caminho-chip {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ font-family: var(--badge-font);
59
+ font-size: var(--badge-size);
60
+ letter-spacing: var(--badge-tracking);
61
+ border-radius: var(--badge-radius);
62
+ padding: var(--badge-padding-y) var(--badge-padding-x);
63
+ white-space: nowrap;
64
+ }
65
+
66
+ /* to-verify: informational — horizon pending */
67
+ .caminho-chip-to-verify {
68
+ background: var(--color-info-subtle);
69
+ color: var(--color-info);
70
+ }
71
+
72
+ /* verified: success — observation confirmed */
73
+ .caminho-chip-verified {
74
+ background: var(--color-success-subtle);
75
+ color: var(--color-success);
76
+ }
77
+
78
+ /* not-verified: destructive — horizon passed, no observation */
79
+ .caminho-chip-not-verified {
80
+ background: var(--color-destructive-subtle);
81
+ color: var(--color-destructive);
82
+ }
83
+
84
+ /* not-done: neutral — path discarded, verification not applicable */
85
+ .caminho-chip-not-done {
86
+ background: var(--badge-neutral-bg);
87
+ color: var(--badge-neutral-text);
88
+ }
89
+ </style>
@@ -0,0 +1,38 @@
1
+ export default CaminhoStateChip;
2
+ type CaminhoStateChip = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * CaminhoStateChip
8
+ *
9
+ * A Badge-variant chip rendering the four verification states as WORDS.
10
+ *
11
+ * The four-state machine (H1 Slice 6 / verification surface):
12
+ * to-verify — horizon not yet reached; awaiting observation
13
+ * verified — observed and confirmed at the horizon
14
+ * not-verified — horizon passed; observation explicitly absent
15
+ * not-done — path discarded; verification not applicable
16
+ *
17
+ * States are rendered as English words, not icons or abbreviations.
18
+ * Screen readers announce the full word; no icon-only encoding.
19
+ *
20
+ * Built over --badge-* component tokens. English-only canonical state names
21
+ * (shared-by-default doctrine — no urban vocabulary here).
22
+ *
23
+ * H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
24
+ *
25
+ * @example All four states
26
+ * <CaminhoStateChip state="to-verify" />
27
+ * <CaminhoStateChip state="verified" />
28
+ * <CaminhoStateChip state="not-verified" />
29
+ * <CaminhoStateChip state="not-done" />
30
+ */
31
+ declare const CaminhoStateChip: import("svelte").Component<{
32
+ state: any;
33
+ class?: string;
34
+ } & Record<string, any>, {}, "">;
35
+ type $$ComponentProps = {
36
+ state: any;
37
+ class?: string;
38
+ } & Record<string, any>;
@@ -0,0 +1,158 @@
1
+ <!--
2
+ @component KpiRegister
3
+
4
+ A StatCard-variant register for KPI values where a measured reading
5
+ visually out-ranks a projected value (larger display text, higher position
6
+ in the visual hierarchy).
7
+
8
+ BOUNDARY ELEMENT: throws when a projected value is provided without a seal.
9
+ This is the enforcement point for the seal rule (prd.md §5.1): no unsealed
10
+ projected value may cross this boundary into the rendering surface. The throw
11
+ is unconditional — production code must never pass an unsealed projected value
12
+ here; the guard exists to catch contract violations early.
13
+
14
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
15
+ References sistema#67 for .type-overline (--type-overline-* tokens).
16
+
17
+ @example Measured only
18
+ <KpiRegister label="ENERGY OUTPUT" measuredValue="2.3 MW" />
19
+
20
+ @example With sealed projected value
21
+ <KpiRegister
22
+ label="ENERGY OUTPUT"
23
+ measuredValue="2.3 MW"
24
+ projectedValue="4.1 MW"
25
+ projectedSeal={{ evidence: 'projected', stale: false }}
26
+ />
27
+ -->
28
+ <script>
29
+ import SealChip from './SealChip.svelte';
30
+
31
+ /**
32
+ * @typedef {'measured' | 'inferred' | 'projected'} EvidenceState
33
+ * @typedef {{ evidence: EvidenceState; stale: boolean }} SealRef
34
+ */
35
+
36
+ let {
37
+ /** @type {string} — overline label (KPI name / dimension) */
38
+ label,
39
+ /** @type {string} — the measured (authoritative) value; always required */
40
+ measuredValue,
41
+ /** @type {string | undefined} — a projected value for the same KPI */
42
+ projectedValue = undefined,
43
+ /**
44
+ * @type {SealRef | undefined}
45
+ * Seal metadata for the projected value. REQUIRED when projectedValue
46
+ * is provided. The boundary guard throws when this is absent.
47
+ * Obtain from assignSeal() in @aiaiai-pt/widget-system/core.
48
+ */
49
+ projectedSeal = undefined,
50
+ /** @type {string} */
51
+ class: className = '',
52
+ ...rest
53
+ } = $props();
54
+
55
+ /*
56
+ * BOUNDARY GUARD — the seal rule enforcement point.
57
+ *
58
+ * Runs synchronously at component initialisation. Also runs reactively
59
+ * via $effect so prop changes after mount are also checked.
60
+ *
61
+ * Throws unconditionally when projectedValue is provided without a seal;
62
+ * production surfaces must never reach this state (prd.md §5.1 #3:
63
+ * "the product, deterministically" assigns the seal — a missing seal
64
+ * means the caller skipped assignSeal, which is the defect).
65
+ */
66
+ if (projectedValue !== undefined && projectedSeal == null) {
67
+ throw new Error(
68
+ '[KpiRegister] A projected value must carry a seal. ' +
69
+ 'Provide the `projectedSeal` prop (result of assignSeal()). ' +
70
+ 'An unsealed projected value may not cross this boundary.',
71
+ );
72
+ }
73
+
74
+ $effect(() => {
75
+ if (projectedValue !== undefined && projectedSeal == null) {
76
+ throw new Error(
77
+ '[KpiRegister] A projected value must carry a seal. ' +
78
+ 'Provide the `projectedSeal` prop (result of assignSeal()). ' +
79
+ 'An unsealed projected value may not cross this boundary.',
80
+ );
81
+ }
82
+ });
83
+ </script>
84
+
85
+ <div class="kpi-register {className}" {...rest}>
86
+ <!--
87
+ Overline label — micro-heading above the value.
88
+ Uses --type-overline-* tokens directly (sistema#67 .type-overline utility).
89
+ -->
90
+ <span class="kpi-register-label">{label}</span>
91
+
92
+ <!--
93
+ Measured value: the authoritative reading.
94
+ Display-scale type — visually out-ranks the projected row.
95
+ -->
96
+ <div class="kpi-register-measured">
97
+ <span class="kpi-register-measured-value">{measuredValue}</span>
98
+ </div>
99
+
100
+ <!--
101
+ Projected row: subordinate to the measured value (smaller type, lower position).
102
+ Only rendered when a sealed projected value is provided.
103
+ projectedSeal is guaranteed non-null here (boundary guard above).
104
+ -->
105
+ {#if projectedValue !== undefined && projectedSeal != null}
106
+ <div class="kpi-register-projected">
107
+ <SealChip
108
+ value={projectedValue}
109
+ evidence={projectedSeal.evidence}
110
+ stale={projectedSeal.stale}
111
+ />
112
+ </div>
113
+ {/if}
114
+ </div>
115
+
116
+ <style>
117
+ .kpi-register {
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: var(--space-xs);
121
+ padding: var(--stat-padding);
122
+ border: var(--stat-border);
123
+ border-radius: var(--stat-radius);
124
+ background: var(--stat-bg);
125
+ }
126
+
127
+ /*
128
+ * Overline label — consumes --type-overline-* tokens (sistema#67).
129
+ * Equivalent to the .type-overline utility class from utilities.css,
130
+ * declared here so the component is self-contained.
131
+ */
132
+ .kpi-register-label {
133
+ font-family: var(--type-overline-font);
134
+ font-size: var(--type-overline-size);
135
+ font-weight: var(--type-overline-weight);
136
+ line-height: var(--type-overline-leading);
137
+ letter-spacing: var(--type-overline-tracking);
138
+ text-transform: uppercase;
139
+ color: var(--stat-label-color);
140
+ }
141
+
142
+ /* Measured value: display scale — the dominant visual element */
143
+ .kpi-register-measured-value {
144
+ font-family: var(--stat-value-font);
145
+ font-size: var(--stat-value-size);
146
+ font-weight: var(--stat-value-weight);
147
+ letter-spacing: var(--stat-value-tracking);
148
+ color: var(--color-text);
149
+ line-height: 1;
150
+ }
151
+
152
+ /* Projected row: body-scale — subordinate to the measured value */
153
+ .kpi-register-projected {
154
+ display: flex;
155
+ align-items: center;
156
+ margin-top: var(--space-2xs);
157
+ }
158
+ </style>
@@ -0,0 +1,46 @@
1
+ export default KpiRegister;
2
+ type KpiRegister = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * KpiRegister
8
+ *
9
+ * A StatCard-variant register for KPI values where a measured reading
10
+ * visually out-ranks a projected value (larger display text, higher position
11
+ * in the visual hierarchy).
12
+ *
13
+ * BOUNDARY ELEMENT: throws when a projected value is provided without a seal.
14
+ * This is the enforcement point for the seal rule (prd.md §5.1): no unsealed
15
+ * projected value may cross this boundary into the rendering surface. The throw
16
+ * is unconditional — production code must never pass an unsealed projected value
17
+ * here; the guard exists to catch contract violations early.
18
+ *
19
+ * H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
20
+ * References sistema#67 for .type-overline (--type-overline-* tokens).
21
+ *
22
+ * @example Measured only
23
+ * <KpiRegister label="ENERGY OUTPUT" measuredValue="2.3 MW" />
24
+ *
25
+ * @example With sealed projected value
26
+ * <KpiRegister
27
+ * label="ENERGY OUTPUT"
28
+ * measuredValue="2.3 MW"
29
+ * projectedValue="4.1 MW"
30
+ * projectedSeal={{ evidence: 'projected', stale: false }}
31
+ * />
32
+ */
33
+ declare const KpiRegister: import("svelte").Component<{
34
+ label: any;
35
+ measuredValue: any;
36
+ projectedValue?: any;
37
+ projectedSeal?: any;
38
+ class?: string;
39
+ } & Record<string, any>, {}, "">;
40
+ type $$ComponentProps = {
41
+ label: any;
42
+ measuredValue: any;
43
+ projectedValue?: any;
44
+ projectedSeal?: any;
45
+ class?: string;
46
+ } & Record<string, any>;
@@ -0,0 +1,133 @@
1
+ <!--
2
+ @component SealChip
3
+
4
+ Renders a value paired with its evidence seal as ONE phrase.
5
+
6
+ The seal must survive quotation: an sr-only span adjacent to the value
7
+ carries the evidence state word so copying or citing the value also
8
+ carries the seal (prd.md §5.1 #5: "a citation carries the seal of the
9
+ value it cites").
10
+
11
+ DS-owned evidence vocabulary: measured | inferred | projected.
12
+ Built over --badge-* + --seal-* component tokens.
13
+
14
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
15
+
16
+ @example Measured reading
17
+ <SealChip value="2.3 MW" evidence="measured" />
18
+
19
+ @example Projected (future window)
20
+ <SealChip value="4.1 MW" evidence="projected" />
21
+
22
+ @example Stale seal (horizon has passed)
23
+ <SealChip value="1.9 MW" evidence="projected" stale />
24
+ -->
25
+ <script>
26
+ /**
27
+ * @typedef {'measured' | 'inferred' | 'projected'} EvidenceState
28
+ */
29
+
30
+ let {
31
+ /** @type {string} — the formatted display value */
32
+ value,
33
+ /** @type {EvidenceState} — evidence state (from assignSeal) */
34
+ evidence,
35
+ /** @type {boolean} — true when the validity window has passed */
36
+ stale = false,
37
+ /** @type {string} */
38
+ class: className = '',
39
+ ...rest
40
+ } = $props();
41
+
42
+ const EVIDENCE_LABELS = /** @type {Record<EvidenceState, string>} */ ({
43
+ measured: 'measured',
44
+ inferred: 'inferred',
45
+ projected: 'projected',
46
+ });
47
+
48
+ const evidenceLabel = $derived(EVIDENCE_LABELS[evidence] ?? evidence);
49
+
50
+ /** Label as spoken by AT and included in citations. */
51
+ const srLabel = $derived(stale ? `${evidenceLabel} (stale)` : evidenceLabel);
52
+ </script>
53
+
54
+ <!--
55
+ The chip is one inline phrase. Structure:
56
+ [value text]
57
+ [visual badge — aria-hidden, not read by AT]
58
+ [sr-only span — read by AT; position:absolute but in DOM so text
59
+ travels with the value when the surrounding content is copied]
60
+ -->
61
+ <span
62
+ class="seal-chip seal-chip-{evidence} {stale ? 'seal-chip-stale' : ''} {className}"
63
+ {...rest}
64
+ >
65
+ <span class="seal-chip-value">{value}</span><!--
66
+ --><span class="seal-chip-badge" aria-hidden="true">{evidenceLabel}{stale ? ' (stale)' : ''}</span><!--
67
+ --><span class="seal-chip-sr-text">&thinsp;{srLabel}</span>
68
+ </span>
69
+
70
+ <style>
71
+ .seal-chip {
72
+ display: inline-flex;
73
+ align-items: baseline;
74
+ gap: var(--space-xs);
75
+ position: relative;
76
+ }
77
+
78
+ /* Value text: no override — reads as its context's surrounding type */
79
+ .seal-chip-value {
80
+ /* intentionally empty */
81
+ }
82
+
83
+ /* Visual evidence badge — seen by sighted users; hidden from AT */
84
+ .seal-chip-badge {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ font-family: var(--badge-font);
88
+ font-size: var(--badge-size);
89
+ letter-spacing: var(--badge-tracking);
90
+ border-radius: var(--badge-radius);
91
+ padding: var(--badge-padding-y) var(--badge-padding-x);
92
+ white-space: nowrap;
93
+ line-height: 1;
94
+ }
95
+
96
+ /* Evidence-state colours via --seal-* component tokens */
97
+ .seal-chip-measured .seal-chip-badge {
98
+ color: var(--seal-measured-text);
99
+ background: var(--seal-measured-bg);
100
+ }
101
+
102
+ .seal-chip-inferred .seal-chip-badge {
103
+ color: var(--seal-inferred-text);
104
+ background: var(--seal-inferred-bg);
105
+ }
106
+
107
+ .seal-chip-projected .seal-chip-badge {
108
+ color: var(--seal-projected-text);
109
+ background: var(--seal-projected-bg);
110
+ }
111
+
112
+ /* Stale modifier: muted on all evidence states */
113
+ .seal-chip-stale .seal-chip-badge {
114
+ color: var(--seal-stale-text);
115
+ background: var(--seal-stale-bg);
116
+ }
117
+
118
+ /*
119
+ * sr-only — visually hidden but present in the DOM so the text travels
120
+ * with the value when content is copied or cited (prd.md §5.1 #5).
121
+ * clip-path: inset(50%) is the modern replacement for deprecated clip: rect().
122
+ */
123
+ .seal-chip-sr-text {
124
+ position: absolute;
125
+ width: 1px;
126
+ height: 1px;
127
+ padding: 0;
128
+ margin: -1px;
129
+ overflow: hidden;
130
+ clip-path: inset(50%);
131
+ white-space: nowrap;
132
+ }
133
+ </style>
@@ -0,0 +1,41 @@
1
+ export default SealChip;
2
+ type SealChip = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * SealChip
8
+ *
9
+ * Renders a value paired with its evidence seal as ONE phrase.
10
+ *
11
+ * The seal must survive quotation: an sr-only span adjacent to the value
12
+ * carries the evidence state word so copying or citing the value also
13
+ * carries the seal (prd.md §5.1 #5: "a citation carries the seal of the
14
+ * value it cites").
15
+ *
16
+ * DS-owned evidence vocabulary: measured | inferred | projected.
17
+ * Built over --badge-* + --seal-* component tokens.
18
+ *
19
+ * H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
20
+ *
21
+ * @example Measured reading
22
+ * <SealChip value="2.3 MW" evidence="measured" />
23
+ *
24
+ * @example Projected (future window)
25
+ * <SealChip value="4.1 MW" evidence="projected" />
26
+ *
27
+ * @example Stale seal (horizon has passed)
28
+ * <SealChip value="1.9 MW" evidence="projected" stale />
29
+ */
30
+ declare const SealChip: import("svelte").Component<{
31
+ value: any;
32
+ evidence: any;
33
+ stale?: boolean;
34
+ class?: string;
35
+ } & Record<string, any>, {}, "">;
36
+ type $$ComponentProps = {
37
+ value: any;
38
+ evidence: any;
39
+ stale?: boolean;
40
+ class?: string;
41
+ } & Record<string, any>;
@@ -3,6 +3,9 @@ export { default as Tag } from "./Tag.svelte";
3
3
  export { default as Status } from "./Status.svelte";
4
4
  export { default as Skeleton } from "./Skeleton.svelte";
5
5
  export { default as KeyValue } from "./KeyValue.svelte";
6
+ export { default as SealChip } from "./SealChip.svelte";
7
+ export { default as KpiRegister } from "./KpiRegister.svelte";
8
+ export { default as CaminhoStateChip } from "./CaminhoStateChip.svelte";
6
9
  export { default as Button } from "./Button.svelte";
7
10
  export { default as Input } from "./Input.svelte";
8
11
  export { default as Textarea } from "./Textarea.svelte";
@@ -15,6 +15,12 @@ export { default as Status } from "./Status.svelte";
15
15
  export { default as Skeleton } from "./Skeleton.svelte";
16
16
  export { default as KeyValue } from "./KeyValue.svelte";
17
17
 
18
+ // Evidence seal (H1 Slice 2 — westeuropeco/atelier-urban-workspace#57)
19
+ // DS-owned evidence vocabulary: measured | inferred | projected
20
+ export { default as SealChip } from "./SealChip.svelte";
21
+ export { default as KpiRegister } from "./KpiRegister.svelte";
22
+ export { default as CaminhoStateChip } from "./CaminhoStateChip.svelte";
23
+
18
24
  // Form controls
19
25
  export { default as Button } from "./Button.svelte";
20
26
  export { default as Input } from "./Input.svelte";
@@ -1,23 +1,31 @@
1
1
  /**
2
- * Widget dispatcher — tester/priority, not a flat map (D4, JSONForms model).
2
+ * Widget dispatcher — COMPATIBILITY ADAPTER over @aiaiai-pt/widget-system/core.
3
3
  *
4
- * Generalises the admin app's XSS-hardened layout registry
5
- * (`admin/src/lib/renderer-layouts/resolve.ts`) from form-row LayoutKeys to
6
- * widget `kind`s. Each entry carries a `tester(binding, schema) → score`; the
7
- * highest score wins. A more specific tester (e.g. "list of `occurrence`")
8
- * outranks the generic "any list" and overrides one widget for one entity
9
- * WITHOUT editing the catalog (open/closed).
4
+ * @deprecated Import the generic dispatch from `@aiaiai-pt/widget-system/core`
5
+ * instead. This module is the S2 (#60) compatibility shim: it preserves the
6
+ * Atelier-shaped `@aiaiai-pt/design-system/renderer/dispatch` import surface for
7
+ * one migration window while the ranking ALGORITHM lives in exactly one place —
8
+ * `@aiaiai-pt/widget-system/core`. Scheduled for removal in the next MAJOR of
9
+ * `@aiaiai-pt/design-system` (S3). See `docs/migration/widget-system.md`.
10
10
  *
11
- * The ranking core here is generic over the payload `P` so it stays
12
- * component-free and fully unit/mutation-testable; the real registry (73b)
13
- * instantiates it with Svelte `Component`s.
11
+ * Classification (#60 AC5): the tester/priority ranking loop is a GENERIC
12
+ * widget-system API it is delegated below and NOT reimplemented here. The
13
+ * `(binding, schema, type)` tester SIGNATURE is ATELIER-COUPLED (it names
14
+ * `Binding`/`OntologySchema`/`Block` from ./types) and stays here as a thin
15
+ * adapter until consumers migrate to the ctx-shaped `WidgetMatchContext`.
14
16
  *
15
- * TH-08 (R-SEC-07) preserved: the resolved `key` is a known-good literal from
16
- * the matched entry — the operator's raw `binding.kind`/`entity`/block `type`
17
- * is NEVER returned as the key, so it never reaches `class`/`data-*`/`style`.
17
+ * TH-08 (R-SEC-07) preserved end-to-end: `core.selectEntry` returns the matched
18
+ * entry's known-good `key` literal — the operator's raw `binding.kind`/`type`
19
+ * is never returned as the key, so it never reaches `class`/`data-*`/`style`.
18
20
  */
21
+ import { type Match as CoreMatch, type RenderDecision as CoreRenderDecision } from "@aiaiai-pt/widget-system/core";
19
22
  import type { Binding, Block, OntologySchema } from "./types";
20
- /** Tester sentinel — "this widget does not apply to this binding". */
23
+ /**
24
+ * Tester sentinel — "this widget does not apply to this binding".
25
+ *
26
+ * @deprecated Re-exported from `@aiaiai-pt/widget-system/core`. Import it from
27
+ * there directly.
28
+ */
21
29
  export declare const NOT_APPLICABLE = -1;
22
30
  /**
23
31
  * A tester ranks a registry entry against a binding (+ optional schema and the
@@ -26,38 +34,67 @@ export declare const NOT_APPLICABLE = -1;
26
34
  * generic kind widget (`entity-list`) for the SAME `binding.kind` without
27
35
  * editing the kind entry. `type` is UNTRUSTED — it only influences ranking; the
28
36
  * resolved `key` still comes from the matched entry (TH-08), never from `type`.
37
+ *
38
+ * @deprecated ATELIER-COUPLED signature. The generic tester in
39
+ * `@aiaiai-pt/widget-system/core` is `(ctx: WidgetMatchContext) => number`.
40
+ * Migrate custom testers to read `ctx.kind`/`ctx.type` from a match context.
29
41
  */
30
42
  export type WidgetTester = (binding: Binding, schema: OntologySchema | null, type?: string) => number;
43
+ /**
44
+ * @deprecated ATELIER-COUPLED entry shape (its `tester` names `Binding`). The
45
+ * generic entry lives in `@aiaiai-pt/widget-system/core` as
46
+ * `RegistryEntry<P, Ctx>`.
47
+ */
31
48
  export interface RegistryEntry<P> {
32
49
  /** Known-good literal. Safe to render as `data-widget={key}` (TH-08). */
33
50
  key: string;
34
51
  payload: P;
35
52
  tester: WidgetTester;
36
53
  }
37
- export interface Match<P> {
38
- key: string;
39
- payload: P;
40
- }
54
+ /**
55
+ * The matched entry. Structurally identical to
56
+ * `@aiaiai-pt/widget-system/core`'s `Match<P>` — re-exported so consumers can
57
+ * migrate the import without a type change.
58
+ *
59
+ * @deprecated Import `Match` from `@aiaiai-pt/widget-system/core`.
60
+ */
61
+ export type Match<P> = CoreMatch<P>;
62
+ /**
63
+ * The render decision. Structurally identical to
64
+ * `@aiaiai-pt/widget-system/core`'s `RenderDecision`.
65
+ *
66
+ * @deprecated Import `RenderDecision` from `@aiaiai-pt/widget-system/core`.
67
+ */
68
+ export type RenderDecision = CoreRenderDecision;
41
69
  /**
42
70
  * Run every tester against the binding; the highest applicable score wins.
43
71
  * Ties resolve to the first-registered entry (stable, deterministic). Returns
44
- * `null` when no tester is applicable — the caller fails closed per blast
45
- * radius; nothing about the binding is interpolated.
72
+ * `null` when no tester is applicable.
73
+ *
74
+ * DELEGATION (#60): the ranking loop is NOT implemented here. Each
75
+ * Atelier-shaped entry is adapted into a `core.RegistryEntry` whose tester
76
+ * closes over `(binding, schema, type)` — so the full `(binding, schema, type)`
77
+ * tester contract (including `schema`, which the ctx model omits) is preserved
78
+ * — and the loop itself runs in `@aiaiai-pt/widget-system/core.selectEntry`.
79
+ * The `ctx` handed to core is a formality: core invokes `tester(ctx)` once per
80
+ * entry, and our adapted tester ignores it in favour of the closed-over args.
81
+ *
82
+ * @deprecated Use `selectEntry(entries, ctx)` from
83
+ * `@aiaiai-pt/widget-system/core` with `ctx: WidgetMatchContext`.
46
84
  */
47
85
  export declare function selectEntry<P>(entries: ReadonlyArray<RegistryEntry<P>>, binding: Binding, schema: OntologySchema | null, type?: string): Match<P> | null;
48
- export type RenderDecision = {
49
- render: "widget";
50
- } | {
51
- render: "empty";
52
- } | {
53
- render: "error";
54
- };
55
86
  /**
56
87
  * Fail-closed per blast radius (§14.8). Given whether a widget matched AND
57
88
  * whether its data resolved, decide how the slot renders:
58
89
  * - matched + data ok → render the widget
59
90
  * - failed, optional slot → soft-empty (render nothing)
60
91
  * - failed, structural slot → visible error (MUST NOT silently vanish)
61
- * The operator's raw `type`/`kind` is never interpolated in any branch.
92
+ *
93
+ * DELEGATION (#60): the decision is `@aiaiai-pt/widget-system/core.decideRender`;
94
+ * this adapter only projects the Atelier `Block.importance` field onto the
95
+ * generic `importance` argument.
96
+ *
97
+ * @deprecated Use `decideRender(importance, matched, dataOk)` from
98
+ * `@aiaiai-pt/widget-system/core`, passing `block.importance` directly.
62
99
  */
63
100
  export declare function decideRender(block: Pick<Block, "importance">, matched: boolean, dataOk: boolean): RenderDecision;
@@ -1,25 +1,39 @@
1
1
  /**
2
- * Widget dispatcher — tester/priority, not a flat map (D4, JSONForms model).
2
+ * Widget dispatcher — COMPATIBILITY ADAPTER over @aiaiai-pt/widget-system/core.
3
3
  *
4
- * Generalises the admin app's XSS-hardened layout registry
5
- * (`admin/src/lib/renderer-layouts/resolve.ts`) from form-row LayoutKeys to
6
- * widget `kind`s. Each entry carries a `tester(binding, schema) → score`; the
7
- * highest score wins. A more specific tester (e.g. "list of `occurrence`")
8
- * outranks the generic "any list" and overrides one widget for one entity
9
- * WITHOUT editing the catalog (open/closed).
4
+ * @deprecated Import the generic dispatch from `@aiaiai-pt/widget-system/core`
5
+ * instead. This module is the S2 (#60) compatibility shim: it preserves the
6
+ * Atelier-shaped `@aiaiai-pt/design-system/renderer/dispatch` import surface for
7
+ * one migration window while the ranking ALGORITHM lives in exactly one place —
8
+ * `@aiaiai-pt/widget-system/core`. Scheduled for removal in the next MAJOR of
9
+ * `@aiaiai-pt/design-system` (S3). See `docs/migration/widget-system.md`.
10
10
  *
11
- * The ranking core here is generic over the payload `P` so it stays
12
- * component-free and fully unit/mutation-testable; the real registry (73b)
13
- * instantiates it with Svelte `Component`s.
11
+ * Classification (#60 AC5): the tester/priority ranking loop is a GENERIC
12
+ * widget-system API it is delegated below and NOT reimplemented here. The
13
+ * `(binding, schema, type)` tester SIGNATURE is ATELIER-COUPLED (it names
14
+ * `Binding`/`OntologySchema`/`Block` from ./types) and stays here as a thin
15
+ * adapter until consumers migrate to the ctx-shaped `WidgetMatchContext`.
14
16
  *
15
- * TH-08 (R-SEC-07) preserved: the resolved `key` is a known-good literal from
16
- * the matched entry — the operator's raw `binding.kind`/`entity`/block `type`
17
- * is NEVER returned as the key, so it never reaches `class`/`data-*`/`style`.
17
+ * TH-08 (R-SEC-07) preserved end-to-end: `core.selectEntry` returns the matched
18
+ * entry's known-good `key` literal — the operator's raw `binding.kind`/`type`
19
+ * is never returned as the key, so it never reaches `class`/`data-*`/`style`.
18
20
  */
21
+ import {
22
+ decideRender as coreDecideRender,
23
+ NOT_APPLICABLE as CORE_NOT_APPLICABLE,
24
+ selectEntry as coreSelectEntry,
25
+ type Match as CoreMatch,
26
+ type RenderDecision as CoreRenderDecision,
27
+ } from "@aiaiai-pt/widget-system/core";
19
28
  import type { Binding, Block, OntologySchema } from "./types";
20
29
 
21
- /** Tester sentinel — "this widget does not apply to this binding". */
22
- export const NOT_APPLICABLE = -1;
30
+ /**
31
+ * Tester sentinel — "this widget does not apply to this binding".
32
+ *
33
+ * @deprecated Re-exported from `@aiaiai-pt/widget-system/core`. Import it from
34
+ * there directly.
35
+ */
36
+ export const NOT_APPLICABLE = CORE_NOT_APPLICABLE;
23
37
 
24
38
  /**
25
39
  * A tester ranks a registry entry against a binding (+ optional schema and the
@@ -28,6 +42,10 @@ export const NOT_APPLICABLE = -1;
28
42
  * generic kind widget (`entity-list`) for the SAME `binding.kind` without
29
43
  * editing the kind entry. `type` is UNTRUSTED — it only influences ranking; the
30
44
  * resolved `key` still comes from the matched entry (TH-08), never from `type`.
45
+ *
46
+ * @deprecated ATELIER-COUPLED signature. The generic tester in
47
+ * `@aiaiai-pt/widget-system/core` is `(ctx: WidgetMatchContext) => number`.
48
+ * Migrate custom testers to read `ctx.kind`/`ctx.type` from a match context.
31
49
  */
32
50
  export type WidgetTester = (
33
51
  binding: Binding,
@@ -35,6 +53,11 @@ export type WidgetTester = (
35
53
  type?: string,
36
54
  ) => number;
37
55
 
56
+ /**
57
+ * @deprecated ATELIER-COUPLED entry shape (its `tester` names `Binding`). The
58
+ * generic entry lives in `@aiaiai-pt/widget-system/core` as
59
+ * `RegistryEntry<P, Ctx>`.
60
+ */
38
61
  export interface RegistryEntry<P> {
39
62
  /** Known-good literal. Safe to render as `data-widget={key}` (TH-08). */
40
63
  key: string;
@@ -42,16 +65,38 @@ export interface RegistryEntry<P> {
42
65
  tester: WidgetTester;
43
66
  }
44
67
 
45
- export interface Match<P> {
46
- key: string;
47
- payload: P;
48
- }
68
+ /**
69
+ * The matched entry. Structurally identical to
70
+ * `@aiaiai-pt/widget-system/core`'s `Match<P>` — re-exported so consumers can
71
+ * migrate the import without a type change.
72
+ *
73
+ * @deprecated Import `Match` from `@aiaiai-pt/widget-system/core`.
74
+ */
75
+ export type Match<P> = CoreMatch<P>;
76
+
77
+ /**
78
+ * The render decision. Structurally identical to
79
+ * `@aiaiai-pt/widget-system/core`'s `RenderDecision`.
80
+ *
81
+ * @deprecated Import `RenderDecision` from `@aiaiai-pt/widget-system/core`.
82
+ */
83
+ export type RenderDecision = CoreRenderDecision;
49
84
 
50
85
  /**
51
86
  * Run every tester against the binding; the highest applicable score wins.
52
87
  * Ties resolve to the first-registered entry (stable, deterministic). Returns
53
- * `null` when no tester is applicable — the caller fails closed per blast
54
- * radius; nothing about the binding is interpolated.
88
+ * `null` when no tester is applicable.
89
+ *
90
+ * DELEGATION (#60): the ranking loop is NOT implemented here. Each
91
+ * Atelier-shaped entry is adapted into a `core.RegistryEntry` whose tester
92
+ * closes over `(binding, schema, type)` — so the full `(binding, schema, type)`
93
+ * tester contract (including `schema`, which the ctx model omits) is preserved
94
+ * — and the loop itself runs in `@aiaiai-pt/widget-system/core.selectEntry`.
95
+ * The `ctx` handed to core is a formality: core invokes `tester(ctx)` once per
96
+ * entry, and our adapted tester ignores it in favour of the closed-over args.
97
+ *
98
+ * @deprecated Use `selectEntry(entries, ctx)` from
99
+ * `@aiaiai-pt/widget-system/core` with `ctx: WidgetMatchContext`.
55
100
  */
56
101
  export function selectEntry<P>(
57
102
  entries: ReadonlyArray<RegistryEntry<P>>,
@@ -59,41 +104,35 @@ export function selectEntry<P>(
59
104
  schema: OntologySchema | null,
60
105
  type?: string,
61
106
  ): Match<P> | null {
62
- let best: RegistryEntry<P> | null = null;
63
- let bestScore: number = NOT_APPLICABLE;
64
- for (const entry of entries) {
65
- const score = entry.tester(binding, schema, type);
66
- // Strictly greater first registered wins ties.
67
- if (score > bestScore) {
68
- bestScore = score;
69
- best = entry;
70
- }
71
- }
72
- if (best === null) return null;
73
- // TH-08: key from the matched entry, never from the binding/block.
74
- return { key: best.key, payload: best.payload };
107
+ const ctx = { kind: binding.kind, type };
108
+ const adapted = entries.map((entry) => ({
109
+ key: entry.key,
110
+ payload: entry.payload,
111
+ // Close over the Atelier-shaped selection inputs; core supplies `ctx` but
112
+ // this adapter reads the richer (binding, schema, type) triple instead.
113
+ tester: () => entry.tester(binding, schema, type),
114
+ }));
115
+ return coreSelectEntry(adapted, ctx);
75
116
  }
76
117
 
77
- export type RenderDecision =
78
- | { render: "widget" }
79
- | { render: "empty" }
80
- | { render: "error" };
81
-
82
118
  /**
83
119
  * Fail-closed per blast radius (§14.8). Given whether a widget matched AND
84
120
  * whether its data resolved, decide how the slot renders:
85
121
  * - matched + data ok → render the widget
86
122
  * - failed, optional slot → soft-empty (render nothing)
87
123
  * - failed, structural slot → visible error (MUST NOT silently vanish)
88
- * The operator's raw `type`/`kind` is never interpolated in any branch.
124
+ *
125
+ * DELEGATION (#60): the decision is `@aiaiai-pt/widget-system/core.decideRender`;
126
+ * this adapter only projects the Atelier `Block.importance` field onto the
127
+ * generic `importance` argument.
128
+ *
129
+ * @deprecated Use `decideRender(importance, matched, dataOk)` from
130
+ * `@aiaiai-pt/widget-system/core`, passing `block.importance` directly.
89
131
  */
90
132
  export function decideRender(
91
133
  block: Pick<Block, "importance">,
92
134
  matched: boolean,
93
135
  dataOk: boolean,
94
136
  ): RenderDecision {
95
- if (matched && dataOk) return { render: "widget" };
96
- return block.importance === "structural"
97
- ? { render: "error" }
98
- : { render: "empty" };
137
+ return coreDecideRender(block.importance, matched, dataOk);
99
138
  }
@@ -1,14 +1,23 @@
1
1
  /**
2
- * DS renderer widget registry — the extensible dispatcher layer (#492).
2
+ * DS renderer widget registry — the Atelier-coupled BASE registry (#492).
3
3
  *
4
- * Ships a BASE registry with the two clean DS widgets (stat-grid + results-chart)
5
- * and exposes a `registerWidget` hook so host apps (the portal, the admin) can
6
- * append their own coupled widgets at startup without modifying this module
7
- * (open/closed principle, JSONForms model).
4
+ * @deprecated This is an S2 (#60) COMPATIBILITY surface. It is NOT a generic
5
+ * widget-system API do not reclassify it as one (#60 AC5). It is an
6
+ * Atelier-coupled base registry: a module-global entry list preloaded with the
7
+ * DS widgets (stat-grid, results-chart, chart, metabase-embed) whose testers
8
+ * name `Binding`/`Block`/`WidgetKind` from ./types.
8
9
  *
9
- * `dispatch.ts` (the tester/priority core) is the single dispatcher both the DS
10
- * base registry and host-extended registries share; the registry is generic over
11
- * the payload so `selectEntry` stays component-free and unit-testable.
10
+ * Migration destination: the generic isolated-registry machinery lives in
11
+ * `@aiaiai-pt/widget-system/core` (`createRegistry()` an isolated, override-
12
+ * aware factory, not a module-global singleton) and its base-widget preload in
13
+ * `@aiaiai-pt/widget-system/widgets` (`registerBaseWidgets`). The Atelier host
14
+ * owns which widgets it preloads. This module is scheduled for removal in the
15
+ * next MAJOR of `@aiaiai-pt/design-system` (S3). See
16
+ * `docs/migration/widget-system.md`.
17
+ *
18
+ * NO DUPLICATE DISPATCH (#60 AC3): this module holds ZERO ranking logic. It
19
+ * only stores entries and delegates resolution to `./dispatch.selectEntry`,
20
+ * which itself delegates the ranking loop to `@aiaiai-pt/widget-system/core`.
12
21
  *
13
22
  * TH-08 preserved: `resolveWidget` returns the matched entry's known-good `key`
14
23
  * literal — the operator's raw block `type`/`binding.kind` is never the key.
@@ -22,9 +31,18 @@ export type WidgetComponent = Component<WidgetProps>;
22
31
  * `type` hint ranks it ABOVE the kind-generic widget (score 20 > 10) for the
23
32
  * same binding. `type` is UNTRUSTED — it only influences ranking; the resolved
24
33
  * key is the entry literal (TH-08).
34
+ *
35
+ * @deprecated ATELIER-COUPLED builder (its `kind` is a `WidgetKind` and its
36
+ * tester names `Binding`). Migrate to `byTypeOnKind` from
37
+ * `@aiaiai-pt/widget-system/core`, whose tester reads a ctx `WidgetMatchContext`.
25
38
  */
26
39
  export declare function byTypeOnKind(key: string, kind: WidgetKind, component: unknown): RegistryEntry<WidgetComponent>;
27
- /** A widget whose tester is the generic "binding.kind === K" check (score 10). */
40
+ /**
41
+ * A widget whose tester is the generic "binding.kind === K" check (score 10).
42
+ *
43
+ * @deprecated ATELIER-COUPLED builder. Migrate to `byKind` from
44
+ * `@aiaiai-pt/widget-system/core` (ctx-shaped tester).
45
+ */
28
46
  export declare function byKind(key: string, kind: WidgetKind, component: unknown): RegistryEntry<WidgetComponent>;
29
47
  /**
30
48
  * Register a widget in the host-extensible registry. Call at app startup
@@ -37,6 +55,11 @@ export declare function byKind(key: string, kind: WidgetKind, component: unknown
37
55
  *
38
56
  * @param entry - A registry entry built with `byKind`, `byTypeOnKind`, or a
39
57
  * hand-crafted `{ key, payload, tester }` for a custom tester.
58
+ *
59
+ * @deprecated ATELIER-COUPLED module-global registration. Migrate to an
60
+ * isolated `createRegistry()` from `@aiaiai-pt/widget-system/core` plus
61
+ * `registerBaseWidgets` from `@aiaiai-pt/widget-system/widgets`, and call
62
+ * `registry.register(...)` on the host-owned instance.
40
63
  */
41
64
  export declare function registerWidget(entry: RegistryEntry<WidgetComponent>): void;
42
65
  /**
@@ -44,5 +67,11 @@ export declare function registerWidget(entry: RegistryEntry<WidgetComponent>): v
44
67
  * the full registry (base DS entries + host-registered). Returns `null` when
45
68
  * no widget applies — the caller renders per blast radius (`decideRender`),
46
69
  * never interpolating the block's raw `type`/`kind`.
70
+ *
71
+ * Resolution delegates to `./dispatch.selectEntry` (→ widget-system/core); this
72
+ * module contributes only the entry list, never the ranking loop.
73
+ *
74
+ * @deprecated Migrate to `registry.resolve(ctx)` on an isolated
75
+ * `createRegistry()` from `@aiaiai-pt/widget-system/core`.
47
76
  */
48
77
  export declare function resolveWidget(block: Block, schema?: OntologySchema | null): Match<WidgetComponent> | null;
@@ -1,14 +1,23 @@
1
1
  /**
2
- * DS renderer widget registry — the extensible dispatcher layer (#492).
2
+ * DS renderer widget registry — the Atelier-coupled BASE registry (#492).
3
3
  *
4
- * Ships a BASE registry with the two clean DS widgets (stat-grid + results-chart)
5
- * and exposes a `registerWidget` hook so host apps (the portal, the admin) can
6
- * append their own coupled widgets at startup without modifying this module
7
- * (open/closed principle, JSONForms model).
4
+ * @deprecated This is an S2 (#60) COMPATIBILITY surface. It is NOT a generic
5
+ * widget-system API do not reclassify it as one (#60 AC5). It is an
6
+ * Atelier-coupled base registry: a module-global entry list preloaded with the
7
+ * DS widgets (stat-grid, results-chart, chart, metabase-embed) whose testers
8
+ * name `Binding`/`Block`/`WidgetKind` from ./types.
8
9
  *
9
- * `dispatch.ts` (the tester/priority core) is the single dispatcher both the DS
10
- * base registry and host-extended registries share; the registry is generic over
11
- * the payload so `selectEntry` stays component-free and unit-testable.
10
+ * Migration destination: the generic isolated-registry machinery lives in
11
+ * `@aiaiai-pt/widget-system/core` (`createRegistry()` an isolated, override-
12
+ * aware factory, not a module-global singleton) and its base-widget preload in
13
+ * `@aiaiai-pt/widget-system/widgets` (`registerBaseWidgets`). The Atelier host
14
+ * owns which widgets it preloads. This module is scheduled for removal in the
15
+ * next MAJOR of `@aiaiai-pt/design-system` (S3). See
16
+ * `docs/migration/widget-system.md`.
17
+ *
18
+ * NO DUPLICATE DISPATCH (#60 AC3): this module holds ZERO ranking logic. It
19
+ * only stores entries and delegates resolution to `./dispatch.selectEntry`,
20
+ * which itself delegates the ranking loop to `@aiaiai-pt/widget-system/core`.
12
21
  *
13
22
  * TH-08 preserved: `resolveWidget` returns the matched entry's known-good `key`
14
23
  * literal — the operator's raw block `type`/`binding.kind` is never the key.
@@ -34,6 +43,10 @@ export type WidgetComponent = Component<WidgetProps>;
34
43
  * `type` hint ranks it ABOVE the kind-generic widget (score 20 > 10) for the
35
44
  * same binding. `type` is UNTRUSTED — it only influences ranking; the resolved
36
45
  * key is the entry literal (TH-08).
46
+ *
47
+ * @deprecated ATELIER-COUPLED builder (its `kind` is a `WidgetKind` and its
48
+ * tester names `Binding`). Migrate to `byTypeOnKind` from
49
+ * `@aiaiai-pt/widget-system/core`, whose tester reads a ctx `WidgetMatchContext`.
37
50
  */
38
51
  export function byTypeOnKind(
39
52
  key: string,
@@ -48,7 +61,12 @@ export function byTypeOnKind(
48
61
  };
49
62
  }
50
63
 
51
- /** A widget whose tester is the generic "binding.kind === K" check (score 10). */
64
+ /**
65
+ * A widget whose tester is the generic "binding.kind === K" check (score 10).
66
+ *
67
+ * @deprecated ATELIER-COUPLED builder. Migrate to `byKind` from
68
+ * `@aiaiai-pt/widget-system/core` (ctx-shaped tester).
69
+ */
52
70
  export function byKind(
53
71
  key: string,
54
72
  kind: WidgetKind,
@@ -92,6 +110,11 @@ const _entries: RegistryEntry<WidgetComponent>[] = [
92
110
  *
93
111
  * @param entry - A registry entry built with `byKind`, `byTypeOnKind`, or a
94
112
  * hand-crafted `{ key, payload, tester }` for a custom tester.
113
+ *
114
+ * @deprecated ATELIER-COUPLED module-global registration. Migrate to an
115
+ * isolated `createRegistry()` from `@aiaiai-pt/widget-system/core` plus
116
+ * `registerBaseWidgets` from `@aiaiai-pt/widget-system/widgets`, and call
117
+ * `registry.register(...)` on the host-owned instance.
95
118
  */
96
119
  export function registerWidget(entry: RegistryEntry<WidgetComponent>): void {
97
120
  _entries.push(entry);
@@ -102,6 +125,12 @@ export function registerWidget(entry: RegistryEntry<WidgetComponent>): void {
102
125
  * the full registry (base DS entries + host-registered). Returns `null` when
103
126
  * no widget applies — the caller renders per blast radius (`decideRender`),
104
127
  * never interpolating the block's raw `type`/`kind`.
128
+ *
129
+ * Resolution delegates to `./dispatch.selectEntry` (→ widget-system/core); this
130
+ * module contributes only the entry list, never the ranking loop.
131
+ *
132
+ * @deprecated Migrate to `registry.resolve(ctx)` on an isolated
133
+ * `createRegistry()` from `@aiaiai-pt/widget-system/core`.
105
134
  */
106
135
  export function resolveWidget(
107
136
  block: Block,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,6 +74,9 @@
74
74
  "components",
75
75
  "tokens"
76
76
  ],
77
+ "dependencies": {
78
+ "@aiaiai-pt/widget-system": ">=0.1.0 <1"
79
+ },
77
80
  "peerDependencies": {
78
81
  "@codemirror/commands": "^6.10.3",
79
82
  "@codemirror/lang-json": "^6.0.2",
@@ -248,6 +248,21 @@
248
248
  --badge-neutral-bg: var(--color-surface-tertiary);
249
249
  --badge-neutral-text: var(--color-text-secondary);
250
250
 
251
+ /* Evidence Seal — DS-owned vocabulary: measured | inferred | projected
252
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
253
+ Defaults resolve via semantic color tokens so they work for every theme.
254
+ UBP dark scheme overrides below in tokens/themes/ubp.css to maintain
255
+ WCAG AA on the cool-steel dark surface (#0b182a). */
256
+ --seal-measured-text: var(--color-success);
257
+ --seal-measured-bg: var(--color-success-subtle);
258
+ --seal-inferred-text: var(--color-info);
259
+ --seal-inferred-bg: var(--color-info-subtle);
260
+ --seal-projected-text: var(--color-warning);
261
+ --seal-projected-bg: var(--color-warning-subtle);
262
+ /* stale modifier: muted regardless of base evidence state */
263
+ --seal-stale-text: var(--color-text-muted);
264
+ --seal-stale-bg: var(--color-surface-secondary);
265
+
251
266
  /* Tag */
252
267
  --tag-font: var(--type-label-font);
253
268
  --tag-size: var(--type-label-size);
@@ -138,6 +138,23 @@
138
138
  /* Total color overrides: 22. Elevation: 2. Focus alias: 1.
139
139
  Grand total: ~25 tokens. Typography/motion/spacing/grid unchanged.
140
140
  --color-info and --color-info-subtle NOT overridden (inherit DS teal-blue). */
141
+
142
+ /* ═══════════════════════════════════════════════════════════════
143
+ EVIDENCE SEAL — light-scheme text overrides (H1 Slice 2, uw#57)
144
+ --color-success (#16a34a) and --color-warning (#ca8a04) are
145
+ "large UI only" on the steel surface (3.15:1 and 2.83:1 resp.).
146
+ Seal badge text tokens use darker variants to clear WCAG AA (≥4.5:1)
147
+ on their respective subtle backgrounds.
148
+
149
+ Contrast on subtle backgrounds (AA = 4.5:1 for small badge text):
150
+ measured #256f20 (green-700) on #f0fdf4 (success-subtle): ~5.46:1 [AA]
151
+ projected #9e6308 (amber-700) on #fffbeb (warning-subtle): ~4.79:1 [AA]
152
+ --seal-inferred-text inherits --color-info (#2e63a3): 5.66:1 on
153
+ info-subtle (#f0f5fa) — no override needed.
154
+ --seal-stale-text inherits --color-text-muted (≥3.0:1 non-essential).
155
+ ═══════════════════════════════════════════════════════════════ */
156
+ --seal-measured-text: #256f20; /* green-700 → 5.46:1 on success-subtle [AA] */
157
+ --seal-projected-text: #9e6308; /* amber-700 → 4.79:1 on warning-subtle [AA] */
141
158
  }
142
159
 
143
160
  /* Desktop density: compact button heights for toolbar/sidebar layouts.
@@ -222,3 +239,27 @@
222
239
  /* Focus ring: accent blue, visible on all dark surfaces */
223
240
  --focus-ring-color: var(--color-accent);
224
241
  }
242
+
243
+ [data-theme="ubp"][data-scheme="dark"] {
244
+ /* ═══════════════════════════════════════════════════════════════
245
+ EVIDENCE SEAL — dark-scheme overrides (H1 Slice 2, uw#57)
246
+ Text tokens lifted to 400-series for WCAG AA/AAA on steel-900
247
+ (#0b182a). Background tokens use color-mix() and are NOT
248
+ verified by the automated contrast suite (see ubp-theme-contrast
249
+ coverage-gap note) — visual QA is the complement.
250
+
251
+ Contrast on steel-900 (#0b182a, lum ≈ 0.0082):
252
+ measured #4ade80 (green-400) lum ≈ 0.403 → ~7.76:1 [AAA]
253
+ inferred #60a5fa (blue-400) lum ≈ 0.386 → ~7.49:1 [AAA]
254
+ projected #fbbf24 (amber-400) lum ≈ 0.598 → ~11.14:1 [AAA]
255
+ stale inherits --seal-stale-text: var(--color-text-muted) = #60758a (3.74:1)
256
+ ═══════════════════════════════════════════════════════════════ */
257
+ --seal-measured-text: #4ade80;
258
+ --seal-measured-bg: color-mix(in srgb, #4ade80 12%, #0b182a);
259
+ --seal-inferred-text: #60a5fa;
260
+ --seal-inferred-bg: color-mix(in srgb, #60a5fa 12%, #0b182a);
261
+ --seal-projected-text: #fbbf24;
262
+ --seal-projected-bg: color-mix(in srgb, #fbbf24 12%, #0b182a);
263
+ /* stale: inherits --seal-stale-text (--color-text-muted = #60758a, 3.74:1 non-essential)
264
+ and --seal-stale-bg (--color-surface-secondary = #1a2a3a) */
265
+ }