@eifi1/ui-kit 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +82 -38
  2. package/dist/billing/billing-banner.d.ts +5 -2
  3. package/dist/billing/billing-banner.js.map +1 -1
  4. package/dist/billing/plan-limit-notice.d.ts +5 -2
  5. package/dist/billing/plan-limit-notice.js.map +1 -1
  6. package/dist/chart.js.map +1 -1
  7. package/dist/components/amount-input.d.ts +0 -3
  8. package/dist/components/amount-input.js +2 -2
  9. package/dist/components/amount-input.js.map +1 -1
  10. package/dist/components/calculator.d.ts +1 -4
  11. package/dist/components/calculator.js +1 -3
  12. package/dist/components/calculator.js.map +1 -1
  13. package/dist/components/chart-zoom.d.ts +0 -37
  14. package/dist/components/chart-zoom.js +1 -28
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chip-input.d.ts +0 -4
  17. package/dist/components/chip-input.js +1 -5
  18. package/dist/components/chip-input.js.map +1 -1
  19. package/dist/components/combobox.d.ts +1 -1
  20. package/dist/components/combobox.js.map +1 -1
  21. package/dist/components/copy-button.d.ts +5 -1
  22. package/dist/components/copy-button.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +3 -8
  24. package/dist/components/currency-option-list.js +1 -29
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/danger-confirm.d.ts +3 -3
  27. package/dist/components/danger-confirm.js.map +1 -1
  28. package/dist/components/data-table-filter-popover.d.ts +1 -1
  29. package/dist/components/data-table-filter-popover.js +1 -1
  30. package/dist/components/data-table-filter-popover.js.map +1 -1
  31. package/dist/components/data-table-labels.d.ts +1 -42
  32. package/dist/components/data-table-labels.js +2 -31
  33. package/dist/components/data-table-labels.js.map +1 -1
  34. package/dist/components/data-table-shared.d.ts +17 -1
  35. package/dist/components/data-table-shared.js +22 -0
  36. package/dist/components/data-table-shared.js.map +1 -1
  37. package/dist/components/data-table.d.ts +12 -2
  38. package/dist/components/data-table.js +18 -14
  39. package/dist/components/data-table.js.map +1 -1
  40. package/dist/components/dialog-frame.d.ts +3 -1
  41. package/dist/components/dialog-frame.js.map +1 -1
  42. package/dist/components/dropdown.d.ts +18 -15
  43. package/dist/components/dropdown.js +11 -3
  44. package/dist/components/dropdown.js.map +1 -1
  45. package/dist/components/entity-combobox.d.ts +1 -1
  46. package/dist/components/entity-combobox.js.map +1 -1
  47. package/dist/components/field-base.d.ts +0 -5
  48. package/dist/components/field-base.js +0 -5
  49. package/dist/components/field-base.js.map +1 -1
  50. package/dist/components/field-parts.d.ts +3 -11
  51. package/dist/components/field-parts.js +1 -9
  52. package/dist/components/field-parts.js.map +1 -1
  53. package/dist/components/field-strip.d.ts +8 -23
  54. package/dist/components/field-strip.js +24 -22
  55. package/dist/components/field-strip.js.map +1 -1
  56. package/dist/components/field-sync.d.ts +0 -4
  57. package/dist/components/field-sync.js +0 -4
  58. package/dist/components/field-sync.js.map +1 -1
  59. package/dist/components/field.d.ts +4 -4
  60. package/dist/components/field.js.map +1 -1
  61. package/dist/components/full-bleed-dialog.js +2 -2
  62. package/dist/components/full-bleed-dialog.js.map +1 -1
  63. package/dist/components/hover-menu.d.ts +6 -13
  64. package/dist/components/hover-menu.js +3 -3
  65. package/dist/components/hover-menu.js.map +1 -1
  66. package/dist/components/inline-entity-combobox.d.ts +1 -1
  67. package/dist/components/inline-entity-combobox.js.map +1 -1
  68. package/dist/components/modal.d.ts +9 -1
  69. package/dist/components/modal.js +7 -4
  70. package/dist/components/modal.js.map +1 -1
  71. package/dist/components/multi-select.d.ts +5 -16
  72. package/dist/components/multi-select.js +5 -10
  73. package/dist/components/multi-select.js.map +1 -1
  74. package/dist/components/number-input.d.ts +0 -3
  75. package/dist/components/number-input.js +2 -4
  76. package/dist/components/number-input.js.map +1 -1
  77. package/dist/components/popover.js +2 -2
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/series-chart.js.map +1 -1
  80. package/dist/components/server-wake.d.ts +0 -6
  81. package/dist/components/server-wake.js.map +1 -1
  82. package/dist/components/settings-fields.d.ts +4 -2
  83. package/dist/components/settings-fields.js.map +1 -1
  84. package/dist/components/stat-tile.d.ts +8 -2
  85. package/dist/components/stat-tile.js.map +1 -1
  86. package/dist/components/swipeable-row.d.ts +2 -17
  87. package/dist/components/swipeable-row.js +11 -16
  88. package/dist/components/swipeable-row.js.map +1 -1
  89. package/dist/components/tooltip.d.ts +9 -1
  90. package/dist/components/tooltip.js +20 -5
  91. package/dist/components/tooltip.js.map +1 -1
  92. package/dist/components/translation-review.d.ts +10 -2
  93. package/dist/components/translation-review.js +11 -3
  94. package/dist/components/translation-review.js.map +1 -1
  95. package/dist/components/use-table-state.d.ts +31 -3
  96. package/dist/components/use-table-state.js +53 -8
  97. package/dist/components/use-table-state.js.map +1 -1
  98. package/dist/components/user-avatar.d.ts +2 -2
  99. package/dist/components/user-avatar.js.map +1 -1
  100. package/dist/components/wizard-stepper.d.ts +1 -4
  101. package/dist/components/wizard-stepper.js +2 -3
  102. package/dist/components/wizard-stepper.js.map +1 -1
  103. package/dist/currencies.d.ts +23 -0
  104. package/dist/currencies.js +38 -0
  105. package/dist/currencies.js.map +1 -0
  106. package/dist/demo/demo-banner.d.ts +7 -2
  107. package/dist/demo/demo-banner.js.map +1 -1
  108. package/dist/eslint/index.d.ts +83 -0
  109. package/dist/eslint/index.js +69 -0
  110. package/dist/eslint/index.js.map +1 -0
  111. package/dist/eslint/role-utilities.d.ts +48 -0
  112. package/dist/eslint/role-utilities.generated.d.ts +2 -0
  113. package/dist/eslint/role-utilities.generated.js +143 -0
  114. package/dist/eslint/role-utilities.generated.js.map +1 -0
  115. package/dist/eslint/role-utilities.js +43 -0
  116. package/dist/eslint/role-utilities.js.map +1 -0
  117. package/dist/feedback/feedback-menu.d.ts +2 -2
  118. package/dist/feedback/feedback-menu.js.map +1 -1
  119. package/dist/feedback/feedback-thread.d.ts +3 -8
  120. package/dist/feedback/feedback-thread.js +0 -2
  121. package/dist/feedback/feedback-thread.js.map +1 -1
  122. package/dist/hooks/use-dismiss.d.ts +16 -3
  123. package/dist/hooks/use-dismiss.js +14 -2
  124. package/dist/hooks/use-dismiss.js.map +1 -1
  125. package/dist/i18n/kit-labels.d.ts +14 -4
  126. package/dist/i18n/kit-labels.js.map +1 -1
  127. package/dist/i18n/locales/en.js +1 -1
  128. package/dist/i18n/locales/en.js.map +2 -2
  129. package/dist/i18n/locales/fr.js +3 -2
  130. package/dist/i18n/locales/fr.js.map +1 -1
  131. package/dist/i18n/locales/it.js +3 -2
  132. package/dist/i18n/locales/it.js.map +1 -1
  133. package/dist/index.d.ts +2 -1
  134. package/dist/index.js +2 -2
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/refs.d.ts +13 -0
  137. package/dist/lib/refs.js +15 -0
  138. package/dist/lib/refs.js.map +1 -0
  139. package/dist/rhf/fields.d.ts +10 -10
  140. package/dist/rhf/fields.js.map +1 -1
  141. package/dist/rhf/form-core.d.ts +5 -4
  142. package/dist/rhf/form-core.js.map +1 -1
  143. package/dist/rhf/form.d.ts +7 -36
  144. package/dist/rhf/form.js +1 -16
  145. package/dist/rhf/form.js.map +1 -1
  146. package/dist/rhf/line-items.d.ts +1 -1
  147. package/dist/rhf/line-items.js.map +1 -1
  148. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  149. package/dist/rhf.js.map +1 -1
  150. package/dist/shell/option-switcher-menu.d.ts +5 -17
  151. package/dist/shell/option-switcher-menu.js +1 -3
  152. package/dist/shell/option-switcher-menu.js.map +1 -1
  153. package/dist/shell/role-switcher.d.ts +1 -4
  154. package/dist/shell/role-switcher.js +2 -3
  155. package/dist/shell/role-switcher.js.map +1 -1
  156. package/dist/shell/topbar-action-menu.d.ts +7 -19
  157. package/dist/shell/topbar-action-menu.js +1 -3
  158. package/dist/shell/topbar-action-menu.js.map +1 -1
  159. package/dist/shell/topbar-controls.d.ts +3 -12
  160. package/dist/shell/topbar-controls.js +6 -9
  161. package/dist/shell/topbar-controls.js.map +1 -1
  162. package/dist/testing/index.d.ts +1 -1
  163. package/dist/testing/index.js +2 -0
  164. package/dist/testing/index.js.map +1 -1
  165. package/dist/testing/viewport.d.ts +25 -2
  166. package/dist/testing/viewport.js +61 -10
  167. package/dist/testing/viewport.js.map +1 -1
  168. package/dist/theme/palette-presets.d.ts +0 -7
  169. package/dist/theme/palette-presets.js +0 -279
  170. package/dist/theme/palette-presets.js.map +1 -1
  171. package/dist/wizard/types.d.ts +0 -5
  172. package/dist/wizard/types.js +1 -5
  173. package/dist/wizard/types.js.map +1 -1
  174. package/package.json +9 -1
  175. package/src/billing/billing-banner.tsx +5 -2
  176. package/src/billing/plan-limit-notice.tsx +5 -2
  177. package/src/chart.ts +5 -3
  178. package/src/components/amount-input.tsx +2 -6
  179. package/src/components/calculator.tsx +2 -7
  180. package/src/components/chart-zoom.tsx +4 -47
  181. package/src/components/chip-input.tsx +0 -7
  182. package/src/components/combobox.tsx +1 -1
  183. package/src/components/copy-button.tsx +5 -1
  184. package/src/components/currency-option-list.tsx +8 -42
  185. package/src/components/danger-confirm.tsx +3 -3
  186. package/src/components/data-table-filter-popover.tsx +2 -1
  187. package/src/components/data-table-labels.ts +2 -84
  188. package/src/components/data-table-shared.tsx +55 -2
  189. package/src/components/data-table.tsx +29 -17
  190. package/src/components/dialog-frame.tsx +3 -1
  191. package/src/components/dropdown.tsx +42 -18
  192. package/src/components/entity-combobox.tsx +1 -1
  193. package/src/components/field-base.tsx +0 -11
  194. package/src/components/field-parts.tsx +5 -19
  195. package/src/components/field-strip.tsx +9 -25
  196. package/src/components/field-sync.tsx +0 -7
  197. package/src/components/field.tsx +4 -4
  198. package/src/components/full-bleed-dialog.tsx +4 -3
  199. package/src/components/hover-menu.tsx +12 -16
  200. package/src/components/inline-entity-combobox.tsx +1 -1
  201. package/src/components/modal.tsx +20 -4
  202. package/src/components/multi-select.tsx +13 -30
  203. package/src/components/number-input.tsx +3 -8
  204. package/src/components/popover.tsx +5 -3
  205. package/src/components/series-chart.tsx +1 -1
  206. package/src/components/server-wake.tsx +0 -6
  207. package/src/components/settings-fields.tsx +4 -2
  208. package/src/components/stat-tile.tsx +8 -2
  209. package/src/components/swipeable-row.tsx +15 -37
  210. package/src/components/tooltip.tsx +37 -6
  211. package/src/components/translation-review.tsx +25 -7
  212. package/src/components/use-table-state.ts +91 -10
  213. package/src/components/user-avatar.tsx +2 -2
  214. package/src/components/wizard-stepper.tsx +2 -6
  215. package/src/currencies.ts +70 -0
  216. package/src/demo/demo-banner.tsx +7 -2
  217. package/src/eslint/index.ts +161 -0
  218. package/src/eslint/role-utilities.generated.ts +146 -0
  219. package/src/eslint/role-utilities.ts +99 -0
  220. package/src/feedback/feedback-menu.tsx +2 -2
  221. package/src/feedback/feedback-thread.tsx +3 -8
  222. package/src/hooks/use-dismiss.ts +39 -4
  223. package/src/i18n/kit-labels.tsx +15 -5
  224. package/src/i18n/locales/fr.ts +3 -2
  225. package/src/i18n/locales/it.ts +3 -2
  226. package/src/index.ts +3 -1
  227. package/src/lib/refs.ts +22 -0
  228. package/src/rhf/fields.tsx +10 -10
  229. package/src/rhf/form-core.tsx +5 -4
  230. package/src/rhf/form.tsx +7 -43
  231. package/src/rhf/line-items.tsx +1 -1
  232. package/src/rhf/use-rhf-wizard-step.ts +4 -4
  233. package/src/rhf.ts +3 -2
  234. package/src/shell/option-switcher-menu.tsx +5 -22
  235. package/src/shell/role-switcher.tsx +2 -6
  236. package/src/shell/topbar-action-menu.tsx +7 -24
  237. package/src/shell/topbar-controls.tsx +6 -18
  238. package/src/testing/index.ts +3 -1
  239. package/src/testing/viewport.ts +125 -19
  240. package/src/theme/palette-presets.ts +7 -293
  241. package/src/wizard/types.ts +0 -8
