@aiaiai-pt/design-system 0.47.0 → 0.49.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.
@@ -142,9 +142,13 @@
142
142
  return buildChartOption(chartData, {
143
143
  tokens: {
144
144
  accent: token("--color-accent", "#2563eb"),
145
- textPrimary: token("--color-text-primary", "#1f2937"),
145
+ // Semantic tokens name the primary text colour `--color-text` (see
146
+ // tokens/semantic.css); `--color-text-primary` never existed, so this
147
+ // read silently fell back to #1f2937 — invisible on dark schemes (#89).
148
+ textPrimary: token("--color-text", token("--color-text-primary", "#1f2937")),
146
149
  textSecondary: token("--color-text-secondary", "#6b7280"),
147
150
  border: token("--color-border", "#e5e7eb"),
151
+ surface: token("--color-surface", "#ffffff"),
148
152
  },
149
153
  locale,
150
154
  legend,
@@ -186,7 +186,7 @@ export function watchTheme(onchange) {
186
186
  const mo = new MutationObserver(() => onchange());
187
187
  mo.observe(document.documentElement, {
188
188
  attributes: true,
189
- attributeFilter: ["data-theme", "class"],
189
+ attributeFilter: ["data-theme", "data-scheme", "data-contrast", "class"],
190
190
  });
191
191
 
192
192
  const mql = window.matchMedia("(prefers-color-scheme: dark)");
@@ -26,6 +26,9 @@ export interface ChartTokens {
26
26
  textPrimary: string;
27
27
  textSecondary: string;
28
28
  border: string;
29
+ /** Panel surface behind floating chrome (tooltip); themes the default-white
30
+ ECharts tooltip for dark/high-contrast schemes (#89). */
31
+ surface: string;
29
32
  }
30
33
  export interface BuildChartOptionOpts {
31
34
  tokens: ChartTokens;
@@ -28,6 +28,9 @@ export interface ChartTokens {
28
28
  textPrimary: string;
29
29
  textSecondary: string;
30
30
  border: string;
31
+ /** Panel surface behind floating chrome (tooltip); themes the default-white
32
+ ECharts tooltip for dark/high-contrast schemes (#89). */
33
+ surface: string;
31
34
  }
32
35
 
33
36
  export interface BuildChartOptionOpts {
@@ -44,6 +47,16 @@ export interface BuildChartOptionOpts {
44
47
  innerRadius?: number;
45
48
  }
46
49
 
50
+ /** Token-driven tooltip chrome — ECharts defaults to a white panel that
51
+ * ignores the scheme entirely (#89). */
52
+ function tooltipChrome(tokens: ChartTokens): Record<string, unknown> {
53
+ return {
54
+ backgroundColor: tokens.surface,
55
+ borderColor: tokens.border,
56
+ textStyle: { color: tokens.textPrimary },
57
+ };
58
+ }
59
+
47
60
  /** A localised number formatter (DS-supplied — never operator code). */
48
61
  function makeFmt(locale: string): (value: number) => string {
49
62
  const nf = new Intl.NumberFormat(locale);
@@ -78,12 +91,13 @@ function buildPieOption(
78
91
  return {
79
92
  animation: false,
80
93
  tooltip: {
94
+ ...tooltipChrome(tokens),
81
95
  trigger: "item",
82
96
  formatter: (p: { name: string; value: number; percent: number }) =>
83
97
  `${p.name}: ${fmt(p.value)} (${p.percent}%)`,
84
98
  },
85
99
  ...(opts.legend
86
- ? { legend: { show: true, textStyle: { color: tokens.textPrimary } } }
100
+ ? { legend: { show: true, top: 0, textStyle: { color: tokens.textPrimary } } }
87
101
  : {}),
88
102
  color: palette,
89
103
  series: [
@@ -208,6 +222,7 @@ function buildCartesianOption(
208
222
  containLabel: true,
209
223
  },
210
224
  tooltip: {
225
+ ...tooltipChrome(tokens),
211
226
  trigger: "axis",
212
227
  axisPointer: { type: "shadow" },
213
228
  formatter: (
@@ -227,6 +242,9 @@ function buildCartesianOption(
227
242
  ? {
228
243
  legend: {
229
244
  show: true,
245
+ // echarts 6 lays the unpositioned legend over the bottom category
246
+ // labels; pin it to the top strip grid.top already reserves (#89).
247
+ top: 0,
230
248
  data: series.map((s) => s.name),
231
249
  textStyle: { color: tokens.textPrimary },
232
250
  },
@@ -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.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,8 @@
12
12
  "access": "public"
13
13
  },
14
14
  "workspaces": [
15
- "site"
15
+ "site",
16
+ "packages/widget-system"
16
17
  ],
17
18
  "scripts": {
18
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",
@@ -73,6 +74,9 @@
73
74
  "components",
74
75
  "tokens"
75
76
  ],
77
+ "dependencies": {
78
+ "@aiaiai-pt/widget-system": ">=0.1.0 <1"
79
+ },
76
80
  "peerDependencies": {
77
81
  "@codemirror/commands": "^6.10.3",
78
82
  "@codemirror/lang-json": "^6.0.2",
@@ -230,11 +230,11 @@
230
230
  dark wash for free. Per-theme bespoke dark = a `[data-theme="x"]`
231
231
  block in the theme file, not here.
232
232
 
233
- Selector is `:root[data-scheme="dark"]` (0,1,1): it TIES tenant theme
234
- blocks (`:root[data-theme="x"]`, runtime-injected in <head>) and wins
235
- on order — DS styles load after the injected theme block. Consumers
236
- resolving an "auto" preference must set the RESOLVED value on <html>
237
- (a pre-paint `prefers-color-scheme` read), never "auto" itself. */
233
+ Selector is `:root[data-scheme="dark"]` (0,2,0 pseudo-class + attribute):
234
+ it TIES runtime-injected tenant theme blocks (`:root[data-theme="x"]`,
235
+ also 0,2,0) and wins on order — DS styles load after the injected theme
236
+ block. Consumers resolving an "auto" preference must set the RESOLVED value
237
+ on <html> (a pre-paint `prefers-color-scheme` read), never "auto" itself. */
238
238
 
239
239
  :root[data-scheme="dark"] {
240
240
  color-scheme: dark;
@@ -0,0 +1,224 @@
1
+ /*
2
+ * aiaiai Design System — UBP Theme (ubp.css)
3
+ *
4
+ * Professional instrument theme for the urban intelligence workspace
5
+ * (ubp-ai-workspace). Tier 2 (Bespoke): distinct cool-steel identity
6
+ * optimised for data-dense, map-centric, dark-primary interfaces.
7
+ *
8
+ * Selector: [data-theme="ubp"]
9
+ * Paired scheme: [data-scheme="dark"] for the workspace default.
10
+ * The generic dark layer (semantic.css) handles neutral role inversion;
11
+ * the [data-theme="ubp"][data-scheme="dark"] block below adjusts only
12
+ * the brand values that need different treatment on a cool-dark surface.
13
+ *
14
+ * ─── Neutral palette — owned, not verbatim Tailwind slate ───────────────
15
+ * The UBP neutral ramp is derived from Tailwind slate shifted 10° toward
16
+ * teal/steel in OKLCH. This is a deliberate identity decision: the hue
17
+ * drift gives mid-tones (400–600) a visible cool-steel signature while
18
+ * keeping the lightest stops (50–200) effectively neutral, making the
19
+ * palette perceptually distinct from Tailwind defaults without tipping
20
+ * into obvious teal.
21
+ *
22
+ * Before → After (10° OKLCH hue shift, all other OKLCH params unchanged):
23
+ * #f8fafc → #f8fafc (slate-50, unchanged at 8-bit precision)
24
+ * #f1f5f9 → #f0f5f9 (slate-100, surface-secondary)
25
+ * #e2e8f0 → #e1e9f0 (slate-200, surface-tertiary)
26
+ * #cbd5e1 → #c9d6e0 (slate-300, border)
27
+ * #94a3b8 → #91a4b7 (slate-400, border-strong / dark text-secondary)
28
+ * #64748b → #60758a (slate-500, text-muted)
29
+ * #475569 → #435668 (slate-600, dark border-strong)
30
+ * #334155 → #2f4254 (slate-700, text-secondary light / dark surfaces)
31
+ * #1e293b → #1a2a3a (slate-800, dark surface-secondary)
32
+ * #0f172a → #0b182a (slate-900, text light / dark surface)
33
+ *
34
+ * ─── Font decision: inherit DS pairing ──────────────────────────────────
35
+ * UBP inherits Sistema's default type pairing unchanged:
36
+ * sans → Instrument Sans (--font-sans / --raw-font-sans)
37
+ * mono → JetBrains Mono, ui-monospace (--font-mono / --raw-font-mono)
38
+ * (Berkeley Mono leads the mono stack in the token but ships no @font-face;
39
+ * JetBrains Mono is the effective shipped face — see sistema #82.)
40
+ * The theme sets NO --font-sans or --font-mono override; font role-tokens
41
+ * resolve to the DS defaults via semantic.css inheritance.
42
+ * Full decision record: dev_docs/solutions/ubp-font-self-hosting.md
43
+ *
44
+ * ─── Historical 3.0:1 subtitle regression control ────────────────────────
45
+ * The 3.0:1 defect occurred when --color-text-muted was used in subtitle
46
+ * roles, giving only 3.0:1 contrast on the surface. This theme prevents it
47
+ * by ensuring --color-text-secondary (the subtitle token) clears WCAG AA
48
+ * (≥4.5:1) on every declared surface. See tests/ubp-theme-contrast.test.ts.
49
+ *
50
+ * ─── Owned by: DS-H0 #65, implemented under #66 ─────────────────────────
51
+ * Apply via: <html data-theme="ubp"> (light) or
52
+ * <html data-theme="ubp" data-scheme="dark"> (dark / workspace default)
53
+ * Layer independently with data-contrast, data-text-size, data-link-highlight.
54
+ */
55
+
56
+ [data-theme="ubp"] {
57
+ /* ═══════════════════════════════════════════════════════════════
58
+ TYPOGRAPHY — inherited from DS (no override)
59
+ Instrument Sans (sans) + JetBrains Mono (mono) via semantic.css.
60
+ ═══════════════════════════════════════════════════════════════ */
61
+
62
+ /* ═══════════════════════════════════════════════════════════════
63
+ COLOR — Cool steel palette (light-scheme base)
64
+ Neutrals: 10° OKLCH teal-shift from Tailwind slate — see file header.
65
+ ═══════════════════════════════════════════════════════════════ */
66
+
67
+ /* Surfaces: cool off-white — sterile precision vs aiaiai warm paper */
68
+ --color-surface: #f8fafc; /* steel-50 (unchanged at 8-bit) */
69
+ --color-surface-secondary: #f0f5f9; /* steel-100 */
70
+ --color-surface-tertiary: #e1e9f0; /* steel-200 */
71
+ --color-surface-raised: #ffffff; /* pure white for popovers/menus */
72
+ --color-overlay: rgba(
73
+ 11,
74
+ 24,
75
+ 42,
76
+ 0.5
77
+ ); /* cool dark overlay (steel-900 base) */
78
+
79
+ /* Borders: cool steel */
80
+ --color-border: #c9d6e0; /* steel-300 */
81
+ --color-border-strong: #91a4b7; /* steel-400 */
82
+
83
+ /* Text: cool dark steel — contrast validated in ubp-theme-contrast.test.ts
84
+ text (#0b182a) on surface (#f8fafc): 17.03:1 [AAA]
85
+ text-secondary (#2f4254) on surface (#f8fafc): 9.90:1 [AAA — subtitle safe]
86
+ text-muted (#60758a) on surface (#f8fafc): 4.55:1 [AA — above 3.0 defect floor] */
87
+ --color-text: #0b182a; /* steel-900 */
88
+ --color-text-secondary: #2f4254; /* steel-700 */
89
+ --color-text-muted: #60758a; /* steel-500 */
90
+ --color-text-on-accent: #ffffff; /* white on blue-600: 5.17:1 [AA] */
91
+
92
+ /* Accent: UBP blue — maps, focus rings, primary actions
93
+ accent (#2563eb) on surface (#f8fafc): 4.94:1 [AA for normal text, ≥3:1 for UI] */
94
+ --color-accent: #2563eb; /* blue-600 */
95
+ --color-accent-hover: #1d4ed8; /* blue-700 */
96
+ --color-accent-subtle: #eff6ff; /* blue-50 equivalent — selected states */
97
+
98
+ /* Status semantic colours: override for cool-palette compatibility.
99
+ WCAG AA on --color-surface (#f8fafc) and --color-surface-secondary (#f1f5f9).
100
+ Subtle washes re-tuned for the cool surfaces.
101
+ NOTE: --color-info is NOT overridden; inherits DS teal-blue
102
+ (--raw-color-blue-600, #2e63a3 — 5.85:1 on surface), distinct from
103
+ --color-accent (#2563eb). See contrast suite: tests/ubp-theme-contrast.test.ts */
104
+ --color-destructive: #dc2626; /* red-600, 4.62:1 on surface */
105
+ --color-destructive-hover: #b91c1c; /* red-700 */
106
+ --color-destructive-subtle: #fef2f2;
107
+
108
+ --color-success: #16a34a; /* green-600, 3.15:1 on surface — large UI only */
109
+ --color-success-subtle: #f0fdf4;
110
+
111
+ --color-warning: #ca8a04; /* amber-600, 3.1:1 on surface — large UI only */
112
+ --color-warning-subtle: #fffbeb;
113
+
114
+ /* Focus: accent blue, visible on all surfaces */
115
+ --focus-ring-color: var(--color-accent);
116
+
117
+ /* ═══════════════════════════════════════════════════════════════
118
+ RADIUS — Keep sharp (precision instrument)
119
+ Already: sm=2px md=4px lg=8px from semantic.css defaults
120
+ ═══════════════════════════════════════════════════════════════ */
121
+ /* No override — sharp radii match the professional platform aesthetic */
122
+
123
+ /* ═══════════════════════════════════════════════════════════════
124
+ ELEVATION — Subtle shadow on raised surfaces (instrument feel)
125
+ Uses steel-900 (#0b182a) as the shadow base (10° teal-shifted).
126
+ ═══════════════════════════════════════════════════════════════ */
127
+ --elevation-raised:
128
+ 0 1px 3px rgba(11, 24, 42, 0.08), 0 1px 2px rgba(11, 24, 42, 0.04);
129
+ --elevation-overlay:
130
+ 0 4px 16px rgba(11, 24, 42, 0.12), 0 2px 6px rgba(11, 24, 42, 0.06);
131
+
132
+ /* ═══════════════════════════════════════════════════════════════
133
+ MOTION — unchanged from DS defaults.
134
+ Product choreography (map transitions, panel morphs) belongs in
135
+ the host's ws.css, not the theme.
136
+ ═══════════════════════════════════════════════════════════════ */
137
+
138
+ /* Total color overrides: 22. Elevation: 2. Focus alias: 1.
139
+ Grand total: ~25 tokens. Typography/motion/spacing/grid unchanged.
140
+ --color-info and --color-info-subtle NOT overridden (inherit DS teal-blue). */
141
+ }
142
+
143
+ /* Desktop density: compact button heights for toolbar/sidebar layouts.
144
+ Scoped to ≥1024px so mobile targets stay at the DS default (44px for
145
+ --button-lg-height), satisfying the Tier-2 a11y checklist minimum.
146
+ Analogous to how other bespoke themes adjust density only where a
147
+ pointer device makes smaller targets safe (WCAG 2.5.5 advisory). */
148
+ @media (min-width: 1024px) {
149
+ [data-theme="ubp"] {
150
+ --button-sm-height: 28px; /* DS Tier-2 floor: 28px min (reference/theming.md:221) */
151
+ --button-md-height: 32px;
152
+ --button-lg-height: 44px; /* DS Tier-2 floor: 44px min (reference/theming.md:221) */
153
+ }
154
+ }
155
+
156
+ /*
157
+ * Dark-scheme brand override.
158
+ *
159
+ * The generic dark layer (`:root[data-scheme="dark"]` in semantic.css, specificity
160
+ * 0,2,0 — pseudo-class + attribute) inverts neutral-derived roles to the warm
161
+ * neutral-950 ramp. This block overrides only the brand values that require a
162
+ * cool-steel dark base:
163
+ * - Surfaces / borders / text / overlay → cool steel (not warm neutral)
164
+ * - Accent → lighter blue (link contrast: 7.03:1 on steel-900 [AAA])
165
+ * - text-on-accent → dark text on the lighter blue fill (7.03:1 [AAA])
166
+ * - Subtle washes re-derived against the cool base via color-mix
167
+ *
168
+ * Selector specificity: (0,2,0) — ties the generic dark layer (also 0,2,0) when
169
+ * both `data-theme="ubp"` and `data-scheme="dark"` are present; wins by source
170
+ * order because ubp.css loads after semantic.css. The `auto` scheme is resolved
171
+ * before paint; "dark" here always matches an explicit decision.
172
+ *
173
+ * Subtitle regression control:
174
+ * text (#f8fafc) on surface (#0b182a): 17.03:1 [AAA]
175
+ * text-secondary (#91a4b7) on surface (#0b182a): 6.96:1 [AA — subtitle safe]
176
+ * text-muted (#60758a) on surface (#0b182a): 3.74:1 — correctly limited to
177
+ * non-essential UI labels, NOT body text or subtitles.
178
+ */
179
+ [data-theme="ubp"][data-scheme="dark"] {
180
+ /* Surfaces: cool steel (deeper than the warm neutral-950 generic layer)
181
+ 10° OKLCH teal-shift from Tailwind slate — see file header for before/after. */
182
+ --color-surface: #0b182a; /* steel-900 */
183
+ --color-surface-secondary: #1a2a3a; /* steel-800 */
184
+ --color-surface-tertiary: #2f4254; /* steel-700 */
185
+ --color-surface-raised: #1a2a3a; /* steel-800 */
186
+ --color-overlay: rgba(0, 0, 0, 0.7);
187
+
188
+ /* Borders */
189
+ --color-border: #2f4254; /* steel-700 */
190
+ --color-border-strong: #435668; /* steel-600 */
191
+
192
+ /* Text */
193
+ --color-text: #f8fafc; /* steel-50: 17.03:1 on surface [AAA] */
194
+ --color-text-secondary: #91a4b7; /* steel-400: 6.96:1 on surface [AA] */
195
+ --color-text-muted: #60758a; /* steel-500: 3.74:1 — non-essential only */
196
+
197
+ /* Accent: blue-400 lifts to 7.03:1 on steel-900, clearing AAA for link text.
198
+ Dual-use: as a fill, dark text (#0b182a) on blue-400 gives 7.03:1 [AAA]. */
199
+ --color-accent: #60a5fa; /* blue-400 */
200
+ --color-accent-hover: #93c5fd; /* blue-300 */
201
+ --color-text-on-accent: #0b182a; /* steel-900 on blue-400: 7.03:1 [AAA] */
202
+
203
+ /* Accent subtle: re-derived for the cool-steel base */
204
+ --color-accent-subtle: color-mix(in srgb, #60a5fa 16%, #0b182a);
205
+
206
+ /* Status subtles: re-derived for cool-steel surface */
207
+ --color-destructive-subtle: color-mix(
208
+ in srgb,
209
+ var(--color-destructive) 14%,
210
+ #0b182a
211
+ );
212
+ --color-success-subtle: color-mix(in srgb, var(--color-success) 14%, #0b182a);
213
+ --color-warning-subtle: color-mix(in srgb, var(--color-warning) 14%, #0b182a);
214
+ --color-info-subtle: color-mix(in srgb, var(--color-info) 14%, #0b182a);
215
+
216
+ /* Elevation: more dramatic on dark */
217
+ --elevation-raised:
218
+ 0 1px 4px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
219
+ --elevation-overlay:
220
+ 0 8px 24px rgba(0, 0, 0, 0.5), 0 3px 8px rgba(0, 0, 0, 0.3);
221
+
222
+ /* Focus ring: accent blue, visible on all dark surfaces */
223
+ --focus-ring-color: var(--color-accent);
224
+ }
@@ -1056,6 +1056,15 @@
1056
1056
  letter-spacing: var(--type-caption-tracking);
1057
1057
  }
1058
1058
 
1059
+ .type-overline {
1060
+ font-family: var(--type-overline-font);
1061
+ font-size: var(--type-overline-size);
1062
+ font-weight: var(--type-overline-weight);
1063
+ line-height: var(--type-overline-leading);
1064
+ letter-spacing: var(--type-overline-tracking);
1065
+ text-transform: uppercase;
1066
+ }
1067
+
1059
1068
  /* ═══════════════════════════════════════════════
1060
1069
  TYPOGRAPHY UTILITIES
1061
1070
  ═══════════════════════════════════════════════ */