@sentropic/h2a 0.85.16 → 0.85.17

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.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +29 -0
  2. package/.codex-plugin/plugin.json +35 -0
  3. package/.mcp.json +19 -0
  4. package/README.md +3 -0
  5. package/dist/bin-routing.d.ts +14 -3
  6. package/dist/bin-routing.d.ts.map +1 -1
  7. package/dist/bin-routing.js +31 -6
  8. package/dist/bin-routing.js.map +1 -1
  9. package/dist/bin.js +45 -14
  10. package/dist/bin.js.map +1 -1
  11. package/dist/cli-contract.d.ts.map +1 -1
  12. package/dist/cli-contract.js +41 -1
  13. package/dist/cli-contract.js.map +1 -1
  14. package/dist/cli.d.ts +4 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +146 -18
  17. package/dist/cli.js.map +1 -1
  18. package/dist/index.d.ts +4 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/mcp.d.ts +1 -1
  23. package/dist/mcp.d.ts.map +1 -1
  24. package/dist/mcp.js +1 -0
  25. package/dist/mcp.js.map +1 -1
  26. package/dist/runtime/focus/serve.d.ts +42 -0
  27. package/dist/runtime/focus/serve.d.ts.map +1 -0
  28. package/dist/runtime/focus/serve.js +366 -0
  29. package/dist/runtime/focus/serve.js.map +1 -0
  30. package/dist/runtime/loop/engine/adapters.d.ts +62 -6
  31. package/dist/runtime/loop/engine/adapters.d.ts.map +1 -1
  32. package/dist/runtime/loop/engine/adapters.js +272 -36
  33. package/dist/runtime/loop/engine/adapters.js.map +1 -1
  34. package/dist/runtime/loop/engine/decision.d.ts.map +1 -1
  35. package/dist/runtime/loop/engine/decision.js +20 -2
  36. package/dist/runtime/loop/engine/decision.js.map +1 -1
  37. package/dist/runtime/loop/engine/execute.d.ts +13 -4
  38. package/dist/runtime/loop/engine/execute.d.ts.map +1 -1
  39. package/dist/runtime/loop/engine/execute.js +17 -7
  40. package/dist/runtime/loop/engine/execute.js.map +1 -1
  41. package/dist/runtime/loop/index.d.ts +16 -0
  42. package/dist/runtime/loop/index.d.ts.map +1 -1
  43. package/dist/runtime/loop/index.js +146 -13
  44. package/dist/runtime/loop/index.js.map +1 -1
  45. package/dist/runtime/mcp/agent-launch.d.ts +33 -0
  46. package/dist/runtime/mcp/agent-launch.d.ts.map +1 -0
  47. package/dist/runtime/mcp/agent-launch.js +250 -0
  48. package/dist/runtime/mcp/agent-launch.js.map +1 -0
  49. package/dist/runtime/mcp/handlers.d.ts +8 -0
  50. package/dist/runtime/mcp/handlers.d.ts.map +1 -1
  51. package/dist/runtime/mcp/handlers.js +34 -4
  52. package/dist/runtime/mcp/handlers.js.map +1 -1
  53. package/dist/runtime/mcp/index.d.ts +2 -1
  54. package/dist/runtime/mcp/index.d.ts.map +1 -1
  55. package/dist/runtime/mcp/index.js +2 -1
  56. package/dist/runtime/mcp/index.js.map +1 -1
  57. package/dist/runtime/mcp/server.d.ts +5 -0
  58. package/dist/runtime/mcp/server.d.ts.map +1 -1
  59. package/dist/runtime/mcp/server.js +3 -0
  60. package/dist/runtime/mcp/server.js.map +1 -1
  61. package/dist/runtime/mcp/stdio.d.ts +17 -0
  62. package/dist/runtime/mcp/stdio.d.ts.map +1 -1
  63. package/dist/runtime/mcp/stdio.js +65 -13
  64. package/dist/runtime/mcp/stdio.js.map +1 -1
  65. package/dist/runtime/mcp/tools.d.ts.map +1 -1
  66. package/dist/runtime/mcp/tools.js +96 -5
  67. package/dist/runtime/mcp/tools.js.map +1 -1
  68. package/dist/runtime/path-safety.d.ts +3 -0
  69. package/dist/runtime/path-safety.d.ts.map +1 -0
  70. package/dist/runtime/path-safety.js +12 -0
  71. package/dist/runtime/path-safety.js.map +1 -0
  72. package/dist/runtime/reporting/config.d.ts +21 -0
  73. package/dist/runtime/reporting/config.d.ts.map +1 -0
  74. package/dist/runtime/reporting/config.js +76 -0
  75. package/dist/runtime/reporting/config.js.map +1 -0
  76. package/dist/runtime/reporting/context.d.ts +27 -0
  77. package/dist/runtime/reporting/context.d.ts.map +1 -0
  78. package/dist/runtime/reporting/context.js +265 -0
  79. package/dist/runtime/reporting/context.js.map +1 -0
  80. package/dist/runtime/reporting/index.d.ts +4 -0
  81. package/dist/runtime/reporting/index.d.ts.map +1 -0
  82. package/dist/runtime/reporting/index.js +4 -0
  83. package/dist/runtime/reporting/index.js.map +1 -0
  84. package/dist/runtime/reporting/report-ai.d.ts +49 -0
  85. package/dist/runtime/reporting/report-ai.d.ts.map +1 -0
  86. package/dist/runtime/reporting/report-ai.js +325 -0
  87. package/dist/runtime/reporting/report-ai.js.map +1 -0
  88. package/focus-app/client/_app/immutable/assets/0.BdzHFb9q.css +1 -0
  89. package/focus-app/client/_app/immutable/assets/0.BdzHFb9q.css.br +2 -0
  90. package/focus-app/client/_app/immutable/assets/0.BdzHFb9q.css.gz +0 -0
  91. package/focus-app/client/_app/immutable/assets/2.Ddxq02y4.css +1 -0
  92. package/focus-app/client/_app/immutable/assets/2.Ddxq02y4.css.br +0 -0
  93. package/focus-app/client/_app/immutable/assets/2.Ddxq02y4.css.gz +0 -0
  94. package/focus-app/client/_app/immutable/chunks/Bs3f1UbD.js +2 -0
  95. package/focus-app/client/_app/immutable/chunks/Bs3f1UbD.js.br +0 -0
  96. package/focus-app/client/_app/immutable/chunks/Bs3f1UbD.js.gz +0 -0
  97. package/focus-app/client/_app/immutable/chunks/Bv8OL8ib.js +1 -0
  98. package/focus-app/client/_app/immutable/chunks/Bv8OL8ib.js.br +0 -0
  99. package/focus-app/client/_app/immutable/chunks/Bv8OL8ib.js.gz +0 -0
  100. package/focus-app/client/_app/immutable/chunks/CVq_IFFr.js +2 -0
  101. package/focus-app/client/_app/immutable/chunks/CVq_IFFr.js.br +0 -0
  102. package/focus-app/client/_app/immutable/chunks/CVq_IFFr.js.gz +0 -0
  103. package/focus-app/client/_app/immutable/chunks/Cj6wo28J.js +1 -0
  104. package/focus-app/client/_app/immutable/chunks/Cj6wo28J.js.br +0 -0
  105. package/focus-app/client/_app/immutable/chunks/Cj6wo28J.js.gz +0 -0
  106. package/focus-app/client/_app/immutable/chunks/DEmooz0s.js +1 -0
  107. package/focus-app/client/_app/immutable/chunks/DEmooz0s.js.br +0 -0
  108. package/focus-app/client/_app/immutable/chunks/DEmooz0s.js.gz +0 -0
  109. package/focus-app/client/_app/immutable/chunks/DQIRXgdC.js +2 -0
  110. package/focus-app/client/_app/immutable/chunks/DQIRXgdC.js.br +0 -0
  111. package/focus-app/client/_app/immutable/chunks/DQIRXgdC.js.gz +0 -0
  112. package/focus-app/client/_app/immutable/chunks/ZxFklh9T.js +1 -0
  113. package/focus-app/client/_app/immutable/chunks/ZxFklh9T.js.br +0 -0
  114. package/focus-app/client/_app/immutable/chunks/ZxFklh9T.js.gz +0 -0
  115. package/focus-app/client/_app/immutable/chunks/_wRuVGPs.js +1 -0
  116. package/focus-app/client/_app/immutable/chunks/_wRuVGPs.js.br +0 -0
  117. package/focus-app/client/_app/immutable/chunks/_wRuVGPs.js.gz +0 -0
  118. package/focus-app/client/_app/immutable/chunks/wbPk3Yxo.js +81 -0
  119. package/focus-app/client/_app/immutable/chunks/wbPk3Yxo.js.br +0 -0
  120. package/focus-app/client/_app/immutable/chunks/wbPk3Yxo.js.gz +0 -0
  121. package/focus-app/client/_app/immutable/entry/app.6DKouWRt.js +2 -0
  122. package/focus-app/client/_app/immutable/entry/app.6DKouWRt.js.br +0 -0
  123. package/focus-app/client/_app/immutable/entry/app.6DKouWRt.js.gz +0 -0
  124. package/focus-app/client/_app/immutable/entry/start.DlLmkUb7.js +1 -0
  125. package/focus-app/client/_app/immutable/entry/start.DlLmkUb7.js.br +2 -0
  126. package/focus-app/client/_app/immutable/entry/start.DlLmkUb7.js.gz +0 -0
  127. package/focus-app/client/_app/immutable/nodes/0.CuSOF0Of.js +5 -0
  128. package/focus-app/client/_app/immutable/nodes/0.CuSOF0Of.js.br +0 -0
  129. package/focus-app/client/_app/immutable/nodes/0.CuSOF0Of.js.gz +0 -0
  130. package/focus-app/client/_app/immutable/nodes/1.D39jdnqm.js +1 -0
  131. package/focus-app/client/_app/immutable/nodes/1.D39jdnqm.js.br +0 -0
  132. package/focus-app/client/_app/immutable/nodes/1.D39jdnqm.js.gz +0 -0
  133. package/focus-app/client/_app/immutable/nodes/2.CnU8ifRl.js +1 -0
  134. package/focus-app/client/_app/immutable/nodes/2.CnU8ifRl.js.br +0 -0
  135. package/focus-app/client/_app/immutable/nodes/2.CnU8ifRl.js.gz +0 -0
  136. package/focus-app/client/_app/version.json +1 -0
  137. package/focus-app/client/_app/version.json.br +0 -0
  138. package/focus-app/client/_app/version.json.gz +0 -0
  139. package/focus-app/env.js +70 -0
  140. package/focus-app/env.js.map +1 -0
  141. package/focus-app/h2a-focus.json +114 -0
  142. package/focus-app/handler.js +24 -0
  143. package/focus-app/handler.js.map +1 -0
  144. package/focus-app/index.js +147 -0
  145. package/focus-app/index.js.map +1 -0
  146. package/focus-app/server/chunks/chunks/exports.js-DUTkELZq.js +217 -0
  147. package/focus-app/server/chunks/chunks/exports.js-DUTkELZq.js.map +1 -0
  148. package/focus-app/server/chunks/chunks/false.js-B2gHlHjM.js +4 -0
  149. package/focus-app/server/chunks/chunks/false.js-B2gHlHjM.js.map +1 -0
  150. package/focus-app/server/chunks/chunks/friendly.js-D4fjovnZ.js +246 -0
  151. package/focus-app/server/chunks/chunks/friendly.js-D4fjovnZ.js.map +1 -0
  152. package/focus-app/server/chunks/chunks/index.js-Di21TltP.js +1315 -0
  153. package/focus-app/server/chunks/chunks/index.js-Di21TltP.js.map +1 -0
  154. package/focus-app/server/chunks/chunks/internal.js-33vbd70h.js +126 -0
  155. package/focus-app/server/chunks/chunks/internal.js-33vbd70h.js.map +1 -0
  156. package/focus-app/server/chunks/chunks/root.js-DO1m-gJA.js +2967 -0
  157. package/focus-app/server/chunks/chunks/root.js-DO1m-gJA.js.map +1 -0
  158. package/focus-app/server/chunks/chunks/server.js-Bf8x1V_n.js +16 -0
  159. package/focus-app/server/chunks/chunks/server.js-Bf8x1V_n.js.map +1 -0
  160. package/focus-app/server/chunks/chunks/utils.js-C_3_iViC.js +2545 -0
  161. package/focus-app/server/chunks/chunks/utils.js-C_3_iViC.js.map +1 -0
  162. package/focus-app/server/chunks/chunks/utils2.js-BQzn9ikS.js +38 -0
  163. package/focus-app/server/chunks/chunks/utils2.js-BQzn9ikS.js.map +1 -0
  164. package/focus-app/server/chunks/entries/endpoints/api/actions/launch/_server.ts.js-DUQcf9xd.js +63 -0
  165. package/focus-app/server/chunks/entries/endpoints/api/actions/launch/_server.ts.js-DUQcf9xd.js.map +1 -0
  166. package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/_server.ts.js-j0NNcOSZ.js +100 -0
  167. package/focus-app/server/chunks/entries/endpoints/api/decisions/inject/_server.ts.js-j0NNcOSZ.js.map +1 -0
  168. package/focus-app/server/chunks/entries/fallbacks/error.svelte.js-Dj3sETSX.js +50 -0
  169. package/focus-app/server/chunks/entries/fallbacks/error.svelte.js-Dj3sETSX.js.map +1 -0
  170. package/focus-app/server/chunks/entries/pages/_layout.svelte.js-BoLsbuC2.js +1420 -0
  171. package/focus-app/server/chunks/entries/pages/_layout.svelte.js-BoLsbuC2.js.map +1 -0
  172. package/focus-app/server/chunks/entries/pages/_page.server.ts.js-ChRBj_xK.js +246 -0
  173. package/focus-app/server/chunks/entries/pages/_page.server.ts.js-ChRBj_xK.js.map +1 -0
  174. package/focus-app/server/chunks/entries/pages/_page.svelte.js-QDldMYhk.js +2725 -0
  175. package/focus-app/server/chunks/entries/pages/_page.svelte.js-QDldMYhk.js.map +1 -0
  176. package/focus-app/server/chunks/handler-Cp_5hLyY.js +1474 -0
  177. package/focus-app/server/chunks/handler-Cp_5hLyY.js.map +1 -0
  178. package/focus-app/server/chunks/index.js-CBh7Wjqp.js +4242 -0
  179. package/focus-app/server/chunks/index.js-CBh7Wjqp.js.map +1 -0
  180. package/focus-app/server/chunks/manifest.js-Cm6cC7mC.js +56 -0
  181. package/focus-app/server/chunks/manifest.js-Cm6cC7mC.js.map +1 -0
  182. package/focus-app/server/chunks/nodes/0.js-4wlmdsk8.js +9 -0
  183. package/focus-app/server/chunks/nodes/0.js-4wlmdsk8.js.map +1 -0
  184. package/focus-app/server/chunks/nodes/1.js-B-LpY-lC.js +9 -0
  185. package/focus-app/server/chunks/nodes/1.js-B-LpY-lC.js.map +1 -0
  186. package/focus-app/server/chunks/nodes/2.js-F6jp5eD9.js +19 -0
  187. package/focus-app/server/chunks/nodes/2.js-F6jp5eD9.js.map +1 -0
  188. package/focus-app/shims.js +33 -0
  189. package/focus-app/shims.js.map +1 -0
  190. package/package.json +8 -4
  191. package/skills/h2a/SKILL.md +7 -5
  192. package/skills/harness/SKILL.md +17 -0
  193. package/skills/harness/using-harness/SKILL.md +26 -0
