@eifi1/ui-kit 0.52.0 → 0.54.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 (290) hide show
  1. package/README.md +52 -59
  2. package/dist/account/delete-account-setting.d.ts +3 -2
  3. package/dist/account/delete-account-setting.js.map +1 -1
  4. package/dist/auth/auth-errors.d.ts +36 -0
  5. package/dist/auth/auth-errors.js +20 -0
  6. package/dist/auth/auth-errors.js.map +1 -1
  7. package/dist/billing/cancel-subscription.d.ts +5 -14
  8. package/dist/billing/cancel-subscription.js +1 -3
  9. package/dist/billing/cancel-subscription.js.map +1 -1
  10. package/dist/billing/plan-card.d.ts +5 -13
  11. package/dist/billing/plan-card.js +3 -11
  12. package/dist/billing/plan-card.js.map +1 -1
  13. package/dist/billing/plan-switch.d.ts +5 -14
  14. package/dist/billing/plan-switch.js +1 -3
  15. package/dist/billing/plan-switch.js.map +1 -1
  16. package/dist/chart.d.ts +3 -14
  17. package/dist/chart.js +64 -11
  18. package/dist/chart.js.map +1 -1
  19. package/dist/components/amount-input.d.ts +2 -1
  20. package/dist/components/amount-input.js.map +1 -1
  21. package/dist/components/calendar-heatmap.js +4 -2
  22. package/dist/components/calendar-heatmap.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +2 -1
  24. package/dist/components/currency-option-list.js +10 -3
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/currency-select.d.ts +3 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/danger-confirm.d.ts +22 -1
  29. package/dist/components/danger-confirm.js +50 -28
  30. package/dist/components/danger-confirm.js.map +1 -1
  31. package/dist/components/data-table-filter-popover.d.ts +2 -1
  32. package/dist/components/data-table-filter-popover.js +3 -2
  33. package/dist/components/data-table-filter-popover.js.map +1 -1
  34. package/dist/components/data-table-filters.d.ts +33 -3
  35. package/dist/components/data-table-filters.js +6 -4
  36. package/dist/components/data-table-filters.js.map +1 -1
  37. package/dist/components/date-field.d.ts +1 -1
  38. package/dist/components/date-field.js.map +1 -1
  39. package/dist/components/date-picker.d.ts +1 -1
  40. package/dist/components/date-picker.js.map +1 -1
  41. package/dist/components/date-range-picker.d.ts +4 -17
  42. package/dist/components/date-range-picker.js +2 -2
  43. package/dist/components/date-range-picker.js.map +1 -1
  44. package/dist/components/dialog-frame.d.ts +3 -1
  45. package/dist/components/dialog-frame.js +6 -2
  46. package/dist/components/dialog-frame.js.map +1 -1
  47. package/dist/components/mini-calendar.js +3 -2
  48. package/dist/components/mini-calendar.js.map +1 -1
  49. package/dist/components/modal.d.ts +27 -0
  50. package/dist/components/modal.js +5 -3
  51. package/dist/components/modal.js.map +1 -1
  52. package/dist/components/nav-pills.d.ts +1 -1
  53. package/dist/components/nav-pills.js.map +1 -1
  54. package/dist/components/number-input.js.map +1 -1
  55. package/dist/components/series-chart-axes.d.ts +1 -1
  56. package/dist/components/series-chart-axes.js.map +1 -1
  57. package/dist/components/series-chart.d.ts +64 -19
  58. package/dist/components/series-chart.js +2 -1
  59. package/dist/components/series-chart.js.map +1 -1
  60. package/dist/components/stat-tile.d.ts +21 -2
  61. package/dist/components/stat-tile.js +4 -2
  62. package/dist/components/stat-tile.js.map +1 -1
  63. package/dist/components/treemap-labels.d.ts +6 -1
  64. package/dist/components/treemap-labels.js +2 -2
  65. package/dist/components/treemap-labels.js.map +1 -1
  66. package/dist/components/treemap.d.ts +29 -4
  67. package/dist/components/treemap.js +22 -9
  68. package/dist/components/treemap.js.map +1 -1
  69. package/dist/currencies.d.ts +16 -2
  70. package/dist/currencies.js +2 -1
  71. package/dist/currencies.js.map +1 -1
  72. package/dist/data-table.d.ts +1 -6
  73. package/dist/data-table.js +1 -9
  74. package/dist/data-table.js.map +1 -1
  75. package/dist/demo/demo-countdown.d.ts +0 -5
  76. package/dist/demo/demo-countdown.js +0 -4
  77. package/dist/demo/demo-countdown.js.map +1 -1
  78. package/dist/eslint/index.js +1 -1
  79. package/dist/feedback/feedback-context-labels.js +2 -1
  80. package/dist/feedback/feedback-context-labels.js.map +1 -1
  81. package/dist/feedback/feedback-context.d.ts +19 -3
  82. package/dist/feedback/feedback-context.js +12 -1
  83. package/dist/feedback/feedback-context.js.map +1 -1
  84. package/dist/feedback/feedback-diagnostics.d.ts +17 -0
  85. package/dist/feedback/feedback-diagnostics.js +27 -0
  86. package/dist/feedback/feedback-diagnostics.js.map +1 -0
  87. package/dist/feedback/feedback-dialog.d.ts +10 -0
  88. package/dist/feedback/feedback-dialog.js +6 -4
  89. package/dist/feedback/feedback-dialog.js.map +1 -1
  90. package/dist/feedback/feedback-mutation.d.ts +11 -23
  91. package/dist/feedback/feedback-mutation.js +1 -9
  92. package/dist/feedback/feedback-mutation.js.map +1 -1
  93. package/dist/feedback/feedback-public-list.d.ts +1 -6
  94. package/dist/feedback/feedback-public-list.js +1 -2
  95. package/dist/feedback/feedback-public-list.js.map +1 -1
  96. package/dist/feedback/feedback-record.d.ts +5 -0
  97. package/dist/feedback/feedback-record.js.map +1 -1
  98. package/dist/feedback/feedback-row-detail-labels.js +1 -8
  99. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  100. package/dist/feedback/feedback-row-detail.d.ts +6 -50
  101. package/dist/feedback/feedback-row-detail.js +3 -8
  102. package/dist/feedback/feedback-row-detail.js.map +1 -1
  103. package/dist/feedback/feedback-status-undo.d.ts +3 -19
  104. package/dist/feedback/feedback-status-undo.js.map +1 -1
  105. package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
  106. package/dist/feedback/feedback-submit-dialog.js +15 -3
  107. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  108. package/dist/feedback/feedback-submit.d.ts +30 -2
  109. package/dist/feedback/feedback-submit.js +5 -2
  110. package/dist/feedback/feedback-submit.js.map +1 -1
  111. package/dist/feedback/feedback-thread.d.ts +0 -9
  112. package/dist/feedback/feedback-thread.js.map +1 -1
  113. package/dist/feedback.d.ts +6 -40
  114. package/dist/feedback.js +88 -46
  115. package/dist/feedback.js.map +1 -1
  116. package/dist/hooks/use-hotkey.js +11 -1
  117. package/dist/hooks/use-hotkey.js.map +1 -1
  118. package/dist/i18n/defaults.js +0 -4
  119. package/dist/i18n/defaults.js.map +1 -1
  120. package/dist/i18n/german.js +5 -20
  121. package/dist/i18n/german.js.map +1 -1
  122. package/dist/i18n/kit-labels.d.ts +5 -14
  123. package/dist/i18n/kit-labels.js.map +1 -1
  124. package/dist/i18n/locales/en.js +5 -31
  125. package/dist/i18n/locales/en.js.map +3 -3
  126. package/dist/i18n/locales/es.js +5 -20
  127. package/dist/i18n/locales/es.js.map +1 -1
  128. package/dist/i18n/locales/fr.js +5 -20
  129. package/dist/i18n/locales/fr.js.map +1 -1
  130. package/dist/i18n/locales/hu.js +5 -20
  131. package/dist/i18n/locales/hu.js.map +1 -1
  132. package/dist/i18n/locales/it.js +5 -21
  133. package/dist/i18n/locales/it.js.map +1 -1
  134. package/dist/i18n/locales/zh.js +5 -20
  135. package/dist/i18n/locales/zh.js.map +1 -1
  136. package/dist/i18n/review.js +9 -4
  137. package/dist/i18n/review.js.map +1 -1
  138. package/dist/i18n/shared-labels.js +0 -1
  139. package/dist/i18n/shared-labels.js.map +1 -1
  140. package/dist/index.d.ts +18 -67
  141. package/dist/index.js +28 -103
  142. package/dist/index.js.map +1 -1
  143. package/dist/lib/dates.d.ts +7 -1
  144. package/dist/lib/dates.js +8 -1
  145. package/dist/lib/dates.js.map +1 -1
  146. package/dist/lib/format.d.ts +37 -2
  147. package/dist/lib/format.js +25 -1
  148. package/dist/lib/format.js.map +1 -1
  149. package/dist/lib/intl-cache.d.ts +4 -2
  150. package/dist/lib/intl-cache.js.map +1 -1
  151. package/dist/lib/translation-review.d.ts +26 -0
  152. package/dist/lib/translation-review.js +14 -0
  153. package/dist/lib/translation-review.js.map +1 -1
  154. package/dist/rhf/fields.d.ts +2 -2
  155. package/dist/rhf/fields.js.map +1 -1
  156. package/dist/search/search-index.d.ts +0 -6
  157. package/dist/search/search-index.js +0 -4
  158. package/dist/search/search-index.js.map +1 -1
  159. package/dist/search.d.ts +1 -8
  160. package/dist/search.js +2 -7
  161. package/dist/search.js.map +1 -1
  162. package/dist/shell/account-menu-language.d.ts +7 -21
  163. package/dist/shell/account-menu-language.js +1 -3
  164. package/dist/shell/account-menu-language.js.map +1 -1
  165. package/dist/shell/account-menu-text-size.d.ts +9 -23
  166. package/dist/shell/account-menu-text-size.js +2 -3
  167. package/dist/shell/account-menu-text-size.js.map +1 -1
  168. package/dist/shell/app-shell.d.ts +0 -10
  169. package/dist/shell/app-shell.js +1 -7
  170. package/dist/shell/app-shell.js.map +1 -1
  171. package/dist/shell/company-switcher.d.ts +1 -1
  172. package/dist/shell/company-switcher.js.map +1 -1
  173. package/dist/shell/dev-menu.d.ts +1 -1
  174. package/dist/shell/dev-menu.js.map +1 -1
  175. package/dist/shell/top-bar-parts-labels.d.ts +1 -1
  176. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  177. package/dist/shell.d.ts +7 -29
  178. package/dist/shell.js +12 -42
  179. package/dist/shell.js.map +1 -1
  180. package/dist/sync.d.ts +2 -6
  181. package/dist/sync.js +2 -15
  182. package/dist/sync.js.map +1 -1
  183. package/dist/theme/chart-palette.d.ts +0 -5
  184. package/dist/theme/chart-palette.js +0 -2
  185. package/dist/theme/chart-palette.js.map +1 -1
  186. package/package.json +1 -1
  187. package/src/account/delete-account-setting.tsx +3 -2
  188. package/src/auth/auth-errors.ts +69 -0
  189. package/src/billing/cancel-subscription.tsx +5 -16
  190. package/src/billing/plan-card.tsx +7 -24
  191. package/src/billing/plan-switch.tsx +5 -16
  192. package/src/chart.ts +62 -13
  193. package/src/components/amount-input.tsx +2 -1
  194. package/src/components/calendar-heatmap.tsx +8 -2
  195. package/src/components/currency-option-list.tsx +12 -4
  196. package/src/components/currency-select.tsx +3 -1
  197. package/src/components/danger-confirm.tsx +80 -27
  198. package/src/components/data-table-filter-popover.tsx +19 -11
  199. package/src/components/data-table-filters.ts +36 -8
  200. package/src/components/date-field.tsx +1 -1
  201. package/src/components/date-picker.tsx +0 -1
  202. package/src/components/date-range-picker.tsx +6 -22
  203. package/src/components/dialog-frame.tsx +12 -4
  204. package/src/components/mini-calendar.tsx +5 -2
  205. package/src/components/modal.tsx +36 -7
  206. package/src/components/nav-pills.tsx +1 -1
  207. package/src/components/number-input.tsx +4 -3
  208. package/src/components/series-chart-axes.ts +6 -1
  209. package/src/components/series-chart.tsx +78 -21
  210. package/src/components/stat-tile.tsx +32 -3
  211. package/src/components/treemap-labels.ts +11 -3
  212. package/src/components/treemap.tsx +65 -12
  213. package/src/currencies.ts +23 -3
  214. package/src/data-table.ts +1 -11
  215. package/src/demo/demo-countdown.ts +3 -8
  216. package/src/feedback/feedback-context-labels.ts +1 -0
  217. package/src/feedback/feedback-context.tsx +34 -2
  218. package/src/feedback/feedback-diagnostics.tsx +41 -0
  219. package/src/feedback/feedback-dialog.tsx +16 -4
  220. package/src/feedback/feedback-mutation.ts +12 -33
  221. package/src/feedback/feedback-public-list.tsx +1 -7
  222. package/src/feedback/feedback-record.ts +5 -0
  223. package/src/feedback/feedback-row-detail-labels.ts +1 -8
  224. package/src/feedback/feedback-row-detail.tsx +21 -58
  225. package/src/feedback/feedback-status-undo.ts +4 -18
  226. package/src/feedback/feedback-submit-dialog.tsx +15 -2
  227. package/src/feedback/feedback-submit.tsx +36 -3
  228. package/src/feedback/feedback-thread.tsx +0 -10
  229. package/src/feedback.ts +89 -48
  230. package/src/hooks/use-hotkey.ts +24 -1
  231. package/src/i18n/defaults.ts +0 -4
  232. package/src/i18n/german.ts +4 -19
  233. package/src/i18n/kit-labels.tsx +5 -14
  234. package/src/i18n/locales/es.ts +4 -19
  235. package/src/i18n/locales/fr.ts +4 -19
  236. package/src/i18n/locales/hu.ts +4 -19
  237. package/src/i18n/locales/it.ts +4 -20
  238. package/src/i18n/locales/zh.ts +4 -19
  239. package/src/i18n/review.ts +9 -4
  240. package/src/i18n/shared-labels.ts +0 -1
  241. package/src/index.ts +44 -114
  242. package/src/lib/dates.ts +24 -2
  243. package/src/lib/format.ts +66 -3
  244. package/src/lib/intl-cache.ts +4 -2
  245. package/src/lib/translation-review.ts +52 -0
  246. package/src/rhf/fields.tsx +2 -2
  247. package/src/search/search-index.ts +0 -15
  248. package/src/search.ts +8 -8
  249. package/src/shell/account-menu-language.tsx +7 -27
  250. package/src/shell/account-menu-text-size.tsx +13 -32
  251. package/src/shell/app-shell.tsx +0 -13
  252. package/src/shell/company-switcher.tsx +1 -1
  253. package/src/shell/dev-menu.tsx +1 -1
  254. package/src/shell/top-bar-parts-labels.ts +1 -1
  255. package/src/shell.ts +17 -44
  256. package/src/sync.ts +2 -15
  257. package/src/theme/chart-palette.ts +0 -2
  258. package/tokens.css +4 -4
  259. package/dist/billing/dialog-close.d.ts +0 -29
  260. package/dist/billing/dialog-close.js +0 -8
  261. package/dist/billing/dialog-close.js.map +0 -1
  262. package/dist/components/field-sync-labels.d.ts +0 -3
  263. package/dist/components/field-sync-labels.js +0 -12
  264. package/dist/components/field-sync-labels.js.map +0 -1
  265. package/dist/components/field-sync.d.ts +0 -186
  266. package/dist/components/field-sync.js +0 -269
  267. package/dist/components/field-sync.js.map +0 -1
  268. package/dist/components/section-nav.d.ts +0 -44
  269. package/dist/components/section-nav.js +0 -33
  270. package/dist/components/section-nav.js.map +0 -1
  271. package/dist/components/sign-chip-labels.d.ts +0 -3
  272. package/dist/components/sign-chip-labels.js +0 -11
  273. package/dist/components/sign-chip-labels.js.map +0 -1
  274. package/dist/components/sign-chip.d.ts +0 -77
  275. package/dist/components/sign-chip.js +0 -47
  276. package/dist/components/sign-chip.js.map +0 -1
  277. package/dist/lib/isin.d.ts +0 -38
  278. package/dist/lib/isin.js +0 -36
  279. package/dist/lib/isin.js.map +0 -1
  280. package/dist/shell/role-switcher.d.ts +0 -28
  281. package/dist/shell/role-switcher.js +0 -31
  282. package/dist/shell/role-switcher.js.map +0 -1
  283. package/src/billing/dialog-close.ts +0 -34
  284. package/src/components/field-sync-labels.ts +0 -14
  285. package/src/components/field-sync.tsx +0 -504
  286. package/src/components/section-nav.tsx +0 -72
  287. package/src/components/sign-chip-labels.ts +0 -13
  288. package/src/components/sign-chip.tsx +0 -133
  289. package/src/lib/isin.ts +0 -72
  290. package/src/shell/role-switcher.tsx +0 -53
