@eifi1/ui-kit 0.11.0 → 0.12.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 (342) hide show
  1. package/README.md +57 -32
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +24 -4
  12. package/dist/components/amount-input.js +5 -2
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/button-group.d.ts +51 -2
  21. package/dist/components/button-group.js +5 -1
  22. package/dist/components/button-group.js.map +1 -1
  23. package/dist/components/calculator.d.ts +15 -4
  24. package/dist/components/calendar-heatmap.d.ts +41 -3
  25. package/dist/components/calendar-heatmap.js +48 -7
  26. package/dist/components/calendar-heatmap.js.map +1 -1
  27. package/dist/components/chip.d.ts +2 -1
  28. package/dist/components/chip.js +6 -3
  29. package/dist/components/chip.js.map +1 -1
  30. package/dist/components/choice-card.d.ts +35 -3
  31. package/dist/components/choice-card.js +83 -25
  32. package/dist/components/choice-card.js.map +1 -1
  33. package/dist/components/combobox-core.js +2 -0
  34. package/dist/components/combobox-core.js.map +1 -1
  35. package/dist/components/combobox.d.ts +2 -2
  36. package/dist/components/combobox.js +31 -9
  37. package/dist/components/combobox.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +51 -70
  39. package/dist/components/copy-button.js +1 -1
  40. package/dist/components/copy-button.js.map +1 -1
  41. package/dist/components/data-table-cells.d.ts +63 -0
  42. package/dist/components/data-table-cells.js +64 -0
  43. package/dist/components/data-table-cells.js.map +1 -0
  44. package/dist/components/data-table-filter-popover.d.ts +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +31 -1
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +13 -0
  49. package/dist/components/data-table-labels.js +8 -1
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table.d.ts +1 -1
  52. package/dist/components/data-table.js +102 -12
  53. package/dist/components/data-table.js.map +1 -1
  54. package/dist/components/date-picker.d.ts +3 -1
  55. package/dist/components/date-picker.js +15 -2
  56. package/dist/components/date-picker.js.map +1 -1
  57. package/dist/components/description-list.d.ts +23 -3
  58. package/dist/components/description-list.js +23 -3
  59. package/dist/components/description-list.js.map +1 -1
  60. package/dist/components/dialog-frame.d.ts +1 -0
  61. package/dist/components/entity-combobox.d.ts +1 -1
  62. package/dist/components/entity-combobox.js +18 -3
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/error-boundary.d.ts +92 -0
  65. package/dist/components/error-boundary.js +111 -0
  66. package/dist/components/error-boundary.js.map +1 -0
  67. package/dist/components/field.d.ts +30 -4
  68. package/dist/components/field.js +40 -17
  69. package/dist/components/field.js.map +1 -1
  70. package/dist/components/file-button.d.ts +50 -208
  71. package/dist/components/file-dropzone.d.ts +49 -2
  72. package/dist/components/floating-panel.d.ts +13 -4
  73. package/dist/components/floating-panel.js +7 -3
  74. package/dist/components/floating-panel.js.map +1 -1
  75. package/dist/components/form-actions.d.ts +53 -0
  76. package/dist/components/form-actions.js +98 -0
  77. package/dist/components/form-actions.js.map +1 -0
  78. package/dist/components/image-grid.d.ts +67 -0
  79. package/dist/components/image-grid.js +112 -0
  80. package/dist/components/image-grid.js.map +1 -0
  81. package/dist/components/lightbox.d.ts +104 -0
  82. package/dist/components/lightbox.js +229 -0
  83. package/dist/components/lightbox.js.map +1 -0
  84. package/dist/components/line-items.d.ts +114 -0
  85. package/dist/components/line-items.js +238 -0
  86. package/dist/components/line-items.js.map +1 -0
  87. package/dist/components/list.d.ts +23 -2
  88. package/dist/components/list.js +32 -13
  89. package/dist/components/list.js.map +1 -1
  90. package/dist/components/loading-state.d.ts +24 -0
  91. package/dist/components/loading-state.js +32 -0
  92. package/dist/components/loading-state.js.map +1 -0
  93. package/dist/components/menu-item.d.ts +2 -1
  94. package/dist/components/menu-item.js +5 -1
  95. package/dist/components/menu-item.js.map +1 -1
  96. package/dist/components/mini-calendar.d.ts +13 -1
  97. package/dist/components/mini-calendar.js +46 -2
  98. package/dist/components/mini-calendar.js.map +1 -1
  99. package/dist/components/modal.d.ts +20 -1
  100. package/dist/components/modal.js +26 -2
  101. package/dist/components/modal.js.map +1 -1
  102. package/dist/components/month-picker.d.ts +21 -1
  103. package/dist/components/month-picker.js +139 -26
  104. package/dist/components/month-picker.js.map +1 -1
  105. package/dist/components/multi-entity-combobox.d.ts +1 -1
  106. package/dist/components/multi-entity-combobox.js +19 -3
  107. package/dist/components/multi-entity-combobox.js.map +1 -1
  108. package/dist/components/nav-pills.d.ts +2 -1
  109. package/dist/components/nav-pills.js +5 -1
  110. package/dist/components/nav-pills.js.map +1 -1
  111. package/dist/components/number-field.d.ts +24 -5
  112. package/dist/components/number-field.js +5 -7
  113. package/dist/components/number-field.js.map +1 -1
  114. package/dist/components/number-input.d.ts +25 -5
  115. package/dist/components/number-input.js +7 -1
  116. package/dist/components/number-input.js.map +1 -1
  117. package/dist/components/numpad-sheet.d.ts +15 -4
  118. package/dist/components/passkeys-setting.d.ts +67 -0
  119. package/dist/components/passkeys-setting.js +214 -0
  120. package/dist/components/passkeys-setting.js.map +1 -0
  121. package/dist/components/picker-sheet.js +2 -1
  122. package/dist/components/picker-sheet.js.map +1 -1
  123. package/dist/components/pie-chart-labels.d.ts +20 -0
  124. package/dist/components/pie-chart-labels.js +12 -0
  125. package/dist/components/pie-chart-labels.js.map +1 -0
  126. package/dist/components/pie-chart.d.ts +101 -0
  127. package/dist/components/pie-chart.js +283 -0
  128. package/dist/components/pie-chart.js.map +1 -0
  129. package/dist/components/progress-bar.d.ts +44 -3
  130. package/dist/components/progress-bar.js +58 -22
  131. package/dist/components/progress-bar.js.map +1 -1
  132. package/dist/components/qr-code.d.ts +34 -0
  133. package/dist/components/qr-code.js +57 -0
  134. package/dist/components/qr-code.js.map +1 -0
  135. package/dist/components/search-field.d.ts +15 -3
  136. package/dist/components/search-field.js +5 -1
  137. package/dist/components/search-field.js.map +1 -1
  138. package/dist/components/settings-fields.d.ts +49 -1
  139. package/dist/components/signed-amount.d.ts +121 -0
  140. package/dist/components/signed-amount.js +152 -0
  141. package/dist/components/signed-amount.js.map +1 -0
  142. package/dist/components/skeleton.d.ts +10 -3
  143. package/dist/components/skeleton.js +12 -2
  144. package/dist/components/skeleton.js.map +1 -1
  145. package/dist/components/stat-tile.d.ts +2 -1
  146. package/dist/components/stat-tile.js +5 -2
  147. package/dist/components/stat-tile.js.map +1 -1
  148. package/dist/components/table.d.ts +39 -5
  149. package/dist/components/table.js +45 -11
  150. package/dist/components/table.js.map +1 -1
  151. package/dist/components/text-link.d.ts +176 -0
  152. package/dist/components/text-link.js +109 -0
  153. package/dist/components/text-link.js.map +1 -0
  154. package/dist/components/time-input.d.ts +49 -1
  155. package/dist/components/toggle-group.d.ts +13 -0
  156. package/dist/components/toggle-group.js +35 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +36 -3
  159. package/dist/components/toggle-legend.js +49 -0
  160. package/dist/components/toggle-legend.js.map +1 -1
  161. package/dist/components/ui.d.ts +51 -779
  162. package/dist/components/ui.js +215 -23
  163. package/dist/components/ui.js.map +1 -1
  164. package/dist/components/use-table-state.d.ts +3 -52
  165. package/dist/components/use-table-state.js +128 -30
  166. package/dist/components/use-table-state.js.map +1 -1
  167. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  168. package/dist/data-table.d.ts +2 -1
  169. package/dist/data-table.js +5 -1
  170. package/dist/data-table.js.map +1 -1
  171. package/dist/feedback/feedback-dialog.d.ts +26 -3
  172. package/dist/feedback/feedback-dialog.js +16 -2
  173. package/dist/feedback/feedback-dialog.js.map +1 -1
  174. package/dist/feedback/feedback-thread.d.ts +133 -0
  175. package/dist/feedback/feedback-thread.js +237 -0
  176. package/dist/feedback/feedback-thread.js.map +1 -0
  177. package/dist/feedback.d.ts +2 -1
  178. package/dist/feedback.js +1 -0
  179. package/dist/feedback.js.map +1 -1
  180. package/dist/hooks/use-authed-src.d.ts +71 -0
  181. package/dist/hooks/use-authed-src.js +63 -0
  182. package/dist/hooks/use-authed-src.js.map +1 -0
  183. package/dist/hooks/use-file-drop.d.ts +49 -2
  184. package/dist/hooks/use-hotkey.d.ts +59 -0
  185. package/dist/hooks/use-hotkey.js +105 -0
  186. package/dist/hooks/use-hotkey.js.map +1 -0
  187. package/dist/hooks/use-search-param-state.d.ts +83 -0
  188. package/dist/hooks/use-search-param-state.js +94 -0
  189. package/dist/hooks/use-search-param-state.js.map +1 -0
  190. package/dist/i18n/defaults.d.ts +15 -4
  191. package/dist/i18n/defaults.js +31 -1
  192. package/dist/i18n/defaults.js.map +1 -1
  193. package/dist/i18n/kit-labels.d.ts +46 -312
  194. package/dist/i18n/kit-labels.js +20 -5
  195. package/dist/i18n/kit-labels.js.map +1 -1
  196. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  197. package/dist/i18n/locales/de-CH.d.ts +15 -4
  198. package/dist/i18n/locales/de-informal.d.ts +15 -4
  199. package/dist/i18n/locales/de.d.ts +15 -4
  200. package/dist/i18n/locales/de.js +156 -3
  201. package/dist/i18n/locales/de.js.map +1 -1
  202. package/dist/i18n/locales/es.d.ts +15 -4
  203. package/dist/i18n/locales/es.js +153 -3
  204. package/dist/i18n/locales/es.js.map +1 -1
  205. package/dist/i18n/locales/fr.d.ts +15 -4
  206. package/dist/i18n/locales/fr.js +153 -3
  207. package/dist/i18n/locales/fr.js.map +1 -1
  208. package/dist/i18n/locales/hu.d.ts +15 -4
  209. package/dist/i18n/locales/hu.js +153 -3
  210. package/dist/i18n/locales/hu.js.map +1 -1
  211. package/dist/i18n/locales/it.d.ts +15 -4
  212. package/dist/i18n/locales/it.js +153 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.d.ts +15 -4
  215. package/dist/i18n/locales/zh.js +153 -3
  216. package/dist/i18n/locales/zh.js.map +1 -1
  217. package/dist/index.d.ts +37 -17
  218. package/dist/index.js +40 -1
  219. package/dist/index.js.map +1 -1
  220. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  221. package/dist/lib/format.d.ts +144 -0
  222. package/dist/lib/format.js +149 -0
  223. package/dist/lib/format.js.map +1 -0
  224. package/dist/lib/qr-encode.d.ts +44 -0
  225. package/dist/lib/qr-encode.js +338 -0
  226. package/dist/lib/qr-encode.js.map +1 -0
  227. package/dist/rhf/fields.d.ts +241 -0
  228. package/dist/rhf/fields.js +601 -0
  229. package/dist/rhf/fields.js.map +1 -0
  230. package/dist/rhf/form.d.ts +49 -1
  231. package/dist/rhf/line-items.d.ts +31 -0
  232. package/dist/rhf/line-items.js +59 -0
  233. package/dist/rhf/line-items.js.map +1 -0
  234. package/dist/rhf.d.ts +61 -1
  235. package/dist/rhf.js +2 -0
  236. package/dist/rhf.js.map +1 -1
  237. package/dist/shell/app-shell.d.ts +99 -2
  238. package/dist/shell/app-shell.js +58 -3
  239. package/dist/shell/app-shell.js.map +1 -1
  240. package/dist/shell/auth-layout.d.ts +74 -0
  241. package/dist/shell/auth-layout.js +97 -0
  242. package/dist/shell/auth-layout.js.map +1 -0
  243. package/dist/shell/top-bar-brand.d.ts +81 -0
  244. package/dist/shell/top-bar-brand.js +47 -0
  245. package/dist/shell/top-bar-brand.js.map +1 -0
  246. package/dist/shell/topbar-action-menu.d.ts +12 -1
  247. package/dist/shell/topbar-action-menu.js +16 -1
  248. package/dist/shell/topbar-action-menu.js.map +1 -1
  249. package/dist/shell.d.ts +56 -1
  250. package/dist/shell.js +2 -0
  251. package/dist/shell.js.map +1 -1
  252. package/dist/wizard/stepper-nav.d.ts +48 -1
  253. package/dist/wizard/wizard-context.d.ts +24 -1
  254. package/dist/wizard/wizard-context.js +19 -2
  255. package/dist/wizard/wizard-context.js.map +1 -1
  256. package/dist/wizard/wizard-step.d.ts +20 -5
  257. package/dist/wizard/wizard-step.js +16 -3
  258. package/dist/wizard/wizard-step.js.map +1 -1
  259. package/dist/wizard.d.ts +50 -3
  260. package/package.json +1 -1
  261. package/src/chart.ts +4 -0
  262. package/src/components/account-settings-labels.ts +190 -0
  263. package/src/components/account-settings.tsx +110 -63
  264. package/src/components/amount-input.tsx +13 -1
  265. package/src/components/authed-image.tsx +190 -0
  266. package/src/components/breadcrumbs.tsx +8 -2
  267. package/src/components/button-group.tsx +7 -2
  268. package/src/components/calendar-heatmap.tsx +105 -10
  269. package/src/components/chip.tsx +8 -4
  270. package/src/components/choice-card.tsx +136 -25
  271. package/src/components/combobox-core.tsx +5 -0
  272. package/src/components/combobox.tsx +31 -9
  273. package/src/components/copy-button.tsx +3 -2
  274. package/src/components/data-table-cells.tsx +119 -0
  275. package/src/components/data-table-filters.ts +77 -0
  276. package/src/components/data-table-labels.ts +23 -0
  277. package/src/components/data-table.tsx +224 -13
  278. package/src/components/date-picker.tsx +19 -3
  279. package/src/components/description-list.tsx +52 -3
  280. package/src/components/entity-combobox.tsx +18 -3
  281. package/src/components/error-boundary.tsx +216 -0
  282. package/src/components/field.tsx +56 -5
  283. package/src/components/floating-panel.tsx +20 -7
  284. package/src/components/form-actions.tsx +191 -0
  285. package/src/components/image-grid.tsx +181 -0
  286. package/src/components/lightbox.tsx +374 -0
  287. package/src/components/line-items.tsx +385 -0
  288. package/src/components/list.tsx +65 -16
  289. package/src/components/loading-state.tsx +47 -0
  290. package/src/components/menu-item.tsx +7 -2
  291. package/src/components/mini-calendar.tsx +72 -3
  292. package/src/components/modal.tsx +61 -3
  293. package/src/components/month-picker.tsx +181 -26
  294. package/src/components/multi-entity-combobox.tsx +19 -3
  295. package/src/components/nav-pills.tsx +8 -1
  296. package/src/components/number-field.tsx +15 -10
  297. package/src/components/number-input.tsx +16 -1
  298. package/src/components/passkeys-setting.tsx +331 -0
  299. package/src/components/picker-sheet.tsx +7 -1
  300. package/src/components/pie-chart-labels.ts +32 -0
  301. package/src/components/pie-chart.tsx +479 -0
  302. package/src/components/progress-bar.tsx +129 -19
  303. package/src/components/qr-code.tsx +83 -0
  304. package/src/components/search-field.tsx +20 -4
  305. package/src/components/signed-amount.tsx +286 -0
  306. package/src/components/skeleton.tsx +21 -3
  307. package/src/components/stat-tile.tsx +8 -2
  308. package/src/components/table.tsx +102 -13
  309. package/src/components/text-link.tsx +248 -0
  310. package/src/components/toggle-group.tsx +58 -10
  311. package/src/components/toggle-legend.tsx +87 -2
  312. package/src/components/ui.tsx +426 -36
  313. package/src/components/use-table-state.ts +265 -33
  314. package/src/data-table.ts +10 -0
  315. package/src/feedback/feedback-dialog.tsx +42 -3
  316. package/src/feedback/feedback-thread.tsx +434 -0
  317. package/src/feedback.ts +1 -0
  318. package/src/hooks/use-authed-src.ts +161 -0
  319. package/src/hooks/use-hotkey.ts +172 -0
  320. package/src/hooks/use-search-param-state.ts +187 -0
  321. package/src/i18n/defaults.ts +30 -0
  322. package/src/i18n/kit-labels.tsx +102 -8
  323. package/src/i18n/locales/de.ts +153 -0
  324. package/src/i18n/locales/es.ts +150 -0
  325. package/src/i18n/locales/fr.ts +153 -0
  326. package/src/i18n/locales/hu.ts +151 -0
  327. package/src/i18n/locales/it.ts +151 -0
  328. package/src/i18n/locales/zh.ts +150 -0
  329. package/src/index.ts +59 -0
  330. package/src/lib/format.ts +325 -0
  331. package/src/lib/qr-encode.ts +420 -0
  332. package/src/rhf/fields.tsx +1054 -0
  333. package/src/rhf/line-items.tsx +130 -0
  334. package/src/rhf.ts +6 -0
  335. package/src/shell/app-shell.tsx +147 -2
  336. package/src/shell/auth-layout.tsx +190 -0
  337. package/src/shell/top-bar-brand.tsx +76 -0
  338. package/src/shell/topbar-action-menu.tsx +21 -0
  339. package/src/shell.ts +2 -0
  340. package/src/wizard/wizard-context.tsx +41 -1
  341. package/src/wizard/wizard-step.tsx +43 -6
  342. package/tokens.css +12 -0