@@ -0,0 +1,1420 @@
1
+ import { a2 as head, a3 as attr, Z as derived } from '../../chunks/index.js-Di21TltP.js';
2
+ import '../../chunks/utils.js-C_3_iViC.js';
3
+ import '../../chunks/utils2.js-BQzn9ikS.js';
4
+
5
+ const foundation = {
6
+ color: {
7
+ blue: {
8
+ 10: "oklch(97% 0.02 242)",
9
+ 60: "oklch(50% 0.134 242.749)",
10
+ 80: "oklch(32% 0.11 242)"
11
+ },
12
+ cyan: {
13
+ 10: "oklch(96% 0.04 195)",
14
+ 50: "oklch(70.4% 0.14 182.503)",
15
+ 70: "oklch(48% 0.12 190)"
16
+ },
17
+ slate: {
18
+ 0: "#ffffff",
19
+ 10: "#f8fafc",
20
+ 20: "#e2e8f0",
21
+ 60: "#475569",
22
+ 80: "#1e293b",
23
+ 90: "#0f172a"
24
+ },
25
+ feedback: {
26
+ success: "#16a34a",
27
+ warning: "#d97706",
28
+ error: "#dc2626",
29
+ info: "#2563eb"
30
+ }
31
+ },
32
+ font: {
33
+ sans: "Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
34
+ display: "Inter, system-ui, sans-serif",
35
+ mono: "'SFMono-Regular', Consolas, 'Liberation Mono', monospace"
36
+ },
37
+ spacing: {
38
+ 0: "0",
39
+ 1: "0.25rem",
40
+ 2: "0.5rem",
41
+ 3: "0.75rem",
42
+ 4: "1rem",
43
+ 6: "1.5rem",
44
+ 8: "2rem",
45
+ 12: "3rem",
46
+ 16: "4rem"
47
+ },
48
+ radius: {
49
+ none: "0",
50
+ sm: "0.25rem",
51
+ md: "0.375rem",
52
+ lg: "0.5rem",
53
+ pill: "999px"
54
+ },
55
+ shadow: {
56
+ subtle: "0 1px 2px rgb(15 23 42 / 0.08)",
57
+ medium: "0 8px 24px rgb(15 23 42 / 0.12)",
58
+ floating: "0 18px 45px rgb(15 23 42 / 0.18)"
59
+ },
60
+ motion: {
61
+ fast: "120ms",
62
+ normal: "180ms",
63
+ slow: "280ms",
64
+ easing: "cubic-bezier(0.4, 0, 0.2, 1)"
65
+ },
66
+ z: {
67
+ header: 50,
68
+ toast: 60,
69
+ overlay: 80,
70
+ modal: 100,
71
+ chat: 110
72
+ },
73
+ // --- Anatomy primitives (Phase 1) ----------------------------------------
74
+ // Border weights — controls how heavy outlines/dividers read per theme.
75
+ borderWidth: {
76
+ none: "0",
77
+ thin: "1px",
78
+ thick: "2px"
79
+ },
80
+ // Default stroke style for control borders.
81
+ borderStyle: {
82
+ solid: "solid"
83
+ },
84
+ // Density grid: control geometry per size token. controlHeight is the target
85
+ // box height; paddingBlock/Inline are intrinsic insets; gap is the inline
86
+ // gap between icon + label; minWidth is the smallest allowed control width.
87
+ density: {
88
+ sm: {
89
+ controlHeight: "2rem", // 32px
90
+ paddingBlock: "0",
91
+ paddingInline: "0.75rem",
92
+ gap: "0.375rem",
93
+ minWidth: "2rem"
94
+ },
95
+ md: {
96
+ controlHeight: "2.5rem", // 40px
97
+ paddingBlock: "0",
98
+ paddingInline: "1rem",
99
+ gap: "0.5rem",
100
+ minWidth: "2.5rem"
101
+ },
102
+ lg: {
103
+ controlHeight: "3rem", // 48px
104
+ paddingBlock: "0",
105
+ paddingInline: "1.25rem",
106
+ gap: "0.5rem",
107
+ minWidth: "3rem"
108
+ }
109
+ },
110
+ // Typography by role. Each role is a self-contained set of text properties so
111
+ // a theme can re-shape headings/labels/links without touching components.
112
+ typography: {
113
+ // Buttons / interactive control labels.
114
+ control: {
115
+ family: "var(--st-font-sans)",
116
+ size: "0.9375rem", // 15px
117
+ weight: "600",
118
+ lineHeight: "1.2",
119
+ letterSpacing: "0",
120
+ textTransform: "none",
121
+ textDecoration: "none",
122
+ decorationThickness: "auto",
123
+ decorationOffset: "auto"
124
+ },
125
+ // Form field input text.
126
+ field: {
127
+ family: "var(--st-font-sans)",
128
+ size: "1rem", // 16px
129
+ weight: "400",
130
+ lineHeight: "1.5",
131
+ letterSpacing: "0",
132
+ textTransform: "none",
133
+ textDecoration: "none",
134
+ decorationThickness: "auto",
135
+ decorationOffset: "auto"
136
+ },
137
+ // Field labels / form group legends.
138
+ label: {
139
+ family: "var(--st-font-sans)",
140
+ size: "0.875rem", // 14px
141
+ weight: "600",
142
+ lineHeight: "1.4",
143
+ letterSpacing: "0",
144
+ textTransform: "none",
145
+ textDecoration: "none",
146
+ decorationThickness: "auto",
147
+ decorationOffset: "auto"
148
+ },
149
+ // Hyperlinks.
150
+ link: {
151
+ family: "inherit",
152
+ size: "inherit",
153
+ weight: "inherit",
154
+ lineHeight: "inherit",
155
+ letterSpacing: "0",
156
+ textTransform: "none",
157
+ textDecoration: "underline",
158
+ decorationThickness: "auto",
159
+ decorationOffset: "0.18em"
160
+ }
161
+ },
162
+ // Multiplier applied to disabled controls.
163
+ disabledOpacity: "0.55",
164
+ // Shared interaction transition tokens (consumed by anatomy `transition`).
165
+ transition: {
166
+ property: "background-color, border-color, color, box-shadow, outline-color",
167
+ duration: "120ms",
168
+ easing: "cubic-bezier(0.4, 0, 0.2, 1)"
169
+ },
170
+ // Pointer affordance roles.
171
+ cursor: {
172
+ interactive: "pointer",
173
+ disabled: "not-allowed",
174
+ text: "text"
175
+ },
176
+ // Inline icon sizing per control size token.
177
+ iconSize: {
178
+ sm: "1rem",
179
+ md: "1.125rem",
180
+ lg: "1.25rem"
181
+ },
182
+ // FOCUS STRATEGY = first-class primitive. The `strategy` selects WHICH CSS
183
+ // technique a shared mixin uses (outline | ring | inset | double); the other
184
+ // fields parametrise it. This is what lets DSFR (offset outline), Carbon
185
+ // (inset box-shadow) and the base differ by *technique*, not just values.
186
+ focus: {
187
+ strategy: "outline", // outline | ring | inset | double
188
+ width: "2px",
189
+ offset: "2px",
190
+ color: "var(--st-semantic-border-interactive)",
191
+ inset: "0" // inset distance used by the `inset` strategy
192
+ },
193
+ // FIELD STYLE = first-class primitive (anatomy v1.2.0). Selects how form
194
+ // fields are drawn: `outline` (boxed, 4 equal borders — the base Sent Tech
195
+ // look) vs `filled-underline` (filled background + a single bottom rule —
196
+ // the DSFR/Carbon signature). `fillBg`/`underlineColor`/`underlineWidth` are
197
+ // only consumed when style = filled-underline; for `outline` the builder uses
198
+ // surface.default + border.subtle. Themes override this block to switch style.
199
+ field: {
200
+ style: "outline", // outline | filled-underline
201
+ fillBg: "var(--st-semantic-surface-default)",
202
+ underlineColor: "var(--st-semantic-border-strong)",
203
+ underlineWidth: "1px"
204
+ }
205
+ };
206
+
207
+ const semantic = {
208
+ surface: {
209
+ default: foundation.color.slate[0],
210
+ subtle: foundation.color.slate[10],
211
+ raised: foundation.color.slate[0],
212
+ inverse: foundation.color.slate[90],
213
+ overlay: "rgb(15 23 42 / 0.48)"
214
+ },
215
+ text: {
216
+ primary: foundation.color.slate[90],
217
+ secondary: foundation.color.slate[60],
218
+ muted: "#64748b",
219
+ inverse: foundation.color.slate[0],
220
+ link: foundation.color.blue[60]
221
+ },
222
+ border: {
223
+ subtle: foundation.color.slate[20],
224
+ strong: "#94a3b8",
225
+ interactive: foundation.color.blue[60]
226
+ },
227
+ action: {
228
+ primary: foundation.color.blue[60],
229
+ // Darker primary used on :hover (anatomy v1.1.0 — states.hover.bg).
230
+ primaryHover: foundation.color.blue[80],
231
+ primaryText: foundation.color.slate[0],
232
+ secondary: foundation.color.slate[10],
233
+ // Slightly stronger neutral for the secondary :hover surface.
234
+ secondaryHover: foundation.color.slate[20],
235
+ secondaryText: foundation.color.slate[90],
236
+ danger: foundation.color.feedback.error
237
+ },
238
+ feedback: {
239
+ success: foundation.color.feedback.success,
240
+ warning: foundation.color.feedback.warning,
241
+ error: foundation.color.feedback.error,
242
+ info: foundation.color.feedback.info
243
+ },
244
+ status: {
245
+ pending: foundation.color.feedback.warning,
246
+ processing: foundation.color.feedback.info,
247
+ completed: foundation.color.feedback.success,
248
+ failed: foundation.color.feedback.error
249
+ },
250
+ data: {
251
+ category1: "#4E79A7",
252
+ category2: "#F28E2B",
253
+ category3: "#E15759",
254
+ category4: "#76B7B2",
255
+ category5: "#59A14F",
256
+ category6: "#EDC948",
257
+ category7: "#B07AA1",
258
+ category8: "#FF9DA7"
259
+ }
260
+ };
261
+
262
+ // Defaults used when a theme omits an anatomy primitive. These mirror the base
263
+ // foundation so the OUTPUT is always a complete, typed ComponentAnatomy.
264
+ const FALLBACK = {
265
+ borderWidth: { none: "0", thin: "1px", thick: "2px" },
266
+ borderStyle: "solid",
267
+ density: {
268
+ // fontSize per size (v1.1.0): md mirrors the control typography size; sm/lg
269
+ // carry the label scale so the font rides with the control geometry.
270
+ sm: { controlHeight: "2rem", paddingBlock: "0", paddingInline: "0.75rem", gap: "0.375rem", minWidth: "2rem", fontSize: "0.875rem" },
271
+ md: { controlHeight: "2.5rem", paddingBlock: "0", paddingInline: "1rem", gap: "0.5rem", minWidth: "2.5rem", fontSize: "0.9375rem" },
272
+ lg: { controlHeight: "3rem", paddingBlock: "0", paddingInline: "1.25rem", gap: "0.5rem", minWidth: "3rem", fontSize: "1rem" }
273
+ },
274
+ typography: {
275
+ control: { family: "var(--st-font-sans)", size: "0.9375rem", weight: "600", lineHeight: "1.2", letterSpacing: "0", textTransform: "none", textDecoration: "none", decorationThickness: "auto", decorationOffset: "auto" },
276
+ field: { family: "var(--st-font-sans)", size: "1rem", weight: "400", lineHeight: "1.5", letterSpacing: "0", textTransform: "none", textDecoration: "none", decorationThickness: "auto", decorationOffset: "auto" },
277
+ label: { family: "var(--st-font-sans)", size: "0.875rem", weight: "600", lineHeight: "1.4", letterSpacing: "0", textTransform: "none", textDecoration: "none", decorationThickness: "auto", decorationOffset: "auto" },
278
+ // Base/DSFR links are underlined at rest → underline on hover is a no-op.
279
+ // decorationThicknessHover / decorationOffsetHover default to rest metrics so
280
+ // themes that do not animate underline geometry (Sent Tech / Carbon) stay
281
+ // stable without phantom var requirements.
282
+ link: {
283
+ family: "inherit", size: "inherit", weight: "inherit", lineHeight: "inherit", letterSpacing: "0", textTransform: "none",
284
+ textDecoration: "underline", decorationThickness: "auto", decorationOffset: "0.18em",
285
+ decorationThicknessHover: "auto", decorationOffsetHover: "0.18em", textDecorationHover: "underline"
286
+ }
287
+ },
288
+ disabledOpacity: "0.55",
289
+ iconSize: { sm: "1rem", md: "1.125rem", lg: "1.25rem" },
290
+ focus: { strategy: "outline", width: "2px", offset: "2px", color: "var(--st-semantic-border-interactive)", inset: "0" },
291
+ // Field style fallback = boxed outline (base Sent Tech). underlineColor /
292
+ // underlineWidth are inert for outline; they only drive filled-underline.
293
+ // v1.6.0: radiusTop/radiusBottom "" = inherit the theme's shape radius
294
+ // (resolved in fieldOf).
295
+ // v1.4.0: selectAppearance "auto" (native arrow, base unchanged), no chevron,
296
+ // and the prior 2rem right arrow gap.
297
+ field: { style: "outline", selectAppearance: "auto", selectChevron: "none", selectPaddingRight: "2rem" }
298
+ };
299
+ function densityOf(f, size) {
300
+ const base = FALLBACK.density[size];
301
+ const themed = f.density?.[size] ?? {};
302
+ return {
303
+ controlHeight: themed.controlHeight ?? base.controlHeight,
304
+ paddingBlock: themed.paddingBlock ?? base.paddingBlock,
305
+ paddingInline: themed.paddingInline ?? base.paddingInline,
306
+ gap: themed.gap ?? base.gap,
307
+ minWidth: themed.minWidth ?? base.minWidth,
308
+ fontSize: themed.fontSize ?? base.fontSize
309
+ };
310
+ }
311
+ /**
312
+ * Button density (F9): the shared control density for `size`, overlaid with any
313
+ * BUTTON-specific override (`buttonDensity`). Adds an optional `paddingInlineEnd`
314
+ * leaf so a button can be asymmetric (Carbon's large trailing gutter) without
315
+ * touching the shared density the 100%-fidelity fields read. When the theme omits
316
+ * `buttonDensity` (base/DSFR), this is identical to `densityOf` → no change, and
317
+ * `paddingInlineEnd` mirrors `paddingInline` (symmetric).
318
+ */
319
+ function buttonDensityOf(f, size) {
320
+ const base = densityOf(f, size);
321
+ const override = f.buttonDensity?.[size] ?? {};
322
+ const paddingInline = override.paddingInline ?? base.paddingInline;
323
+ return {
324
+ controlHeight: override.controlHeight ?? base.controlHeight,
325
+ paddingBlock: override.paddingBlock ?? base.paddingBlock,
326
+ paddingInline,
327
+ gap: override.gap ?? base.gap,
328
+ minWidth: override.minWidth ?? base.minWidth,
329
+ fontSize: override.fontSize ?? base.fontSize,
330
+ // Trailing inline padding: an explicit asymmetric value (Carbon) or = paddingInline.
331
+ paddingInlineEnd: override.paddingInlineEnd ?? paddingInline
332
+ };
333
+ }
334
+ /**
335
+ * Tabs ACTIVE-tab resolution (F7/F8). Resolves the per-theme selected-tab
336
+ * primitive into a flat, CSS-ready set the Tabs component consumes verbatim.
337
+ * Every leaf DEFAULTS to the prior base render (12px/4px padding, inherited
338
+ * font-size, control weight/line-height, transparent active bg, primary text,
339
+ * BOTTOM indicator) so the base Sent Tech tab is byte-identical; DSFR / Carbon
340
+ * override the real selected-tab metrics. `indicatorSide` resolves into two
341
+ * border-width channels so a theme can put its accent on the top edge (DSFR,
342
+ * which then has NO bottom border) or the bottom edge (base / Carbon).
343
+ */
344
+ function tabsOf(f, controlTypography, indicatorWidth, indicatorColor, activeTextDefault) {
345
+ const t = f.tabs ?? {};
346
+ const indicatorSide = t.indicatorSide ?? "bottom";
347
+ const indicatorMode = t.indicatorMode ?? "border";
348
+ // The indicator lives on ONE edge; the opposite edge collapses to 0 so the
349
+ // active tab matches the reference (DSFR active = border-bottom 0 / top accent).
350
+ const onTop = indicatorSide === "top";
351
+ // "shadow" mode draws the accent as an inset box-shadow so BOTH border sides
352
+ // stay 0 (DSFR, whose real accent is a background-image filet, not a border).
353
+ // "border" mode keeps the real per-side border (base / Carbon) and no shadow.
354
+ const isShadow = indicatorMode === "shadow";
355
+ const shadowOffset = onTop ? indicatorWidth : `-${indicatorWidth}`;
356
+ return {
357
+ activeText: t.activeText || activeTextDefault,
358
+ activeBackground: t.activeBackground || "transparent",
359
+ // G2: resting-tab fill. Default "transparent" (base/Carbon unchanged); DSFR
360
+ // sets a light grey-blue fill so the white active tab reads as raised.
361
+ inactiveBackground: t.inactiveBackground || "transparent",
362
+ activeWeight: t.activeWeight ?? controlTypography.weight,
363
+ paddingBlock: t.paddingBlock ?? "0.75rem",
364
+ paddingInline: t.paddingInline ?? "0.25rem",
365
+ fontSize: t.fontSize ?? "inherit",
366
+ lineHeight: t.lineHeight ?? controlTypography.lineHeight,
367
+ indicatorSide,
368
+ activeBorderTopWidth: isShadow ? "0" : onTop ? indicatorWidth : "0",
369
+ activeBorderBottomWidth: isShadow ? "0" : onTop ? "0" : indicatorWidth,
370
+ activeShadow: isShadow ? `inset 0 ${shadowOffset} 0 0 ${indicatorColor}` : "none"
371
+ };
372
+ }
373
+ /**
374
+ * Button SECONDARY-variant resolution (G1). Resolves the per-theme secondary
375
+ * button surface into a flat, CSS-ready set the Button component consumes
376
+ * verbatim (background / border colour / hover background). Every leaf DEFAULTS
377
+ * to the prior base render (filled `action.secondary`, `border.subtle` stroke,
378
+ * `action.secondaryHover` hover) so the base Sent Tech secondary button is
379
+ * byte-identical; DSFR overrides them to render its OUTLINED secondary button
380
+ * (transparent fill + Bleu France border + light fill on hover).
381
+ */
382
+ function buttonSecondaryOf(semantic, f) {
383
+ const b = f.buttonSecondary ?? {};
384
+ return {
385
+ background: b.background || semantic.action.secondary,
386
+ border: b.border || semantic.border.subtle,
387
+ hoverBackground: b.hoverBackground || semantic.action.secondaryHover || semantic.action.secondary
388
+ };
389
+ }
390
+ /**
391
+ * Pagination resolution (F10). Resolves the per-theme pagination primitive into
392
+ * a flat, CSS-ready set the Pagination component consumes verbatim. Every leaf
393
+ * DEFAULTS to the prior base render (1px subtle stroke, radius.md, 0 block /
394
+ * 12px inline padding, 36px min size, filled action.primary active page,
395
+ * inherited font metrics) so the base Sent Tech pagination is byte-identical.
396
+ * DSFR / Carbon override the real active-page metrics.
397
+ */
398
+ function paginationOf(semantic, f, thin, radiusMd) {
399
+ const p = f.pagination ?? {};
400
+ const border = p.border || semantic.border.subtle;
401
+ const borderWidth = p.borderWidth ?? thin;
402
+ return {
403
+ background: p.background || semantic.surface.default,
404
+ border,
405
+ borderWidth,
406
+ text: p.text || semantic.text.primary,
407
+ radius: p.radius ?? radiusMd,
408
+ activeBackground: p.activeBackground || semantic.action.primary,
409
+ activeText: p.activeText || semantic.action.primaryText,
410
+ // The active page can drop its border (DSFR/Carbon active page has none);
411
+ // default = the resting page border so the base render is unchanged.
412
+ activeBorder: p.activeBorder || border,
413
+ activeBorderWidth: p.activeBorderWidth ?? borderWidth,
414
+ activeWeight: p.activeWeight ?? "inherit",
415
+ disabledText: p.disabledText || semantic.text.muted,
416
+ paddingBlock: p.paddingBlock ?? "0",
417
+ paddingInline: p.paddingInline ?? "0.75rem",
418
+ minSize: p.minSize ?? "2.25rem",
419
+ fontSize: p.fontSize ?? "inherit",
420
+ lineHeight: p.lineHeight ?? "normal",
421
+ letterSpacing: p.letterSpacing ?? "normal"
422
+ };
423
+ }
424
+ /**
425
+ * Breadcrumb resolution (F10). Resolves the per-theme breadcrumb primitive into
426
+ * a flat set the Breadcrumb component consumes verbatim. Every leaf DEFAULTS to
427
+ * the prior base render (link = text.link, no explicit font metrics → inherited
428
+ * 16px / normal, current 600 weight on the current page). DSFR / Carbon pin the
429
+ * real breadcrumb link colour + typography.
430
+ */
431
+ function breadcrumbOf(semantic, f) {
432
+ const b = f.breadcrumb ?? {};
433
+ return {
434
+ text: b.text || semantic.text.secondary,
435
+ linkText: b.linkText || semantic.text.link,
436
+ currentText: b.currentText || semantic.text.primary,
437
+ separator: b.separator || semantic.text.muted,
438
+ fontSize: b.fontSize ?? "inherit",
439
+ lineHeight: b.lineHeight ?? "normal",
440
+ letterSpacing: b.letterSpacing ?? "normal",
441
+ currentWeight: b.currentWeight ?? "600"
442
+ };
443
+ }
444
+ /**
445
+ * Alert resolution (P-B). Resolves the per-theme alert primitive into a flat,
446
+ * CSS-ready set the Alert component consumes verbatim. Every leaf DEFAULTS to the
447
+ * prior base render (surface.raised fill, 1px subtle box on top/right/bottom, a
448
+ * 4px left accent edge, 16px padding all sides, inherited font / `normal`
449
+ * line-height) so the base Sent Tech alert is byte-identical. DSFR drops the box
450
+ * + fill (accent becomes a `::before` filet → left border 0); Carbon paints a
451
+ * dark banner with a 3px coloured left bar (a real border).
452
+ */
453
+ function alertOf(semantic, f, thin, borderStyle) {
454
+ const a = f.alert ?? {};
455
+ // The base box border = 1px subtle on top/right/bottom (the left edge is the
456
+ // accent, sized by accentWidth and coloured per severity by the component).
457
+ const box = `${thin} ${borderStyle} ${semantic.border.subtle}`;
458
+ return {
459
+ background: a.background || semantic.surface.raised,
460
+ text: a.text || semantic.text.primary,
461
+ borderTop: a.borderTop || box,
462
+ borderRight: a.borderRight || box,
463
+ borderBottom: a.borderBottom || box,
464
+ accentWidth: a.accentWidth ?? "0.25rem", // 4px (current)
465
+ filetWidth: a.filetWidth ?? "0", // no filet (base/Carbon use a real left border)
466
+ paddingTop: a.paddingTop ?? "1rem",
467
+ paddingRight: a.paddingRight ?? "1rem",
468
+ paddingBottom: a.paddingBottom ?? "1rem",
469
+ paddingLeft: a.paddingLeft ?? "1rem",
470
+ fontSize: a.fontSize ?? "inherit",
471
+ lineHeight: a.lineHeight ?? "normal",
472
+ letterSpacing: a.letterSpacing ?? "normal",
473
+ accentInfo: a.accentInfo || semantic.feedback.info,
474
+ accentSuccess: a.accentSuccess || semantic.feedback.success,
475
+ accentWarning: a.accentWarning || semantic.feedback.warning,
476
+ accentError: a.accentError || semantic.feedback.error
477
+ };
478
+ }
479
+ /**
480
+ * Accordion resolution (P-B). Resolves the per-theme accordion-trigger primitive
481
+ * into a flat set the Accordion component consumes verbatim. Every leaf DEFAULTS
482
+ * to the prior base render (14px block / 8px inline padding, inherited font-size,
483
+ * weight 600, `normal` line-height, primary text) so the base Sent Tech accordion
484
+ * is byte-identical. DSFR / Carbon pin the real header metrics.
485
+ */
486
+ function accordionOf(semantic, f) {
487
+ const a = f.accordion ?? {};
488
+ return {
489
+ text: a.text || semantic.text.primary,
490
+ paddingBlock: a.paddingBlock ?? "0.875rem", // 14px (current)
491
+ paddingInline: a.paddingInline ?? "0.5rem", // 8px (current)
492
+ fontSize: a.fontSize ?? "inherit",
493
+ fontWeight: a.fontWeight ?? "600",
494
+ lineHeight: a.lineHeight ?? "normal"
495
+ };
496
+ }
497
+ /**
498
+ * Tag resolution (P-C). Resolves the per-theme tag primitive into a flat,
499
+ * CSS-ready set the Tag component consumes verbatim. Every leaf DEFAULTS to the
500
+ * prior base render (pill radius 999px, 4px/10px padding, 12px font, weight 600,
501
+ * line-height 1, no transform, no min-height, NEUTRAL tone = surface.subtle fill
502
+ * + text.secondary text) so the base Sent Tech tag is byte-identical. DSFR /
503
+ * Carbon override the real `.fr-tag` / `.bx--tag` metrics + neutral colours.
504
+ */
505
+ function tagOf(semantic, f) {
506
+ const t = f.tag ?? {};
507
+ return {
508
+ radius: t.radius ?? "999px",
509
+ paddingBlock: t.paddingBlock ?? "0.25rem", // 4px (current md)
510
+ paddingInline: t.paddingInline ?? "0.625rem", // 10px (current md)
511
+ fontSize: t.fontSize ?? "0.75rem", // 12px (current md)
512
+ fontWeight: t.fontWeight ?? "600",
513
+ lineHeight: t.lineHeight ?? "1",
514
+ letterSpacing: t.letterSpacing ?? "normal",
515
+ textTransform: t.textTransform ?? "none",
516
+ minHeight: t.minHeight ?? "0",
517
+ neutralBackground: t.neutralBackground || semantic.surface.subtle,
518
+ neutralText: t.neutralText || semantic.text.secondary
519
+ };
520
+ }
521
+ /**
522
+ * Badge resolution (P-C). Resolves the per-theme badge primitive into a flat,
523
+ * CSS-ready set the Badge component consumes verbatim. Every leaf DEFAULTS to
524
+ * the prior base render (pill radius 999px, 4px/8px padding, 12px font, weight
525
+ * 650, line-height 1, no transform, no min-height; tone colours stay the per-tone
526
+ * feedback mix) so the base Sent Tech badge is byte-identical. DSFR overrides the
527
+ * real `.fr-badge` metrics + recolours the INFO tone (the bench-rendered one) to
528
+ * the measured grey badge.
529
+ */
530
+ function badgeOf(semantic, f) {
531
+ const b = f.badge ?? {};
532
+ return {
533
+ radius: b.radius ?? "999px",
534
+ paddingBlock: b.paddingBlock ?? "0.25rem", // 4px (current)
535
+ paddingInline: b.paddingInline ?? "0.5rem", // 8px (current)
536
+ fontSize: b.fontSize ?? "0.75rem", // 12px (current)
537
+ fontWeight: b.fontWeight ?? "650",
538
+ lineHeight: b.lineHeight ?? "1",
539
+ letterSpacing: b.letterSpacing ?? "normal",
540
+ textTransform: b.textTransform ?? "none",
541
+ minHeight: b.minHeight ?? "0",
542
+ // Default INFO fill reproduces the current `color-mix(... feedback.info 14%,
543
+ // white)`; a theme can replace it with a flat measured colour.
544
+ infoBackground: b.infoBackground || `color-mix(in srgb, ${semantic.feedback.info} 14%, white)`,
545
+ infoText: b.infoText || semantic.feedback.info
546
+ };
547
+ }
548
+ /**
549
+ * Choice (Checkbox/Radio) LABEL resolution (P-D). Resolves the per-theme label
550
+ * typography into a flat, CSS-ready set the `.st-choice__label` consumes
551
+ * verbatim. Every leaf DEFAULTS to the prior base render (15px font, `normal`
552
+ * line-height + letter-spacing, primary text) so the base Sent Tech
553
+ * checkbox/radio is byte-identical. `radioLineHeight` defaults to the checkbox
554
+ * line-height (single value) so a theme that does not split them stays
555
+ * consistent; Carbon splits 18px (checkbox) / 20px (radio).
556
+ */
557
+ function choiceOf(semantic, f) {
558
+ const c = f.choice ?? {};
559
+ const labelLineHeight = c.labelLineHeight ?? "normal";
560
+ return {
561
+ labelFontSize: c.labelFontSize ?? "0.9375rem", // 15px (current)
562
+ labelLineHeight,
563
+ radioLineHeight: c.radioLineHeight ?? labelLineHeight,
564
+ labelLetterSpacing: c.labelLetterSpacing ?? "normal",
565
+ labelColor: c.labelColor || semantic.text.primary
566
+ };
567
+ }
568
+ /**
569
+ * Search FIELD resolution (P-D). Resolves the per-theme search-box padding +
570
+ * input typography into a flat set the `.st-search` consumes verbatim. Every
571
+ * leaf DEFAULTS to the prior base render (0 padding on the wrapper, inherited
572
+ * 16px / `normal` typography) so the base Sent Tech search is byte-identical.
573
+ * The field box (fill / borders / radius) is the shared field anatomy, unchanged.
574
+ */
575
+ function searchOf(f) {
576
+ const s = f.search ?? {};
577
+ const paddingInline = s.paddingInline ?? "0";
578
+ return {
579
+ paddingBlock: s.paddingBlock ?? "0",
580
+ paddingInline: paddingInline,
581
+ // v1.6.0 (additive): left/right wrapper padding. Preserve backward
582
+ // compatibility by defaulting each side to the single paddingInline value.
583
+ paddingLeft: s.paddingLeft ?? paddingInline,
584
+ paddingRight: s.paddingRight ?? paddingInline,
585
+ fontSize: s.fontSize ?? "1rem", // 16px (current inherited)
586
+ lineHeight: s.lineHeight ?? "normal",
587
+ letterSpacing: s.letterSpacing ?? "normal"
588
+ };
589
+ }
590
+ /**
591
+ * Toggle / Switch resolution (P-D). Resolves the per-theme track geometry +
592
+ * colours + label typography into a flat set the Toggle/Switch components
593
+ * consume verbatim. Every leaf DEFAULTS to the prior base render (pill radius
594
+ * 999px, 2px inner padding, 36×20 md track, 16px md thumb, border.strong resting
595
+ * track, action.primary checked track, surface.default thumb, inherited `normal`
596
+ * typography, primary text) so the base Sent Tech toggle is byte-identical.
597
+ */
598
+ function toggleOf(semantic, f) {
599
+ const t = f.toggle ?? {};
600
+ return {
601
+ trackRadius: t.trackRadius ?? "999px",
602
+ trackPadding: t.trackPadding ?? "0.125rem", // 2px (current)
603
+ trackWidth: t.trackWidth ?? "2.25rem", // 36px (current md)
604
+ trackHeight: t.trackHeight ?? "1.25rem", // 20px (current md)
605
+ thumbSize: t.thumbSize ?? "1rem", // 16px (current md)
606
+ trackColor: t.trackColor || semantic.border.strong,
607
+ trackCheckedColor: t.trackCheckedColor || semantic.action.primary,
608
+ thumbColor: t.thumbColor || semantic.surface.default,
609
+ fontSize: t.fontSize ?? "inherit",
610
+ lineHeight: t.lineHeight ?? "normal",
611
+ letterSpacing: t.letterSpacing ?? "normal",
612
+ textColor: t.textColor || semantic.text.primary
613
+ };
614
+ }
615
+ /**
616
+ * SelectableRow / SelectableList SELECTED-item resolution (UAT8). Resolves the
617
+ * per-theme selected-item colours into a flat, CSS-ready set both components
618
+ * consume verbatim. Every leaf DEFAULTS to a value derived from this theme's own
619
+ * `action.primary`:
620
+ * - selectedBackground: a calm 12% tint of the primary over the surface.
621
+ * - selectedAccent: the primary itself (opt-in left accent bar).
622
+ * - selectedText: a DARKER accent (`primary 78% + black`) that clears
623
+ * ≥ 7:1 on the tint for every light theme (the plain primary only reaches
624
+ * ~4.2–4.9:1, sub/at-AA). Verified via WCAG relative luminance.
625
+ * The `*Dark` leaves carry dark-mode-safe variants (text LIGHTENED, tint over a
626
+ * dark surface) so a future dark theme inherits accessible values automatically.
627
+ * `color-mix(in oklch, …)` keeps the derivation perceptually-uniform; the
628
+ * components ship a flat fallback + an `@supports` guard for engines without it.
629
+ */
630
+ function selectableRowOf(semantic, f) {
631
+ const s = f.selectableRow ?? {};
632
+ const primary = semantic.action.primary;
633
+ return {
634
+ selectedBackground: s.selectedBackground || `color-mix(in oklch, ${primary} 12%, transparent)`,
635
+ selectedAccent: s.selectedAccent || primary,
636
+ // Darker accent for ≥ 7:1 on the tint (vs ~4.2–4.9:1 for the plain primary).
637
+ selectedText: s.selectedText || `color-mix(in oklch, ${primary} 78%, black)`,
638
+ // Dark-mode variants: a slightly stronger tint + a LIGHTENED text so the
639
+ // accent stays legible on a dark tinted surface.
640
+ selectedBackgroundDark: s.selectedBackgroundDark || `color-mix(in oklch, ${primary} 22%, transparent)`,
641
+ selectedAccentDark: s.selectedAccentDark || primary,
642
+ selectedTextDark: s.selectedTextDark || `color-mix(in oklch, ${primary} 70%, white)`
643
+ };
644
+ }
645
+ function typographyOf(f, role) {
646
+ // Widen to TypographyAnatomy so the optional textDecorationHover leaf is
647
+ // readable across all roles (only `link` carries it in the FALLBACK literal).
648
+ const base = FALLBACK.typography[role];
649
+ const themed = f.typography?.[role] ?? {};
650
+ return {
651
+ family: themed.family ?? base.family,
652
+ size: themed.size ?? base.size,
653
+ weight: themed.weight ?? base.weight,
654
+ lineHeight: themed.lineHeight ?? base.lineHeight,
655
+ letterSpacing: themed.letterSpacing ?? base.letterSpacing,
656
+ textTransform: themed.textTransform ?? base.textTransform,
657
+ textDecoration: themed.textDecoration ?? base.textDecoration,
658
+ decorationThickness: themed.decorationThickness ?? base.decorationThickness,
659
+ decorationOffset: themed.decorationOffset ?? base.decorationOffset,
660
+ decorationThicknessHover: themed.decorationThicknessHover ?? base.decorationThicknessHover,
661
+ decorationOffsetHover: themed.decorationOffsetHover ?? base.decorationOffsetHover,
662
+ textDecorationHover: themed.textDecorationHover ?? base.textDecorationHover
663
+ };
664
+ }
665
+ function focusOf(f) {
666
+ const base = FALLBACK.focus;
667
+ const themed = f.focus ?? {};
668
+ const strategy = (themed.strategy ?? base.strategy);
669
+ const width = themed.width ?? base.width;
670
+ const offset = themed.offset ?? base.offset;
671
+ const color = themed.color ?? base.color;
672
+ const inset = themed.inset ?? base.inset;
673
+ // The focus STRATEGY is resolved here into the two CSS channels a shared
674
+ // mixin needs: `outline` and `boxShadow`. A component applies BOTH on
675
+ // :focus-visible; the strategy decides which is "live" (the other is a no-op).
676
+ // This is what lets DSFR (offset outline) and Carbon (inset box-shadow) differ
677
+ // by *technique*, not just values, while staying 100% token-driven.
678
+ let outline = "none";
679
+ let boxShadow = "none";
680
+ switch (strategy) {
681
+ case "outline":
682
+ // DSFR-like: native outline, offset away from the box.
683
+ outline = `${width} solid ${color}`;
684
+ break;
685
+ case "ring":
686
+ // Base default: a soft ring drawn just outside the box.
687
+ boxShadow = `0 0 0 ${width} ${color}`;
688
+ break;
689
+ case "inset":
690
+ // Carbon: a ring drawn INSIDE the box via inset box-shadow.
691
+ boxShadow = `inset 0 0 0 ${width} ${color}`;
692
+ break;
693
+ case "double":
694
+ // Two-tone accessibility ring (outer color + inner light gap).
695
+ outline = `${width} solid ${color}`;
696
+ boxShadow = `0 0 0 ${inset} #ffffff`;
697
+ break;
698
+ }
699
+ return { strategy, width, offset, color, inset, outline, boxShadow };
700
+ }
701
+ /**
702
+ * Resolves the FIELD STYLE (v1.2.0) into a fully-typed FieldAnatomy: a fill
703
+ * background + four per-side border shorthands a component applies verbatim.
704
+ *
705
+ * - `outline` (default, base Sent Tech): `fillBg = surface.default`, the four
706
+ * borders all = `<borderWidth.thin> solid <border.subtle>`. This reproduces
707
+ * the existing boxed input EXACTLY → no Sent Tech regression.
708
+ * - `filled-underline` (DSFR / Carbon): `fillBg` = the theme's field fill tone,
709
+ * top/right/left = `none`, the bottom rule is the only stroke. HOW the bottom
710
+ * rule is drawn depends on the theme's real technique:
711
+ * · DSFR (`underlineAsShadow: true`) draws it as a `box-shadow inset` (its
712
+ * real CSS), so `borderBottom` is `none` and the rule adds no box height.
713
+ * · Carbon (default) genuinely uses a real `border-bottom: 1px solid` — so
714
+ * we keep the geometric `borderBottom` and leave `underline` = `none` to
715
+ * stay pixel-identical to the official `.bx--text-input`.
716
+ *
717
+ * v1.3.0 (additive): `radiusTop` rounds only the field's TOP corners (defaults
718
+ * to the theme's `shapeRadius` so a boxed field stays uniform — no regression);
719
+ * `underline` carries the filled-underline bottom rule as an inset box-shadow
720
+ * when `underlineAsShadow` is set; `focusShadow` composes it with the focus ring.
721
+ */
722
+ function fieldOf(semantic, f, bw, borderStyle, shapeRadius, focusBoxShadow) {
723
+ const themed = f.field ?? {};
724
+ const style = (themed.style ?? FALLBACK.field.style);
725
+ const thin = bw.thin ?? "1px";
726
+ // v1.4.0 (F5/F9) — native <select> rendering. These are independent of the
727
+ // outline/filled-underline branch, so resolve them once and spread into each
728
+ // returned FieldAnatomy. selectAppearance "auto" keeps the base native arrow
729
+ // (and its UA-forced `line-height: normal`); "none" lets the anatomy
730
+ // line-height take effect, the chevron then drawn by selectChevron.
731
+ const selectAppearance = themed.selectAppearance ?? FALLBACK.field.selectAppearance;
732
+ const selectChevron = themed.selectChevron ?? FALLBACK.field.selectChevron;
733
+ const selectPaddingRight = themed.selectPaddingRight ?? FALLBACK.field.selectPaddingRight;
734
+ const selectLeaves = { selectAppearance, selectChevron, selectPaddingRight };
735
+ // Top corners inherit the theme's shape radius unless the theme rounds them
736
+ // explicitly (DSFR field = 4px top). Bottom corners default to shapeRadius.
737
+ const radiusTop = themed.radiusTop || shapeRadius;
738
+ const radiusBottom = themed.radiusBottom || shapeRadius;
739
+ // Compose the field focus box-shadow so the resting underline is never lost
740
+ // incoherently: an outline-strategy theme (focusBoxShadow === "none") keeps
741
+ // the underline; an inset/ring theme stacks its ring + the underline.
742
+ const composeFocus = (underline) => {
743
+ const ring = focusBoxShadow && focusBoxShadow !== "none" ? focusBoxShadow : "";
744
+ if (underline === "none")
745
+ return ring || "none";
746
+ if (!ring)
747
+ return underline; // outline theme: keep the underline at focus
748
+ return `${ring}, ${underline}`; // inset/ring theme: ring + underline
749
+ };
750
+ if (style === "filled-underline") {
751
+ const fillBg = themed.fillBg || semantic.surface.subtle;
752
+ const underlineColor = themed.underlineColor || semantic.border.strong;
753
+ const underlineWidth = themed.underlineWidth || thin;
754
+ // DSFR draws the rule as an inset box-shadow (its real technique, cf. rule
755
+ // `underline-hardcoded-border`); Carbon keeps a real geometric border-bottom
756
+ // (its real technique) so it stays pixel-identical to `.bx--text-input`.
757
+ if (themed.underlineMode === "shadow") {
758
+ const underline = `inset 0 -${underlineWidth} 0 0 ${underlineColor}`;
759
+ return {
760
+ style,
761
+ fillBg,
762
+ borderTop: "none",
763
+ borderRight: "none",
764
+ borderBottom: "none",
765
+ borderLeft: "none",
766
+ radiusTop,
767
+ radiusBottom,
768
+ underline,
769
+ focusShadow: composeFocus(underline),
770
+ ...selectLeaves
771
+ };
772
+ }
773
+ return {
774
+ style,
775
+ fillBg,
776
+ borderTop: "none",
777
+ borderRight: "none",
778
+ borderBottom: `${underlineWidth} ${borderStyle} ${underlineColor}`,
779
+ borderLeft: "none",
780
+ radiusTop,
781
+ radiusBottom,
782
+ underline: "none",
783
+ focusShadow: composeFocus("none"),
784
+ ...selectLeaves
785
+ };
786
+ }
787
+ // outline (default): boxed, 4 equal borders — identical to the prior look.
788
+ const fillBg = themed.fillBg || semantic.surface.default;
789
+ const border = `${thin} ${borderStyle} ${semantic.border.subtle}`;
790
+ return {
791
+ style: "outline",
792
+ fillBg,
793
+ borderTop: border,
794
+ borderRight: border,
795
+ borderBottom: border,
796
+ borderLeft: border,
797
+ radiusTop,
798
+ radiusBottom,
799
+ underline: "none",
800
+ focusShadow: composeFocus("none"),
801
+ ...selectLeaves
802
+ };
803
+ }
804
+ /**
805
+ * Construit la couche `component` à partir d'un `semantic` et d'un `foundation`
806
+ * donnés. Les rôles composant sont CÂBLÉS sur les rôles semantic/foundation du
807
+ * thème appelant — c'est ce qui permet à un thème (DSFR, Carbon, forge…) de
808
+ * propager sa marque jusqu'aux composants. Réutiliser un `component` figé sur
809
+ * une autre base rendrait les composants inertes au changement de thème.
810
+ */
811
+ function createComponent(semantic, foundation) {
812
+ const bw = { ...FALLBACK.borderWidth, ...(foundation.borderWidth ?? {}) };
813
+ const borderStyle = foundation.borderStyle?.solid ?? FALLBACK.borderStyle;
814
+ const disabledOpacity = foundation.disabledOpacity ?? FALLBACK.disabledOpacity;
815
+ ({ ...(foundation.cursor ?? {}) });
816
+ const icon = { ...FALLBACK.iconSize, ...(foundation.iconSize ?? {}) };
817
+ const focus = focusOf(foundation);
818
+ // Anatomy block for the 5 pilot components. Typed against ComponentAnatomy so
819
+ // a theme cannot drift the *shape*; values resolve from this theme's own
820
+ // foundation (radius/density/typography/focus) → the brand reaches anatomy.
821
+ const buttonAnatomy = {
822
+ shape: { radius: foundation.radius.md, borderWidth: bw.thin, borderStyle },
823
+ // F9: button-specific density (shared control density + optional button-only
824
+ // override) so Carbon's tall, asymmetric primary button doesn't regress the
825
+ // fields that share the control density. Base/DSFR get the shared density.
826
+ density: { sm: buttonDensityOf(foundation, "sm"), md: buttonDensityOf(foundation, "md"), lg: buttonDensityOf(foundation, "lg") },
827
+ typography: typographyOf(foundation, "control"),
828
+ focus,
829
+ icon: { size: icon.md, gap: densityOf(foundation, "md").gap },
830
+ states: {
831
+ // v1.1.0: hover bg sourced from the semantic layer (primaryHover, with a
832
+ // fallback to primary so a theme that omits it stays inert, not blank).
833
+ hover: { bg: semantic.action.primaryHover ?? semantic.action.primary, transform: "none" },
834
+ active: { transform: "none" },
835
+ disabled: { opacity: disabledOpacity }
836
+ }
837
+ };
838
+ const inputAnatomy = {
839
+ shape: { radius: foundation.radius.md, borderWidth: bw.thin, borderStyle },
840
+ density: { sm: densityOf(foundation, "sm"), md: densityOf(foundation, "md"), lg: densityOf(foundation, "lg") },
841
+ typography: typographyOf(foundation, "field"),
842
+ focus,
843
+ // Field style (v1.2.0): outline (boxed, base) vs filled-underline (DSFR/Carbon).
844
+ // v1.3.0: radiusTop defaults to the field's own shape radius (radius.md);
845
+ // the underline is an inset box-shadow, composed with the focus ring.
846
+ field: fieldOf(semantic, foundation, bw, borderStyle, foundation.radius.md, focus.boxShadow),
847
+ states: {
848
+ hover: { border: semantic.border.strong },
849
+ focus: { border: semantic.border.interactive },
850
+ // Inputs convey disabled via bg + text, not via global dimming.
851
+ disabled: { bg: semantic.surface.subtle, text: semantic.text.muted }
852
+ }
853
+ };
854
+ const linkTypography = typographyOf(foundation, "link");
855
+ const linkAnatomy = {
856
+ shape: { radius: foundation.radius.sm ?? foundation.radius.md, borderWidth: bw.none, borderStyle },
857
+ typography: linkTypography,
858
+ focus,
859
+ states: {
860
+ // v1.1.0: hover decoration sourced from the link role typography
861
+ // (textDecorationHover). DSFR/base = underline (no-op vs rest); Carbon
862
+ // goes none → underline on hover. Fallback to underline if a theme omits.
863
+ hover: { text: semantic.action.primary, decoration: linkTypography.textDecorationHover ?? "underline" },
864
+ disabled: { text: semantic.text.muted, decoration: "none", opacity: disabledOpacity }
865
+ }
866
+ };
867
+ // Card surface (additive): borderWidth defaults to the base `thin` stroke so
868
+ // Sent Tech is unchanged; DSFR/Carbon set it to 0 (their cards/tiles have no
869
+ // border). The fill defaults to surface.raised (base), Carbon overrides it to
870
+ // its $layer-01 tone via `card.background`.
871
+ const cardBorderWidth = foundation.card?.borderWidth ?? bw.thin;
872
+ const cardBackground = foundation.card?.background || semantic.surface.raised;
873
+ const cardHoverBackground = foundation.card?.hoverBackground || cardBackground;
874
+ // F5 (additive): the card body typography. The base `.st-card` carries NO
875
+ // explicit font-size / line-height / letter-spacing, so the defaults here
876
+ // REPRODUCE that exact render (inherit / normal / normal). DSFR/Carbon pin
877
+ // their real tile body metrics so the card text matches the measured
878
+ // reference instead of `normal`. Family/weight stay on the field role (no
879
+ // visible change — the card already inherits the brand sans + 400).
880
+ const cardTypographyBase = typographyOf(foundation, "field");
881
+ const cardTypography = {
882
+ ...cardTypographyBase,
883
+ size: foundation.card?.fontSize ?? "inherit",
884
+ lineHeight: foundation.card?.lineHeight ?? "normal",
885
+ letterSpacing: foundation.card?.letterSpacing ?? "normal"
886
+ };
887
+ const cardAnatomy = {
888
+ shape: { radius: foundation.radius.lg, borderWidth: cardBorderWidth, borderStyle },
889
+ typography: cardTypography,
890
+ focus,
891
+ states: {
892
+ hover: {
893
+ bg: cardHoverBackground,
894
+ transform: "translateY(-1px)"
895
+ }
896
+ }
897
+ };
898
+ const tabsControlTypography = typographyOf(foundation, "control");
899
+ const tabsAnatomy = {
900
+ shape: { radius: foundation.radius.none ?? "0", borderWidth: bw.thin, borderStyle },
901
+ density: { md: densityOf(foundation, "md") },
902
+ typography: tabsControlTypography,
903
+ focus,
904
+ states: {
905
+ hover: { text: semantic.text.primary },
906
+ disabled: { opacity: disabledOpacity }
907
+ }
908
+ };
909
+ // F7/F8 — active-tab metrics (additive, per-theme; base render unchanged).
910
+ // The indicator width is the tab's stroke width (thin); `tabsOf` resolves it
911
+ // onto the top edge (DSFR) or the bottom edge (base/Carbon), as a real border
912
+ // (base/Carbon) or an inset box-shadow accent (DSFR).
913
+ const tabsResolved = tabsOf(foundation, tabsControlTypography, bw.thin, semantic.action.primary, semantic.text.primary);
914
+ // G1 — secondary button surface (per theme; base render unchanged). DSFR
915
+ // overrides it to a transparent fill + Bleu France border + text; base/Carbon
916
+ // keep the filled neutral default.
917
+ const buttonSecondary = buttonSecondaryOf(semantic, foundation);
918
+ // F10 — Pagination / Breadcrumb anatomy (per theme; base render unchanged).
919
+ const paginationResolved = paginationOf(semantic, foundation, bw.thin, foundation.radius.md);
920
+ const breadcrumbResolved = breadcrumbOf(semantic, foundation);
921
+ // P-B — Alert / Accordion anatomy (per theme; base render unchanged).
922
+ const alertResolved = alertOf(semantic, foundation, bw.thin, borderStyle);
923
+ const accordionResolved = accordionOf(semantic, foundation);
924
+ // P-C — Tag / Badge anatomy (per theme; base render unchanged).
925
+ const tagResolved = tagOf(semantic, foundation);
926
+ const badgeResolved = badgeOf(semantic, foundation);
927
+ // P-D — Choice (Checkbox/Radio) / Search / Toggle anatomy (per theme; base
928
+ // render unchanged via the resolver defaults).
929
+ const choiceResolved = choiceOf(semantic, foundation);
930
+ const searchResolved = searchOf(foundation);
931
+ const toggleResolved = toggleOf(semantic, foundation);
932
+ // UAT8 — SelectableRow / SelectableList selected-item colours (per theme;
933
+ // derived from action.primary via the resolver defaults).
934
+ const selectableRowResolved = selectableRowOf(semantic, foundation);
935
+ return {
936
+ button: {
937
+ radius: foundation.radius.md,
938
+ primaryBackground: semantic.action.primary,
939
+ primaryText: semantic.action.primaryText,
940
+ // G1: the secondary surface is resolved per theme (transparent + bordered
941
+ // for DSFR's outlined secondary; filled neutral for base/Carbon).
942
+ secondaryBackground: buttonSecondary.background,
943
+ secondaryBorder: buttonSecondary.border,
944
+ secondaryHoverBackground: buttonSecondary.hoverBackground,
945
+ secondaryText: semantic.action.secondaryText,
946
+ anatomy: buttonAnatomy
947
+ },
948
+ link: {
949
+ text: semantic.text.link,
950
+ hoverText: semantic.action.primary,
951
+ disabledText: semantic.text.muted,
952
+ focusRing: semantic.border.interactive,
953
+ anatomy: linkAnatomy
954
+ },
955
+ alert: {
956
+ // Existing leaves keep their names (consumers/docs unchanged); background
957
+ // and text now resolve through `alertOf` (identical defaults = unchanged base).
958
+ background: alertResolved.background,
959
+ text: alertResolved.text,
960
+ border: semantic.border.subtle,
961
+ // Per-severity accent colours now resolve through `alertOf` (default = the
962
+ // matching feedback role → base unchanged; Carbon overrides accentInfo).
963
+ infoBorder: alertResolved.accentInfo,
964
+ successBorder: alertResolved.accentSuccess,
965
+ warningBorder: alertResolved.accentWarning,
966
+ errorBorder: alertResolved.accentError,
967
+ radius: foundation.radius.lg,
968
+ // P-B additive leaves — per-theme alert anatomy (base = unchanged).
969
+ borderTop: alertResolved.borderTop,
970
+ borderRight: alertResolved.borderRight,
971
+ borderBottom: alertResolved.borderBottom,
972
+ // Left accent: a real left border of `accentWidth` (base/Carbon) OR a
973
+ // `::before` filet of `filetWidth` drawn inside the box (DSFR) so the
974
+ // measured left border stays 0. Both coloured per severity by the component.
975
+ accentWidth: alertResolved.accentWidth,
976
+ filetWidth: alertResolved.filetWidth,
977
+ paddingTop: alertResolved.paddingTop,
978
+ paddingRight: alertResolved.paddingRight,
979
+ paddingBottom: alertResolved.paddingBottom,
980
+ paddingLeft: alertResolved.paddingLeft,
981
+ fontSize: alertResolved.fontSize,
982
+ lineHeight: alertResolved.lineHeight,
983
+ letterSpacing: alertResolved.letterSpacing
984
+ },
985
+ accordion: {
986
+ // P-B — per-theme accordion-trigger anatomy (base = unchanged).
987
+ text: accordionResolved.text,
988
+ paddingBlock: accordionResolved.paddingBlock,
989
+ paddingInline: accordionResolved.paddingInline,
990
+ fontSize: accordionResolved.fontSize,
991
+ fontWeight: accordionResolved.fontWeight,
992
+ lineHeight: accordionResolved.lineHeight
993
+ },
994
+ tag: {
995
+ // P-C — per-theme tag anatomy (base = unchanged via resolver defaults).
996
+ radius: tagResolved.radius,
997
+ paddingBlock: tagResolved.paddingBlock,
998
+ paddingInline: tagResolved.paddingInline,
999
+ fontSize: tagResolved.fontSize,
1000
+ fontWeight: tagResolved.fontWeight,
1001
+ lineHeight: tagResolved.lineHeight,
1002
+ letterSpacing: tagResolved.letterSpacing,
1003
+ textTransform: tagResolved.textTransform,
1004
+ minHeight: tagResolved.minHeight,
1005
+ neutralBackground: tagResolved.neutralBackground,
1006
+ neutralText: tagResolved.neutralText
1007
+ },
1008
+ badge: {
1009
+ // P-C — per-theme badge anatomy (base = unchanged via resolver defaults).
1010
+ radius: badgeResolved.radius,
1011
+ paddingBlock: badgeResolved.paddingBlock,
1012
+ paddingInline: badgeResolved.paddingInline,
1013
+ fontSize: badgeResolved.fontSize,
1014
+ fontWeight: badgeResolved.fontWeight,
1015
+ lineHeight: badgeResolved.lineHeight,
1016
+ letterSpacing: badgeResolved.letterSpacing,
1017
+ textTransform: badgeResolved.textTransform,
1018
+ minHeight: badgeResolved.minHeight,
1019
+ infoBackground: badgeResolved.infoBackground,
1020
+ infoText: badgeResolved.infoText
1021
+ },
1022
+ card: {
1023
+ background: cardBackground,
1024
+ border: semantic.border.subtle,
1025
+ radius: foundation.radius.lg,
1026
+ shadow: foundation.shadow.subtle,
1027
+ anatomy: cardAnatomy
1028
+ },
1029
+ menu: {
1030
+ background: semantic.surface.raised,
1031
+ border: semantic.border.subtle,
1032
+ text: semantic.text.primary,
1033
+ itemHoverBackground: semantic.surface.subtle,
1034
+ dangerText: semantic.feedback.error,
1035
+ dangerHoverBackground: "rgba(185, 28, 28, 0.08)",
1036
+ disabledText: semantic.text.muted,
1037
+ radius: foundation.radius.md,
1038
+ shadow: foundation.shadow.medium
1039
+ },
1040
+ popover: {
1041
+ background: semantic.surface.raised,
1042
+ border: semantic.border.subtle,
1043
+ text: semantic.text.primary,
1044
+ shadow: foundation.shadow.floating,
1045
+ radius: foundation.radius.lg,
1046
+ zIndex: foundation.z.overlay
1047
+ },
1048
+ dropdown: {
1049
+ background: semantic.surface.default,
1050
+ border: semantic.border.subtle,
1051
+ text: semantic.text.primary,
1052
+ optionHoverBackground: semantic.surface.subtle,
1053
+ selectedBackground: semantic.action.primary,
1054
+ selectedText: semantic.action.primaryText,
1055
+ radius: foundation.radius.md,
1056
+ shadow: foundation.shadow.medium
1057
+ },
1058
+ input: {
1059
+ background: semantic.surface.default,
1060
+ border: semantic.border.subtle,
1061
+ focusRing: semantic.border.interactive,
1062
+ radius: foundation.radius.md
1063
+ },
1064
+ field: {
1065
+ labelText: semantic.text.primary,
1066
+ helpText: semantic.text.secondary,
1067
+ errorText: semantic.feedback.error,
1068
+ gap: foundation.spacing[2],
1069
+ maxWidth: "28rem",
1070
+ labelTypography: typographyOf(foundation, "label")
1071
+ },
1072
+ control: {
1073
+ background: semantic.surface.default,
1074
+ text: semantic.text.primary,
1075
+ placeholderText: semantic.text.muted,
1076
+ border: semantic.border.subtle,
1077
+ hoverBorder: semantic.border.strong,
1078
+ hoverBackground: semantic.surface.subtle,
1079
+ focusRing: semantic.border.interactive,
1080
+ invalidBorder: semantic.feedback.error,
1081
+ disabledBackground: semantic.surface.subtle,
1082
+ disabledText: semantic.text.muted,
1083
+ radius: foundation.radius.md,
1084
+ smHeight: "2rem",
1085
+ mdHeight: "2.5rem",
1086
+ lgHeight: "3rem",
1087
+ anatomy: inputAnatomy
1088
+ },
1089
+ selection: {
1090
+ checkedBackground: semantic.action.primary,
1091
+ checkedText: semantic.action.primaryText,
1092
+ border: semantic.border.subtle,
1093
+ // Existing switch colour leaves now resolve through `toggleOf` (identical
1094
+ // defaults = unchanged base; DSFR/Carbon set their measured track colours).
1095
+ switchTrack: toggleResolved.trackColor,
1096
+ switchTrackChecked: toggleResolved.trackCheckedColor,
1097
+ switchThumb: toggleResolved.thumbColor,
1098
+ // P-D additive — Choice (Checkbox/Radio) label typography (base = unchanged).
1099
+ choiceLabelFontSize: choiceResolved.labelFontSize,
1100
+ choiceLabelLineHeight: choiceResolved.labelLineHeight,
1101
+ choiceRadioLineHeight: choiceResolved.radioLineHeight,
1102
+ choiceLabelLetterSpacing: choiceResolved.labelLetterSpacing,
1103
+ choiceLabelColor: choiceResolved.labelColor,
1104
+ // P-D additive — Toggle/Switch track geometry + label typography (base =
1105
+ // unchanged via the resolver defaults).
1106
+ toggleTrackRadius: toggleResolved.trackRadius,
1107
+ toggleTrackPadding: toggleResolved.trackPadding,
1108
+ toggleTrackWidth: toggleResolved.trackWidth,
1109
+ toggleTrackHeight: toggleResolved.trackHeight,
1110
+ toggleThumbSize: toggleResolved.thumbSize,
1111
+ toggleFontSize: toggleResolved.fontSize,
1112
+ toggleLineHeight: toggleResolved.lineHeight,
1113
+ toggleLetterSpacing: toggleResolved.letterSpacing,
1114
+ toggleTextColor: toggleResolved.textColor
1115
+ },
1116
+ search: {
1117
+ // P-D additive — Search field box padding + input typography (base =
1118
+ // unchanged via the resolver defaults). The field box (fill/border/radius)
1119
+ // stays the shared `control.anatomy.field` already mapped like Input.
1120
+ paddingBlock: searchResolved.paddingBlock,
1121
+ paddingInline: searchResolved.paddingInline,
1122
+ paddingLeft: searchResolved.paddingLeft,
1123
+ paddingRight: searchResolved.paddingRight,
1124
+ fontSize: searchResolved.fontSize,
1125
+ lineHeight: searchResolved.lineHeight,
1126
+ letterSpacing: searchResolved.letterSpacing
1127
+ },
1128
+ overlay: {
1129
+ backdrop: semantic.surface.overlay,
1130
+ surface: semantic.surface.raised,
1131
+ border: semantic.border.subtle,
1132
+ shadow: foundation.shadow.floating,
1133
+ radius: foundation.radius.lg,
1134
+ zIndex: foundation.z.modal
1135
+ },
1136
+ drawer: {
1137
+ backdrop: semantic.surface.overlay,
1138
+ surface: semantic.surface.raised,
1139
+ border: semantic.border.subtle,
1140
+ shadow: foundation.shadow.floating,
1141
+ width: "24rem",
1142
+ zIndex: foundation.z.modal
1143
+ },
1144
+ emptyState: {
1145
+ background: semantic.surface.subtle,
1146
+ border: semantic.border.subtle,
1147
+ titleText: semantic.text.primary,
1148
+ messageText: semantic.text.secondary,
1149
+ radius: foundation.radius.lg
1150
+ },
1151
+ loadingState: {
1152
+ indicator: semantic.action.primary,
1153
+ track: semantic.surface.subtle,
1154
+ text: semantic.text.secondary,
1155
+ radius: foundation.radius.pill
1156
+ },
1157
+ tooltip: {
1158
+ background: semantic.surface.inverse,
1159
+ text: semantic.text.inverse,
1160
+ radius: foundation.radius.md,
1161
+ shadow: foundation.shadow.medium,
1162
+ zIndex: foundation.z.overlay
1163
+ },
1164
+ toast: {
1165
+ background: semantic.surface.raised,
1166
+ text: semantic.text.primary,
1167
+ border: semantic.border.subtle,
1168
+ shadow: foundation.shadow.floating,
1169
+ radius: foundation.radius.lg,
1170
+ infoBorder: semantic.feedback.info,
1171
+ successBorder: semantic.feedback.success,
1172
+ warningBorder: semantic.feedback.warning,
1173
+ errorBorder: semantic.feedback.error,
1174
+ zIndex: foundation.z.toast
1175
+ },
1176
+ dataTable: {
1177
+ headerBackground: semantic.surface.subtle,
1178
+ rowBackground: semantic.surface.default,
1179
+ rowHoverBackground: semantic.surface.subtle,
1180
+ border: semantic.border.subtle,
1181
+ text: semantic.text.primary,
1182
+ captionText: semantic.text.secondary,
1183
+ radius: foundation.radius.lg
1184
+ },
1185
+ tabs: {
1186
+ // F7/F8: active-tab roles/metrics resolved per theme (base render = current).
1187
+ activeText: tabsResolved.activeText,
1188
+ activeBackground: tabsResolved.activeBackground,
1189
+ // G2: resting-tab fill (default transparent; DSFR = light grey-blue).
1190
+ inactiveBackground: tabsResolved.inactiveBackground,
1191
+ activeWeight: tabsResolved.activeWeight,
1192
+ inactiveText: semantic.text.secondary,
1193
+ border: semantic.border.subtle,
1194
+ indicator: semantic.action.primary,
1195
+ panelBackground: semantic.surface.default,
1196
+ tabPaddingBlock: tabsResolved.paddingBlock,
1197
+ tabPaddingInline: tabsResolved.paddingInline,
1198
+ tabFontSize: tabsResolved.fontSize,
1199
+ tabLineHeight: tabsResolved.lineHeight,
1200
+ activeBorderTopWidth: tabsResolved.activeBorderTopWidth,
1201
+ activeBorderBottomWidth: tabsResolved.activeBorderBottomWidth,
1202
+ activeShadow: tabsResolved.activeShadow,
1203
+ anatomy: tabsAnatomy
1204
+ },
1205
+ pagination: {
1206
+ // Existing leaves keep their names (consumers/docs unchanged); they now
1207
+ // resolve through `paginationOf` (identical defaults = unchanged base).
1208
+ background: paginationResolved.background,
1209
+ border: paginationResolved.border,
1210
+ text: paginationResolved.text,
1211
+ activeBackground: paginationResolved.activeBackground,
1212
+ activeText: paginationResolved.activeText,
1213
+ disabledText: paginationResolved.disabledText,
1214
+ radius: paginationResolved.radius,
1215
+ // F10 additive leaves — per-theme active-page metrics (base = unchanged).
1216
+ borderWidth: paginationResolved.borderWidth,
1217
+ activeBorder: paginationResolved.activeBorder,
1218
+ activeBorderWidth: paginationResolved.activeBorderWidth,
1219
+ activeWeight: paginationResolved.activeWeight,
1220
+ paddingBlock: paginationResolved.paddingBlock,
1221
+ paddingInline: paginationResolved.paddingInline,
1222
+ minSize: paginationResolved.minSize,
1223
+ fontSize: paginationResolved.fontSize,
1224
+ lineHeight: paginationResolved.lineHeight,
1225
+ letterSpacing: paginationResolved.letterSpacing
1226
+ },
1227
+ paginationNav: {
1228
+ // Phase 2 aliases for the ellipsis/previous/next pagination control. These
1229
+ // mirror `pagination` where the visual role is identical and use the shared
1230
+ // control hover colour for the interactive page/nav buttons.
1231
+ background: paginationResolved.background,
1232
+ border: paginationResolved.border,
1233
+ radius: paginationResolved.radius,
1234
+ text: paginationResolved.text,
1235
+ hoverBackground: semantic.surface.subtle,
1236
+ activeBackground: paginationResolved.activeBackground,
1237
+ activeText: paginationResolved.activeText,
1238
+ disabledText: paginationResolved.disabledText,
1239
+ ellipsisText: semantic.text.muted
1240
+ },
1241
+ breadcrumb: {
1242
+ text: breadcrumbResolved.text,
1243
+ currentText: breadcrumbResolved.currentText,
1244
+ separator: breadcrumbResolved.separator,
1245
+ linkText: breadcrumbResolved.linkText,
1246
+ // F10 additive leaves — per-theme breadcrumb typography (base = unchanged).
1247
+ fontSize: breadcrumbResolved.fontSize,
1248
+ lineHeight: breadcrumbResolved.lineHeight,
1249
+ letterSpacing: breadcrumbResolved.letterSpacing,
1250
+ currentWeight: breadcrumbResolved.currentWeight
1251
+ },
1252
+ sideNav: {
1253
+ background: semantic.surface.default,
1254
+ border: semantic.border.subtle,
1255
+ itemText: semantic.text.secondary,
1256
+ activeBackground: semantic.surface.subtle,
1257
+ activeText: semantic.text.primary,
1258
+ width: "16rem"
1259
+ },
1260
+ selectableRow: {
1261
+ // UAT8 — selected list-item colours (tinted surface + accent bar + a
1262
+ // contrast-safe darker text), derived from action.primary per theme.
1263
+ selectedBackground: selectableRowResolved.selectedBackground,
1264
+ selectedAccent: selectableRowResolved.selectedAccent,
1265
+ selectedText: selectableRowResolved.selectedText,
1266
+ // Dark-mode-safe variants (emitted now so a future dark theme inherits them).
1267
+ selectedBackgroundDark: selectableRowResolved.selectedBackgroundDark,
1268
+ selectedAccentDark: selectableRowResolved.selectedAccentDark,
1269
+ selectedTextDark: selectableRowResolved.selectedTextDark
1270
+ },
1271
+ chat: {
1272
+ userBubbleBackground: semantic.action.primary,
1273
+ userBubbleText: semantic.action.primaryText,
1274
+ assistantBubbleBackground: semantic.surface.subtle,
1275
+ assistantBubbleText: semantic.text.primary,
1276
+ composerSurface: semantic.surface.raised,
1277
+ toolCallSurface: semantic.surface.subtle
1278
+ },
1279
+ graph: {
1280
+ panelBackground: semantic.surface.inverse,
1281
+ panelText: semantic.text.inverse,
1282
+ edgeDefault: "rgb(226 232 240 / 0.56)",
1283
+ community1: semantic.data.category1,
1284
+ community2: semantic.data.category2,
1285
+ community3: semantic.data.category3,
1286
+ community4: semantic.data.category4
1287
+ }
1288
+ };
1289
+ }
1290
+ const component = createComponent(semantic, foundation);
1291
+
1292
+ function flattenTokens(tree, prefix = []) {
1293
+ const output = {};
1294
+ for (const [key, value] of Object.entries(tree)) {
1295
+ if (value === undefined)
1296
+ continue;
1297
+ const path = [...prefix, key];
1298
+ if (typeof value === "object" && value !== null) {
1299
+ Object.assign(output, flattenTokens(value, path));
1300
+ }
1301
+ else {
1302
+ output[path.join("-")] = value;
1303
+ }
1304
+ }
1305
+ return output;
1306
+ }
1307
+ function toCssVariables(tree, selector = ":root", namespace = "st") {
1308
+ const entries = Object.entries(flattenTokens(tree));
1309
+ const lines = [];
1310
+ for (const [name, value] of entries) {
1311
+ lines.push(` --${namespace}-${name}: ${String(value)};`);
1312
+ // Les composants consomment la couche foundation sous des noms COURTS
1313
+ // (--st-font-sans, --st-radius-md, --st-spacing-2, --st-shadow-medium,
1314
+ // --st-motion-fast, --st-z-*…) tandis que l'arbre l'expose sous
1315
+ // --st-foundation-*. On émet donc aussi l'alias court pour que la
1316
+ // typographie / le radius / l'espacement / le motion suivent le thème.
1317
+ // Additif : les noms pleinement qualifiés --st-foundation-* restent émis.
1318
+ if (name.startsWith("foundation-")) {
1319
+ const short = name.slice("foundation-".length);
1320
+ lines.push(` --${namespace}-${short}: ${String(value)};`);
1321
+ // Alias z-index : la couche `z` est consommée comme --st-zindex-*.
1322
+ if (short.startsWith("z-")) {
1323
+ lines.push(` --${namespace}-zindex-${short.slice(2)}: ${String(value)};`);
1324
+ }
1325
+ }
1326
+ }
1327
+ return `${selector} {\n${lines.join("\n")}\n}\n`;
1328
+ }
1329
+
1330
+ function html(value) {
1331
+ var html2 = String(value ?? "");
1332
+ var open = "<!---->";
1333
+ return open + html2 + "<!---->";
1334
+ }
1335
+ function assertTenantTheme(input) {
1336
+ if (!input || typeof input !== "object") {
1337
+ throw new Error("Theme must be an object");
1338
+ }
1339
+ const theme = input;
1340
+ if (typeof theme.id !== "string" || theme.id.trim().length === 0) {
1341
+ throw new Error("Theme id is required");
1342
+ }
1343
+ if (typeof theme.label !== "string" || theme.label.trim().length === 0) {
1344
+ throw new Error("Theme label is required");
1345
+ }
1346
+ if (theme.mode !== "light" && theme.mode !== "dark") {
1347
+ throw new Error("Theme mode must be light or dark");
1348
+ }
1349
+ if (!theme.tokens || typeof theme.tokens !== "object") {
1350
+ throw new Error("Theme tokens are required");
1351
+ }
1352
+ }
1353
+ function compileTheme(input, options = {}) {
1354
+ assertTenantTheme(input);
1355
+ const theme = input;
1356
+ const selector = options.selector ?? `[data-st-theme="${theme.id}"]`;
1357
+ const namespace = options.namespace ?? "st";
1358
+ return toCssVariables(theme.tokens, selector, namespace);
1359
+ }
1360
+ const sentTechTheme = {
1361
+ id: "sent-tech",
1362
+ label: "Sent Tech",
1363
+ mode: "light",
1364
+ tokens: {
1365
+ foundation,
1366
+ semantic,
1367
+ component
1368
+ }
1369
+ };
1370
+ const entropicSemantic = {
1371
+ ...semantic,
1372
+ action: {
1373
+ ...semantic.action,
1374
+ primary: "oklch(50% 0.134 242.749)",
1375
+ primaryText: semantic.action.primaryText
1376
+ }
1377
+ };
1378
+ const entropicComponent = createComponent(entropicSemantic, foundation);
1379
+ const entropicTheme = {
1380
+ id: "entropic",
1381
+ label: "Entropic",
1382
+ mode: "light",
1383
+ tokens: {
1384
+ foundation,
1385
+ semantic: entropicSemantic,
1386
+ component: {
1387
+ ...entropicComponent,
1388
+ chat: {
1389
+ ...entropicComponent.chat,
1390
+ composerSurface: entropicSemantic.surface.raised,
1391
+ toolCallSurface: entropicSemantic.surface.subtle
1392
+ }
1393
+ }
1394
+ }
1395
+ };
1396
+ function ThemeProvider($$renderer, $$props) {
1397
+ $$renderer.component(($$renderer2) => {
1398
+ let { theme = sentTechTheme, namespace = "st", children } = $$props;
1399
+ let css = derived(() => compileTheme(theme, { selector: `[data-st-theme="${theme.id}"]`, namespace }));
1400
+ head("1lrpa8r", $$renderer2, ($$renderer3) => {
1401
+ $$renderer3.push(`${html(`<style data-st-theme-provider="${theme.id}">${css()}</style>`)}`);
1402
+ });
1403
+ $$renderer2.push(`<div${attr("data-st-theme", theme.id)}>`);
1404
+ children?.($$renderer2);
1405
+ $$renderer2.push(`<!----></div>`);
1406
+ });
1407
+ }
1408
+ function _layout($$renderer, $$props) {
1409
+ let { children } = $$props;
1410
+ ThemeProvider($$renderer, {
1411
+ theme: entropicTheme,
1412
+ children: ($$renderer2) => {
1413
+ children($$renderer2);
1414
+ $$renderer2.push(`<!---->`);
1415
+ }
1416
+ });
1417
+ }
1418
+
1419
+ export { _layout as default };
1420
+ //# sourceMappingURL=_layout.svelte.js-BoLsbuC2.js.map