@aiaiai-pt/design-system 0.49.0 → 0.51.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,201 @@
1
+ <!--
2
+ @component KpiRegister
3
+
4
+ The two-register composition for one KPI: the more authoritative value above,
5
+ the less authoritative beneath. It is where "measured out-ranks projected"
6
+ becomes concrete — in type SIZE, in POSITION, and in SCREEN-READER READING
7
+ ORDER, all three.
8
+
9
+ The ranking is enforced STRUCTURALLY, by evidence class, not by word. A
10
+ reading outranks a model output, which outranks a forecast
11
+ (EVIDENCE_CLASS_RANK in @aiaiai-pt/widget-system/core). That ordering holds
12
+ under every enumeration in the corpus, so this component needs no vocabulary
13
+ to enforce it — and it throws if a caller tries to seat a more authoritative
14
+ value in the lower register.
15
+
16
+ BOUNDARY ELEMENT: throws when a value that requires a seal is handed over
17
+ without one. No unsealed seal-required value crosses this boundary onto a
18
+ rendering surface.
19
+
20
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
21
+ Uses --type-overline-* tokens (sistema#67) for the overline label.
22
+
23
+ @example A reading alone
24
+ <KpiRegister label="WATER LEVEL" primaryValue="+14 cm/h" />
25
+
26
+ @example Reading above a sealed forecast, why one tap away
27
+ <KpiRegister
28
+ label="WATER LEVEL"
29
+ primaryValue="+14 cm/h"
30
+ secondaryValue="−18 cm"
31
+ secondarySeal={resolveSeal(assignment, vocabulary)}
32
+ secondaryWhy="derived from the 14:00–16:00 tide model run"
33
+ />
34
+ -->
35
+ <script>
36
+ import SealChip from './SealChip.svelte';
37
+
38
+ /**
39
+ * @typedef {'direct_reading' | 'model_derived' | 'future_window'} EvidenceClass
40
+ * @typedef {'positive' | 'info' | 'caution' | 'neutral'} SealTone
41
+ * @typedef {{ value: string, label: string, tone?: SealTone }} SealTerm
42
+ * @typedef {{ evidence: SealTerm, evidenceClass: EvidenceClass, probability?: SealTerm, stale: boolean }} ResolvedSeal
43
+ */
44
+
45
+ let {
46
+ /** @type {string} — overline label (KPI name / dimension) */
47
+ label,
48
+ /** @type {string} — the more authoritative value; always required */
49
+ primaryValue,
50
+ /**
51
+ * @type {ResolvedSeal | undefined}
52
+ * Seal for the primary value. Omit only when the value needs none — a
53
+ * direct reading of a current or past window (assignSeal.sealRequired).
54
+ */
55
+ primarySeal = undefined,
56
+ /** @type {string | undefined} — the less authoritative value for the same KPI */
57
+ secondaryValue = undefined,
58
+ /**
59
+ * @type {ResolvedSeal | undefined}
60
+ * Seal for the secondary value. REQUIRED when secondaryValue is provided:
61
+ * a value seated in the lower register is, by construction, not a plain
62
+ * current reading.
63
+ */
64
+ secondarySeal = undefined,
65
+ /**
66
+ * @type {string | undefined}
67
+ * The provenance one-liner behind the secondary value's seal — what makes
68
+ * the «why» reachable one tap from the seal.
69
+ */
70
+ secondaryWhy = undefined,
71
+ /** @type {string} */
72
+ class: className = '',
73
+ ...rest
74
+ } = $props();
75
+
76
+ /** Structural authority order — highest first. Mirrors EVIDENCE_CLASS_RANK. */
77
+ const RANK = {
78
+ direct_reading: 3,
79
+ model_derived: 2,
80
+ future_window: 1,
81
+ };
82
+
83
+ /*
84
+ * BOUNDARY GUARDS. Both run synchronously at initialisation and again
85
+ * reactively, so a prop change after mount cannot smuggle a violation in.
86
+ */
87
+ const assertRegisters = (
88
+ /** @type {string | undefined} */ secondary,
89
+ /** @type {ResolvedSeal | undefined} */ secSeal,
90
+ /** @type {ResolvedSeal | undefined} */ primSeal,
91
+ ) => {
92
+ // 1. No unsealed value in the lower register.
93
+ if (secondary !== undefined && secSeal == null) {
94
+ throw new Error(
95
+ '[KpiRegister] The secondary value must carry a seal. Pass ' +
96
+ '`secondarySeal` (from resolveSeal()). An unsealed value may not ' +
97
+ 'cross this boundary onto a rendering surface.',
98
+ );
99
+ }
100
+ // 2. The lower register may never out-rank the upper one.
101
+ if (secSeal != null && primSeal != null) {
102
+ const secRank = RANK[secSeal.evidenceClass];
103
+ const primRank = RANK[primSeal.evidenceClass];
104
+ if (secRank > primRank) {
105
+ throw new Error(
106
+ `[KpiRegister] The secondary value's evidence class ` +
107
+ `"${secSeal.evidenceClass}" out-ranks the primary's ` +
108
+ `"${primSeal.evidenceClass}". The more authoritative value takes ` +
109
+ 'the upper register — swap them.',
110
+ );
111
+ }
112
+ }
113
+ };
114
+
115
+ assertRegisters(secondaryValue, secondarySeal, primarySeal);
116
+ $effect(() => assertRegisters(secondaryValue, secondarySeal, primarySeal));
117
+ </script>
118
+
119
+ <div class="kpi-register {className}" {...rest}>
120
+ <!-- Overline label — micro-heading above the value (sistema#67 tokens). -->
121
+ <span class="kpi-register-label">{label}</span>
122
+
123
+ <!--
124
+ The primary register: display-scale type, first in the DOM, and therefore
125
+ first in reading order. Out-ranks the secondary on all three axes.
126
+ -->
127
+ <div class="kpi-register-primary">
128
+ {#if primarySeal != null}
129
+ <SealChip
130
+ class="kpi-register-primary-value"
131
+ value={primaryValue}
132
+ evidence={primarySeal.evidence}
133
+ probability={primarySeal.probability}
134
+ stale={primarySeal.stale}
135
+ />
136
+ {:else}
137
+ <span class="kpi-register-primary-value">{primaryValue}</span>
138
+ {/if}
139
+ </div>
140
+
141
+ <!--
142
+ The secondary register: body-scale, lower position, read last.
143
+ secondarySeal is guaranteed non-null here by the boundary guard.
144
+ -->
145
+ {#if secondaryValue !== undefined && secondarySeal != null}
146
+ <div class="kpi-register-secondary">
147
+ <SealChip
148
+ value={secondaryValue}
149
+ evidence={secondarySeal.evidence}
150
+ probability={secondarySeal.probability}
151
+ stale={secondarySeal.stale}
152
+ why={secondaryWhy}
153
+ />
154
+ </div>
155
+ {/if}
156
+ </div>
157
+
158
+ <style>
159
+ .kpi-register {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: var(--space-xs);
163
+ padding: var(--stat-padding);
164
+ border: var(--stat-border);
165
+ border-radius: var(--stat-radius);
166
+ background: var(--stat-bg);
167
+ }
168
+
169
+ /*
170
+ * Overline label — consumes --type-overline-* tokens (sistema#67).
171
+ * Equivalent to the .type-overline utility, declared here so the component
172
+ * is self-contained.
173
+ */
174
+ .kpi-register-label {
175
+ font-family: var(--type-overline-font);
176
+ font-size: var(--type-overline-size);
177
+ font-weight: var(--type-overline-weight);
178
+ line-height: var(--type-overline-leading);
179
+ letter-spacing: var(--type-overline-tracking);
180
+ text-transform: uppercase;
181
+ color: var(--stat-label-color);
182
+ }
183
+
184
+ /* Primary: display scale — the dominant element. */
185
+ .kpi-register-primary :global(.kpi-register-primary-value),
186
+ .kpi-register-primary .kpi-register-primary-value {
187
+ font-family: var(--stat-value-font);
188
+ font-size: var(--stat-value-size);
189
+ font-weight: var(--stat-value-weight);
190
+ letter-spacing: var(--stat-value-tracking);
191
+ color: var(--color-text);
192
+ line-height: 1;
193
+ }
194
+
195
+ /* Secondary: body scale — subordinate to the primary register. */
196
+ .kpi-register-secondary {
197
+ display: flex;
198
+ align-items: center;
199
+ margin-top: var(--space-2xs);
200
+ }
201
+ </style>
@@ -0,0 +1,57 @@
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
+ * The two-register composition for one KPI: the more authoritative value above,
10
+ * the less authoritative beneath. It is where "measured out-ranks projected"
11
+ * becomes concrete — in type SIZE, in POSITION, and in SCREEN-READER READING
12
+ * ORDER, all three.
13
+ *
14
+ * The ranking is enforced STRUCTURALLY, by evidence class, not by word. A
15
+ * reading outranks a model output, which outranks a forecast
16
+ * (EVIDENCE_CLASS_RANK in @aiaiai-pt/widget-system/core). That ordering holds
17
+ * under every enumeration in the corpus, so this component needs no vocabulary
18
+ * to enforce it — and it throws if a caller tries to seat a more authoritative
19
+ * value in the lower register.
20
+ *
21
+ * BOUNDARY ELEMENT: throws when a value that requires a seal is handed over
22
+ * without one. No unsealed seal-required value crosses this boundary onto a
23
+ * rendering surface.
24
+ *
25
+ * H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
26
+ * Uses --type-overline-* tokens (sistema#67) for the overline label.
27
+ *
28
+ * @example A reading alone
29
+ * <KpiRegister label="WATER LEVEL" primaryValue="+14 cm/h" />
30
+ *
31
+ * @example Reading above a sealed forecast, why one tap away
32
+ * <KpiRegister
33
+ * label="WATER LEVEL"
34
+ * primaryValue="+14 cm/h"
35
+ * secondaryValue="−18 cm"
36
+ * secondarySeal={resolveSeal(assignment, vocabulary)}
37
+ * secondaryWhy="derived from the 14:00–16:00 tide model run"
38
+ * />
39
+ */
40
+ declare const KpiRegister: import("svelte").Component<{
41
+ label: any;
42
+ primaryValue: any;
43
+ primarySeal?: any;
44
+ secondaryValue?: any;
45
+ secondarySeal?: any;
46
+ secondaryWhy?: any;
47
+ class?: string;
48
+ } & Record<string, any>, {}, "">;
49
+ type $$ComponentProps = {
50
+ label: any;
51
+ primaryValue: any;
52
+ primarySeal?: any;
53
+ secondaryValue?: any;
54
+ secondarySeal?: any;
55
+ secondaryWhy?: any;
56
+ class?: string;
57
+ } & Record<string, any>;
@@ -0,0 +1,356 @@
1
+ <!--
2
+ @component SealChip
3
+
4
+ The one seal renderer for every value surface — KPI registers, map badges,
5
+ paths-strip columns, record lines, citations, saved projections, briefs.
6
+ No surface renders a seal any other way.
7
+
8
+ THIS COMPONENT KNOWS NO SEAL WORDS. The design system owns the LAWS and a
9
+ presentation tone scale; it does not own the vocabulary. Terms arrive as
10
+ declared data — `{ value, label, tone }` resolved from ontology vocabulary
11
+ rows at read time — so widening an axis or re-wording a term is a declaration
12
+ change with zero code change and no package release.
13
+
14
+ The laws it does enforce, all of which hold under every enumeration in the
15
+ corpus:
16
+ · two orthogonal axes, rendered as two adjacent chips, never one phrase
17
+ · never label-alone — a seal never appears without its evidence chip
18
+ · evidence alone is valid; probability alone is not
19
+ · the seal rides the DATUM, not the styling — it is adjacent DOM text, so
20
+ it survives re-render, copy, quotation and screen readers
21
+ · a «why» is always one tap away, keyboard-reachable
22
+
23
+ Value and seal read to a screen reader as ONE stitched phrase:
24
+ «{value} — {evidence}[, {probability}][, stale]».
25
+
26
+ `stale` is a separate orthogonal state, never a member of either vocabulary.
27
+
28
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
29
+
30
+ @example A reading, using whatever terms the deployment declares
31
+ <SealChip value="+14 cm/h" evidence={{ value: 'measured', label: 'medido', tone: 'positive' }} />
32
+
33
+ @example Both axes, horizon passed, why one tap away
34
+ <SealChip
35
+ value="2,1 m"
36
+ evidence={{ value: 'inferred', label: 'inferido', tone: 'info' }}
37
+ probability={{ value: 'uncertain', label: 'incerto' }}
38
+ stale
39
+ why="the 16:00 window has already passed"
40
+ />
41
+ -->
42
+ <script>
43
+ /**
44
+ * @typedef {'positive' | 'info' | 'caution' | 'neutral'} SealTone
45
+ * @typedef {{ value: string, label: string, tone?: SealTone }} SealTerm
46
+ */
47
+
48
+ let {
49
+ /** @type {string} — the formatted display value */
50
+ value,
51
+ /**
52
+ * @type {SealTerm}
53
+ * The declared evidence term. Required: a seal never appears without its
54
+ * evidence chip. Resolve it with `resolveSeal` from
55
+ * `@aiaiai-pt/widget-system/core` — never construct one from a literal.
56
+ */
57
+ evidence,
58
+ /**
59
+ * @type {SealTerm | undefined}
60
+ * The declared probability term. Orthogonal to evidence and rendered as its
61
+ * own adjacent chip. Optional because evidence alone is a valid seal.
62
+ */
63
+ probability = undefined,
64
+ /** @type {boolean} — true when the validity window has passed */
65
+ stale = false,
66
+ /**
67
+ * @type {string | undefined}
68
+ * The provenance one-liner behind the seal. When supplied, the chip is a
69
+ * real button that discloses it — the «why», one tap away.
70
+ */
71
+ why = undefined,
72
+ /**
73
+ * @type {string}
74
+ * Accessible name for the why trigger. Localized by the caller, since this
75
+ * package owns no product copy. `{term}` is replaced by the evidence label.
76
+ */
77
+ whyLabel = 'why — {term}',
78
+ /** @type {string} — accessible name for the disclosure's close affordance */
79
+ closeLabel = 'Close',
80
+ /** @type {string} — screen-reader word for the stale state; localized by the caller */
81
+ staleLabel = 'stale',
82
+ /** @type {string} */
83
+ class: className = '',
84
+ ...rest
85
+ } = $props();
86
+
87
+ const TONES = ['positive', 'info', 'caution', 'neutral'];
88
+
89
+ /**
90
+ * BOUNDARY GUARD — "never label-alone; a seal never appears without evidence
91
+ * chips", and "evidence alone is valid, probability alone is not".
92
+ *
93
+ * Without this, an absent term renders an empty badge and an empty sr-text:
94
+ * a value dressed as a sealed datum, carrying no evidence and announcing
95
+ * nothing to a screen reader. That failure is silent, which is exactly why
96
+ * it is a throw and not a fallback.
97
+ *
98
+ * The guard checks SHAPE, never membership — this component has no opinion
99
+ * about which words are legal, only that a term is present and renderable.
100
+ */
101
+ const assertTerm = (
102
+ /** @type {unknown} */ term,
103
+ /** @type {string} */ axis,
104
+ ) => {
105
+ const ok =
106
+ typeof term === 'object' &&
107
+ term !== null &&
108
+ typeof (/** @type {SealTerm} */ (term).label) === 'string' &&
109
+ /** @type {SealTerm} */ (term).label.trim() !== '';
110
+ if (!ok) {
111
+ throw new Error(
112
+ `[SealChip] The ${axis} axis needs a declared term ` +
113
+ '{ value, label, tone? } with a non-empty label. Received ' +
114
+ `${JSON.stringify(term)}. Resolve it from the declared vocabulary ` +
115
+ 'with resolveSeal(); this component enumerates no seal words.',
116
+ );
117
+ }
118
+ const tone = /** @type {SealTerm} */ (term).tone;
119
+ if (tone !== undefined && !TONES.includes(tone)) {
120
+ throw new Error(
121
+ `[SealChip] Unknown tone ${JSON.stringify(tone)} on the ${axis} axis. ` +
122
+ `The design system's tone scale is ${TONES.join(' | ')}.`,
123
+ );
124
+ }
125
+ };
126
+
127
+ const assertSeal = (/** @type {unknown} */ ev, /** @type {unknown} */ prob) => {
128
+ // Evidence first: probability-alone must fail as a MISSING EVIDENCE error,
129
+ // because that is the law it breaks.
130
+ assertTerm(ev, 'evidence');
131
+ if (prob !== undefined) assertTerm(prob, 'probability');
132
+ };
133
+
134
+ assertSeal(evidence, probability);
135
+ $effect(() => assertSeal(evidence, probability));
136
+
137
+ const evidenceTone = $derived(evidence?.tone ?? 'neutral');
138
+ const probabilityTone = $derived(probability?.tone ?? 'neutral');
139
+
140
+ /**
141
+ * The stitched phrase AT reads and a citation carries:
142
+ * «{value} — {evidence}[, {probability}][, stale]».
143
+ */
144
+ const srPhrase = $derived(
145
+ [
146
+ `${value} — ${evidence.label}`,
147
+ probability?.label,
148
+ stale ? staleLabel : undefined,
149
+ ]
150
+ .filter(Boolean)
151
+ .join(', '),
152
+ );
153
+
154
+ const whyTriggerLabel = $derived(whyLabel.replace('{term}', evidence.label));
155
+
156
+ /** Disclosure state lives here, never in a DOM attribute. */
157
+ let open = $state(false);
158
+ /** @type {HTMLButtonElement | undefined} */
159
+ let trigger = $state();
160
+
161
+ const panelId = $props.id();
162
+
163
+ const closeWhy = () => {
164
+ open = false;
165
+ trigger?.focus();
166
+ };
167
+
168
+ /** Esc anywhere inside the open disclosure closes it (cascade slot 2). */
169
+ const onDocumentKeydown = (/** @type {KeyboardEvent} */ event) => {
170
+ if (event.key === 'Escape' && open) closeWhy();
171
+ };
172
+ </script>
173
+
174
+ <svelte:document onkeydown={onDocumentKeydown} />
175
+
176
+ {#snippet chips()}
177
+ <span class="seal-chip-badge seal-tone-{evidenceTone}" aria-hidden="true"
178
+ >{evidence.label}{stale ? ` (${staleLabel})` : ''}</span
179
+ >{#if probability}<span
180
+ class="seal-chip-probability seal-tone-{probabilityTone}"
181
+ aria-hidden="true">{probability.label}</span
182
+ >{/if}
183
+ {/snippet}
184
+
185
+ <!--
186
+ One inline phrase. The visible value and chips are aria-hidden so the
187
+ stitched sr-text is the single thing announced — nothing reads twice.
188
+ The sr-text follows the value in DOM order, so a sweep or a copy captures
189
+ «value seal» in that order.
190
+ -->
191
+ <span
192
+ class="seal-chip {stale ? 'seal-chip-stale' : ''} {className}"
193
+ data-evidence={evidence.value}
194
+ {...rest}
195
+ >
196
+ <span class="seal-chip-value" aria-hidden="true">{value}</span><!--
197
+ -->{#if why}<button
198
+ type="button"
199
+ class="seal-chip-why-trigger"
200
+ aria-expanded={open}
201
+ aria-controls={panelId}
202
+ aria-label={whyTriggerLabel}
203
+ bind:this={trigger}
204
+ onclick={() => (open = !open)}>{@render chips()}</button
205
+ >{:else}{@render chips()}{/if}<!--
206
+ --><span class="seal-chip-sr-text">&thinsp;{srPhrase}</span>
207
+
208
+ {#if why && open}
209
+ <span id={panelId} class="seal-chip-why-panel">
210
+ <span class="seal-chip-why-text">{why}</span>
211
+ <button
212
+ type="button"
213
+ class="seal-chip-why-close"
214
+ aria-label={closeLabel}
215
+ onclick={closeWhy}>×</button
216
+ >
217
+ </span>
218
+ {/if}
219
+ </span>
220
+
221
+ <style>
222
+ .seal-chip {
223
+ display: inline-flex;
224
+ align-items: baseline;
225
+ gap: var(--space-xs);
226
+ position: relative;
227
+ }
228
+
229
+ /* Value text carries no override — it reads as its surrounding type. */
230
+
231
+ /* Both axes share one chip shape; only the tone differs. */
232
+ .seal-chip-badge,
233
+ .seal-chip-probability {
234
+ display: inline-flex;
235
+ align-items: center;
236
+ font-family: var(--badge-font);
237
+ font-size: var(--badge-size);
238
+ letter-spacing: var(--badge-tracking);
239
+ border-radius: var(--badge-radius);
240
+ padding: var(--badge-padding-y) var(--badge-padding-x);
241
+ white-space: nowrap;
242
+ line-height: 1;
243
+ }
244
+
245
+ /* The second axis is always its own element beside the first, so the two
246
+ never merge into one phrase. */
247
+ .seal-chip-probability {
248
+ margin-left: var(--space-2xs);
249
+ }
250
+
251
+ /*
252
+ * TONES — the presentation scale the design system owns. Which declared term
253
+ * wears which tone is data, declared beside the word and never inferred from
254
+ * it, so these rules carry no vocabulary.
255
+ */
256
+ .seal-tone-positive {
257
+ color: var(--seal-positive-text);
258
+ background: var(--seal-positive-bg);
259
+ }
260
+
261
+ .seal-tone-info {
262
+ color: var(--seal-info-text);
263
+ background: var(--seal-info-bg);
264
+ }
265
+
266
+ .seal-tone-caution {
267
+ color: var(--seal-caution-text);
268
+ background: var(--seal-caution-bg);
269
+ }
270
+
271
+ .seal-tone-neutral {
272
+ color: var(--badge-neutral-text);
273
+ background: var(--badge-neutral-bg);
274
+ }
275
+
276
+ /* Stale is an orthogonal state, not a vocabulary member: it mutes whatever
277
+ tone the evidence term carries. The word carries the meaning; the wash is
278
+ redundant emphasis. */
279
+ .seal-chip-stale .seal-chip-badge {
280
+ color: var(--seal-stale-text);
281
+ background: var(--seal-stale-bg);
282
+ }
283
+
284
+ /*
285
+ * The why trigger is a real button wrapping the chips — the whole seal is
286
+ * the affordance, so the «why» is one tap away from the seal itself.
287
+ */
288
+ .seal-chip-why-trigger {
289
+ display: inline-flex;
290
+ align-items: center;
291
+ gap: var(--space-2xs);
292
+ padding: 0;
293
+ border: none;
294
+ background: none;
295
+ font: inherit;
296
+ color: inherit;
297
+ cursor: pointer;
298
+ }
299
+
300
+ .seal-chip-why-trigger:focus-visible,
301
+ .seal-chip-why-close:focus-visible {
302
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
303
+ outline-offset: var(--focus-ring-offset);
304
+ border-radius: var(--radius-sm);
305
+ }
306
+
307
+ /* The provenance one-liner. Plain text plus a visible close (Esc/X parity). */
308
+ .seal-chip-why-panel {
309
+ position: absolute;
310
+ top: 100%;
311
+ left: 0;
312
+ z-index: 1;
313
+ display: inline-flex;
314
+ align-items: flex-start;
315
+ gap: var(--space-xs);
316
+ margin-top: var(--space-2xs);
317
+ padding: var(--space-xs) var(--space-sm);
318
+ max-width: 32ch;
319
+ border: var(--elevation-border);
320
+ border-radius: var(--radius-md);
321
+ background: var(--color-surface-secondary);
322
+ color: var(--color-text-secondary);
323
+ font-family: var(--type-caption-font);
324
+ font-size: var(--type-caption-size);
325
+ font-weight: var(--type-caption-weight);
326
+ line-height: var(--type-caption-leading);
327
+ letter-spacing: var(--type-caption-tracking);
328
+ text-align: left;
329
+ }
330
+
331
+ .seal-chip-why-close {
332
+ padding: 0;
333
+ border: none;
334
+ background: none;
335
+ font: inherit;
336
+ line-height: 1;
337
+ color: inherit;
338
+ cursor: pointer;
339
+ }
340
+
341
+ /*
342
+ * sr-only — visually hidden but present in the DOM so the text travels
343
+ * with the value when content is copied or cited: a citation carries the
344
+ * seal of the value it cites.
345
+ */
346
+ .seal-chip-sr-text {
347
+ position: absolute;
348
+ width: 1px;
349
+ height: 1px;
350
+ padding: 0;
351
+ margin: -1px;
352
+ overflow: hidden;
353
+ clip-path: inset(50%);
354
+ white-space: nowrap;
355
+ }
356
+ </style>
@@ -0,0 +1,68 @@
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
+ * The one seal renderer for every value surface — KPI registers, map badges,
10
+ * paths-strip columns, record lines, citations, saved projections, briefs.
11
+ * No surface renders a seal any other way.
12
+ *
13
+ * THIS COMPONENT KNOWS NO SEAL WORDS. The design system owns the LAWS and a
14
+ * presentation tone scale; it does not own the vocabulary. Terms arrive as
15
+ * declared data — `{ value, label, tone }` resolved from ontology vocabulary
16
+ * rows at read time — so widening an axis or re-wording a term is a declaration
17
+ * change with zero code change and no package release.
18
+ *
19
+ * The laws it does enforce, all of which hold under every enumeration in the
20
+ * corpus:
21
+ * · two orthogonal axes, rendered as two adjacent chips, never one phrase
22
+ * · never label-alone — a seal never appears without its evidence chip
23
+ * · evidence alone is valid; probability alone is not
24
+ * · the seal rides the DATUM, not the styling — it is adjacent DOM text, so
25
+ * it survives re-render, copy, quotation and screen readers
26
+ * · a «why» is always one tap away, keyboard-reachable
27
+ *
28
+ * Value and seal read to a screen reader as ONE stitched phrase:
29
+ * «{value} — {evidence}[, {probability}][, stale]».
30
+ *
31
+ * `stale` is a separate orthogonal state, never a member of either vocabulary.
32
+ *
33
+ * H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
34
+ *
35
+ * @example A reading, using whatever terms the deployment declares
36
+ * <SealChip value="+14 cm/h" evidence={{ value: 'measured', label: 'medido', tone: 'positive' }} />
37
+ *
38
+ * @example Both axes, horizon passed, why one tap away
39
+ * <SealChip
40
+ * value="2,1 m"
41
+ * evidence={{ value: 'inferred', label: 'inferido', tone: 'info' }}
42
+ * probability={{ value: 'uncertain', label: 'incerto' }}
43
+ * stale
44
+ * why="the 16:00 window has already passed"
45
+ * />
46
+ */
47
+ declare const SealChip: import("svelte").Component<{
48
+ value: any;
49
+ evidence: any;
50
+ probability?: any;
51
+ stale?: boolean;
52
+ why?: any;
53
+ whyLabel?: string;
54
+ closeLabel?: string;
55
+ staleLabel?: string;
56
+ class?: string;
57
+ } & Record<string, any>, {}, "">;
58
+ type $$ComponentProps = {
59
+ value: any;
60
+ evidence: any;
61
+ probability?: any;
62
+ stale?: boolean;
63
+ why?: any;
64
+ whyLabel?: string;
65
+ closeLabel?: string;
66
+ staleLabel?: string;
67
+ class?: string;
68
+ } & 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,13 @@ 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
+ // These own the LAWS and a presentation tone scale. They own NO seal
20
+ // vocabulary: terms arrive as declared data resolved by resolveSeal().
21
+ export { default as SealChip } from "./SealChip.svelte";
22
+ export { default as KpiRegister } from "./KpiRegister.svelte";
23
+ export { default as CaminhoStateChip } from "./CaminhoStateChip.svelte";
24
+
18
25
  // Form controls
19
26
  export { default as Button } from "./Button.svelte";
20
27
  export { default as Input } from "./Input.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "packages/widget-system"
17
17
  ],
18
18
  "scripts": {
19
- "build:types": "rm -f components/*.d.ts components/renderer/*.d.ts && svelte-package --input . --output dist && cp dist/components/*.d.ts components/ && cp dist/components/renderer/*.d.ts components/renderer/ && rm -rf dist",
19
+ "build:types": "rm -rf node_modules/@aiaiai-pt/design-system packages/widget-system/node_modules/@aiaiai-pt/design-system && rm -f components/*.d.ts components/renderer/*.d.ts && svelte-package --input . --output dist && cp dist/components/*.d.ts components/ && cp dist/components/renderer/*.d.ts components/renderer/ && rm -rf dist",
20
20
  "test": "vitest run",
21
21
  "prepublishOnly": "npm run build:types"
22
22
  },
package/tokens/base.css CHANGED
@@ -96,6 +96,10 @@
96
96
  --raw-font-size-18: 1.125rem;
97
97
  --raw-font-size-24: 1.5rem;
98
98
  --raw-font-size-36: 2.25rem;
99
+ /* Marketing steps. Application surfaces stop at 36; these exist for public
100
+ pages where the headline is the message, not a page title. */
101
+ --raw-font-size-48: 3rem;
102
+ --raw-font-size-64: 4rem;
99
103
 
100
104
  /* ─── Typography: Font Weights ─── */
101
105
  --raw-font-weight-regular: 400;
@@ -248,6 +248,27 @@
248
248
  --badge-neutral-bg: var(--color-surface-tertiary);
249
249
  --badge-neutral-text: var(--color-text-secondary);
250
250
 
251
+ /* Evidence Seal — TONES, not seal words.
252
+ H1 Slice 2 — westeuropeco/atelier-urban-workspace#57.
253
+
254
+ The design system owns the presentation scale (positive / info / caution /
255
+ stale); it does NOT own the seal vocabulary. Which word wears which tone is
256
+ declared data resolved outside this package, so widening or re-wording an
257
+ axis is a declaration change and never a DS release.
258
+
259
+ Defaults resolve via semantic color tokens so they work for every theme.
260
+ UBP dark scheme overrides below in tokens/themes/ubp.css to maintain
261
+ WCAG AA on the cool-steel dark surface (#0b182a). */
262
+ --seal-positive-text: var(--color-success);
263
+ --seal-positive-bg: var(--color-success-subtle);
264
+ --seal-info-text: var(--color-info);
265
+ --seal-info-bg: var(--color-info-subtle);
266
+ --seal-caution-text: var(--color-warning);
267
+ --seal-caution-bg: var(--color-warning-subtle);
268
+ /* stale modifier: muted regardless of base evidence state */
269
+ --seal-stale-text: var(--color-text-muted);
270
+ --seal-stale-bg: var(--color-surface-secondary);
271
+
251
272
  /* Tag */
252
273
  --tag-font: var(--type-label-font);
253
274
  --tag-size: var(--type-label-size);
@@ -63,7 +63,21 @@
63
63
  --font-sans: var(--raw-font-sans);
64
64
  --font-mono: var(--raw-font-mono);
65
65
 
66
- /* Type scale — categorical */
66
+ /* Type scale — categorical.
67
+ display-2xl / display-xl are MARKETING roles: public pages where the
68
+ headline is the message. Application surfaces start at `display`. */
69
+ --type-display-2xl-size: var(--raw-font-size-64);
70
+ --type-display-2xl-weight: var(--raw-font-weight-semibold);
71
+ --type-display-2xl-leading: var(--raw-line-height-tight);
72
+ --type-display-2xl-tracking: var(--raw-tracking-tight);
73
+ --type-display-2xl-font: var(--font-sans);
74
+
75
+ --type-display-xl-size: var(--raw-font-size-48);
76
+ --type-display-xl-weight: var(--raw-font-weight-semibold);
77
+ --type-display-xl-leading: var(--raw-line-height-tight);
78
+ --type-display-xl-tracking: var(--raw-tracking-tight);
79
+ --type-display-xl-font: var(--font-sans);
80
+
67
81
  --type-display-size: var(--raw-font-size-36);
68
82
  --type-display-weight: var(--raw-font-weight-semibold);
69
83
  --type-display-leading: var(--raw-line-height-tight);
@@ -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-info-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-positive-text: #256f20; /* green-700 → 5.46:1 on success-subtle [AA] */
157
+ --seal-caution-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-positive-text: #4ade80;
258
+ --seal-positive-bg: color-mix(in srgb, #4ade80 12%, #0b182a);
259
+ --seal-info-text: #60a5fa;
260
+ --seal-info-bg: color-mix(in srgb, #60a5fa 12%, #0b182a);
261
+ --seal-caution-text: #fbbf24;
262
+ --seal-caution-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
+ }
@@ -984,6 +984,36 @@
984
984
  Instrument Sans is the quiet backdrop for prose.
985
985
  ═══════════════════════════════════════════════ */
986
986
 
987
+ /* Marketing roles — public pages where the headline is the message.
988
+ Application surfaces start at .type-display. Both step down one role at
989
+ narrow viewports, which holds a headline near its intended line count
990
+ rather than doubling it. */
991
+ .type-display-2xl {
992
+ font-family: var(--type-display-2xl-font);
993
+ font-size: var(--type-display-2xl-size);
994
+ font-weight: var(--type-display-2xl-weight);
995
+ line-height: var(--type-display-2xl-leading);
996
+ letter-spacing: var(--type-display-2xl-tracking);
997
+ }
998
+
999
+ .type-display-xl {
1000
+ font-family: var(--type-display-xl-font);
1001
+ font-size: var(--type-display-xl-size);
1002
+ font-weight: var(--type-display-xl-weight);
1003
+ line-height: var(--type-display-xl-leading);
1004
+ letter-spacing: var(--type-display-xl-tracking);
1005
+ }
1006
+
1007
+ @media (max-width: 900px) {
1008
+ .type-display-2xl {
1009
+ font-size: var(--type-display-xl-size);
1010
+ }
1011
+
1012
+ .type-display-xl {
1013
+ font-size: var(--type-display-size);
1014
+ }
1015
+ }
1016
+
987
1017
  .type-display {
988
1018
  font-family: var(--type-display-font);
989
1019
  font-size: var(--type-display-size);