flet-box 1.1.2 → 1.1.3

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 (154) hide show
  1. package/AGENTS_FOR_CODE_GEN.md +284 -0
  2. package/CONTRIBUTING.md +238 -0
  3. package/README.md +2 -2
  4. package/bin/cli.js +88 -1
  5. package/bin/commands/auditFix.js +181 -27
  6. package/bin/commands/checkApi.js +14 -3
  7. package/bin/commands/checkStructure.js +368 -0
  8. package/bin/commands/checkSyntax.js +293 -0
  9. package/bin/commands/checkUi.js +666 -14
  10. package/bin/commands/create.js +14 -0
  11. package/bin/commands/createBundle.js +2 -2
  12. package/bin/commands/metrics.js +448 -0
  13. package/bin/commands/release.js +18 -0
  14. package/bin/commands/report.js +849 -0
  15. package/bin/commands/tools.js +76 -0
  16. package/bin/commands/unpack.js +39 -11
  17. package/bin/commands/verify.js +879 -0
  18. package/bin/mcp.js +280 -0
  19. package/bin/utils/contract.js +27 -0
  20. package/bin/utils/dtsProps.js +5 -1
  21. package/bin/utils/migrationGuide.js +146 -0
  22. package/bin/utils/moduleExports.js +9 -5
  23. package/bin/utils/templates.js +225 -18
  24. package/bin/utils/toolkit.js +191 -0
  25. package/docs/README.md +5 -1
  26. package/docs/arquitectura.md +1 -1
  27. package/docs/cli/README.md +168 -2
  28. package/docs/guides/README.md +6 -0
  29. package/docs/guides/accessibility.md +2 -1
  30. package/docs/guides/ai-toolkit.md +166 -0
  31. package/docs/guides/compatibility.md +1 -1
  32. package/docs/guides/migration.md +92 -0
  33. package/docs/guides/minification-and-protection.md +2 -1
  34. package/docs/guides/mobile-and-platforms.md +1 -1
  35. package/docs/guides/online-updates.md +2 -0
  36. package/docs/server/deployment.md +1 -1
  37. package/docs/tools/styling.md +2 -2
  38. package/docs/tools/theme.md +139 -4
  39. package/docs/widget/Accordion.md +74 -8
  40. package/docs/widget/AdMob.md +68 -3
  41. package/docs/widget/AdSense.md +67 -2
  42. package/docs/widget/AlertDialog.md +1 -1
  43. package/docs/widget/AnimatedBox.md +80 -39
  44. package/docs/widget/AnimatedText.md +74 -18
  45. package/docs/widget/Audio.md +73 -14
  46. package/docs/widget/Avatar.md +69 -4
  47. package/docs/widget/Badge.md +68 -3
  48. package/docs/widget/BottomSheet.md +2 -2
  49. package/docs/widget/Button.md +69 -3
  50. package/docs/widget/Card.md +69 -4
  51. package/docs/widget/Carousel.md +67 -2
  52. package/docs/widget/Chart.md +67 -2
  53. package/docs/widget/Checkbox.md +74 -8
  54. package/docs/widget/Chip.md +68 -3
  55. package/docs/widget/CircularBar.md +74 -15
  56. package/docs/widget/CircularChart.md +71 -10
  57. package/docs/widget/CodeViewer.md +67 -2
  58. package/docs/widget/Column.md +68 -3
  59. package/docs/widget/Container.md +68 -3
  60. package/docs/widget/DataTable.md +67 -2
  61. package/docs/widget/Divider.md +67 -2
  62. package/docs/widget/DraggBox.md +67 -2
  63. package/docs/widget/Dropdown.md +76 -10
  64. package/docs/widget/DroppBox.md +67 -2
  65. package/docs/widget/FloatingActionButton.md +68 -3
  66. package/docs/widget/GridView.md +67 -2
  67. package/docs/widget/Icon.md +71 -5
  68. package/docs/widget/Image.md +68 -3
  69. package/docs/widget/Input.md +68 -3
  70. package/docs/widget/Inspector.md +2 -2
  71. package/docs/widget/InstallButton.md +73 -13
  72. package/docs/widget/ListTile.md +69 -3
  73. package/docs/widget/ListView.md +67 -2
  74. package/docs/widget/Maps.md +68 -3
  75. package/docs/widget/Markdown.md +74 -15
  76. package/docs/widget/MatrixRain.md +72 -11
  77. package/docs/widget/Menu.md +209 -0
  78. package/docs/widget/Modal.md +3 -3
  79. package/docs/widget/Pagination.md +68 -3
  80. package/docs/widget/ParallaxBox.md +72 -12
  81. package/docs/widget/ProgressBar.md +69 -4
  82. package/docs/widget/QRCode.md +67 -2
  83. package/docs/widget/README.md +54 -50
  84. package/docs/widget/Radio.md +76 -10
  85. package/docs/widget/Rating.md +70 -6
  86. package/docs/widget/Row.md +68 -3
  87. package/docs/widget/Skeleton.md +68 -3
  88. package/docs/widget/Slider.md +75 -9
  89. package/docs/widget/SnackBar.md +2 -2
  90. package/docs/widget/Stack.md +68 -3
  91. package/docs/widget/Stepper.md +68 -3
  92. package/docs/widget/Switch.md +74 -8
  93. package/docs/widget/Text.md +71 -4
  94. package/docs/widget/ThemePicker.md +197 -0
  95. package/docs/widget/Tooltip.md +72 -7
  96. package/docs/widget/TreeView.md +70 -10
  97. package/docs/widget/Video.md +71 -10
  98. package/package.json +10 -3
  99. package/skills/fletbox-design/SKILL.md +89 -5
  100. package/skills/fletbox-motion/SKILL.md +1 -1
  101. package/skills/fletbox-rules/SKILL.md +56 -4
  102. package/src/core/runApp.js +12 -0
  103. package/src/index.d.ts +374 -101
  104. package/src/index.js +9 -0
  105. package/src/navigations/AdaptiveScaffold.js +2 -2
  106. package/src/navigations/BottomNavigation.js +1 -1
  107. package/src/navigations/Scaffold.js +18 -6
  108. package/src/tools/useState.js +11 -4
  109. package/src/utils/colors.js +275 -0
  110. package/src/utils/errorBoundary.js +288 -0
  111. package/src/utils/index.js +10 -2
  112. package/src/utils/stackPosition.js +39 -24
  113. package/src/utils/syntaxHighlight.js +4 -1
  114. package/src/utils/themes.js +279 -3
  115. package/src/widget-factory/addChildren.js +17 -10
  116. package/src/widget-factory/assignProps.js +18 -6
  117. package/src/widget-factory/createWidget.js +7 -0
  118. package/src/widget-factory/deprecations.js +77 -0
  119. package/src/widget-factory/translateProps.js +11 -1
  120. package/src/widget-factory/warnings.js +1 -27
  121. package/src/widget-factory/widgetFactory.js +59 -17
  122. package/src/widgets/Accordion.js +21 -7
  123. package/src/widgets/Badge.js +11 -2
  124. package/src/widgets/Button.js +15 -3
  125. package/src/widgets/Carousel.js +12 -4
  126. package/src/widgets/Chart.js +7 -2
  127. package/src/widgets/Checkbox.js +1 -2
  128. package/src/widgets/Chip.js +24 -10
  129. package/src/widgets/CircularBar.js +9 -3
  130. package/src/widgets/CircularChart.js +5 -3
  131. package/src/widgets/Container.js +6 -0
  132. package/src/widgets/Divider.js +3 -1
  133. package/src/widgets/DraggBox.js +9 -0
  134. package/src/widgets/Dropdown.js +3 -3
  135. package/src/widgets/DroppBox.js +5 -0
  136. package/src/widgets/FloatingActionButton.js +1 -1
  137. package/src/widgets/Icon.js +5 -1
  138. package/src/widgets/InstallButton.js +4 -2
  139. package/src/widgets/Markdown.js +171 -103
  140. package/src/widgets/Menu.js +340 -0
  141. package/src/widgets/Modal.js +7 -1
  142. package/src/widgets/Pagination.js +10 -5
  143. package/src/widgets/ProgressBar.js +5 -4
  144. package/src/widgets/QRCode.js +4 -0
  145. package/src/widgets/Radio.js +1 -2
  146. package/src/widgets/Rating.js +9 -0
  147. package/src/widgets/Slider.js +4 -1
  148. package/src/widgets/SnackBar.js +2 -2
  149. package/src/widgets/Switch.js +1 -2
  150. package/src/widgets/Text.js +9 -5
  151. package/src/widgets/ThemePicker.js +234 -0
  152. package/src/widgets/TreeView.js +2 -1
  153. package/src/widgets/index.js +2 -0
  154. package/docs/CONTRIBUTING.md +0 -156