@@ -0,0 +1,161 @@
1
+ // `@eifi1/ui-kit/eslint` — the kit's role-utility rule for an app's own markup (round 0.35
2
+ // R5, Kurvenschmiede's Q6).
3
+ //
4
+ // The kit holds its markup to "write `text-muted`, not `text-[var(--text-muted)]`" with
5
+ // `npm run check:tokens`, at zero since 0.34. The apps are asked to follow the same rule
6
+ // (README "How to write a colour"), but nothing in their builds objected when a
7
+ // `[var(--role)]` class came back. This is that check as an ESLint plugin, so an app's
8
+ // `eslint.config.js` turns it on with one line:
9
+ //
10
+ // import uiKit from "@eifi1/ui-kit/eslint";
11
+ // export default [..., uiKit.configs.recommended];
12
+ //
13
+ // It reads every string literal and template-literal quasi — a JSX `className`, a `cn()` or
14
+ // `clsx()` argument, a class constant — and reports a `[var(--x)]` class where the property
15
+ // has a role utility for that token, naming it. One utility is an autofix that keeps the
16
+ // variants and the opacity: `hover:text-[var(--text-muted)]/70` → `hover:text-muted/70`.
17
+ //
18
+ // Plain Node, like the rest of an ESLint config: no React, no stylesheet, and no import of
19
+ // `eslint` itself, which is the app's (packaging-contract.test.ts refuses an undeclared
20
+ // package in src/), so the few types it needs are written out below.
21
+ import { findRoleClasses } from "./role-utilities";
22
+ import { ROLE_UTILITIES } from "./role-utilities.generated";
23
+
24
+ interface Position {
25
+ line: number;
26
+ column: number;
27
+ }
28
+
29
+ /** The part of an ESTree string node the rule reads. */
30
+ interface StringNode {
31
+ type: string;
32
+ value?: unknown;
33
+ range?: [number, number];
34
+ loc?: { start: Position; end: Position } | null;
35
+ }
36
+
37
+ interface Fix {
38
+ range: [number, number];
39
+ text: string;
40
+ }
41
+
42
+ /**
43
+ * The part of ESLint's rule context the rule uses — `sourceCode.text` and `report`, which
44
+ * every ESLint since flat config has — so that ESLint's own RuleContext, and @eslint/core's
45
+ * that `defineConfig` checks a plugin against, are assignable to it. The source is only an
46
+ * `object` here because a language's source may be binary, with no `text` at all.
47
+ */
48
+ interface RoleUtilitiesContext {
49
+ sourceCode: object;
50
+ report(descriptor: {
51
+ loc: { start: Position; end: Position };
52
+ messageId: string;
53
+ data: Record<string, string>;
54
+ fix: ((fixer: { replaceTextRange(range: [number, number], text: string): Fix }) => Fix) | null;
55
+ }): void;
56
+ }
57
+
58
+ interface RoleUtilitiesRule {
59
+ meta: {
60
+ type: "suggestion";
61
+ docs: { description: string; url: string };
62
+ fixable: "code";
63
+ schema: [];
64
+ messages: { useRoleUtility: string };
65
+ };
66
+ create(context: RoleUtilitiesContext): {
67
+ Literal(node: StringNode): void;
68
+ TemplateElement(node: StringNode): void;
69
+ };
70
+ }
71
+
72
+ const roleUtilities: RoleUtilitiesRule = {
73
+ meta: {
74
+ type: "suggestion",
75
+ docs: {
76
+ description: "Write a colour role's utility (text-muted) rather than its [var(--…)] class",
77
+ url: "https://github.com/Eifi1/ui-kit#lint-the-role-utilities-in-an-app",
78
+ },
79
+ fixable: "code",
80
+ schema: [],
81
+ messages: {
82
+ useRoleUtility:
83
+ "Write the role utility `{{utilities}}`, not `{{found}}`: variants and an opacity work on it the same (tokens.css, @theme inline).",
84
+ },
85
+ },
86
+ create(context) {
87
+ const source = context.sourceCode as { text?: unknown };
88
+ const text = typeof source.text === "string" ? source.text : "";
89
+ const check = (node: StringNode) => {
90
+ if (!node.range || !node.loc) return;
91
+ const [start] = node.range;
92
+ const { line, column } = node.loc.start;
93
+ // An offset in the node's source text — quotes included, so it is an offset in the
94
+ // file — as a position. A template literal may span lines.
95
+ const at = (offset: number): Position => {
96
+ const lines = text.slice(start, start + offset).split("\n");
97
+ const last = lines.length - 1;
98
+ return { line: line + last, column: last ? lines[last].length : column + offset };
99
+ };
100
+ for (const hit of findRoleClasses(text.slice(start, node.range[1]), ROLE_UTILITIES)) {
101
+ const end = hit.index + hit.found.length;
102
+ const range: [number, number] = [start + hit.index, start + end];
103
+ const [only] = hit.utilities;
104
+ context.report({
105
+ loc: { start: at(hit.index), end: at(end) },
106
+ messageId: "useRoleUtility",
107
+ data: { found: hit.found, utilities: hit.utilities.join("` or `") },
108
+ // Several names for one token are a choice, not a fix.
109
+ fix: hit.utilities.length === 1 ? (fixer) => fixer.replaceTextRange(range, only) : null,
110
+ });
111
+ }
112
+ };
113
+ return {
114
+ Literal(node) {
115
+ if (typeof node.value === "string") check(node);
116
+ },
117
+ TemplateElement: check,
118
+ };
119
+ },
120
+ };
121
+
122
+ // The kit's version, baked in by tsup's `define` (tsup.config.ts). ESLint's cache keys a
123
+ // file's result on each plugin's name@version, so without it an app's `eslint --cache`
124
+ // would keep a file's old verdict after a kit release that gave a role a utility. Unset
125
+ // when the source runs (the tests), where nothing is cached.
126
+ declare const __UI_KIT_VERSION__: string | undefined;
127
+ const VERSION = typeof __UI_KIT_VERSION__ === "string" ? __UI_KIT_VERSION__ : "0.0.0-source";
128
+
129
+ /** Spelled out, self-reference included, so a typed config (`defineConfig`, `tseslint.config`) takes it. */
130
+ interface UiKitEslintPlugin {
131
+ meta: { name: string; version: string; namespace: string };
132
+ rules: { "role-utilities": RoleUtilitiesRule };
133
+ configs: {
134
+ /** The rule as an error, on every file the app's other config objects lint but its tests. */
135
+ recommended: {
136
+ name: string;
137
+ ignores: string[];
138
+ plugins: { "@eifi1/ui-kit": UiKitEslintPlugin };
139
+ rules: { "@eifi1/ui-kit/role-utilities": "error" };
140
+ };
141
+ };
142
+ }
143
+
144
+ const recommended: UiKitEslintPlugin["configs"]["recommended"] = {
145
+ name: "@eifi1/ui-kit/recommended",
146
+ // Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
147
+ // is gone (keksdose's cleared-status-toggle.test.tsx does, three times).
148
+ ignores: ["**/__tests__/**", "**/*.test.*", "**/*.spec.*"],
149
+ plugins: {} as UiKitEslintPlugin["configs"]["recommended"]["plugins"],
150
+ rules: { "@eifi1/ui-kit/role-utilities": "error" },
151
+ };
152
+
153
+ /** The flat-config plugin: add `configs.recommended`, or register it under `@eifi1/ui-kit`. */
154
+ const plugin: UiKitEslintPlugin = {
155
+ meta: { name: "@eifi1/ui-kit", version: VERSION, namespace: "@eifi1/ui-kit" },
156
+ rules: { "role-utilities": roleUtilities },
157
+ configs: { recommended },
158
+ };
159
+ recommended.plugins["@eifi1/ui-kit"] = plugin;
160
+
161
+ export default plugin;
@@ -0,0 +1,146 @@
1
+ // GENERATED by scripts/gen-role-utilities.mjs from tokens.css's `@theme inline` block — do
2
+ // not edit. `npm run check:tokens` fails while it is stale; re-run the script.
3
+ //
4
+ // Per property, each token that has a role utility → the utility's name after the
5
+ // property's prefix (`border`: `--border` → `subtle`, i.e. `border-subtle`). Read by
6
+ // `@eifi1/ui-kit/eslint` and scripts/check-token-discipline.mjs (round 0.35 R5).
7
+ import type { RoleUtilityTable } from "./role-utilities";
8
+
9
+ export const ROLE_UTILITIES: RoleUtilityTable = {
10
+ "text": {
11
+ "--text-primary": ["primary"],
12
+ "--text-secondary": ["secondary"],
13
+ "--text-muted": ["muted"],
14
+ "--text-placeholder": ["placeholder"],
15
+ "--text-inverse": ["inverse"],
16
+ "--brand": ["brand"],
17
+ "--brand-muted": ["brand-muted"],
18
+ "--danger": ["danger"],
19
+ "--warning": ["warning"],
20
+ "--success": ["success"],
21
+ "--info": ["info"],
22
+ "--money-income": ["money-income"],
23
+ "--money-expense": ["money-expense"],
24
+ "--money-net": ["money-net"],
25
+ "--money-neutral": ["money-neutral"],
26
+ "--hue-blue": ["hue-blue"],
27
+ "--hue-indigo": ["hue-indigo"],
28
+ "--hue-purple": ["hue-purple"],
29
+ "--hue-teal": ["hue-teal"],
30
+ "--hue-orange": ["hue-orange"],
31
+ "--brand-contrast": ["brand-contrast"],
32
+ "--danger-contrast": ["danger-contrast"],
33
+ "--warning-contrast": ["warning-contrast"],
34
+ "--success-contrast": ["success-contrast"],
35
+ "--info-contrast": ["info-contrast"],
36
+ "--neutral-contrast": ["neutral-contrast"],
37
+ "--money-income-contrast": ["money-income-contrast"],
38
+ "--money-expense-contrast": ["money-expense-contrast"],
39
+ "--money-net-contrast": ["money-net-contrast"],
40
+ "--money-neutral-contrast": ["money-neutral-contrast"],
41
+ "--hue-blue-contrast": ["hue-blue-contrast"],
42
+ "--hue-indigo-contrast": ["hue-indigo-contrast"],
43
+ "--hue-purple-contrast": ["hue-purple-contrast"],
44
+ "--hue-teal-contrast": ["hue-teal-contrast"],
45
+ "--hue-orange-contrast": ["hue-orange-contrast"],
46
+ "--media-ink": ["media-ink"],
47
+ },
48
+ "bg": {
49
+ "--bg-page": ["page"],
50
+ "--bg-surface": ["surface"],
51
+ "--bg-surface-2": ["surface-2"],
52
+ "--bg-hover": ["hover"],
53
+ "--bg-active": ["active"],
54
+ "--bg-inverse": ["inverse"],
55
+ "--brand": ["brand"],
56
+ "--brand-hover": ["brand-hover"],
57
+ "--brand-bg": ["brand-soft"],
58
+ "--brand-bg-hover": ["brand-soft-hover"],
59
+ "--danger": ["danger"],
60
+ "--danger-hover": ["danger-hover"],
61
+ "--warning": ["warning"],
62
+ "--success": ["success"],
63
+ "--info": ["info"],
64
+ "--neutral": ["neutral"],
65
+ "--money-income": ["money-income"],
66
+ "--money-expense": ["money-expense"],
67
+ "--money-net": ["money-net"],
68
+ "--money-neutral": ["money-neutral"],
69
+ "--danger-bg": ["danger-soft"],
70
+ "--warning-bg": ["warning-soft"],
71
+ "--success-bg": ["success-soft"],
72
+ "--info-bg": ["info-soft"],
73
+ "--hue-blue": ["hue-blue"],
74
+ "--hue-indigo": ["hue-indigo"],
75
+ "--hue-purple": ["hue-purple"],
76
+ "--hue-teal": ["hue-teal"],
77
+ "--hue-orange": ["hue-orange"],
78
+ "--hue-blue-bg": ["hue-blue-soft"],
79
+ "--hue-indigo-bg": ["hue-indigo-soft"],
80
+ "--hue-purple-bg": ["hue-purple-soft"],
81
+ "--hue-teal-bg": ["hue-teal-soft"],
82
+ "--hue-orange-bg": ["hue-orange-soft"],
83
+ "--media-scrim": ["media-scrim"],
84
+ "--media-scrim-hover": ["media-scrim-hover"],
85
+ },
86
+ "border": {
87
+ "--border": ["subtle"],
88
+ "--border-strong": ["strong"],
89
+ "--brand": ["brand"],
90
+ "--danger-border": ["danger"],
91
+ "--warning-border": ["warning"],
92
+ "--success-border": ["success"],
93
+ "--info-border": ["info"],
94
+ "--danger-border-strong": ["danger-strong"],
95
+ "--warning-border-strong": ["warning-strong"],
96
+ "--hue-blue-border": ["hue-blue"],
97
+ "--hue-indigo-border": ["hue-indigo"],
98
+ "--hue-purple-border": ["hue-purple"],
99
+ "--hue-teal-border": ["hue-teal"],
100
+ "--hue-orange-border": ["hue-orange"],
101
+ "--media-ink": ["media-ink"],
102
+ },
103
+ "ring": {
104
+ "--border": ["subtle"],
105
+ "--border-strong": ["strong"],
106
+ "--brand": ["brand"],
107
+ "--brand-contrast": ["brand-contrast"],
108
+ "--danger-border-strong": ["danger-strong"],
109
+ "--warning-border-strong": ["warning-strong"],
110
+ "--media-ink": ["media-ink"],
111
+ },
112
+ "ring-offset": {
113
+ "--bg-page": ["page"],
114
+ "--bg-surface": ["surface"],
115
+ "--media-scrim-hover": ["media-scrim-hover"],
116
+ },
117
+ "outline": {
118
+ "--border": ["subtle"],
119
+ "--border-strong": ["strong"],
120
+ "--brand": ["brand"],
121
+ "--danger-border-strong": ["danger-strong"],
122
+ "--warning-border-strong": ["warning-strong"],
123
+ },
124
+ "fill": {
125
+ "--text-primary": ["primary"],
126
+ "--text-secondary": ["secondary"],
127
+ "--text-muted": ["muted"],
128
+ "--bg-surface": ["surface"],
129
+ "--border": ["subtle"],
130
+ "--border-strong": ["strong"],
131
+ },
132
+ "stroke": {
133
+ "--text-primary": ["primary"],
134
+ "--text-secondary": ["secondary"],
135
+ "--text-muted": ["muted"],
136
+ "--bg-surface": ["surface"],
137
+ "--border": ["subtle"],
138
+ "--border-strong": ["strong"],
139
+ },
140
+ "ring-width": {
141
+ "--focus-ring-width": ["focus"],
142
+ },
143
+ "outline-width": {
144
+ "--focus-ring-width": ["focus"],
145
+ },
146
+ };
@@ -0,0 +1,99 @@
1
+ // Which `[var(--token)]` classes have a role utility, and what it is called — the one
2
+ // definition `@eifi1/ui-kit/eslint`'s `role-utilities` rule and `npm run check:tokens`
3
+ // (scripts/check-token-discipline.mjs) both read (round 0.35 R5, Kurvenschmiede's Q6).
4
+ //
5
+ // The table itself is role-utilities.generated.ts, re-rendered from tokens.css's
6
+ // `@theme inline` block by scripts/gen-role-utilities.mjs with the parser below; the
7
+ // check fails while it is stale. So tokens.css stays the source: a role that gains a
8
+ // utility there is flagged in the kit and in the apps from the next release on.
9
+ //
10
+ // This module imports NOTHING, and its TypeScript is erasable only (no enum, no
11
+ // namespace, no parameter property): the check imports it from source under Node's type
12
+ // stripping, before any build, and the plugin ships it to an app's ESLint, which is
13
+ // plain Node with no React and no stylesheet.
14
+
15
+ /**
16
+ * The properties a role utility exists for, each with the Tailwind theme namespace it is
17
+ * declared in. `ring-width` and `outline-width` are a width typed `length:` —
18
+ * `ring-[length:var(--focus-ring-width)]` is `ring-focus`. `divide-*` and the side borders
19
+ * (`border-t-*`, `border-x-*` …) read `border`'s, as Tailwind does.
20
+ */
21
+ export const ROLE_NAMESPACES = {
22
+ text: "text-color",
23
+ bg: "background-color",
24
+ border: "border-color",
25
+ ring: "ring-color",
26
+ "ring-offset": "ring-offset-color",
27
+ outline: "outline-color",
28
+ fill: "fill",
29
+ stroke: "stroke",
30
+ "ring-width": "ring-width",
31
+ "outline-width": "outline-width",
32
+ } as const;
33
+
34
+ export type RoleProperty = keyof typeof ROLE_NAMESPACES;
35
+
36
+ /**
37
+ * Per property, each token that has a role utility → the utility's name after the
38
+ * property's prefix: `border: { "--border": ["subtle"] }` is `border-subtle`. A list,
39
+ * because nothing stops two names from reading one token; one name is a fix, several a
40
+ * choice.
41
+ */
42
+ export type RoleUtilityTable = {
43
+ readonly [P in RoleProperty]: Readonly<Record<string, readonly string[]>>;
44
+ };
45
+
46
+ /**
47
+ * The table `tokens.css` declares: every `--<namespace>-<name>: var(--<token>)` in its
48
+ * `@theme inline` block, in block order. `null` when there is no such block — the role
49
+ * utilities are gone, or this parser is.
50
+ */
51
+ export function roleUtilitiesFromCss(css: string): RoleUtilityTable | null {
52
+ const theme = /@theme inline\s*\{([^}]*)\}/.exec(css.replace(/\/\*[\s\S]*?\*\//g, ""))?.[1];
53
+ if (theme === undefined) return null;
54
+ const table = {} as Record<RoleProperty, Record<string, string[]>>;
55
+ for (const prop of Object.keys(ROLE_NAMESPACES) as RoleProperty[]) {
56
+ const names: Record<string, string[]> = {};
57
+ const declared = new RegExp(`--${ROLE_NAMESPACES[prop]}-([a-z0-9-]+)\\s*:\\s*var\\((--[a-z0-9-]+)\\)`, "g");
58
+ for (const [, name, token] of theme.matchAll(declared)) (names[token] ??= []).push(name);
59
+ table[prop] = names;
60
+ }
61
+ return table;
62
+ }
63
+
64
+ /**
65
+ * `text-[var(--text-muted)]`, `hover:bg-[var(--bg-hover)]`, `border-t-[var(--border)]`,
66
+ * `focus-visible:ring-[var(--brand)]/40`, `ring-offset-[var(--bg-surface)]`,
67
+ * `ring-[length:var(--focus-ring-width)]`. Preceded by a start, a space, a quote or a
68
+ * variant's colon, which the match does not include — nor the variants before it or an
69
+ * opacity or `!` after it — so replacing the match keeps all of them.
70
+ */
71
+ const ROLE_CLASS =
72
+ /(?<=^|[\s"'`:])(text|bg|border(?:-[xytrblse])?|divide|ring-offset|ring|outline|fill|stroke)-\[(length:)?var\((--[a-zA-Z0-9-]+)\)\]/g;
73
+
74
+ export interface RoleClassHit {
75
+ /** Where the class starts in the scanned text, after its variants. */
76
+ index: number;
77
+ /** The class as written, without variants or opacity: `border-t-[var(--border)]`. */
78
+ found: string;
79
+ /** The role utilities that paint the same: `["border-t-subtle"]`. */
80
+ utilities: string[];
81
+ }
82
+
83
+ /**
84
+ * Every `[var(--token)]` class in `text` whose token has a role utility on that property.
85
+ * A token with no utility there (`text-[var(--chart-1)]`, `--status-*`,
86
+ * `bg-[var(--border)]` as a divider's fill) is not a hit: `[var(--…)]` is right for it.
87
+ */
88
+ export function findRoleClasses(text: string, table: RoleUtilityTable): RoleClassHit[] {
89
+ if (!text.includes("var(--")) return [];
90
+ const hits: RoleClassHit[] = [];
91
+ for (const m of text.matchAll(ROLE_CLASS)) {
92
+ const [found, prop, length, token] = m;
93
+ // `border-t` and `divide` read `border`'s names; a `length:` hint reads the widths.
94
+ const key = length ? `${prop}-width` : prop.startsWith("border-") || prop === "divide" ? "border" : prop;
95
+ const names = (table as Partial<Record<string, Readonly<Record<string, readonly string[]>>>>)[key]?.[token];
96
+ if (names?.length) hits.push({ index: m.index, found, utilities: names.map((name) => `${prop}-${name}`) });
97
+ }
98
+ return hits;
99
+ }
@@ -56,8 +56,8 @@ export interface FeedbackMenuProps {
56
56
  iconBadge?: UserAvatarBadge | null;
57
57
  /** Prop > `<UiKitProvider labels={{ feedbackMenu }}>` > English. */
58
58
  labels?: LabelOverride<FeedbackMenuLabels>;
59
- /** Which edge of the trigger the panel lines up with (see `HoverMenu`, whose type this
60
- * is: `left`/`right` are deprecated 0.34). Default `end`. */
59
+ /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading
60
+ * direction (see `HoverMenu`, whose type this is). Default `end`. */
61
61
  align?: HoverMenuProps["align"];
62
62
  }
63
63
 
@@ -646,7 +646,7 @@ export interface FeedbackComposerProps {
646
646
  }
647
647
 
648
648
  /**
649
- * What a {@link FeedbackComposer} `ref` holds.
649
+ * What a {@link ChatComposer} `ref` holds.
650
650
  *
651
651
  * keksdose F9: the support admin answers the same five questions all day and wants
652
652
  * chips above the box ("Thanks, fixed in the next release") that drop their text in
@@ -679,7 +679,8 @@ const isApple = () =>
679
679
  * Named for what it is since 0.22 (keksdose K17): the component was never
680
680
  * feedback-specific — a textarea, an optional picture or a host's upload slot, Send,
681
681
  * `sendOn="enter"` for the chat way — and keksdose's assistant and support chat are
682
- * chats, not reports. `FeedbackComposer` is its old name, deprecated in 0.34.
682
+ * chats, not reports. `FeedbackComposer`, its old name, was removed in 0.35 (refactor
683
+ * audit 2026-10, API-6d; deprecated in 0.34); its types keep theirs.
683
684
  */
684
685
  export function ChatComposer({
685
686
  onSend,
@@ -892,12 +893,6 @@ function dataAttributes(props: object): Record<string, unknown> {
892
893
  return out;
893
894
  }
894
895
 
895
- /**
896
- * @deprecated 0.34 — `ChatComposer`, the same function under the name that is not about
897
- * feedback; removed in 0.35 (refactor audit 2026-10, API-6d). keksdose's support thread
898
- * is the one app site.
899
- */
900
- export const FeedbackComposer = ChatComposer;
901
896
  /** {@link FeedbackComposerProps}, for {@link ChatComposer}. */
902
897
  export type ChatComposerProps = FeedbackComposerProps;
903
898
  /** {@link FeedbackComposerHandle}, for {@link ChatComposer}. */
@@ -47,6 +47,12 @@ import { isComposing } from "../lib/ime";
47
47
  * anywhere else claims nothing: a search box keeping Chromium from clearing itself
48
48
  * must not hold a dialog open (FullBleedDialog's 0.8.0 fix, which Modal never had).
49
49
  *
50
+ * The owner's handler hears Escape only while its own layer is on top (0.35, KS). It
51
+ * runs before the stack is asked, so with a tooltip, a hover menu or a list open over
52
+ * the dialog and focus on the panel, the gear wizard asked "Discard this gear?" and the
53
+ * tooltip stayed. Now the layer above takes that press, and the next one is the
54
+ * owner's. Every other key reaches the handler as before.
55
+ *
50
56
  * A handler that STOPS the key before it reaches a layer element has taken it too —
51
57
  * that is the DOM, and it is what the kit's cell editors and confirm buttons do.
52
58
  */
@@ -56,6 +62,10 @@ interface Layer {
56
62
  escape: (e: KeyboardEvent) => void;
57
63
  }
58
64
 
65
+ /** What {@link useDismissLayer} hands back: its layer while open, for
66
+ * {@link runOwnKeyDown} to tell whether that layer is the one on top. */
67
+ export type DismissLayer = RefObject<Layer | null>;
68
+
59
69
  /** Open layers, oldest first. Module-level, because the order is ACROSS instances. */
60
70
  const layers: Layer[] = [];
61
71
 
@@ -123,10 +133,11 @@ function pop(layer: Layer): void {
123
133
  * Where focus can be INSIDE the layer, its element's `onKeyDown` calls
124
134
  * {@link takeEscape} too, so the key stops there — see the module note. `onEscape` is
125
135
  * read live, so it need not be memoised; the layer is pushed and popped only when
126
- * `open` flips.
136
+ * `open` flips. The layer it returns is for the owner's {@link runOwnKeyDown}.
127
137
  */
128
- export function useDismissLayer(open: boolean, onEscape: (e: KeyboardEvent) => void): void {
138
+ export function useDismissLayer(open: boolean, onEscape: (e: KeyboardEvent) => void): DismissLayer {
129
139
  const handlerRef = useRef(onEscape);
140
+ const layerRef = useRef<Layer | null>(null);
130
141
  // Written in an effect, not during render, so it satisfies react-hooks/refs: a
131
142
  // render that React starts and then discards — a transition, a Suspense retry —
132
143
  // would otherwise leave this holding a handler from a tree that never committed,
@@ -138,9 +149,14 @@ export function useDismissLayer(open: boolean, onEscape: (e: KeyboardEvent) => v
138
149
  useEffect(() => {
139
150
  if (!open || typeof document === "undefined") return;
140
151
  const layer: Layer = { escape: (e) => handlerRef.current(e) };
152
+ layerRef.current = layer;
141
153
  push(layer);
142
- return () => pop(layer);
154
+ return () => {
155
+ pop(layer);
156
+ layerRef.current = null;
157
+ };
143
158
  }, [open]);
159
+ return layerRef;
144
160
  }
145
161
 
146
162
  /**
@@ -167,14 +183,33 @@ export function claimEscape(e: KeyboardEvent | ReactKeyboardEvent): void {
167
183
  * Runs a layer owner's own key handler — a dialog's `onKeyDown` prop — and turns a
168
184
  * `preventDefault()` it makes on Escape into a claim: the one way, beside
169
185
  * {@link claimEscape}, to keep a layer open.
186
+ *
187
+ * Given the owner's `layer` (what {@link useDismissLayer} returned), an Escape that went
188
+ * down while another layer was above that one skips the handler: the press is the
189
+ * layer's on top, and the owner hears the next (0.35, KS). Without it, or while the
190
+ * owner's layer is not open, the handler sees every key, as before.
170
191
  */
171
- export function runOwnKeyDown<E extends ReactKeyboardEvent>(e: E, handler: ((e: E) => void) | undefined): void {
192
+ export function runOwnKeyDown<E extends ReactKeyboardEvent>(
193
+ e: E,
194
+ handler: ((e: E) => void) | undefined,
195
+ layer?: DismissLayer,
196
+ ): void {
172
197
  if (!handler) return;
198
+ if (layer && aboveOwner(e, layer.current)) return;
173
199
  const before = e.defaultPrevented;
174
200
  handler(e);
175
201
  if (e.key === "Escape" && !before && e.defaultPrevented) claimed.add(nativeOf(e));
176
202
  }
177
203
 
204
+ /** Whether this Escape went down with a layer above `owner` — the topmost one then was
205
+ * another, and `owner` is on the stack. An Escape the stack did not note (one inside an
206
+ * IME composition) is nobody else's. */
207
+ function aboveOwner(e: ReactKeyboardEvent, owner: Layer | null): boolean {
208
+ const flight = inFlight;
209
+ if (e.key !== "Escape" || !owner || !flight || flight.event !== nativeOf(e)) return false;
210
+ return flight.layer !== owner && layers.includes(owner);
211
+ }
212
+
178
213
  /**
179
214
  * Calls `handler` on an Escape while `enabled` — as an Escape layer. Since 0.34 this is
180
215
  * {@link useDismissLayer} with its arguments the other way round: it hears the key only
@@ -701,13 +701,23 @@ function mergeOverrides(
701
701
  return out as UiKitLabelOverrides;
702
702
  }
703
703
 
704
- /** What the nearest provider says about one namespace — `undefined` outside one.
705
- * For a component whose own resolver does more than a merge (the data table
706
- * derives `columnsCount` from `columns`): feed `{ ...overrides, ...props }` to it. */
704
+ /**
705
+ * What the nearest provider says about one namespace — `undefined` outside one. Nested
706
+ * providers' words come merged, at every depth; no English default is filled in.
707
+ *
708
+ * A {@link LabelOverride}, partial at every depth, like the components' `labels` props
709
+ * that take it: a provider that names one key of a section (`accountSettings.passkeys`)
710
+ * or one preset (`dataTable.presets`) hands over just that (round 0.35 R3 — until then
711
+ * it was typed partial at the top only, so a nested section read as complete).
712
+ *
713
+ * For a component whose own resolver does more than a merge (the data table derives
714
+ * `columnsCount` from `columns`): merge the component's prop over it — a record key by
715
+ * key — and feed the result to that resolver.
716
+ */
707
717
  export function useKitLabelOverrides<K extends keyof UiKitLabels>(
708
718
  ns: K,
709
- ): Partial<UiKitLabels[K]> | undefined {
710
- return useContext(KitI18nContext).labels?.[ns] as Partial<UiKitLabels[K]> | undefined;
719
+ ): LabelOverride<UiKitLabels[K]> | undefined {
720
+ return useContext(KitI18nContext).labels?.[ns];
711
721
  }
712
722
 
713
723
  /**
@@ -774,10 +774,11 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
774
774
  submit: "Continuer",
775
775
  cancel: "Annuler",
776
776
  },
777
+ // 0.35: keksdose's reviewed French (its `boot.slow` / `boot.waking`), which is canon.
777
778
  serverWake: {
778
- slow: "Chargement en cours — cela prend plus de temps que d’habitude.",
779
+ slow: "Chargement en cours — c’est plus long que d’habitude.",
779
780
  waking: (appName) =>
780
- `Le serveur se met en veille quand personne n’utilise ${appName ?? "l’application"}, donc la première requête après une pause doit le redémarrer. Cela peut prendre un moment — rien n’est perdu, la page se remplira d’elle-même.`,
781
+ `Le serveur se met en veille quand personne n’utilise ${appName ?? "l’application"}\u202f; la première requête après une pause doit donc le réveiller. Cela peut prendre un instant — rien n’est perdu, la page se remplit toute seule.`,
781
782
  },
782
783
  // 0.28.0 — docs/legal-harmonization.md §4.3/§4.4: keksdose's reviewed French, and
783
784
  // Kurvenschmiede's for the browser section and the beta notice; spaced as this
@@ -762,10 +762,11 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
762
762
  submit: "Continua",
763
763
  cancel: "Annulla",
764
764
  },
765
+ // 0.35: keksdose's reviewed Italian (its `boot.slow` / `boot.waking`), which is canon.
765
766
  serverWake: {
766
- slow: "Caricamento in corso: ci vuole più del solito.",
767
+ slow: "Sta ancora caricando: ci vuole più del solito.",
767
768
  waking: (appName) =>
768
- `Il server va in sospensione quando nessuno usa ${appName ?? "l’app"}, quindi la prima richiesta dopo una pausa deve riavviarlo. Può volerci un momento: non si perde nulla, la pagina si completa da sola.`,
769
+ `Il server si addormenta quando nessuno usa ${appName ?? "l’app"}, perciò la prima richiesta dopo una pausa deve prima riavviarlo. Può volerci un momento: non va perso nulla, la pagina si riempie da sola.`,
769
770
  },
770
771
  // 0.28.0 — docs/legal-harmonization.md §4.3/§4.4: keksdose's reviewed Italian, and
771
772
  // Kurvenschmiede's for the browser section and the beta notice; formal, "Lei" and
package/src/index.ts CHANGED
@@ -245,7 +245,6 @@ export {
245
245
  FloatingField,
246
246
  Label,
247
247
  DEFAULT_PASSWORD_REVEAL_LABELS,
248
- resolvePasswordRevealLabels,
249
248
  DEFAULT_CHARACTER_COUNT_LABELS,
250
249
  type FieldChevronProps,
251
250
  type FloatingFieldProps,
@@ -291,6 +290,9 @@ export * from "./components/calculator";
291
290
  export * from "./components/numpad-sheet";
292
291
  export * from "./components/number-input";
293
292
  export * from "./components/currency-select";
293
+ // 0.35: each currency's flag-icons country. `CURRENCIES` and it are also the pure-data
294
+ // entry "@eifi1/ui-kit/currencies", which imports nothing, for an app's build in Node.
295
+ export { CURRENCY_COUNTRIES } from "./currencies";
294
296
  export * from "./components/amount-input";
295
297
  export * from "./components/money-field";
296
298
  export * from "./components/combobox";
@@ -0,0 +1,22 @@
1
+ /** Internal (not exported from the package): the kit's ref plumbing. Here, rather than in
2
+ * components/field-parts (which re-exports both), since round 0.35: Tooltip needs them,
3
+ * and field-parts renders a Tooltip, so importing them from there closed a cycle. */
4
+ import type { Ref, RefCallback } from "react";
5
+
6
+ /** Point a ref — a callback or an object — at `node`. `undefined` and `null` are no ref. */
7
+ export function assignRef<T>(ref: Ref<T> | undefined, node: T | null): void {
8
+ if (typeof ref === "function") ref(node);
9
+ else if (ref) (ref as { current: T | null }).current = node;
10
+ }
11
+
12
+ /**
13
+ * One callback ref feeding several — the caller's forwarded `ref` and a field's own
14
+ * (DUP-11: AmountInput, FormActions, ScrollArea and SearchField each wrote this by hand).
15
+ * Wrap it in `useCallback` (or `useMemo`) over the same refs where a new callback every
16
+ * render would detach and re-attach the node.
17
+ */
18
+ export function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T> {
19
+ return (node) => {
20
+ for (const ref of refs) assignRef(ref, node);
21
+ };
22
+ }