@@ -0,0 +1,331 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { KeyboardEvent, ReactNode } from "react";
3
+ import { KeyRound } from "lucide-react";
4
+ import { cn } from "../lib/cn";
5
+ import { useKitLocale } from "../i18n/kit-labels";
6
+ import { Button, Card, EmptyState, Input } from "./ui";
7
+ import { Skeleton } from "./skeleton";
8
+ import { useAccountSettingsLabels } from "./account-settings-labels";
9
+ import type { PasskeysSettingLabels } from "./account-settings-labels";
10
+
11
+ /** One registered passkey, as the app's API lists it. */
12
+ export interface PasskeyItem {
13
+ id: string | number;
14
+ name: string;
15
+ /** A `Date`, an ISO string or epoch ms. */
16
+ createdAt?: Date | string | number | null;
17
+ /** Null (or left out) reads as "Never used". */
18
+ lastUsedAt?: Date | string | number | null;
19
+ }
20
+
21
+ export interface PasskeysSettingProps {
22
+ /** `undefined` while the first load is in flight (or pass `loading`). */
23
+ passkeys: readonly PasskeyItem[] | undefined;
24
+ loading?: boolean;
25
+ /**
26
+ * Run the WebAuthn ceremony and register the passkey — app-side, since the options
27
+ * and the attestation are the app's server's business. Called with the trimmed name
28
+ * field, which may be EMPTY: pick your own default ("Passkey", the device name).
29
+ * Return the promise and the field clears when it resolves; a rejection (including
30
+ * the user cancelling the OS prompt) keeps what was typed.
31
+ */
32
+ onAdd: (name: string) => void | Promise<unknown>;
33
+ /** The ceremony is running. The add button says so and does not start a second one. */
34
+ adding?: boolean;
35
+ /** Offer "Rename" on each row. Return a promise to leave edit mode only on success. */
36
+ onRename?: (id: PasskeyItem["id"], name: string) => void | Promise<unknown>;
37
+ /** Offer "Delete" on each row — always behind an inline confirmation. */
38
+ onDelete?: (id: PasskeyItem["id"]) => void | Promise<unknown>;
39
+ /** A row whose rename or delete is in flight; its buttons are disabled. */
40
+ busyId?: PasskeyItem["id"] | null;
41
+ /** Show the name field above the add button. Default true. */
42
+ nameField?: boolean;
43
+ /**
44
+ * Shown INSTEAD of the list and the add button — passkeys are not supported by this
45
+ * browser, or the page is not a secure context. Its wording is the app's: only the
46
+ * app knows which of those it detected.
47
+ */
48
+ unavailable?: ReactNode;
49
+ /** A date for the "Added …" / "Last used …" lines. Default: the locale's medium date. */
50
+ formatDate?: (date: Date) => string;
51
+ /** Overrides the provider's locale for the default date format. */
52
+ locale?: string;
53
+ /** Prop > `<UiKitProvider labels={{ accountSettings: { passkeys } }}>` > English. */
54
+ labels?: Partial<PasskeysSettingLabels>;
55
+ className?: string;
56
+ }
57
+
58
+ /**
59
+ * The passkeys section of an account page: what is registered, when it was last used,
60
+ * rename and delete, and "Add passkey" — the fourth card beside `ProfileSetting`,
61
+ * `PasswordSetting` and `TwoFactorSetting`, and built the same way (a self-contained
62
+ * `<Card>` owning its transient form state; the app owns the data and the API).
63
+ *
64
+ * Lifted from kastlan's passkeys-card. **The WebAuthn ceremony stays app-side**:
65
+ * `navigator.credentials.create` needs options minted by the app's server and hands
66
+ * back an attestation only that server can verify, so the kit calls `onAdd` and waits.
67
+ *
68
+ * **Delete confirms inline**, in the row, rather than through `useConfirm`: this card
69
+ * must work in an app without a `ConfirmProvider`, and the question belongs next to
70
+ * the passkey it names. Focus moves to Cancel, as in every destructive confirm in the
71
+ * kit, so the Enter that pressed Delete cannot also answer it.
72
+ */
73
+ export function PasskeysSetting({
74
+ passkeys,
75
+ loading,
76
+ onAdd,
77
+ adding = false,
78
+ onRename,
79
+ onDelete,
80
+ busyId,
81
+ nameField = true,
82
+ unavailable,
83
+ formatDate,
84
+ locale: localeProp,
85
+ labels: labelsProp,
86
+ className,
87
+ }: PasskeysSettingProps) {
88
+ const labels = useAccountSettingsLabels("passkeys", labelsProp);
89
+ const locale = useKitLocale(localeProp);
90
+ const [name, setName] = useState("");
91
+
92
+ const date = (value: PasskeyItem["createdAt"]): string | null => {
93
+ if (value === null || value === undefined || value === "") return null;
94
+ const d = value instanceof Date ? value : new Date(value);
95
+ if (Number.isNaN(d.getTime())) return null;
96
+ return formatDate ? formatDate(d) : new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(d);
97
+ };
98
+
99
+ const add = () => {
100
+ if (adding) return;
101
+ const typed = name;
102
+ Promise.resolve(onAdd(typed.trim()))
103
+ // Clear only what was sent: the field stays editable while the OS prompt is up.
104
+ .then(() => setName((current) => (current === typed ? "" : current)))
105
+ .catch(() => {});
106
+ };
107
+
108
+ const isLoading = loading || passkeys === undefined;
109
+ const items = passkeys ?? [];
110
+
111
+ const body = (() => {
112
+ if (unavailable != null && unavailable !== false) {
113
+ return <EmptyState variant="inline" size="sm" icon={<KeyRound />} title={unavailable} />;
114
+ }
115
+ return (
116
+ <>
117
+ {isLoading ? (
118
+ <Skeleton lines={2} label={labels.loading} />
119
+ ) : items.length === 0 ? (
120
+ <EmptyState variant="inline" size="sm" icon={<KeyRound />} title={labels.empty} />
121
+ ) : (
122
+ <ul aria-label={labels.list} className="divide-y divide-[var(--border)]">
123
+ {items.map((item) => (
124
+ <PasskeyRow
125
+ key={item.id}
126
+ item={item}
127
+ created={date(item.createdAt)}
128
+ lastUsed={date(item.lastUsedAt)}
129
+ busy={busyId !== undefined && busyId !== null && busyId === item.id}
130
+ onRename={onRename}
131
+ onDelete={onDelete}
132
+ labels={labels}
133
+ />
134
+ ))}
135
+ </ul>
136
+ )}
137
+ <div className="space-y-2 pt-1">
138
+ {nameField && (
139
+ <Input
140
+ label={labels.nameLabel}
141
+ placeholder={labels.namePlaceholder}
142
+ value={name}
143
+ maxLength={120}
144
+ onChange={(e) => setName(e.target.value)}
145
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
146
+ if (e.key === "Enter") {
147
+ e.preventDefault();
148
+ add();
149
+ }
150
+ }}
151
+ />
152
+ )}
153
+ <Button onClick={add} disabled={adding} aria-busy={adding || undefined}>
154
+ <KeyRound className="size-4" aria-hidden />
155
+ {adding ? labels.adding : labels.add}
156
+ </Button>
157
+ </div>
158
+ </>
159
+ );
160
+ })();
161
+
162
+ return (
163
+ <Card className={cn("p-4 space-y-3", className)}>
164
+ <div>
165
+ <div className="text-sm font-medium">{labels.title}</div>
166
+ <div className="text-xs text-[var(--text-muted)]">
167
+ {labels.description}
168
+ </div>
169
+ </div>
170
+ {body}
171
+ </Card>
172
+ );
173
+ }
174
+
175
+ type RowMode = "view" | "rename" | "delete";
176
+
177
+ function PasskeyRow({
178
+ item,
179
+ created,
180
+ lastUsed,
181
+ busy,
182
+ onRename,
183
+ onDelete,
184
+ labels,
185
+ }: {
186
+ item: PasskeyItem;
187
+ created: string | null;
188
+ lastUsed: string | null;
189
+ busy: boolean;
190
+ onRename?: PasskeysSettingProps["onRename"];
191
+ onDelete?: PasskeysSettingProps["onDelete"];
192
+ labels: PasskeysSettingLabels;
193
+ }) {
194
+ const [mode, setMode] = useState<RowMode>("view");
195
+ const [draft, setDraft] = useState(item.name);
196
+ // Which button opened the sub-mode, so leaving it puts focus back there — the
197
+ // confirm or the field unmounts under the focus, which otherwise falls to <body>.
198
+ const [returnTo, setReturnTo] = useState<"rename" | "delete" | null>(null);
199
+ const cancelDeleteRef = useRef<HTMLButtonElement>(null);
200
+ const renameRef = useRef<HTMLInputElement>(null);
201
+ const renameButtonRef = useRef<HTMLButtonElement>(null);
202
+ const deleteButtonRef = useRef<HTMLButtonElement>(null);
203
+
204
+ useEffect(() => {
205
+ if (mode === "delete") cancelDeleteRef.current?.focus();
206
+ else if (mode === "rename") renameRef.current?.select();
207
+ else if (returnTo) (returnTo === "rename" ? renameButtonRef : deleteButtonRef).current?.focus();
208
+ }, [mode, returnTo]);
209
+
210
+ const open = (next: "rename" | "delete") => {
211
+ setReturnTo(next);
212
+ setDraft(item.name);
213
+ setMode(next);
214
+ };
215
+ const leave = () => setMode("view");
216
+ const escapeLeaves = (e: KeyboardEvent<HTMLButtonElement>) => {
217
+ if (e.key === "Escape") {
218
+ e.preventDefault();
219
+ e.stopPropagation();
220
+ leave();
221
+ }
222
+ };
223
+
224
+ const saveRename = () => {
225
+ const next = draft.trim();
226
+ if (!onRename || !next || busy) return;
227
+ if (next === item.name) {
228
+ leave();
229
+ return;
230
+ }
231
+ Promise.resolve(onRename(item.id, next))
232
+ .then(leave)
233
+ .catch(() => {});
234
+ };
235
+
236
+ const confirmDelete = () => {
237
+ if (!onDelete || busy) return;
238
+ // The row unmounts once the list refreshes; on a failure it stays, confirm open.
239
+ Promise.resolve(onDelete(item.id))
240
+ .then(() => setMode("view"))
241
+ .catch(() => {});
242
+ };
243
+
244
+ const meta = [created && labels.created(created), lastUsed ? labels.lastUsed(lastUsed) : labels.neverUsed]
245
+ .filter(Boolean)
246
+ .join(" · ");
247
+
248
+ if (mode === "rename") {
249
+ return (
250
+ <li className="flex flex-wrap items-center gap-2 py-2">
251
+ <Input
252
+ ref={renameRef}
253
+ aria-label={labels.renameField(item.name)}
254
+ className="min-w-0 flex-1"
255
+ value={draft}
256
+ maxLength={120}
257
+ onChange={(e) => setDraft(e.target.value)}
258
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
259
+ if (e.key === "Enter") {
260
+ e.preventDefault();
261
+ saveRename();
262
+ } else if (e.key === "Escape") {
263
+ e.preventDefault();
264
+ e.stopPropagation();
265
+ leave();
266
+ }
267
+ }}
268
+ />
269
+ <Button size="sm" onClick={saveRename} disabled={busy || !draft.trim()}>
270
+ {labels.save}
271
+ </Button>
272
+ <Button size="sm" variant="ghost" onClick={leave}>
273
+ {labels.cancel}
274
+ </Button>
275
+ </li>
276
+ );
277
+ }
278
+
279
+ if (mode === "delete") {
280
+ return (
281
+ <li className="space-y-2 py-2">
282
+ <p role="alert" className="text-sm">
283
+ {labels.deleteConfirm(item.name)}
284
+ </p>
285
+ <div className="flex flex-wrap gap-2">
286
+ <Button size="sm" variant="danger" onClick={confirmDelete} onKeyDown={escapeLeaves} disabled={busy}>
287
+ {labels.confirmDelete}
288
+ </Button>
289
+ <Button ref={cancelDeleteRef} size="sm" variant="ghost" onClick={leave} onKeyDown={escapeLeaves}>
290
+ {labels.cancel}
291
+ </Button>
292
+ </div>
293
+ </li>
294
+ );
295
+ }
296
+
297
+ return (
298
+ <li className="flex items-center gap-3 py-2">
299
+ <KeyRound className="size-4 shrink-0 text-[var(--text-muted)]" aria-hidden />
300
+ <div className="min-w-0 flex-1">
301
+ <div className="truncate text-sm font-medium">{item.name}</div>
302
+ <div className="text-xs text-[var(--text-muted)]">{meta}</div>
303
+ </div>
304
+ {onRename && (
305
+ <Button
306
+ ref={renameButtonRef}
307
+ size="sm"
308
+ variant="ghost"
309
+ disabled={busy}
310
+ onClick={() => open("rename")}
311
+ aria-label={labels.renameItem(item.name)}
312
+ >
313
+ {labels.rename}
314
+ </Button>
315
+ )}
316
+ {onDelete && (
317
+ <Button
318
+ ref={deleteButtonRef}
319
+ size="sm"
320
+ variant="ghost"
321
+ tone="danger"
322
+ disabled={busy}
323
+ onClick={() => open("delete")}
324
+ aria-label={labels.deleteItem(item.name)}
325
+ >
326
+ {labels.delete}
327
+ </Button>
328
+ )}
329
+ </li>
330
+ );
331
+ }
@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
3
3
  import { X } from "lucide-react";