package/dist/lib/isin.js DELETED
@@ -1,36 +0,0 @@
1
- "use client";
2
- function expandLetters(value) {
3
- return value.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));
4
- }
5
- function luhnSum(digits, doubleRightmost) {
6
- let sum = 0;
7
- let double = doubleRightmost;
8
- for (let i = digits.length - 1; i >= 0; i--) {
9
- let d = digits.charCodeAt(i) - 48;
10
- if (double) {
11
- d *= 2;
12
- if (d > 9) d -= 9;
13
- }
14
- sum += d;
15
- double = !double;
16
- }
17
- return sum;
18
- }
19
- function formatIsin(value) {
20
- return value.replace(/[\s-]/g, "").toUpperCase();
21
- }
22
- function isinCheckDigit(body) {
23
- const sum = luhnSum(expandLetters(formatIsin(body)), true);
24
- return String((10 - sum % 10) % 10);
25
- }
26
- function isValidIsin(value) {
27
- const isin = formatIsin(value);
28
- if (!/^[A-Z]{2}[A-Z0-9]{9}\d$/.test(isin)) return false;
29
- return luhnSum(expandLetters(isin), false) % 10 === 0;
30
- }
31
- export {
32
- formatIsin,
33
- isValidIsin,
34
- isinCheckDigit
35
- };
36
- //# sourceMappingURL=isin.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/lib/isin.ts"],"sourcesContent":["/**\n * ISIN helpers — ISO 6166 — for keksdose, whose securities holdings are keyed by ISIN\n * and whose import and edit forms checked only the shape (two letters, ten\n * characters): a mistyped ISIN saved happily and then matched no price feed.\n *\n * An ISIN is twelve characters: a two-letter prefix (the issuing country, or a\n * supranational code such as `XS`), nine letters or digits (the national number) and\n * one check digit. The check digit is the Luhn digit of the first eleven characters\n * with every letter expanded to two digits (A = 10 … Z = 35) — which is all that is\n * checked here. Whether the prefix is an assigned code, and whether the security\n * exists, is a lookup, not a checksum.\n *\n * Deprecated in 0.52, removed in 0.53 (decision 4 of the refactor audit 2026-10-30,\n * MOD-5): keksdose, which they were written for, never imported them, and no other app\n * does.\n */\n\n/** Letters to their two-digit numbers (A = 10 … Z = 35), digits as they are. */\nfunction expandLetters(value: string): string {\n return value.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));\n}\n\n/** The Luhn sum of `digits` with the RIGHTMOST digit doubled first — the shape of the\n * sum when a check digit is about to be appended. */\nfunction luhnSum(digits: string, doubleRightmost: boolean): number {\n let sum = 0;\n let double = doubleRightmost;\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48;\n if (double) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n double = !double;\n }\n return sum;\n}\n\n/**\n * The ISIN as it is printed: upper case, unbroken (ISO 6166 has no grouping), with\n * the spaces and hyphens a paste from a broker's statement carries dropped.\n *\n * @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.\n */\nexport function formatIsin(value: string): string {\n return value.replace(/[\\s-]/g, \"\").toUpperCase();\n}\n\n/**\n * The check digit for the first eleven characters of an ISIN (prefix and national\n * number). For BUILDING one — a test fixture, a demo — rather than checking it.\n *\n * @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.\n */\nexport function isinCheckDigit(body: string): string {\n const sum = luhnSum(expandLetters(formatIsin(body)), true);\n return String((10 - (sum % 10)) % 10);\n}\n\n/**\n * Whether `value` is a valid ISIN: two letters, nine letters or digits, one digit,\n * and a matching Luhn check digit over the letter-expanded form. Spaces, hyphens and\n * lower case are fine (see {@link formatIsin}).\n *\n * @deprecated 0.52 — none: no app checks an ISIN with it; copy lib/isin.ts (MOD-5); removed in 0.53.\n */\nexport function isValidIsin(value: string): boolean {\n const isin = formatIsin(value);\n if (!/^[A-Z]{2}[A-Z0-9]{9}\\d$/.test(isin)) return false;\n return luhnSum(expandLetters(isin), false) % 10 === 0;\n}\n"],"mappings":";AAkBA,SAAS,cAAc,OAAuB;AAC5C,SAAO,MAAM,QAAQ,UAAU,CAAC,MAAM,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;AACpE;AAIA,SAAS,QAAQ,QAAgB,iBAAkC;AACjE,MAAI,MAAM;AACV,MAAI,SAAS;AACb,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,QAAI,IAAI,OAAO,WAAW,CAAC,IAAI;AAC/B,QAAI,QAAQ;AACV,WAAK;AACL,UAAI,IAAI,EAAG,MAAK;AAAA,IAClB;AACA,WAAO;AACP,aAAS,CAAC;AAAA,EACZ;AACA,SAAO;AACT;AAQO,SAAS,WAAW,OAAuB;AAChD,SAAO,MAAM,QAAQ,UAAU,EAAE,EAAE,YAAY;AACjD;AAQO,SAAS,eAAe,MAAsB;AACnD,QAAM,MAAM,QAAQ,cAAc,WAAW,IAAI,CAAC,GAAG,IAAI;AACzD,SAAO,QAAQ,KAAM,MAAM,MAAO,EAAE;AACtC;AASO,SAAS,YAAY,OAAwB;AAClD,QAAM,OAAO,WAAW,KAAK;AAC7B,MAAI,CAAC,0BAA0B,KAAK,IAAI,EAAG,QAAO;AAClD,SAAO,QAAQ,cAAc,IAAI,GAAG,KAAK,IAAI,OAAO;AACtD;","names":[]}
@@ -1,28 +0,0 @@
1
- /** @deprecated 0.52 — `DevMenuProps` (`roles`, `currentRole`, `onRole`) (MOD-5); removed in 0.53. */
2
- export interface RoleSwitcherProps<R extends string> {
3
- roles: readonly R[];
4
- value: R;
5
- onChange: (role: R) => void;
6
- /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */
7
- "aria-label"?: string;
8
- heading?: string;
9
- /** The trigger's tooltip. Default: `topBar.role(value)`. */
10
- title?: string;
11
- }
12
- /**
13
- * A reusable user-role switcher (feedback #337): a thin wrapper over
14
- * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in
15
- * the top bar with just the role list + current value + a handler. Typically
16
- * dev-only / UI-only, but the app decides where and whether to render it.
17
- *
18
- * The flask wears `--warning` rather than a fixed `amber-500`: it is a caution
19
- * marker — the app is not showing you your own role — and the token carries that
20
- * amber across both themes instead of pinning one hue onto every palette.
21
- *
22
- * Deprecated in 0.52, removed in 0.53 (decision 4 of the refactor audit 2026-10-30,
23
- * MOD-5): the role switch every app draws is `DevMenu`'s Account section (0.51), and no
24
- * app renders this one.
25
- *
26
- * @deprecated 0.52 — `DevMenu` with `roles`, `currentRole` and `onRole` (MOD-5); removed in 0.53.
27
- */
28
- export declare function RoleSwitcher<R extends string>({ roles, value, onChange, "aria-label": ariaLabel, heading, title, }: RoleSwitcherProps<R>): import("react").JSX.Element;
@@ -1,31 +0,0 @@
1
- "use client";
2
- import { jsx } from "react/jsx-runtime";
3
- import { FlaskConical } from "lucide-react";
4
- import { OptionSwitcherMenu } from "./option-switcher-menu.js";
5
- import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
6
- function RoleSwitcher({
7
- roles,
8
- value,
9
- onChange,
10
- "aria-label": ariaLabel,
11
- heading,
12
- title
13
- }) {
14
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });
15
- return /* @__PURE__ */ jsx(
16
- OptionSwitcherMenu,
17
- {
18
- "aria-label": labels.switchRole,
19
- title: title ?? labels.role(value),
20
- heading,
21
- icon: /* @__PURE__ */ jsx(FlaskConical, { className: "size-5 text-warning" }),
22
- options: roles.map((r) => ({ value: r, label: r })),
23
- value,
24
- onSelect: onChange
25
- }
26
- );
27
- }
28
- export {
29
- RoleSwitcher
30
- };
31
- //# sourceMappingURL=role-switcher.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/shell/role-switcher.tsx"],"sourcesContent":["import { FlaskConical } from \"lucide-react\";\nimport { OptionSwitcherMenu } from \"./option-switcher-menu\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/** @deprecated 0.52 — `DevMenuProps` (`roles`, `currentRole`, `onRole`) (MOD-5); removed in 0.53. */\nexport interface RoleSwitcherProps<R extends string> {\n roles: readonly R[];\n value: R;\n onChange: (role: R) => void;\n /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n heading?: string;\n /** The trigger's tooltip. Default: `topBar.role(value)`. */\n title?: string;\n}\n\n/**\n * A reusable user-role switcher (feedback #337): a thin wrapper over\n * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in\n * the top bar with just the role list + current value + a handler. Typically\n * dev-only / UI-only, but the app decides where and whether to render it.\n *\n * The flask wears `--warning` rather than a fixed `amber-500`: it is a caution\n * marker — the app is not showing you your own role — and the token carries that\n * amber across both themes instead of pinning one hue onto every palette.\n *\n * Deprecated in 0.52, removed in 0.53 (decision 4 of the refactor audit 2026-10-30,\n * MOD-5): the role switch every app draws is `DevMenu`'s Account section (0.51), and no\n * app renders this one.\n *\n * @deprecated 0.52 — `DevMenu` with `roles`, `currentRole` and `onRole` (MOD-5); removed in 0.53.\n */\nexport function RoleSwitcher<R extends string>({\n roles,\n value,\n onChange,\n \"aria-label\": ariaLabel,\n heading,\n title,\n}: RoleSwitcherProps<R>) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });\n return (\n <OptionSwitcherMenu<R>\n aria-label={labels.switchRole}\n title={title ?? labels.role(value)}\n heading={heading}\n icon={<FlaskConical className=\"size-5 text-warning\" />}\n options={roles.map((r) => ({ value: r, label: r }))}\n value={value}\n onSelect={onChange}\n />\n );\n}\n"],"mappings":";AA8CY;AA9CZ,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB,oBAAoB;AA8B9C,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,YAAY,UAAU,CAAC;AACvF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,OAAO;AAAA,MACnB,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,MACjC;AAAA,MACA,MAAM,oBAAC,gBAAa,WAAU,uBAAsB;AAAA,MACpD,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAClD;AAAA,MACA,UAAU;AAAA;AAAA,EACZ;AAEJ;","names":[]}
@@ -1,34 +0,0 @@
1
- /**
2
- * How the three billing confirms — `CancelSubscriptionConfirm`, `PlanSwitchConfirm`,
3
- * `GrantWarningConfirm` — are closed in 0.52: `onClose`, as every other kit dialog
4
- * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5), and for one minor the deprecated
5
- * `onOpenChange`, called with `false` when there is no `onClose`.
6
- *
7
- * 0.52 made `onOpenChange` optional for its deprecation and gave `onClose` beside it as
8
- * optional too, so a confirm given NEITHER compiled — `<CancelSubscriptionConfirm open
9
- * periodEnd={…} onConfirm={…} />` — and Back, Escape and the backdrop then called nothing:
10
- * a dialog nobody could dismiss, where 0.51's compiler had required `onOpenChange` (0.52
11
- * code review, cancel-subscription.tsx). And the fallback between the two was written
12
- * three times, once per dialog.
13
- *
14
- * @internal Not part of the barrel; each dialog's props type is.
15
- */
16
-
17
- /** The two close props as a dialog's own props interface declares them. */
18
- interface CloseProps {
19
- onClose?: () => void;
20
- onOpenChange?: (open: boolean) => void;
21
- }
22
-
23
- /**
24
- * `P` with ONE of its two close props required — `onClose`, or alone the deprecated
25
- * `onOpenChange`; both may be given, and `onClose` is then the one called. Built from
26
- * `P`'s own members (`Pick`), so their docs and the deprecation stay the ones `P` writes.
27
- */
28
- export type WithDialogClose<P extends CloseProps> = P &
29
- (Required<Pick<P, "onClose">> | Required<Pick<P, "onOpenChange">>);
30
-
31
- /** The dialog's close: `onClose`, else `onOpenChange(false)`. */
32
- export function dialogClose({ onClose, onOpenChange }: CloseProps): () => void {
33
- return () => (onClose ? onClose() : onOpenChange?.(false));
34
- }
@@ -1,14 +0,0 @@
1
- // The English words of ./field-sync, in a module with no component code: the English
2
- // catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one
3
- // chunk like the other languages (0.34, KS U8). ./field-sync re-exports them.
4
-
5
- import type { FieldSyncLabels } from "./field-sync";
6
-
7
- /** @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
8
- export const DEFAULT_FIELD_SYNC_LABELS: FieldSyncLabels = {
9
- synced: "Saved",
10
- edited: "Unsaved changes",
11
- pending: "Saving…",
12
- error: "Could not save",
13
- retry: "Retry",
14
- };
@@ -1,504 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import type { ReactNode } from "react";
3
- import { AlertCircle, Check, Loader2, Pencil, PencilLine } from "lucide-react";
4
- import { cn } from "../lib/cn";
5
- import { Tooltip } from "./tooltip";
6
- import { FOCUS_RING } from "./focus-ring";
7
- import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
8
- import { useLargeText } from "../hooks/use-large-text";
9
- import { DEFAULT_FIELD_SYNC_LABELS } from "./field-sync-labels";
10
-
11
- /**
12
- * Sync state for a field backed by a database row.
13
- *
14
- * The four states a value can be in between the keyboard and the server:
15
- *
16
- * synced the draft equals what was last persisted
17
- * edited the draft differs and nothing has been sent yet
18
- * pending a save is in flight
19
- * error the last save was rejected; `error` carries why
20
- *
21
- * `useFieldSync` is the engine and holds no opinion about markup;
22
- * `FieldSyncIndicator` is the affordance and holds no state. They are separate
23
- * because the state is useful without the dot — a form can disable its submit
24
- * button while any field is `pending`, and a router can block navigation while
25
- * any field is `edited`.
26
- *
27
- * Deprecated in 0.52 as a whole, removed in 0.53 (decision 4 of the refactor audit
28
- * 2026-10-30, MOD-5): no app renders it or calls the hook — 483 lines and a namespace in
29
- * seven catalogues that only this package's tests and showcase read. An app that wants
30
- * it keeps a copy of this file.
31
- *
32
- * @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53.
33
- */
34
- export type FieldSyncState = "synced" | "edited" | "pending" | "error";
35
-
36
- /** @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
37
- export interface FieldSyncLabels {
38
- synced: string;
39
- edited: string;
40
- pending: string;
41
- /** Shown when the failure carries no message of its own. */
42
- error: string;
43
- retry: string;
44
- }
45
-
46
- // Defined in ./field-sync-labels, which holds no component code (0.34, KS U8).
47
- export { DEFAULT_FIELD_SYNC_LABELS };
48
-
49
- /** @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
50
- export interface UseFieldSyncOptions<T> {
51
- /** What the database currently holds. Changing it re-seeds a CLEAN field. */
52
- value: T;
53
- /** Persist `next`. Reject — or throw — to put the field in `error`. */
54
- onSave: (next: T) => Promise<void> | void;
55
- /**
56
- * Quiet period before an edit saves itself, in ms. Default `0`: no auto-save at
57
- * all, so `save()` is the only way to persist — called on blur by `FieldSyncRow`,
58
- * and on Enter by the caller. Saving after a pause in typing wrote half-finished
59
- * values to the database whenever somebody stopped to think; pass a positive
60
- * number only for a field where that is genuinely wanted.
61
- */
62
- debounceMs?: number;
63
- /** How to tell two values apart. Defaults to `Object.is`. */
64
- equals?: (a: T, b: T) => boolean;
65
- /** Called once per rejected save, with whatever `onSave` threw. */
66
- onError?: (error: Error) => void;
67
- }
68
-
69
- /** @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
70
- export interface UseFieldSyncReturn<T> {
71
- /** The draft — bind this to the input, not the `value` option. */
72
- value: T;
73
- setValue: (next: T) => void;
74
- state: FieldSyncState;
75
- /** Non-null exactly when `state === "error"`. */
76
- error: Error | null;
77
- /** True whenever the draft differs from the last persisted value. */
78
- dirty: boolean;
79
- /** Persist now, cancelling any pending debounce. A no-op on a clean field. */
80
- save: () => void;
81
- /** Send the current draft again after a failure. */
82
- retry: () => void;
83
- /** Throw the draft away and go back to the last persisted value. */
84
- reset: () => void;
85
- }
86
-
87
- function toError(cause: unknown): Error {
88
- if (cause instanceof Error) return cause;
89
- // An EMPTY message for a rejection that carried none, not an English one: the
90
- // indicator shows `error.message` when there is one and the translated
91
- // `fieldSync.error` otherwise, so a fallback message here was an English sentence
92
- // that outranked the app's own translation. The original value stays on `cause`
93
- // for whoever logs it from `onError`.
94
- return new Error(typeof cause === "string" ? cause : "", { cause });
95
- }
96
-
97
- /**
98
- * Track one database-backed field: a local draft, a debounced save, and the state
99
- * of the round trip.
100
- *
101
- * Four things this handles that a `useState` + `useEffect` in a page does not:
102
- *
103
- * 1. **An edit during a save is never lost.** A save in flight is not cancelled
104
- * and not raced — when it settles, the draft is compared again and sent once
105
- * more if it moved. Cancelling instead would drop a keystroke that arrived
106
- * mid-flight; firing immediately would let two writes land out of order.
107
- * 2. **The server value is only adopted when the field is clean.** An external
108
- * refresh that overwrote what somebody was typing would be a data-loss bug, so
109
- * a dirty draft wins and keeps winning until it is saved or reset.
110
- * 3. **Nothing is set after unmount.** The save resolves into a component that
111
- * may be gone — a row scrolled out of a virtualised table, a closed dialog.
112
- * 4. **`onSave` is read through a ref**, so an inline arrow — which every call
113
- * site will pass — does not restart the debounce on every render.
114
- *
115
- * @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53.
116
- */
117
- export function useFieldSync<T>({
118
- value,
119
- onSave,
120
- debounceMs = 0,
121
- equals = Object.is,
122
- onError,
123
- }: UseFieldSyncOptions<T>): UseFieldSyncReturn<T> {
124
- const [draft, setDraft] = useState<T>(value);
125
- const [state, setState] = useState<FieldSyncState>("synced");
126
- const [error, setError] = useState<Error | null>(null);
127
-
128
- // The last value known to be on the server. Not state: it changes together with
129
- // `state` inside the save, and a second render for it would be a wasted one.
130
- const savedRef = useRef<T>(value);
131
- const draftRef = useRef<T>(value);
132
- const inFlightRef = useRef(false);
133
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
134
- const mountedRef = useRef(true);
135
-
136
- // Latest-ref for the callbacks and comparators, so a caller passing inline
137
- // functions (all of them) does not invalidate the timer on every render.
138
- const onSaveRef = useRef(onSave);
139
- const onErrorRef = useRef(onError);
140
- const equalsRef = useRef(equals);
141
- useEffect(() => {
142
- onSaveRef.current = onSave;
143
- onErrorRef.current = onError;
144
- equalsRef.current = equals;
145
- });
146
-
147
- useEffect(() => {
148
- mountedRef.current = true;
149
- return () => {
150
- mountedRef.current = false;
151
- if (timerRef.current) clearTimeout(timerRef.current);
152
- };
153
- }, []);
154
-
155
- // A named inner function so the settle path can start the next round by calling it
156
- // again; a `useCallback` cannot refer to its own binding while it is being built.
157
- const flush = useCallback(() => {
158
- const run = async (): Promise<void> => {
159
- if (inFlightRef.current) return; // the settle path below picks the new draft up
160
- const next = draftRef.current;
161
- if (equalsRef.current(next, savedRef.current)) return;
162
-
163
- inFlightRef.current = true;
164
- setState("pending");
165
- setError(null);
166
- try {
167
- await onSaveRef.current(next);
168
- savedRef.current = next;
169
- inFlightRef.current = false;
170
- if (!mountedRef.current) return;
171
- // The draft may have moved while this was in the air. Compare against what is
172
- // on screen NOW, not against what was sent.
173
- if (equalsRef.current(draftRef.current, next)) {
174
- setState("synced");
175
- } else {
176
- setState("edited");
177
- void run();
178
- }
179
- } catch (cause) {
180
- inFlightRef.current = false;
181
- const err = toError(cause);
182
- onErrorRef.current?.(err);
183
- if (!mountedRef.current) return;
184
- setState("error");
185
- setError(err);
186
- }
187
- };
188
- return run();
189
- }, []);
190
-
191
- const setValue = useCallback(
192
- (next: T) => {
193
- draftRef.current = next;
194
- setDraft(next);
195
- if (timerRef.current) clearTimeout(timerRef.current);
196
-
197
- if (equalsRef.current(next, savedRef.current)) {
198
- // Typed back to what the server already has. Nothing to send, and an error
199
- // from the previous attempt no longer describes anything.
200
- setState(inFlightRef.current ? "pending" : "synced");
201
- setError(null);
202
- return;
203
- }
204
-
205
- setState("edited");
206
- setError(null);
207
- if (debounceMs > 0) timerRef.current = setTimeout(() => void flush(), debounceMs);
208
- },
209
- [debounceMs, flush],
210
- );
211
-
212
- const save = useCallback(() => {
213
- if (timerRef.current) clearTimeout(timerRef.current);
214
- void flush();
215
- }, [flush]);
216
-
217
- const retry = save;
218
-
219
- const reset = useCallback(() => {
220
- if (timerRef.current) clearTimeout(timerRef.current);
221
- draftRef.current = savedRef.current;
222
- setDraft(savedRef.current);
223
- setState(inFlightRef.current ? "pending" : "synced");
224
- setError(null);
225
- }, []);
226
-
227
- // Anything that is not `synced` has something not yet on the server: `edited` and
228
- // `error` obviously, and `pending` too — the write is in the air, not landed.
229
- // Derived from state rather than by comparing against `savedRef` during render,
230
- // because a ref read here would not re-render when the comparison's answer changes.
231
- const dirty = state !== "synced";
232
-
233
- // Adopt a new server value ONLY when there is nothing local to lose.
234
- useEffect(() => {
235
- if (inFlightRef.current) return;
236
- if (!equalsRef.current(draftRef.current, savedRef.current)) return; // dirty: keep it
237
- if (equalsRef.current(value, savedRef.current)) return; // unchanged
238
- savedRef.current = value;
239
- draftRef.current = value;
240
- setDraft(value);
241
- setState("synced");
242
- setError(null);
243
- }, [value]);
244
-
245
- return { value: draft, setValue, state, error, dirty, save, retry, reset };
246
- }
247
-
248
- const STATE_STYLE: Record<FieldSyncState, { color: string; Icon: typeof Check }> = {
249
- synced: { color: "var(--status-synced)", Icon: Check },
250
- edited: { color: "var(--status-edited)", Icon: Pencil },
251
- pending: { color: "var(--status-pending)", Icon: Loader2 },
252
- error: { color: "var(--status-error)", Icon: AlertCircle },
253
- };
254
-
255
- /** @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
256
- export interface FieldSyncIndicatorProps {
257
- state: FieldSyncState;
258
- /** The failure. Its message becomes the tooltip when `state === "error"`. */
259
- error?: Error | null;
260
- /** Override any of the five English defaults. */
261
- labels?: LabelOverride<FieldSyncLabels>;
262
- /** Render the label beside the icon instead of only in the tooltip. Always on at
263
- * Large and Extra large (0.32, docs/text-size-harmonization.md §4 "No fact only in a
264
- * tooltip"): the reader who asked for bigger type gets the words, not a 14 px glyph
265
- * and a bubble. */
266
- showLabel?: boolean;
267
- /** When given, the error state renders a retry button after the icon. */
268
- onRetry?: () => void;
269
- className?: string;
270
- }
271
-
272
- /**
273
- * The affordance for {@link useFieldSync}: a coloured icon, and the reason on hover.
274
- *
275
- * **Every state has its own icon, and that is not decoration.** The four colours
276
- * include the green/red pair, which is the one combination this package's palette
277
- * otherwise refuses (see the note in `tokens.css`) because red-green colour blindness
278
- * affects roughly one man in twelve. The icon is what carries the meaning for them,
279
- * and what carries it into a greyscale print or a screenshot. WCAG 1.4.1 is the same
280
- * requirement stated formally.
281
- *
282
- * The state is also announced: the wrapper is a polite live region, so a screen
283
- * reader hears "Saving…" then "Saved" without the user going looking. `error` is
284
- * assertive instead — a failed write is worth interrupting for.
285
- *
286
- * @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53.
287
- */
288
- export function FieldSyncIndicator({
289
- state,
290
- error,
291
- labels,
292
- showLabel = false,
293
- onRetry,
294
- className,
295
- }: FieldSyncIndicatorProps) {
296
- const l = useKitLabels("fieldSync", DEFAULT_FIELD_SYNC_LABELS, labels);
297
- const large = useLargeText();
298
- showLabel = showLabel || large;
299
- const { color, Icon } = STATE_STYLE[state];
300
- const text = state === "error" ? (error?.message?.trim() ? error.message : l.error) : l[state];
301
-
302
- return (
303
- <span
304
- // The ICON carries the status colour; the words are `--text-secondary`, because
305
- // the amber of `edited` is a graphic colour (3:1), not a text colour (4.5:1).
306
- className={cn("inline-flex items-center gap-1.5 text-xs text-secondary", className)}
307
- // Polite for the three ordinary states so save chatter does not interrupt
308
- // typing; assertive for a failure, which the user has to know about now.
309
- role={state === "error" ? "alert" : "status"}
310
- aria-live={state === "error" ? "assertive" : "polite"}
311
- >
312
- <Tooltip label={showLabel ? undefined : text} redact={state === "error"}>
313
- <span className="inline-flex items-center gap-1.5">
314
- <Icon
315
- aria-hidden
316
- style={{ color }}
317
- className={cn("size-3.5 shrink-0", state === "pending" && "animate-spin")}
318
- />
319
- {/* Always in the DOM, so the live region has something to announce even
320
- when the label is visually a tooltip. */}
321
- <span className={showLabel ? undefined : "sr-only"}>{text}</span>
322
- </span>
323
- </Tooltip>
324
- {state === "error" && onRetry && (
325
- <button
326
- type="button"
327
- onClick={onRetry}
328
- // The kit's focus frame (0.32, §5), stood off the word.
329
- className={cn(
330
- "rounded underline underline-offset-2 hover:no-underline focus-visible:ring-offset-2 focus-visible:ring-offset-surface",
331
- FOCUS_RING,
332
- )}
333
- style={{ color }}
334
- >
335
- {l.retry}
336
- </button>
337
- )}
338
- </span>
339
- );
340
- }
341
-
342
- /**
343
- * The field's own frame, coloured by state — applied from the WRAPPER to whatever
344
- * native control sits inside it, so {@link FieldSyncRow} works with any of the kit's
345
- * inputs (and a caller's own) without each one growing a `syncState` prop.
346
- *
347
- * Border AND a 1px ring, for the reason {@link FIELD_INVALID} gives: a lone 1px
348
- * border loses a vertical edge to sub-pixel spread at 125% display scaling. The
349
- * `:focus` twins are what keep the state visible while the reader is typing —
350
- * the base field class's `focus:` brand colour is `(0,2,0)` and would otherwise win
351
- * over a descendant selector exactly when the field is in use.
352
- *
353
- * `synced` is green, but {@link FieldSyncRow} paints it only for a moment after a
354
- * save lands — a form of eight permanently green boxes says nothing the absence of
355
- * colour does not.
356
- *
357
- * Every class is a literal so Tailwind's scanner can see it.
358
- *
359
- * @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53.
360
- */
361
- export const FIELD_SYNC_FRAME: Record<FieldSyncState, string> = {
362
- synced:
363
- "[&_:is(input,select,textarea)]:border-[var(--status-synced)] [&_:is(input,select,textarea)]:ring-1 [&_:is(input,select,textarea)]:ring-[var(--status-synced)] [&_:is(input,select,textarea):focus]:border-[var(--status-synced)] [&_:is(input,select,textarea):focus]:ring-[var(--status-synced)]",
364
- edited:
365
- "[&_:is(input,select,textarea)]:border-[var(--status-edited)] [&_:is(input,select,textarea)]:ring-1 [&_:is(input,select,textarea)]:ring-[var(--status-edited)] [&_:is(input,select,textarea):focus]:border-[var(--status-edited)] [&_:is(input,select,textarea):focus]:ring-[var(--status-edited)]",
366
- pending:
367
- "[&_:is(input,select,textarea)]:border-[var(--status-pending)] [&_:is(input,select,textarea)]:ring-1 [&_:is(input,select,textarea)]:ring-[var(--status-pending)] [&_:is(input,select,textarea):focus]:border-[var(--status-pending)] [&_:is(input,select,textarea):focus]:ring-[var(--status-pending)]",
368
- error:
369
- "[&_:is(input,select,textarea)]:border-[var(--status-error)] [&_:is(input,select,textarea)]:ring-1 [&_:is(input,select,textarea)]:ring-[var(--status-error)] [&_:is(input,select,textarea):focus]:border-[var(--status-error)] [&_:is(input,select,textarea):focus]:ring-[var(--status-error)]",
370
- };
371
-
372
- /** How long the green "saved" frame and check stay after a save lands, in ms.
373
- *
374
- * @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
375
- export const FIELD_SYNC_SAVED_MS = 1500;
376
-
377
- /** @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53. */
378
- export interface FieldSyncRowProps<T> extends Omit<FieldSyncIndicatorProps, "state" | "error" | "showLabel"> {
379
- sync: UseFieldSyncReturn<T>;
380
- children: ReactNode;
381
- /** Call `sync.save()` when focus leaves the field. Default true — together with the
382
- * hook's default `debounceMs: 0` this is "save on blur": nothing is written while
383
- * the reader is still typing, however long they pause. */
384
- saveOnBlur?: boolean;
385
- /** How long the saved confirmation shows. Default {@link FIELD_SYNC_SAVED_MS}. */
386
- savedMs?: number;
387
- }
388
-
389
- /**
390
- * A field wearing its sync state — as its FRAME COLOUR and an ICON AT THE END OF THE
391
- * FIELD, so the field never changes size (a helper line appearing under it moved
392
- * everything below it on every save):
393
- *
394
- * edited amber frame, pencil — changed, not yet saved
395
- * pending blue frame, spinning circle — for as long as the save is in flight
396
- * synced green frame, check — for {@link FIELD_SYNC_SAVED_MS} after a
397
- * save lands, then an ordinary field again
398
- * error red frame, alert mark — hover or focus the mark for the reason;
399
- * click it to retry
400
- *
401
- * It saves when focus LEAVES the field (`saveOnBlur`), not after a pause in typing: a
402
- * database write in the middle of a word is a write nobody asked for. Enter in a
403
- * single-line field is the caller's to wire to `sync.save`.
404
- *
405
- * Colour is never the only signal: every state has its own icon, and the state text is
406
- * always in the DOM as a live region — polite for the ordinary states, assertive for
407
- * a failure. The input gets end padding for the icon whether or not one is showing,
408
- * so the text never reflows when the state changes.
409
- *
410
- * @deprecated 0.52 — none: no app uses the FieldSync family (MOD-5); removed in 0.53.
411
- */
412
- export function FieldSyncRow<T>({
413
- sync,
414
- children,
415
- className,
416
- labels,
417
- onRetry,
418
- saveOnBlur = true,
419
- savedMs = FIELD_SYNC_SAVED_MS,
420
- }: FieldSyncRowProps<T>) {
421
- const l = useKitLabels("fieldSync", DEFAULT_FIELD_SYNC_LABELS, labels);
422
- const { state, error } = sync;
423
- const isError = state === "error";
424
- const text = isError ? (error?.message?.trim() ? error.message : l.error) : l[state];
425
- const retry = onRetry ?? sync.retry;
426
-
427
- // The saved confirmation: on only for the moment after a save lands. Keyed on the
428
- // TRANSITION pending → synced, so a field that starts synced shows nothing. The
429
- // transition is seen while rendering, against the previous state; the effect only
430
- // owns the timer that turns it off again.
431
- const [previous, setPrevious] = useState(state);
432
- const [justSaved, setJustSaved] = useState(false);
433
- if (state !== previous) {
434
- setPrevious(state);
435
- setJustSaved(state === "synced" && previous === "pending");
436
- }
437
- useEffect(() => {
438
- if (!justSaved) return;
439
- const t = setTimeout(() => setJustSaved(false), savedMs);
440
- return () => clearTimeout(t);
441
- }, [justSaved, savedMs]);
442
- const shown: FieldSyncState | null = state === "synced" ? (justSaved ? "synced" : null) : state;
443
-
444
- return (
445
- /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- not an interaction:
446
- `onBlur` listens for focus leaving the row's own controls (which are the
447
- interactive elements) to save; the wrapper itself is never focused. */
448
- <div
449
- data-field-sync={state}
450
- className={cn(
451
- // `relative` for the icon and for the sr-only live region's containing block.
452
- "relative [&_:is(input,select,textarea)]:pe-9",
453
- shown && FIELD_SYNC_FRAME[shown],
454
- className,
455
- )}
456
- onBlur={
457
- saveOnBlur
458
- ? (e) => {
459
- // Focus moving between two controls INSIDE the row is not leaving it.
460
- if (!e.currentTarget.contains(e.relatedTarget as Node | null)) sync.save();
461
- }
462
- : undefined
463
- }
464
- >
465
- {children}
466
- <span className="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-3">
467
- {state === "edited" && <PencilLine aria-hidden className="size-4 text-[var(--status-edited)]" />}
468
- {state === "pending" && (
469
- <Loader2
470
- aria-hidden
471
- className="size-4 animate-spin text-[var(--status-pending)] motion-reduce:animate-none"
472
- />
473
- )}
474
- {state === "synced" && (
475
- <Check
476
- aria-hidden
477
- className={cn(
478
- "size-4 text-[var(--status-synced)] transition-opacity duration-500 motion-reduce:transition-none",
479
- justSaved ? "opacity-100" : "opacity-0",
480
- )}
481
- />
482
- )}
483
- {isError && (
484
- <Tooltip label={text} portal redact>
485
- <button
486
- type="button"
487
- onClick={retry}
488
- aria-label={l.retry}
489
- className={cn(
490
- "pointer-events-auto flex rounded-full text-[var(--status-error)] focus-visible:ring-offset-2 focus-visible:ring-offset-surface",
491
- FOCUS_RING,
492
- )}
493
- >
494
- <AlertCircle aria-hidden className="size-4" />
495
- </button>
496
- </Tooltip>
497
- )}
498
- </span>
499
- <span role={isError ? "alert" : "status"} aria-live={isError ? "assertive" : "polite"} className="sr-only">
500
- {shown === null ? "" : text}
501
- </span>
502
- </div>
503
- );
504
- }