package/src/index.js CHANGED
@@ -146,6 +146,13 @@ export {
146
146
  colors,
147
147
  setTheme,
148
148
  registerPalette,
149
+ registerAccent,
150
+ setAccent,
151
+ applyAccentPreference,
152
+ getAccent,
153
+ getAccentSurfaces,
154
+ listAccents,
155
+ listAccentRamps,
149
156
  getTheme,
150
157
  toggleTheme,
151
158
  subscribeTheme,
@@ -225,6 +232,7 @@ export {
225
232
  AlertDialog,
226
233
  FloatingActionButton,
227
234
  Stepper,
235
+ ThemePicker,
228
236
  Skeleton,
229
237
  CodeViewer,
230
238
  Inspector,
@@ -240,6 +248,7 @@ export {
240
248
  DataTable,
241
249
  Carousel,
242
250
  Tooltip,
251
+ Menu,
243
252
  InstallButton,
244
253
  Video,
245
254
  Audio,
@@ -133,9 +133,9 @@ export const AdaptiveScaffold = (props) => {
133
133
  widthCollapsed: 50,
134
134
  bgColor: colors.surface,
135
135
  // FIX: CollapsibleSideBar aplica `borderRight` por defecto (default
136
- // `1px solid ${colors.border}`), así que un sidebar derecho acababa
136
+ // a right sidebar would end up with a border that was
137
137
  // con doble borde: `borderRight` (heredado del default) + `borderLeft`
138
- // (el que pedimos aquí). Forzamos `borderRight: "none"` para que solo
138
+ // (the one we requested here). We force `borderRight: "none"` so only
139
139
  // tenga el borde izquierdo (el que da al contenido central).
140
140
  borderRight: "none",
141
141
  borderLeft: `1px solid ${colors.border}`,
@@ -238,7 +238,7 @@ export const BottomNavigation = (props) => {
238
238
  if (showLabels && item.label) {
239
239
  // FIX: `value` es una prop `special` que `applySpecial` solo maneja
240
240
  // en <input>/<textarea>. En un <span> no hace nada → el label quedaba
241
- // vacío. La prop correcta para contenido de texto es `text`.
241
+ // empty. The correct prop for text content is `text`.
242
242
  labelWidget = Text({
243
243
  text: item.label,
244
244
  size: 12,
@@ -16,7 +16,7 @@ import { initRouter, getCurrentRoute, subscribe } from "./Router.js";
16
16
  * @property {HTMLElement|Function} [rightNavBar] - Persistent right sidebar widget or factory function.
17
17
  * @property {number} [rightNavBarWidth=260] - Width of the right sidebar in pixels.
18
18
  * @property {HTMLElement|Function} [navSideBar] - Legacy alias for leftNavBar (deprecated).
19
- * @property {number} [navSideBarWidth=260] - Legacy alias for leftNavBarWidth (deprecated).
19
+ * @property {number} [navSideBarWidth=260] - Width of the legacy navSideBar, on whichever side navSideBarPosition picks.
20
20
  * @property {"left"|"right"} [navSideBarPosition="left"] - Legacy: which side navSideBar renders on.
21
21
  * @property {Object} [routes] - Route map object for router mode (path → component).
22
22
  * @property {string} [basePath] - Deployment prefix, e.g. "/flet-box-docs". Auto-detected when omitted.
@@ -94,14 +94,20 @@ export const Scaffold = (props) => {
94
94
  initRouter(routesConfig, currentUrl, { base: basePath });
95
95
  }
96
96
 
97
- // Resolve the legacy navSideBar prop to the appropriate side.
97
+ // Resolve the legacy navSideBar prop to the appropriate side, and carry its
98
+ // own width with it: reading the position but not the width made
99
+ // `navSideBarWidth` a documented no-op that always painted 260.
98
100
  let finalLeftNavBar = leftNavBar;
99
101
  let finalRightNavBar = rightNavBar;
102
+ let finalLeftWidth = leftNavBarWidth;
103
+ let finalRightWidth = rightNavBarWidth;
100
104
  if (navSideBar && !leftNavBar && !rightNavBar) {
101
105
  if (navSideBarPosition === "right") {
102
106
  finalRightNavBar = navSideBar;
107
+ finalRightWidth = navSideBarWidth;
103
108
  } else {
104
109
  finalLeftNavBar = navSideBar;
110
+ finalLeftWidth = navSideBarWidth;
105
111
  }
106
112
  }
107
113
 
@@ -223,7 +229,13 @@ export const Scaffold = (props) => {
223
229
  // the release of the handler it adds, which a hand-written
224
230
  // `addEventListener` would leak.
225
231
  menuIcon.removeAttribute("aria-hidden");
226
- menuIcon.setAttribute("aria-label", "Open menu");
232
+ // The app's name wins. This is a fallback for an icon nobody named —
233
+ // not a rename — and the shell cannot know the app's language, so
234
+ // writing over an explicit label turned "Open the menu" into
235
+ // "Open menu" for every reader on a Spanish screen.
236
+ if (!menuIcon.hasAttribute("aria-label")) {
237
+ menuIcon.setAttribute("aria-label", "Open menu");
238
+ }
227
239
  applyEffects(menuIcon);
228
240
  }
229
241
  }
@@ -295,7 +307,7 @@ export const Scaffold = (props) => {
295
307
  if (widget) {
296
308
  leftNavBarContainer.appendChild(widget);
297
309
  if (mainContentContainer)
298
- mainContentContainer.style.width = `calc(100% - ${toREM(leftNavBarWidth)})`;
310
+ mainContentContainer.style.width = `calc(100% - ${toREM(finalLeftWidth)})`;
299
311
  }
300
312
  };
301
313
 
@@ -409,7 +421,7 @@ export const Scaffold = (props) => {
409
421
 
410
422
  if (finalLeftNavBar) {
411
423
  leftNavBarContainer = document.createElement("div");
412
- leftNavBarContainer.style.width = toREM(leftNavBarWidth);
424
+ leftNavBarContainer.style.width = toREM(finalLeftWidth);
413
425
  leftNavBarContainer.style.flexShrink = "0";
414
426
  leftNavBarContainer.style.overflow = "auto";
415
427
  leftNavBarContainer.style.height = "100%";
@@ -429,7 +441,7 @@ export const Scaffold = (props) => {
429
441
 
430
442
  if (finalRightNavBar) {
431
443
  rightNavBarContainer = document.createElement("div");
432
- rightNavBarContainer.style.width = toREM(rightNavBarWidth);
444
+ rightNavBarContainer.style.width = toREM(finalRightWidth);
433
445
  rightNavBarContainer.style.flexShrink = "0";
434
446
  rightNavBarContainer.style.overflow = "auto";
435
447
  rightNavBarContainer.style.height = "100%";
@@ -1,5 +1,5 @@
1
1
  // src/tools/useState.js
2
- import { saveRam, getRam, subscribeRam } from "../services/RamStore.js";
2
+ import { saveRam, getRam, subscribeRam, hasRam } from "../services/RamStore.js";
3
3
  import {
4
4
  createReactiveValue,
5
5
  isStructuralRead,
@@ -57,6 +57,10 @@ const ensureRamDispatcher = () => {
57
57
  ramDispatcherReady = true;
58
58
 
59
59
  subscribeRam((changedKey, newValue, oldValue) => {
60
+ // A save that stores the value the key already had is not a change. Patching
61
+ // every bound widget and re-running a structural render for it is how a
62
+ // redundant `saveRam(key, same)` became an endless rebuild loop.
63
+ if (changedKey !== null && newValue === oldValue) return;
60
64
  const callbacks = subscribers.get(changedKey);
61
65
  // A key nobody ever passed to `useState` has no listeners: ignore it.
62
66
  if (!callbacks) return;
@@ -157,9 +161,12 @@ export const registerWidget = (key, widget, propName) => {
157
161
  * onCount((newVal) => console.log('Counter changed to', newVal));
158
162
  */
159
163
  export const useState = (key, initialState, widget = null, propName = null) => {
160
- // Initialize the state in RamStore if it does not exist yet
161
- const existing = getRam(key);
162
- if (existing === undefined || existing === null) {
164
+ // Initialize the state in RamStore if it does not exist yet. `hasRam` is the
165
+ // test, not `getRam`: that reader answers `null` both for a missing key and
166
+ // for a key whose value IS null, so a `null` state was re-seeded on every
167
+ // construction — and when the component had read it structurally, the save
168
+ // re-rendered the app, which re-seeded it again, forever.
169
+ if (!hasRam(key)) {
163
170
  saveRam(
164
171
  key,
165
172
  typeof initialState === "function" ? initialState() : initialState,
@@ -0,0 +1,275 @@
1
+ // src/utils/colors.js
2
+ // The framework's selectable accents: ten hue families an app can offer a reader
3
+ // without designing a palette of its own.
4
+ //
5
+ // This file is a token file (`checkUi`'s `isTokenFile`), which is why the hexes
6
+ // live here and not in the widget that paints them: an app that writes
7
+ // `color: "#0f766e"` has hard-coded a brand, and the clean-UI gate says so. A
8
+ // preset table is the same information in the one place the theme can move it.
9
+ //
10
+ // ## A family is a ramp, and the accent is a stop on it
11
+ //
12
+ // Each family carries five stops per theme, dark-to-light in `light` and
13
+ // light-to-dark in `dark`. The accent is never the same index in both themes:
14
+ //
15
+ // light theme → the deepest stop (index 0), which carries white words
16
+ // dark theme → the second stop (index 1), which carries the page's own ink
17
+ //
18
+ // That is the design skill's pair rule (35) — lift the lightness and drop the
19
+ // saturation going dark — applied to a brand instead of a surface. The middle
20
+ // stops of a scale are the ones designers reach for in a mockup and the ones
21
+ // that fail here: #3b82f6 puts white at 3.15:1 and #eab308 reads on a white page
22
+ // at 1.88:1, so a "blue button" copied straight from a palette card is either an
23
+ // unreadable label or an invisible link. Index 0 and index 1 are what survives.
24
+ //
25
+ // ## The veto
26
+ //
27
+ // Seven numbers decide whether a stop ships, in each theme, and the guard in
28
+ // `tests/regression.test.js` re-measures all of them on every run:
29
+ //
30
+ // 1. the accent's own ink on the accent ≥ 4.5 (a button label)
31
+ // 2. the accent read as words on the page ≥ 4.5 (a link, a tab)
32
+ // 3. the accent read as a glyph on a card ≥ 3 (an icon, a rim)
33
+ // 4. the page's ink on the family's wash ≥ 4.5 (a selected row)
34
+ // 5. the page's secondary ink on that wash ≥ 4.5 (its caption)
35
+ // 6. the accent on its own wash ≥ 4.5 (a chip)
36
+ // 7. the wash against the page 1.04–1.5 (a wash, not a plate)
37
+ //
38
+ // Number 7 is why the band is asymmetric in practice: on a white page a hue reads
39
+ // as a tint at 1.10, but on a black page the same family needs up to 1.48 of lift
40
+ // before the eye calls it coloured at all. Both ends of that band are real; a wash
41
+ // over 1.5 stops being a wash and starts competing with a card.
42
+ //
43
+ // Number 6 is the one that bites, and it is what a *predefined* set is for.
44
+ // Yellow's deep stop #a16207 sits 4.42:1 on its own wash #fef3c7 — a chip
45
+ // labelled in yellow fails AA by 0.08 — so that family takes the scale's next step
46
+ // down. Six more families were measured out of this table for the other failure a
47
+ // set can have: marine, violet, coral and amber each duplicate another family's
48
+ // accent in one of the two themes, and a picker with two squares of the same color
49
+ // is not a choice. Adding a family means passing all seven, in both themes, and
50
+ // landing somewhere a neighbour already does not.
51
+
52
+ import { color } from "../tools/color.js";
53
+ import { readableInk } from "./readableInk.js";
54
+
55
+ /**
56
+ * The ink that reads on an accent plate.
57
+ *
58
+ * `readableInk` answers pure black or white; a bright plate in the dark theme is
59
+ * labelled with the dark palette's own page ink instead, because that is what the
60
+ * framework already does for its near-white `primary` and a second black in the
61
+ * system would be one more value to keep in sync.
62
+ *
63
+ * @param {string} primary - The accent's hex.
64
+ * @returns {string} `#ffffff` or `#09090b`.
65
+ */
66
+ export const onAccent = (primary) =>
67
+ readableInk(primary, "#ffffff") === "#ffffff"
68
+ ? "#ffffff"
69
+ : "#09090b";
70
+
71
+ /**
72
+ * Which stop of a family is the accent, and which is its wash, per theme.
73
+ * `light` counts from the deep end and `dark` from the bright end, so index 0 is
74
+ * the deepest stop of one and the brightest of the other.
75
+ */
76
+ const STOPS = {
77
+ light: { primary: 0, tint: 3 },
78
+ dark: { primary: 1, tint: 4 },
79
+ };
80
+
81
+ // How far a wash is pushed from an accent that arrived without a ramp — a brand
82
+ // registered as a pair of hexes by an app that has a color and not a scale. Light
83
+ // lifts it almost to the page, dark drops it almost to the page; both measure
84
+ // 1.04–1.35 against their own background, which is the band a wash has to live
85
+ // in (deep enough to read as a plate, pale enough not to compete with one).
86
+ const WASH_LIGHTEN = 90;
87
+ const WASH_DARKEN = 82;
88
+
89
+ /**
90
+ * The families, in the order a picker should show them: the neutral one first,
91
+ * then the cool end of the wheel, then the warm, then the rose-purples that
92
+ * close it. `label` is the control's accessible name.
93
+ *
94
+ * @type {Array<{id: string, label: string, light: string[], dark: string[]}>}
95
+ */
96
+ export const ACCENT_FAMILIES = [
97
+ {
98
+ id: "neutral",
99
+ label: "Neutral",
100
+ light: ["#0f172a", "#475569", "#94a3b8", "#e2e8f0", "#f8fafc"],
101
+ dark: ["#f8fafc", "#cbd5e1", "#64748b", "#334155", "#0f172a"],
102
+ },
103
+ {
104
+ id: "blue",
105
+ label: "Blue",
106
+ light: ["#1e3a8a", "#3b82f6", "#93c5fd", "#dbeafe", "#eff6ff"],
107
+ dark: ["#bfdbfe", "#60a5fa", "#3b82f6", "#1d4ed8", "#0b1220"],
108
+ },
109
+ {
110
+ id: "indigo",
111
+ label: "Indigo",
112
+ light: ["#3730a3", "#6366f1", "#a5b4fc", "#e0e7ff", "#eef2ff"],
113
+ dark: ["#a5b4fc", "#818cf8", "#6366f1", "#3730a3", "#1e1b4b"],
114
+ },
115
+ {
116
+ id: "purple",
117
+ label: "Purple",
118
+ light: ["#6d28d9", "#a855f7", "#c084fc", "#ede9fe", "#f5f3ff"],
119
+ dark: ["#ddd6fe", "#a78bfa", "#8b5cf6", "#6d28d9", "#2e1065"],
120
+ },
121
+ {
122
+ id: "pink",
123
+ label: "Pink",
124
+ light: ["#be185d", "#ec4899", "#f9a8d4", "#fce7f3", "#fdf2f8"],
125
+ dark: ["#f9a8d4", "#f472b6", "#ec4899", "#be185d", "#500724"],
126
+ },
127
+ {
128
+ id: "rose",
129
+ label: "Rose",
130
+ light: ["#9f1239", "#f43f5e", "#fda4af", "#ffe4e6", "#fff1f2"],
131
+ dark: ["#fecdd3", "#fb7185", "#f43f5e", "#be123c", "#4c0519"],
132
+ },
133
+ {
134
+ id: "red",
135
+ label: "Red",
136
+ light: ["#991b1b", "#ef4444", "#fca5a5", "#fee2e2", "#fef2f2"],
137
+ dark: ["#fecaca", "#f87171", "#ef4444", "#b91c1c", "#450a0a"],
138
+ },
139
+ {
140
+ id: "orange",
141
+ label: "Orange",
142
+ light: ["#c2410c", "#f97316", "#fdba74", "#ffedd5", "#fff7ed"],
143
+ dark: ["#fed7aa", "#fb923c", "#f97316", "#c2410c", "#431407"],
144
+ },
145
+ {
146
+ id: "yellow",
147
+ label: "Yellow",
148
+ // The one family whose deep stop is not deep enough: #a16207 measures 4.42:1
149
+ // on its own wash, so a chip labelled in it fails AA by 0.08. The accent takes
150
+ // the scale's next step down and the ramp keeps the rest.
151
+ light: ["#713f12", "#eab308", "#fde68a", "#fef3c7", "#fffbe8"],
152
+ dark: ["#fde68a", "#facc15", "#eab308", "#a16207", "#422006"],
153
+ },
154
+ {
155
+ id: "green",
156
+ label: "Green",
157
+ light: ["#166534", "#22c55e", "#86efac", "#dcfce7", "#f0fdf4"],
158
+ dark: ["#bbf7d0", "#4ade80", "#22c55e", "#15803d", "#052e16"],
159
+ },
160
+ {
161
+ id: "teal",
162
+ label: "Teal",
163
+ light: ["#0f766e", "#14b8a6", "#99f6e4", "#ccfbf1", "#f0fdfa"],
164
+ dark: ["#99f6e4", "#2dd4bf", "#14b8a6", "#0f766e", "#042f2e"],
165
+ },
166
+ {
167
+ id: "cyan",
168
+ label: "Cyan",
169
+ light: ["#155e75", "#06b6d4", "#67e8f9", "#cffafe", "#ecfeff"],
170
+ dark: ["#a5f3fc", "#22d3ee", "#06b6d4", "#0e7490", "#083344"],
171
+ },
172
+ ];
173
+
174
+ /**
175
+ * The three tokens one accent writes into one theme's palette.
176
+ *
177
+ * @param {string} primary - The accent hex for that theme.
178
+ * @param {"light"|"dark"} theme - Which end of the pair this is.
179
+ * @param {string[]} [ramp] - The family's stops, when the accent came from one.
180
+ * @returns {{ primary: string, onPrimary: string, primaryBg: string }}
181
+ */
182
+ export const accentTokens = (primary, theme, ramp) => ({
183
+ primary,
184
+ onPrimary: onAccent(primary),
185
+ primaryBg: ramp
186
+ ? ramp[STOPS[theme].tint]
187
+ : theme === "dark"
188
+ ? color(primary).darken(WASH_DARKEN).hex()
189
+ : color(primary).lighten(WASH_LIGHTEN).hex(),
190
+ });
191
+
192
+ /**
193
+ * The selectable accents, in the order a picker should show them.
194
+ *
195
+ * `light` and `dark` are the full five-stop ramps of one family, laid out so the
196
+ * accent stop and the wash stop are read off the same list the swatch paints;
197
+ * `accentTokens` is what turns one into the three tokens the theme consumes.
198
+ *
199
+ * @type {Array<{id: string, label: string, light: string[], dark: string[]}>}
200
+ */
201
+ export const ACCENT_PRESETS = ACCENT_FAMILIES;
202
+
203
+ /**
204
+ * The accent stop of a family's ramp — the hex a swatch paints and a label reads.
205
+ *
206
+ * @param {string[]} ramp
207
+ * @param {"light"|"dark"} theme
208
+ * @returns {string}
209
+ */
210
+ export const accentStop = (ramp, theme) => ramp[STOPS[theme].primary];
211
+
212
+ // How far each surface sits from the stop the ramp hands it.
213
+ //
214
+ // The rule both themes follow: **the plate is not the surface layer's business.**
215
+ // `accentTokens` already writes `primaryBg` from the one stop the family was
216
+ // measured against — vetoes 4, 5 and 6 in this file's header put the page's inks
217
+ // and the accent on it at 4.5:1 — so a theme that invented its own plate would be
218
+ // re-deriving a number the set was chosen for passing. The layer adds what the
219
+ // accent never owned: the page, the panels, the cards and the hairlines, built
220
+ // around that stop.
221
+ //
222
+ // Light reads down the ramp: the palest stop is the page, the veto'd wash is the
223
+ // panel one step deeper, and a hairline steps just below it. A card goes back to
224
+ // white, the way it lifts off `#fafafa` today. The cost of keeping the measured
225
+ // wash is one collision — a selected row painted `primaryBg` inside a panel painted
226
+ // `surface` is the same color, exactly as it is in the neutral palette, where
227
+ // `#f4f4f5` sits on `#fafafa` at 1.05.
228
+ //
229
+ // Dark reads the same ramp from the other end, and it scales the *deep accent*
230
+ // stop rather than the darkest one: multiplying a channel toward black keeps the
231
+ // hue exactly (scaling toward white washes it out — `#042f2e` stops reading as
232
+ // teal at all), and the darkest stop is too dark to divide three ways with any
233
+ // visible step. Blue's `#0b1220` split into page/panel/card separates at L* 1.3
234
+ // and 1.5, which is no layers at all; the same three depths taken from `#1d4ed8`
235
+ // land at L* 2.3 / 4.5 / 8.6 — where the shipped neutral dark palette sits
236
+ // (`#09090b` at 2.5, `#18181b` at 8.4) — with the family's hue left intact.
237
+ const DARK_PAGE_DROP = 90;
238
+ const DARK_PANEL_DROP = 82;
239
+ const DARK_CARD_DROP = 72;
240
+ const DARK_EDGE_DROP = 55;
241
+ const LIGHT_EDGE_DROP = 6;
242
+
243
+ /**
244
+ * The surface tokens one family writes when the accent is chosen as a *theme*
245
+ * rather than as a brand color: page, panels, cards and hairlines.
246
+ *
247
+ * Only the surfaces move. The ink scale stays the palette's own neutral, because
248
+ * every one of those grays was measured against a near-white and a near-black page
249
+ * and a tinted stop of a saturated family is within a few percent of both — the
250
+ * text does not need re-deriving, it needs re-measuring, which is what the veto in
251
+ * `tests/regression.test.js` does. The semantic signals (`success`, `danger`,
252
+ * their plates and alert tints) keep their own hues for the same reason a tinted
253
+ * theme must still be able to say "this failed".
254
+ *
255
+ * @param {string[]} ramp - The family's five stops for this theme.
256
+ * @param {"light"|"dark"} theme
257
+ * @returns {{ background: string, surface: string, card: string, border: string, divider: string }}
258
+ */
259
+ export const accentSurfaces = (ramp, theme) =>
260
+ theme === "dark"
261
+ ? {
262
+ background: color(ramp[3]).darken(DARK_PAGE_DROP).hex(),
263
+ surface: color(ramp[3]).darken(DARK_PANEL_DROP).hex(),
264
+ card: color(ramp[3]).darken(DARK_CARD_DROP).hex(),
265
+ border: color(ramp[3]).darken(DARK_EDGE_DROP).hex(),
266
+ divider: color(ramp[3]).darken(DARK_EDGE_DROP).hex(),
267
+ }
268
+ : {
269
+ background: ramp[4],
270
+ surface: ramp[3],
271
+ card: "#ffffff",
272
+ border: color(ramp[3]).darken(LIGHT_EDGE_DROP).hex(),
273
+ divider: color(ramp[3]).darken(LIGHT_EDGE_DROP).hex(),
274
+ };
275
+