4
4
  import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
5
5
  import { cn } from "../lib/cn";
6
+ import { CLIPS_ATTRIBUTE } from "../lib/clipping";
6
7
  import { DropdownSearchHeader } from "./dropdown";
7
8
  import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
8
9
  import { useOverlayHistory } from "../hooks/use-overlay-history";
@@ -237,7 +238,12 @@ export function PickerSheet({
237
238
  />
238
239
  {/* min-h-0 so the LIST scrolls rather than the dialog growing past the
239
240
  viewport — a flex child defaults to min-height:auto. */}
240
- <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">{children}</div>
241
+ {/* Marked as the scroller it is (`CLIPS_ATTRIBUTE`), so a Tooltip in a row —
242
+ `Combobox`'s `optionAdornment` — portals out under jsdom as it does in the
243
+ browser rather than joining the option's name (keksdose E8). */}
244
+ <div {...{ [CLIPS_ATTRIBUTE]: "" }} className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
245
+ {children}
246
+ </div>
241
247
  </div>,
242
248
  document.body,
243
249
  );
@@ -0,0 +1,32 @@
1
+ // All user-facing strings for `PieChart`, as the `pieChart` namespace of
2
+ // `<UiKitProvider labels>`, and per chart the `labels` prop over that.
3
+ //
4
+ // A module of its own, with no recharts in it, for the reason `series-chart-labels`
5
+ // is one: the label registry (src/i18n) imports the defaults, and must not pull the
6
+ // chart library in behind them.
7
+
8
+ export interface PieChartLabels {
9
+ /** Accessible name of the group of keyboard stops laid over the slices — one tab
10
+ * stop, arrow keys between them. */
11
+ slices: string;
12
+ /**
13
+ * One slice's accessible name. `value` arrives through `formatValue`, `percent` as
14
+ * the slice's share of what is drawn, already formatted in the locale ("42 %" in
15
+ * German, "42%" in English) — the function only arranges them.
16
+ */
17
+ slice: (label: string, value: string, percent: string) => string;
18
+ /** The caption over the donut's centre figure. */
19
+ total: string;
20
+ /** What a chart with nothing to draw says, when the caller passes no `empty`. */
21
+ empty: string;
22
+ /** Accessible name of the chart's legend (a group of switches, one per slice). */
23
+ legend: string;
24
+ }
25
+
26
+ export const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {
27
+ slices: "Chart slices",
28
+ slice: (label, value, percent) => `${label}: ${value} (${percent})`,
29
+ total: "Total",
30
+ empty: "No data",
31
+ legend: "Categories",
32
+ };