@krizaka/ui 3.45.0 → 3.48.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 (219) hide show
  1. package/dist/account-menu.d.ts +2 -111
  2. package/dist/account-menu.js +39 -118
  3. package/dist/account-menu.js.map +1 -1
  4. package/dist/account-settings.d.ts +163 -0
  5. package/dist/account-settings.js +354 -0
  6. package/dist/account-settings.js.map +1 -0
  7. package/dist/agenda.d.ts +136 -0
  8. package/dist/agenda.js +90 -0
  9. package/dist/agenda.js.map +1 -0
  10. package/dist/avatar-iajODANU.d.ts +114 -0
  11. package/dist/chunk-3D2YI63L.js +27 -0
  12. package/dist/chunk-3D2YI63L.js.map +1 -0
  13. package/dist/chunk-3RVFD3UX.js +85 -0
  14. package/dist/chunk-3RVFD3UX.js.map +1 -0
  15. package/dist/chunk-S6NYGOJT.js +22 -0
  16. package/dist/chunk-S6NYGOJT.js.map +1 -0
  17. package/dist/docs-layout.js +13 -32
  18. package/dist/docs-layout.js.map +1 -1
  19. package/dist/docs-nav.js +1 -1
  20. package/dist/drop-overlay.d.ts +31 -0
  21. package/dist/drop-overlay.js +86 -0
  22. package/dist/drop-overlay.js.map +1 -0
  23. package/dist/motion.css +27 -0
  24. package/dist/nav-bar.d.ts +41 -1
  25. package/dist/nav-bar.js +26 -1
  26. package/dist/nav-bar.js.map +1 -1
  27. package/dist/settings-layout.d.ts +59 -4
  28. package/dist/settings-layout.js +229 -24
  29. package/dist/settings-layout.js.map +1 -1
  30. package/dist/upload-queue.d.ts +67 -0
  31. package/dist/upload-queue.js +76 -0
  32. package/dist/upload-queue.js.map +1 -0
  33. package/package.json +31 -5
  34. package/registry/account-menu.json +2 -2
  35. package/registry/account-settings.json +721 -0
  36. package/registry/achievement.json +1 -1
  37. package/registry/action-bar.json +1 -1
  38. package/registry/activity-feed.json +1 -1
  39. package/registry/agenda.json +293 -0
  40. package/registry/alert.json +1 -1
  41. package/registry/anatomy-plate.json +1 -1
  42. package/registry/app-header.json +1 -1
  43. package/registry/app-shell.json +1 -1
  44. package/registry/argument-card.json +1 -1
  45. package/registry/audio-player.json +1 -1
  46. package/registry/auth-layout.json +1 -1
  47. package/registry/avatar.json +1 -1
  48. package/registry/badge.json +1 -1
  49. package/registry/bird-plate.json +1 -1
  50. package/registry/button.json +1 -1
  51. package/registry/captcha.json +1 -1
  52. package/registry/card-grid.json +1 -1
  53. package/registry/card.json +1 -1
  54. package/registry/catalogue-page.json +1 -1
  55. package/registry/changelog-page.json +1 -1
  56. package/registry/chat.json +3 -3
  57. package/registry/checkbox.json +1 -1
  58. package/registry/chip-link.json +1 -1
  59. package/registry/chip-row.json +1 -1
  60. package/registry/chip.json +1 -1
  61. package/registry/choice-grid.json +1 -1
  62. package/registry/code-block.json +1 -1
  63. package/registry/combobox.json +1 -1
  64. package/registry/command.json +1 -1
  65. package/registry/comparison-table.json +1 -1
  66. package/registry/component-page.json +1 -1
  67. package/registry/confirm-button.json +1 -1
  68. package/registry/contact-page.json +1 -1
  69. package/registry/continue-prompt.json +1 -1
  70. package/registry/countdown.json +1 -1
  71. package/registry/cover-hero.json +1 -1
  72. package/registry/cta-panel.json +1 -1
  73. package/registry/dashboard-layout.json +1 -1
  74. package/registry/data-table.json +3 -3
  75. package/registry/date-picker.json +1 -1
  76. package/registry/decision-list.json +1 -1
  77. package/registry/description-list.json +1 -1
  78. package/registry/detail-layout.json +1 -1
  79. package/registry/dialog.json +1 -1
  80. package/registry/docs-article-page.json +1 -1
  81. package/registry/docs-hub-page.json +1 -1
  82. package/registry/docs-layout.json +1 -1
  83. package/registry/docs-nav.json +1 -1
  84. package/registry/document-viewer.json +1 -1
  85. package/registry/drop-overlay.json +182 -0
  86. package/registry/dropdown-menu.json +1 -1
  87. package/registry/elephant.json +2 -2
  88. package/registry/empty-state.json +1 -1
  89. package/registry/engine-flow.json +1 -1
  90. package/registry/error-page.json +1 -1
  91. package/registry/examples/account-settings/avatar-picker.d.ts +4 -0
  92. package/registry/examples/account-settings/avatar-picker.js +15 -0
  93. package/registry/examples/account-settings/avatar-picker.tsx +15 -0
  94. package/registry/examples/account-settings/choice-cards.d.ts +4 -0
  95. package/registry/examples/account-settings/choice-cards.js +24 -0
  96. package/registry/examples/account-settings/choice-cards.tsx +20 -0
  97. package/registry/examples/account-settings/language-picker.d.ts +4 -0
  98. package/registry/examples/account-settings/language-picker.js +25 -0
  99. package/registry/examples/account-settings/language-picker.tsx +21 -0
  100. package/registry/examples/account-settings/patterns.d.ts +4 -0
  101. package/registry/examples/account-settings/patterns.js +29 -0
  102. package/registry/examples/account-settings/patterns.tsx +39 -0
  103. package/registry/examples/account-settings/profile.d.ts +4 -0
  104. package/registry/examples/account-settings/profile.js +70 -0
  105. package/registry/examples/account-settings/profile.tsx +80 -0
  106. package/registry/examples/agenda/day.d.ts +4 -0
  107. package/registry/examples/agenda/day.js +43 -0
  108. package/registry/examples/agenda/day.tsx +35 -0
  109. package/registry/examples/drop-overlay/default.d.ts +4 -0
  110. package/registry/examples/drop-overlay/default.js +20 -0
  111. package/registry/examples/drop-overlay/default.tsx +18 -0
  112. package/registry/examples/upload-queue/default.d.ts +4 -0
  113. package/registry/examples/upload-queue/default.js +22 -0
  114. package/registry/examples/upload-queue/default.tsx +20 -0
  115. package/registry/examples/upload-queue/tray.d.ts +4 -0
  116. package/registry/examples/upload-queue/tray.js +21 -0
  117. package/registry/examples/upload-queue/tray.tsx +19 -0
  118. package/registry/field.json +1 -1
  119. package/registry/file-upload.json +1 -1
  120. package/registry/filter-bar.json +1 -1
  121. package/registry/flock.json +1 -1
  122. package/registry/footnote.json +1 -1
  123. package/registry/force-update-screen.json +1 -1
  124. package/registry/form.json +3 -3
  125. package/registry/gallery-page.json +1 -1
  126. package/registry/genui.json +1 -1
  127. package/registry/glow-card.json +1 -1
  128. package/registry/goal-gauge.json +1 -1
  129. package/registry/how-it-works-page.json +1 -1
  130. package/registry/image-cropper.json +1 -1
  131. package/registry/index.json +148 -7
  132. package/registry/iso.json +1 -1
  133. package/registry/kanban.json +1 -1
  134. package/registry/kbd.json +1 -1
  135. package/registry/landing-page.json +1 -1
  136. package/registry/landscape.json +1 -1
  137. package/registry/language-switch.json +1 -1
  138. package/registry/legal-page.json +1 -1
  139. package/registry/link-card.json +1 -1
  140. package/registry/list.json +3 -3
  141. package/registry/loader.json +1 -1
  142. package/registry/longform-page.json +1 -1
  143. package/registry/maintenance-screen.json +1 -1
  144. package/registry/map.json +2 -2
  145. package/registry/markdown.json +1 -1
  146. package/registry/marketing-layout.json +1 -1
  147. package/registry/marquee.json +1 -1
  148. package/registry/mascot-quote.json +3 -3
  149. package/registry/mascot-state.json +3 -3
  150. package/registry/mascots.json +3 -3
  151. package/registry/match-dialog.json +1 -1
  152. package/registry/media-capture.json +1 -1
  153. package/registry/media-gallery.json +1 -1
  154. package/registry/media-grid.json +1 -1
  155. package/registry/meta-row.json +1 -1
  156. package/registry/meter.json +1 -1
  157. package/registry/nav-bar.json +93 -5
  158. package/registry/number-ticker.json +1 -1
  159. package/registry/opening-hours.json +1 -1
  160. package/registry/page-header.json +1 -1
  161. package/registry/page-hero.json +1 -1
  162. package/registry/page-index.json +1 -1
  163. package/registry/page.json +1 -1
  164. package/registry/pagination.json +1 -1
  165. package/registry/photo-grid.json +1 -1
  166. package/registry/pointer-light.json +1 -1
  167. package/registry/popover.json +1 -1
  168. package/registry/pricing-page.json +1 -1
  169. package/registry/product-page.json +1 -1
  170. package/registry/profile-card.json +1 -1
  171. package/registry/progress.json +1 -1
  172. package/registry/promise-card.json +1 -1
  173. package/registry/radio-group.json +93 -5
  174. package/registry/rail-nav.json +2 -2
  175. package/registry/recipes.json +1 -1
  176. package/registry/router.json +1 -1
  177. package/registry/scroll-stage.json +1 -1
  178. package/registry/search-field.json +1 -1
  179. package/registry/section-backdrop.json +1 -1
  180. package/registry/separator.json +1 -1
  181. package/registry/service-card.json +1 -1
  182. package/registry/settings-layout.json +34 -4
  183. package/registry/showcase.json +1 -1
  184. package/registry/site-footer.json +2 -2
  185. package/registry/sites.json +1 -1
  186. package/registry/skeleton.json +1 -1
  187. package/registry/skip-link.json +1 -1
  188. package/registry/slider.json +1 -1
  189. package/registry/slot-picker.json +1 -1
  190. package/registry/slot.json +1 -1
  191. package/registry/specimen-tag.json +1 -1
  192. package/registry/spinner.json +1 -1
  193. package/registry/split-view.json +1 -1
  194. package/registry/stage-banner.json +1 -1
  195. package/registry/stat.json +1 -1
  196. package/registry/status-page.json +1 -1
  197. package/registry/step-progress.json +1 -1
  198. package/registry/step-showcase.json +1 -1
  199. package/registry/stepper.json +1 -1
  200. package/registry/sticky-action.json +1 -1
  201. package/registry/swipe-deck.json +1 -1
  202. package/registry/switch.json +1 -1
  203. package/registry/tabs.json +1 -1
  204. package/registry/tag-input.json +3 -3
  205. package/registry/tag-selector.json +1 -1
  206. package/registry/terminal-replay.json +1 -1
  207. package/registry/theme.json +1 -1
  208. package/registry/toast.json +1 -1
  209. package/registry/toc.json +1 -1
  210. package/registry/tooltip.json +1 -1
  211. package/registry/tree-view.json +1 -1
  212. package/registry/update-prompt.json +1 -1
  213. package/registry/upload-queue.json +243 -0
  214. package/registry/verified-badge.json +1 -1
  215. package/registry/vertical-video.json +1 -1
  216. package/registry/video-player.json +1 -1
  217. package/registry/video-trimmer.json +1 -1
  218. package/registry/virtual.json +1 -1
  219. package/registry/waveform.json +1 -1
@@ -1,8 +1,182 @@
1
- // src/settings-layout/settings-layout.tsx
1
+ import {
2
+ defineRecipe
3
+ } from "./chunk-2VNYHG4E.js";
4
+ import {
5
+ cn
6
+ } from "./chunk-S6NYGOJT.js";
7
+
8
+ // src/settings-layout/save-status.ts
2
9
  import * as React from "react";
10
+ var SaveStatus = { Idle: "idle", Saving: "saving", Saved: "saved", Error: "error" };
11
+ var SAVED_VISIBLE_MS = 4e3;
12
+ function useSaveStatus() {
13
+ const [status, setStatus] = React.useState(SaveStatus.Idle);
14
+ const [error, setError] = React.useState(void 0);
15
+ const timer = React.useRef(void 0);
16
+ const clear = React.useCallback(() => {
17
+ if (timer.current) clearTimeout(timer.current);
18
+ timer.current = void 0;
19
+ }, []);
20
+ React.useEffect(() => clear, [clear]);
21
+ const reset = React.useCallback(() => {
22
+ clear();
23
+ setStatus(SaveStatus.Idle);
24
+ setError(void 0);
25
+ }, [clear]);
26
+ const track = React.useCallback(
27
+ async (save) => {
28
+ clear();
29
+ setStatus(SaveStatus.Saving);
30
+ setError(void 0);
31
+ try {
32
+ await save();
33
+ setStatus(SaveStatus.Saved);
34
+ timer.current = setTimeout(() => setStatus(SaveStatus.Idle), SAVED_VISIBLE_MS);
35
+ return true;
36
+ } catch (failure) {
37
+ setError(failure);
38
+ setStatus(SaveStatus.Error);
39
+ return false;
40
+ }
41
+ },
42
+ [clear]
43
+ );
44
+ return { status, error, track, reset };
45
+ }
46
+
47
+ // src/settings-layout/settings-layout.tsx
48
+ import * as React2 from "react";
49
+ import { tv as tv2 } from "tailwind-variants";
50
+
51
+ // src/button/button.tsx
52
+ import { Slot } from "radix-ui";
3
53
  import { tv } from "tailwind-variants";
54
+
55
+ // src/spinner/glyph.tsx
4
56
  import { jsx, jsxs } from "react/jsx-runtime";
5
- var settingsLayout = tv({
57
+ var HEX = "M12 3 19.79 7.5 19.79 16.5 12 21 4.21 16.5 4.21 7.5Z";
58
+ function LoaderGlyph({ state = "loading", value, ...props }) {
59
+ const known = typeof value === "number" && Number.isFinite(value);
60
+ const fill = state !== "loading" ? 100 : known ? Math.round(Math.min(1, Math.max(0, value)) * 100) : null;
61
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, "data-state": state, "data-progress": fill === null ? void 0 : fill, ...props, children: [
62
+ /* @__PURE__ */ jsx("path", { d: HEX, stroke: "currentColor", strokeOpacity: "0.22", strokeWidth: "2.2", strokeLinejoin: "round" }),
63
+ /* @__PURE__ */ jsx(
64
+ "path",
65
+ {
66
+ d: HEX,
67
+ pathLength: 100,
68
+ stroke: "currentColor",
69
+ strokeWidth: "2.2",
70
+ strokeLinejoin: "round",
71
+ strokeLinecap: "round",
72
+ className: "kz-loader-trace",
73
+ strokeDasharray: fill === null ? void 0 : `${fill} ${100 - fill}`
74
+ }
75
+ ),
76
+ state === "loading" && /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.6", fill: "currentColor", className: "kz-loader-node" }),
77
+ state === "success" && /* @__PURE__ */ jsx("path", { d: "M8.3 12.3l2.6 2.6 4.9-5.3", pathLength: 20, stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", className: "kz-loader-mark" }),
78
+ state === "error" && /* @__PURE__ */ jsx("path", { d: "M9.4 9.4l5.2 5.2M14.6 9.4l-5.2 5.2", pathLength: 20, stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", className: "kz-loader-mark" })
79
+ ] });
80
+ }
81
+
82
+ // src/button/recipe.ts
83
+ var buttonRecipe = defineRecipe({
84
+ base: "inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition duration-150 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:pointer-events-none disabled:opacity-40 data-loading:cursor-progress active:scale-[0.98] [&[data-loading]]:opacity-100",
85
+ variants: {
86
+ variant: {
87
+ primary: "bg-accent text-on-accent shadow-sm hover:bg-accent-hover",
88
+ // The brand's signature: accent → accent-2 (on-accent reads on both stops, tested in @krizaka/tokens).
89
+ gradient: "bg-linear-to-r from-accent to-accent-2 text-on-accent shadow-sm hover:brightness-110",
90
+ secondary: "border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3",
91
+ outline: "border border-border-default bg-transparent text-fg hover:border-accent hover:text-fg-accent",
92
+ ghost: "text-fg-secondary hover:bg-surface-2 hover:text-fg",
93
+ // The border and the tint carry the danger; the label stays the primary text role, legible on the tint at rest
94
+ // and hovered over every surface (`text-fg-danger` is for text set directly on a surface).
95
+ danger: "border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20"
96
+ },
97
+ size: { sm: "h-8 px-3 text-xs", md: "h-10 px-4 text-sm", lg: "h-12 px-6 text-sm", icon: "h-10 w-10" },
98
+ // One clean shape (krizaka-ui#393): an 8 px radius (`--kz-radius-sm`, the radius of a field — a button beside an
99
+ // input lines up with it), the pill on demand. `cut` (the retired chamfer) is a deprecated alias of `rounded`.
100
+ shape: { rounded: "rounded-sm", pill: "rounded-full", cut: "rounded-sm" }
101
+ },
102
+ defaultVariants: { variant: "secondary", size: "md", shape: "rounded" }
103
+ });
104
+
105
+ // src/button/button.tsx
106
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
107
+ var buttonVariants = tv(buttonRecipe);
108
+ var STATUS_TEXT = { success: "text-fg-success", error: "text-fg-danger" };
109
+ var warnedCut = false;
110
+ function warnCut() {
111
+ if (warnedCut || typeof process === "undefined" || process.env.NODE_ENV === "production") return;
112
+ warnedCut = true;
113
+ console.warn('@krizaka/ui/button: shape="cut" is deprecated (krizaka-ui#393) and now draws the default rounded shape. Remove the prop.');
114
+ }
115
+ function Button({
116
+ asChild,
117
+ loading,
118
+ progress,
119
+ status,
120
+ announce,
121
+ indicator = true,
122
+ shimmer: _shimmer,
123
+ magnetic: _magnetic,
124
+ variant,
125
+ size,
126
+ shape,
127
+ className,
128
+ disabled,
129
+ children,
130
+ ...props
131
+ }) {
132
+ const Comp = asChild ? Slot.Root : "button";
133
+ if (shape === "cut") warnCut();
134
+ const known = loading && typeof progress === "number" && Number.isFinite(progress);
135
+ const ratio = known ? Math.min(1, Math.max(0, progress)) : 0;
136
+ const glyph = indicator && !asChild && (loading || status) ? status ?? "loading" : null;
137
+ const filled = variant === "primary" || variant === "gradient";
138
+ const button = /* @__PURE__ */ jsx2(
139
+ Comp,
140
+ {
141
+ type: asChild ? void 0 : "button",
142
+ "aria-busy": loading || void 0,
143
+ "data-loading": loading ? "" : void 0,
144
+ "data-variant": variant ?? "secondary",
145
+ "data-kz-status": status ?? void 0,
146
+ disabled: asChild ? void 0 : disabled || loading,
147
+ "aria-disabled": asChild && (disabled || loading) ? true : void 0,
148
+ className: buttonVariants({
149
+ variant,
150
+ size,
151
+ shape,
152
+ className: cn(known && "relative isolate overflow-hidden", className)
153
+ }),
154
+ ...props,
155
+ children: asChild ? children : /* @__PURE__ */ jsxs2(Fragment, { children: [
156
+ known && /* @__PURE__ */ jsx2("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 -z-10 bg-current opacity-15 transition-[width] duration-500", style: { width: `${ratio * 100}%` } }),
157
+ glyph && /* @__PURE__ */ jsx2(
158
+ "span",
159
+ {
160
+ "aria-hidden": true,
161
+ "data-slot": "indicator",
162
+ className: "-my-1 grid grid-cols-[1fr] transition-[grid-template-columns,margin,opacity] duration-300 starting:-mr-2 starting:grid-cols-[0fr] starting:opacity-0",
163
+ children: /* @__PURE__ */ jsx2("span", { className: "overflow-hidden", children: /* @__PURE__ */ jsx2(LoaderGlyph, { state: glyph, value: known ? ratio : null, className: cn("h-4 w-4", glyph !== "loading" && !filled && STATUS_TEXT[glyph]) }) })
164
+ }
165
+ ),
166
+ children
167
+ ] })
168
+ }
169
+ );
170
+ if (announce === void 0 || asChild) return button;
171
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
172
+ button,
173
+ /* @__PURE__ */ jsx2("span", { role: "status", className: "sr-only", children: announce })
174
+ ] });
175
+ }
176
+
177
+ // src/settings-layout/settings-layout.tsx
178
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
179
+ var settingsLayout = tv2({
6
180
  slots: {
7
181
  root: "flex w-full flex-col gap-6",
8
182
  head: "flex flex-col gap-1",
@@ -43,48 +217,79 @@ var settingsLayout = tv({
43
217
  });
44
218
  function SettingsLayout({ title, description, nav, navLabel, linkAs: Link = "a", variant = "card", className, children, ...props }) {
45
219
  const s = settingsLayout({ variant });
46
- return /* @__PURE__ */ jsxs("div", { className: s.root({ className }), ...props, children: [
47
- (title || description) && /* @__PURE__ */ jsxs("div", { className: s.head(), children: [
48
- title && /* @__PURE__ */ jsx("h1", { className: s.title(), children: title }),
49
- description && /* @__PURE__ */ jsx("p", { className: s.description(), children: description })
220
+ return /* @__PURE__ */ jsxs3("div", { className: s.root({ className }), ...props, children: [
221
+ (title || description) && /* @__PURE__ */ jsxs3("div", { className: s.head(), children: [
222
+ title && /* @__PURE__ */ jsx3("h1", { className: s.title(), children: title }),
223
+ description && /* @__PURE__ */ jsx3("p", { className: s.description(), children: description })
50
224
  ] }),
51
- /* @__PURE__ */ jsxs("div", { className: s.columns(), children: [
52
- /* @__PURE__ */ jsx("nav", { "aria-label": navLabel, className: s.nav(), children: /* @__PURE__ */ jsx("ul", { className: s.navList(), children: nav.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Link, { href: item.href, className: s.navLink(), "aria-current": item.current ? item.href.startsWith("#") ? "location" : "page" : void 0, children: [
225
+ /* @__PURE__ */ jsxs3("div", { className: s.columns(), children: [
226
+ /* @__PURE__ */ jsx3("nav", { "aria-label": navLabel, className: s.nav(), children: /* @__PURE__ */ jsx3("ul", { className: s.navList(), children: nav.map((item) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3(Link, { href: item.href, className: s.navLink(), "aria-current": item.current ? item.href.startsWith("#") ? "location" : "page" : void 0, children: [
53
227
  item.icon,
54
228
  item.label
55
229
  ] }) }, item.id)) }) }),
56
- /* @__PURE__ */ jsx("div", { className: s.content(), children })
230
+ /* @__PURE__ */ jsx3("div", { className: s.content(), children })
57
231
  ] })
58
232
  ] });
59
233
  }
60
- function SettingsSection({ title, description, footer, variant = "card", id, className, children, ...props }) {
234
+ function SettingsSection({ title, description, footer, save, variant = "card", id, className, children, ...props }) {
61
235
  const s = settingsLayout({ variant });
62
- const auto = React.useId();
236
+ const auto = React2.useId();
63
237
  const titleId = `${id ?? auto}-title`;
64
- return /* @__PURE__ */ jsxs("section", { id, "aria-labelledby": titleId, className: s.section({ className }), ...props, children: [
65
- /* @__PURE__ */ jsxs("div", { className: s.sectionHead(), children: [
66
- /* @__PURE__ */ jsx("h2", { id: titleId, className: s.sectionTitle(), children: title }),
67
- description && /* @__PURE__ */ jsx("p", { className: s.sectionDescription(), children: description })
68
- ] }),
69
- /* @__PURE__ */ jsx("div", { className: variant === "flat" ? s.rows() : s.sectionBody(), "data-settings-rows": variant === "flat" ? "" : void 0, children }),
70
- footer && /* @__PURE__ */ jsx("div", { className: s.sectionFooter(), children: footer })
238
+ const head = /* @__PURE__ */ jsxs3("div", { className: s.sectionHead(), children: [
239
+ /* @__PURE__ */ jsx3("h2", { id: titleId, className: s.sectionTitle(), children: title }),
240
+ description && /* @__PURE__ */ jsx3("p", { className: s.sectionDescription(), children: description })
241
+ ] });
242
+ const body = children === void 0 ? null : /* @__PURE__ */ jsx3("div", { className: variant === "flat" ? s.rows() : s.sectionBody(), "data-settings-rows": variant === "flat" ? "" : void 0, children });
243
+ return /* @__PURE__ */ jsx3("section", { id, "aria-labelledby": titleId, "data-dirty": save?.dirty ? "" : void 0, className: s.section({ className }), ...props, children: save ? /* @__PURE__ */ jsxs3(
244
+ "form",
245
+ {
246
+ noValidate: true,
247
+ onSubmit: (event) => {
248
+ event.preventDefault();
249
+ if (save.dirty && save.status !== SaveStatus.Saving) save.onSave();
250
+ },
251
+ children: [
252
+ head,
253
+ body,
254
+ /* @__PURE__ */ jsxs3("div", { className: s.sectionFooter(), children: [
255
+ /* @__PURE__ */ jsx3(SaveFeedback, { save }),
256
+ save.onDiscard && save.dirty ? /* @__PURE__ */ jsx3(Button, { type: "button", variant: "ghost", onClick: save.onDiscard, children: save.labels.discard }) : null,
257
+ /* @__PURE__ */ jsx3(Button, { type: "submit", variant: "primary", disabled: !save.dirty || save.status === SaveStatus.Saving, loading: save.status === SaveStatus.Saving, children: save.labels.save })
258
+ ] })
259
+ ]
260
+ }
261
+ ) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
262
+ head,
263
+ body,
264
+ footer && /* @__PURE__ */ jsx3("div", { className: s.sectionFooter(), children: footer })
265
+ ] }) });
266
+ }
267
+ function SaveFeedback({ save }) {
268
+ const failed = save.status === SaveStatus.Error;
269
+ const saved = save.status === SaveStatus.Saved;
270
+ return /* @__PURE__ */ jsxs3("div", { className: "mr-auto flex min-w-0 items-center gap-2 text-sm", children: [
271
+ /* @__PURE__ */ jsx3("span", { role: failed ? "alert" : "status", "aria-live": failed ? "assertive" : "polite", className: failed ? "text-danger-text" : "text-fg-secondary", children: failed ? save.labels.error : saved ? save.labels.saved : save.status === SaveStatus.Saving ? save.labels.saving : "" }),
272
+ saved && save.onUndo ? /* @__PURE__ */ jsx3(Button, { type: "button", variant: "ghost", size: "sm", onClick: save.onUndo, children: save.labels.undo }) : null
71
273
  ] });
72
274
  }
73
275
  function SettingsRow({ label, description, htmlFor, className, children, ...props }) {
74
276
  const s = settingsLayout();
75
277
  const Label = htmlFor ? "label" : "span";
76
- return /* @__PURE__ */ jsxs("div", { className: s.row({ className }), ...props, children: [
77
- /* @__PURE__ */ jsxs("div", { className: s.rowText(), children: [
78
- /* @__PURE__ */ jsx(Label, { className: s.rowLabel(), ...htmlFor ? { htmlFor } : {}, children: label }),
79
- description && /* @__PURE__ */ jsx("span", { className: s.rowDescription(), children: description })
278
+ return /* @__PURE__ */ jsxs3("div", { className: s.row({ className }), ...props, children: [
279
+ /* @__PURE__ */ jsxs3("div", { className: s.rowText(), children: [
280
+ /* @__PURE__ */ jsx3(Label, { className: s.rowLabel(), ...htmlFor ? { htmlFor } : {}, children: label }),
281
+ description && /* @__PURE__ */ jsx3("span", { className: s.rowDescription(), children: description })
80
282
  ] }),
81
- children !== void 0 && /* @__PURE__ */ jsx("div", { className: s.rowControl(), children })
283
+ children !== void 0 && /* @__PURE__ */ jsx3("div", { className: s.rowControl(), children })
82
284
  ] });
83
285
  }
84
286
  export {
287
+ SAVED_VISIBLE_MS,
288
+ SaveStatus,
85
289
  SettingsLayout,
86
290
  SettingsRow,
87
291
  SettingsSection,
88
- settingsLayout
292
+ settingsLayout,
293
+ useSaveStatus
89
294
  };
90
295
  //# sourceMappingURL=settings-layout.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/settings-layout/settings-layout.tsx"],"sourcesContent":["// Server-safe. Settings, read from the products' account and administration screens: a list of sections on the left\n// (a row of chips that scrolls on a phone), the current section's forms on the right, each form a titled card with its\n// own save at the foot. The sections are links (one page each) or anchors (one long page).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const settingsLayout = tv({\n slots: {\n root: \"flex w-full flex-col gap-6\",\n head: \"flex flex-col gap-1\",\n title: \"font-display text-2xl font-extrabold tracking-tight text-fg\",\n description: \"max-w-2xl text-sm text-fg-secondary\",\n columns: \"grid grid-cols-1 gap-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10\",\n nav: \"-mx-4 overflow-x-auto overflow-y-hidden px-4 py-1 -my-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden lg:mx-0 lg:overflow-visible lg:px-0 lg:py-0 lg:my-0\",\n navList: \"flex w-max gap-2 lg:sticky lg:top-20 lg:w-auto lg:flex-col lg:gap-0.5\",\n navLink:\n \"flex h-9 items-center gap-2.5 whitespace-nowrap rounded-sm border border-border-default bg-surface-1 px-3.5 text-sm font-medium text-fg-secondary no-underline \" +\n \"hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current]:border-accent aria-[current]:text-fg \" +\n \"lg:rounded-lg lg:border-transparent lg:bg-transparent lg:px-3 lg:hover:bg-surface-2 lg:aria-[current]:bg-accent-soft [&>svg]:h-4 [&>svg]:w-4\",\n content: \"flex min-w-0 flex-col gap-6\",\n section: \"rounded-xl border border-border-default bg-surface-1\",\n sectionHead: \"flex flex-col gap-1 px-5 pt-5\",\n sectionTitle: \"text-base font-bold text-fg\",\n sectionDescription: \"text-sm text-fg-secondary\",\n sectionBody: \"flex flex-col gap-4 px-5 py-5\",\n sectionFooter: \"flex flex-wrap items-center justify-end gap-2 border-t border-border-subtle bg-surface-2/40 px-5 py-3 rounded-b-xl\",\n // The flat look: no card, hairline-divided rows (Linear / Vercel / GitHub settings).\n rows: \"flex flex-col divide-y divide-border-subtle border-y border-border-subtle\",\n row: \"flex flex-col gap-2 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6\",\n rowText: \"flex min-w-0 flex-col gap-0.5\",\n rowLabel: \"text-sm font-medium text-fg\",\n rowDescription: \"text-sm text-fg-secondary\",\n rowControl: \"flex min-w-0 shrink-0 items-center gap-2 text-sm text-fg sm:max-w-[60%] sm:justify-end\",\n },\n variants: {\n variant: {\n card: {},\n flat: {\n content: \"max-w-[45rem] gap-10\",\n section: \"rounded-none border-0 bg-transparent\",\n sectionHead: \"gap-0.5 px-0 pb-3 pt-0\",\n sectionBody: \"gap-0 px-0 py-0\",\n sectionFooter: \"justify-start border-t-0 bg-transparent px-0 pb-0 pt-4\",\n },\n },\n },\n defaultVariants: { variant: \"card\" },\n});\n\nexport interface SettingsNavItem {\n /** Its id (the anchor of a section on one long page). */\n id: string;\n /** Its words. */\n label: string;\n /** Where it leads: a page (`/settings/billing`) or an anchor (`#billing`). */\n href: string;\n /** Its icon (a signature icon). */\n icon?: React.ReactNode;\n /** The section shown now: `aria-current`. */\n current?: boolean;\n}\n\nexport type SettingsLayoutProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The screen's title (`Settings`). */\n title?: React.ReactNode;\n /** One sentence under it. */\n description?: React.ReactNode;\n /** The sections. */\n nav: readonly SettingsNavItem[];\n /** The navigation's accessible name (`Settings sections`). */\n navLabel: string;\n /** The app's link component for the sections (Next's `Link`); an `<a>` otherwise. */\n linkAs?: React.ElementType;\n /**\n * `card` (each form a titled card) or `flat` (one calm column of at most 45rem, rows divided by hairlines).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The current section's forms (`SettingsSection`). */\n children: React.ReactNode;\n};\n\n/** A settings screen: sections on the left (chips on a phone), forms on the right. */\nexport function SettingsLayout({ title, description, nav, navLabel, linkAs: Link = \"a\", variant = \"card\", className, children, ...props }: SettingsLayoutProps) {\n const s = settingsLayout({ variant });\n return (\n <div className={s.root({ className })} {...props}>\n {(title || description) && (\n <div className={s.head()}>\n {title && <h1 className={s.title()}>{title}</h1>}\n {description && <p className={s.description()}>{description}</p>}\n </div>\n )}\n <div className={s.columns()}>\n <nav aria-label={navLabel} className={s.nav()}>\n <ul className={s.navList()}>\n {nav.map((item) => (\n <li key={item.id}>\n <Link href={item.href} className={s.navLink()} aria-current={item.current ? (item.href.startsWith(\"#\") ? \"location\" : \"page\") : undefined}>\n {item.icon}\n {item.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n <div className={s.content()}>{children}</div>\n </div>\n </div>\n );\n}\n\nexport type SettingsSectionProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n /** The section's title: it names the region (and is its anchor's target when `id` is set). */\n title: React.ReactNode;\n /** What these settings change, in one sentence. */\n description?: React.ReactNode;\n /** At the foot: the section's own save (a Button), a note. */\n footer?: React.ReactNode;\n /**\n * `card` (a titled card) or `flat` (no card: a title, then hairline-divided `SettingsRow`s).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The fields (or, flat, the `SettingsRow`s). */\n children: React.ReactNode;\n};\n\n/** A group of settings: a titled card with its fields and its own save, or flat rows under a title. */\nexport function SettingsSection({ title, description, footer, variant = \"card\", id, className, children, ...props }: SettingsSectionProps) {\n const s = settingsLayout({ variant });\n const auto = React.useId();\n const titleId = `${id ?? auto}-title`;\n return (\n <section id={id} aria-labelledby={titleId} className={s.section({ className })} {...props}>\n <div className={s.sectionHead()}>\n <h2 id={titleId} className={s.sectionTitle()}>\n {title}\n </h2>\n {description && <p className={s.sectionDescription()}>{description}</p>}\n </div>\n <div className={variant === \"flat\" ? s.rows() : s.sectionBody()} data-settings-rows={variant === \"flat\" ? \"\" : undefined}>\n {children}\n </div>\n {footer && <div className={s.sectionFooter()}>{footer}</div>}\n </section>\n );\n}\n\nexport type SettingsRowProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** What the setting is (`Username`). */\n label: React.ReactNode;\n /** The help text under it, one sentence. */\n description?: React.ReactNode;\n /** The id of the control, so the label names it (a `<label htmlFor>`). Leave out for a plain value. */\n htmlFor?: string;\n /** The control or the value, on the right (stacked under the label on a phone). */\n children?: React.ReactNode;\n};\n\n/** One setting: its label and help on the left, its control or value on the right. Put rows in a flat `SettingsSection`. */\nexport function SettingsRow({ label, description, htmlFor, className, children, ...props }: SettingsRowProps) {\n const s = settingsLayout();\n const Label = htmlFor ? \"label\" : \"span\";\n return (\n <div className={s.row({ className })} {...props}>\n <div className={s.rowText()}>\n <Label className={s.rowLabel()} {...(htmlFor ? { htmlFor } : {})}>\n {label}\n </Label>\n {description && <span className={s.rowDescription()}>{description}</span>}\n </div>\n {children !== undefined && <div className={s.rowControl()}>{children}</div>}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/settings-layout/save-status.ts","../src/settings-layout/settings-layout.tsx","../src/button/button.tsx","../src/spinner/glyph.tsx","../src/button/recipe.ts"],"sourcesContent":["import * as React from \"react\";\n\n/** Where an inline save is: nothing to say, in flight, done (for a moment), or failed. */\nexport const SaveStatus = { Idle: \"idle\", Saving: \"saving\", Saved: \"saved\", Error: \"error\" } as const;\nexport type SaveStatus = (typeof SaveStatus)[keyof typeof SaveStatus];\n\n/** How long « Saved » stays before the section goes quiet again. */\n// @fixed: an interface timing (how long a confirmation reads), not a business setting.\nexport const SAVED_VISIBLE_MS = 4000;\n\nexport interface SaveTracker {\n status: SaveStatus;\n /** The failure of the last try, for the message beside the button. */\n error: unknown;\n /** Runs a save: `saving`, then `saved` (back to `idle` after a moment) or `error`. Never rejects. */\n track: (save: () => Promise<unknown>) => Promise<boolean>;\n /** Clears the status (the form was edited again, or the save was undone). */\n reset: () => void;\n}\n\n/** The status of one section's save. Feed it to `SettingsSection`'s `save.status`. */\nexport function useSaveStatus(): SaveTracker {\n const [status, setStatus] = React.useState<SaveStatus>(SaveStatus.Idle);\n const [error, setError] = React.useState<unknown>(undefined);\n const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const clear = React.useCallback(() => {\n if (timer.current) clearTimeout(timer.current);\n timer.current = undefined;\n }, []);\n React.useEffect(() => clear, [clear]);\n const reset = React.useCallback(() => {\n clear();\n setStatus(SaveStatus.Idle);\n setError(undefined);\n }, [clear]);\n const track = React.useCallback(\n async (save: () => Promise<unknown>) => {\n clear();\n setStatus(SaveStatus.Saving);\n setError(undefined);\n try {\n await save();\n setStatus(SaveStatus.Saved);\n timer.current = setTimeout(() => setStatus(SaveStatus.Idle), SAVED_VISIBLE_MS);\n return true;\n } catch (failure) {\n setError(failure);\n setStatus(SaveStatus.Error);\n return false;\n }\n },\n [clear],\n );\n return { status, error, track, reset };\n}\n","// Server-safe. Settings, read from the products' account and administration screens: a list of sections on the left\n// (a row of chips that scrolls on a phone), the current section's forms on the right, each form a titled card with its\n// own save at the foot. The sections are links (one page each) or anchors (one long page).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Button } from \"../button/button\";\nimport { SaveStatus } from \"./save-status\";\n\nexport const settingsLayout = tv({\n slots: {\n root: \"flex w-full flex-col gap-6\",\n head: \"flex flex-col gap-1\",\n title: \"font-display text-2xl font-extrabold tracking-tight text-fg\",\n description: \"max-w-2xl text-sm text-fg-secondary\",\n columns: \"grid grid-cols-1 gap-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10\",\n nav: \"-mx-4 overflow-x-auto overflow-y-hidden px-4 py-1 -my-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden lg:mx-0 lg:overflow-visible lg:px-0 lg:py-0 lg:my-0\",\n navList: \"flex w-max gap-2 lg:sticky lg:top-20 lg:w-auto lg:flex-col lg:gap-0.5\",\n navLink:\n \"flex h-9 items-center gap-2.5 whitespace-nowrap rounded-sm border border-border-default bg-surface-1 px-3.5 text-sm font-medium text-fg-secondary no-underline \" +\n \"hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current]:border-accent aria-[current]:text-fg \" +\n \"lg:rounded-lg lg:border-transparent lg:bg-transparent lg:px-3 lg:hover:bg-surface-2 lg:aria-[current]:bg-accent-soft [&>svg]:h-4 [&>svg]:w-4\",\n content: \"flex min-w-0 flex-col gap-6\",\n section: \"rounded-xl border border-border-default bg-surface-1\",\n sectionHead: \"flex flex-col gap-1 px-5 pt-5\",\n sectionTitle: \"text-base font-bold text-fg\",\n sectionDescription: \"text-sm text-fg-secondary\",\n sectionBody: \"flex flex-col gap-4 px-5 py-5\",\n sectionFooter: \"flex flex-wrap items-center justify-end gap-2 border-t border-border-subtle bg-surface-2/40 px-5 py-3 rounded-b-xl\",\n // The flat look: no card, hairline-divided rows (Linear / Vercel / GitHub settings).\n rows: \"flex flex-col divide-y divide-border-subtle border-y border-border-subtle\",\n row: \"flex flex-col gap-2 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6\",\n rowText: \"flex min-w-0 flex-col gap-0.5\",\n rowLabel: \"text-sm font-medium text-fg\",\n rowDescription: \"text-sm text-fg-secondary\",\n rowControl: \"flex min-w-0 shrink-0 items-center gap-2 text-sm text-fg sm:max-w-[60%] sm:justify-end\",\n },\n variants: {\n variant: {\n card: {},\n flat: {\n content: \"max-w-[45rem] gap-10\",\n section: \"rounded-none border-0 bg-transparent\",\n sectionHead: \"gap-0.5 px-0 pb-3 pt-0\",\n sectionBody: \"gap-0 px-0 py-0\",\n sectionFooter: \"justify-start border-t-0 bg-transparent px-0 pb-0 pt-4\",\n },\n },\n },\n defaultVariants: { variant: \"card\" },\n});\n\nexport interface SettingsNavItem {\n /** Its id (the anchor of a section on one long page). */\n id: string;\n /** Its words. */\n label: string;\n /** Where it leads: a page (`/settings/billing`) or an anchor (`#billing`). */\n href: string;\n /** Its icon (a signature icon). */\n icon?: React.ReactNode;\n /** The section shown now: `aria-current`. */\n current?: boolean;\n}\n\nexport type SettingsLayoutProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The screen's title (`Settings`). */\n title?: React.ReactNode;\n /** One sentence under it. */\n description?: React.ReactNode;\n /** The sections. */\n nav: readonly SettingsNavItem[];\n /** The navigation's accessible name (`Settings sections`). */\n navLabel: string;\n /** The app's link component for the sections (Next's `Link`); an `<a>` otherwise. */\n linkAs?: React.ElementType;\n /**\n * `card` (each form a titled card) or `flat` (one calm column of at most 45rem, rows divided by hairlines).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The current section's forms (`SettingsSection`). */\n children: React.ReactNode;\n};\n\n/** A settings screen: sections on the left (chips on a phone), forms on the right. */\nexport function SettingsLayout({ title, description, nav, navLabel, linkAs: Link = \"a\", variant = \"card\", className, children, ...props }: SettingsLayoutProps) {\n const s = settingsLayout({ variant });\n return (\n <div className={s.root({ className })} {...props}>\n {(title || description) && (\n <div className={s.head()}>\n {title && <h1 className={s.title()}>{title}</h1>}\n {description && <p className={s.description()}>{description}</p>}\n </div>\n )}\n <div className={s.columns()}>\n <nav aria-label={navLabel} className={s.nav()}>\n <ul className={s.navList()}>\n {nav.map((item) => (\n <li key={item.id}>\n <Link href={item.href} className={s.navLink()} aria-current={item.current ? (item.href.startsWith(\"#\") ? \"location\" : \"page\") : undefined}>\n {item.icon}\n {item.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n <div className={s.content()}>{children}</div>\n </div>\n </div>\n );\n}\n\nexport interface SectionSaveLabels {\n /** “Save”. */\n save: string;\n /** “Saving…”. */\n saving: string;\n /** “Saved”. */\n saved: string;\n /** What failed, one sentence (“Could not save. Try again.”). */\n error: string;\n /** “Undo” — only shown after a save, when `onUndo` is given. */\n undo: string;\n /** “Discard changes” — only shown while dirty, when `onDiscard` is given. */\n discard: string;\n}\n\n/** The one save of a section: its dirty state, its status, its words. */\nexport interface SectionSave {\n /** Whether a field differs from what is saved: the save is enabled only then. */\n dirty: boolean;\n /** Where the save is (`useSaveStatus().status`). */\n status: SaveStatus;\n /** Saves the section (the form's submit, Enter included). */\n onSave: () => void;\n /** Puts the previous values back after a save, optimistic update included. */\n onUndo?: () => void;\n /** Throws the edits away while dirty. */\n onDiscard?: () => void;\n labels: SectionSaveLabels;\n}\n\nexport type SettingsSectionProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n /**\n * One save for the whole section: the section becomes a form, the foot shows the Save (enabled while dirty), the\n * result in a live region (saved, or the error as an alert) and an undo after a save. Replaces `footer`.\n */\n save?: SectionSave;\n /** The section's title: it names the region (and is its anchor's target when `id` is set). */\n title: React.ReactNode;\n /** What these settings change, in one sentence. */\n description?: React.ReactNode;\n /** At the foot: the section's own save (a Button), a note. */\n footer?: React.ReactNode;\n /**\n * `card` (a titled card) or `flat` (no card: a title, then hairline-divided `SettingsRow`s).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The fields (or, flat, the `SettingsRow`s). A section of a title, a sentence and a foot action has none. */\n children?: React.ReactNode;\n};\n\n/** A group of settings: a titled card with its fields and its own save, or flat rows under a title. */\nexport function SettingsSection({ title, description, footer, save, variant = \"card\", id, className, children, ...props }: SettingsSectionProps) {\n const s = settingsLayout({ variant });\n const auto = React.useId();\n const titleId = `${id ?? auto}-title`;\n const head = (\n <div className={s.sectionHead()}>\n <h2 id={titleId} className={s.sectionTitle()}>\n {title}\n </h2>\n {description && <p className={s.sectionDescription()}>{description}</p>}\n </div>\n );\n const body = children === undefined ? null : (\n <div className={variant === \"flat\" ? s.rows() : s.sectionBody()} data-settings-rows={variant === \"flat\" ? \"\" : undefined}>\n {children}\n </div>\n );\n return (\n <section id={id} aria-labelledby={titleId} data-dirty={save?.dirty ? \"\" : undefined} className={s.section({ className })} {...props}>\n {save ? (\n <form\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n if (save.dirty && save.status !== SaveStatus.Saving) save.onSave();\n }}\n >\n {head}\n {body}\n <div className={s.sectionFooter()}>\n <SaveFeedback save={save} />\n {save.onDiscard && save.dirty ? (\n <Button type=\"button\" variant=\"ghost\" onClick={save.onDiscard}>\n {save.labels.discard}\n </Button>\n ) : null}\n <Button type=\"submit\" variant=\"primary\" disabled={!save.dirty || save.status === SaveStatus.Saving} loading={save.status === SaveStatus.Saving}>\n {save.labels.save}\n </Button>\n </div>\n </form>\n ) : (\n <>\n {head}\n {body}\n {footer && <div className={s.sectionFooter()}>{footer}</div>}\n </>\n )}\n </section>\n );\n}\n\n/** The result of the last save: « Saved » (with Undo) in a polite live region, a failure as an alert. The region stays mounted so the change is announced. */\nfunction SaveFeedback({ save }: { save: SectionSave }) {\n const failed = save.status === SaveStatus.Error;\n const saved = save.status === SaveStatus.Saved;\n return (\n <div className=\"mr-auto flex min-w-0 items-center gap-2 text-sm\">\n <span role={failed ? \"alert\" : \"status\"} aria-live={failed ? \"assertive\" : \"polite\"} className={failed ? \"text-danger-text\" : \"text-fg-secondary\"}>\n {failed ? save.labels.error : saved ? save.labels.saved : save.status === SaveStatus.Saving ? save.labels.saving : \"\"}\n </span>\n {saved && save.onUndo ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={save.onUndo}>\n {save.labels.undo}\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport type SettingsRowProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** What the setting is (`Username`). */\n label: React.ReactNode;\n /** The help text under it, one sentence. */\n description?: React.ReactNode;\n /** The id of the control, so the label names it (a `<label htmlFor>`). Leave out for a plain value. */\n htmlFor?: string;\n /** The control or the value, on the right (stacked under the label on a phone). */\n children?: React.ReactNode;\n};\n\n/** One setting: its label and help on the left, its control or value on the right. Put rows in a flat `SettingsSection`. */\nexport function SettingsRow({ label, description, htmlFor, className, children, ...props }: SettingsRowProps) {\n const s = settingsLayout();\n const Label = htmlFor ? \"label\" : \"span\";\n return (\n <div className={s.row({ className })} {...props}>\n <div className={s.rowText()}>\n <Label className={s.rowLabel()} {...(htmlFor ? { htmlFor } : {})}>\n {label}\n </Label>\n {description && <span className={s.rowDescription()}>{description}</span>}\n </div>\n {children !== undefined && <div className={s.rowControl()}>{children}</div>}\n </div>\n );\n}\n","// Server-safe: no hook, no context. `buttonVariants` styles a <Link> or a Server Component without the component.\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { LoaderGlyph } from \"../spinner/glyph\";\nimport { buttonRecipe } from \"./recipe\";\n\nexport const buttonVariants = tv(buttonRecipe);\n\nexport type ButtonVariants = VariantProps<typeof buttonVariants>;\n\nexport type ButtonProps = React.ComponentProps<\"button\"> &\n ButtonVariants & {\n /**\n * The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n * `ghost`, `danger`.\n * @default \"secondary\"\n */\n variant?: ButtonVariants[\"variant\"];\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"md\"\n */\n size?: ButtonVariants[\"size\"];\n /**\n * `rounded` (an 8 px radius, the radius of a field) · `pill`. `cut` is deprecated (krizaka-ui#393: the chamfer is\n * retired) — it draws `rounded` and warns in development; it will be removed in 4.0.\n * @default \"rounded\"\n */\n shape?: ButtonVariants[\"shape\"];\n /** Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged. */\n asChild?: boolean;\n /**\n * Disables and signals the wait (aria-busy, data-loading) and shows the Krizaka loader glyph before the label; the\n * button keeps its colour.\n */\n loading?: boolean;\n /**\n * Between 0 and 1, while `loading`: the glyph's trace and a light fill across the button grow to the value (an\n * upload, an export).\n */\n progress?: number | null;\n /**\n * How the action ended: `success` draws a check, `error` a cross (and the button shakes its head once); a ring of\n * the status colour opens around it. Clear it to return to rest.\n */\n status?: \"success\" | \"error\" | null;\n /** A message announced politely by assistive technology when it changes (“Saved”, “Upload failed”) — passed translated. */\n announce?: string;\n /**\n * Shows the glyph while `loading` / with a `status`. `false`: you draw your own.\n * @default true\n */\n indicator?: boolean;\n /** @deprecated Since 3.31 (krizaka-ui#393): does nothing — the hover is quiet. Removed in 4.0. */\n shimmer?: boolean;\n /** @deprecated Since 3.31 (krizaka-ui#393): does nothing — a button never moves under the pointer. Removed in 4.0. */\n magnetic?: boolean;\n };\n\nconst STATUS_TEXT = { success: \"text-fg-success\", error: \"text-fg-danger\" } as const;\n\nlet warnedCut = false;\n/** `shape=\"cut\"` (the chamfer) is retired: one warning per page in development, never in production. */\nfunction warnCut() {\n if (warnedCut || typeof process === \"undefined\" || process.env.NODE_ENV === \"production\") return;\n warnedCut = true;\n console.warn('@krizaka/ui/button: shape=\"cut\" is deprecated (krizaka-ui#393) and now draws the default rounded shape. Remove the prop.');\n}\n\nexport function Button({\n asChild,\n loading,\n progress,\n status,\n announce,\n indicator = true,\n shimmer: _shimmer,\n magnetic: _magnetic,\n variant,\n size,\n shape,\n className,\n disabled,\n children,\n ...props\n}: ButtonProps) {\n const Comp = asChild ? Slot.Root : \"button\";\n if (shape === \"cut\") warnCut();\n const known = loading && typeof progress === \"number\" && Number.isFinite(progress);\n const ratio = known ? Math.min(1, Math.max(0, progress)) : 0;\n const glyph = indicator && !asChild && (loading || status) ? (status ?? \"loading\") : null;\n // On a filled variant the glyph stays in the label's colour; on the others it takes the status colour.\n const filled = variant === \"primary\" || variant === \"gradient\";\n const button = (\n <Comp\n type={asChild ? undefined : \"button\"}\n aria-busy={loading || undefined}\n data-loading={loading ? \"\" : undefined}\n data-variant={variant ?? \"secondary\"}\n data-kz-status={status ?? undefined}\n disabled={asChild ? undefined : disabled || loading}\n aria-disabled={asChild && (disabled || loading) ? true : undefined}\n className={buttonVariants({\n variant,\n size,\n shape,\n className: cn(known && \"relative isolate overflow-hidden\", className),\n })}\n {...props}\n >\n {asChild ? (\n children\n ) : (\n <>\n {known && (\n <span aria-hidden className=\"absolute inset-y-0 left-0 -z-10 bg-current opacity-15 transition-[width] duration-500\" style={{ width: `${ratio * 100}%` }} />\n )}\n {glyph && (\n <span\n aria-hidden\n data-slot=\"indicator\"\n className=\"-my-1 grid grid-cols-[1fr] transition-[grid-template-columns,margin,opacity] duration-300 starting:-mr-2 starting:grid-cols-[0fr] starting:opacity-0\"\n >\n <span className=\"overflow-hidden\">\n <LoaderGlyph state={glyph} value={known ? ratio : null} className={cn(\"h-4 w-4\", glyph !== \"loading\" && !filled && STATUS_TEXT[glyph])} />\n </span>\n </span>\n )}\n {children}\n </>\n )}\n </Comp>\n );\n if (announce === undefined || asChild) return button;\n return (\n <>\n {button}\n <span role=\"status\" className=\"sr-only\">\n {announce}\n </span>\n </>\n );\n}\n\nexport type IconButtonProps = Omit<ButtonProps, \"size\"> & {\n /** The accessible name (and tooltip) of the button — required, passed translated. */\n label: string;\n /**\n * sm · md · lg, or `icon` (a square).\n * @default \"icon\"\n */\n size?: ButtonProps[\"size\"];\n};\n\n/** An icon-only button: `label` is required, it is its accessible name. Loading or with a status, the glyph takes the icon's place. */\nexport function IconButton({ label, size = \"icon\", children, ...props }: IconButtonProps) {\n const swap = props.indicator !== false && !props.asChild && (props.loading || props.status);\n return (\n <Button aria-label={label} title={label} size={size} {...props}>\n {swap ? null : children}\n </Button>\n );\n}\n","// The Krizaka loader glyph, server-safe: a hexagon (the marks' shape) traced by a segment of light around a node that\n// breathes; determinate, it fills to its value; done, the trace closes and a check (or a cross) draws itself. Animated by\n// motion.css (`kz-loader-*`); still under prefers-reduced-motion — a segment at rest and the node lit, never blank.\nimport type * as React from \"react\";\n\n/** The hexagon, pointy top, radius 9 around (12, 12). */\nconst HEX = \"M12 3 19.79 7.5 19.79 16.5 12 21 4.21 16.5 4.21 7.5Z\";\n\n/** What the glyph shows: a wait (with an optional value), or how it ended. */\nexport type LoaderGlyphState = \"loading\" | \"success\" | \"error\";\n\nexport interface LoaderGlyphProps extends Omit<React.ComponentProps<\"svg\">, \"children\"> {\n /** loading · success · error. */\n state?: LoaderGlyphState;\n /** Between 0 and 1: a determinate wait, the trace grows to it. Absent: indeterminate. */\n value?: number | null;\n}\n\n/** The glyph alone (decorative): the Spinner, the Loader and the Button's `loading` draw it. */\nexport function LoaderGlyph({ state = \"loading\", value, ...props }: LoaderGlyphProps) {\n const known = typeof value === \"number\" && Number.isFinite(value);\n const fill = state !== \"loading\" ? 100 : known ? Math.round(Math.min(1, Math.max(0, value)) * 100) : null;\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden data-state={state} data-progress={fill === null ? undefined : fill} {...props}>\n <path d={HEX} stroke=\"currentColor\" strokeOpacity=\"0.22\" strokeWidth=\"2.2\" strokeLinejoin=\"round\" />\n <path\n d={HEX}\n pathLength={100}\n stroke=\"currentColor\"\n strokeWidth=\"2.2\"\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n className=\"kz-loader-trace\"\n strokeDasharray={fill === null ? undefined : `${fill} ${100 - fill}`}\n />\n {state === \"loading\" && <circle cx=\"12\" cy=\"12\" r=\"2.6\" fill=\"currentColor\" className=\"kz-loader-node\" />}\n {state === \"success\" && <path d=\"M8.3 12.3l2.6 2.6 4.9-5.3\" pathLength={20} stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"kz-loader-mark\" />}\n {state === \"error\" && <path d=\"M9.4 9.4l5.2 5.2M14.6 9.4l-5.2 5.2\" pathLength={20} stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" className=\"kz-loader-mark\" />}\n </svg>\n );\n}\n","// The button's variants, written once as classes of the Krizaka tokens: the web wears them with `tv(buttonRecipe)`\n// (button.tsx), React Native reads the same classes into styles (native/button.tsx, `nativeRecipe`).\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const buttonRecipe = defineRecipe({\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition duration-150 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 \" +\n \"disabled:pointer-events-none disabled:opacity-40 data-loading:cursor-progress active:scale-[0.98] \" +\n // Busy is not unavailable: a loading button keeps its colour (it is disabled only so it is not pressed twice).\n \"[&[data-loading]]:opacity-100\",\n variants: {\n variant: {\n primary: \"bg-accent text-on-accent shadow-sm hover:bg-accent-hover\",\n // The brand's signature: accent → accent-2 (on-accent reads on both stops, tested in @krizaka/tokens).\n gradient: \"bg-linear-to-r from-accent to-accent-2 text-on-accent shadow-sm hover:brightness-110\",\n secondary: \"border border-border-default bg-surface-2 text-fg hover:border-border-strong hover:bg-surface-3\",\n outline: \"border border-border-default bg-transparent text-fg hover:border-accent hover:text-fg-accent\",\n ghost: \"text-fg-secondary hover:bg-surface-2 hover:text-fg\",\n // The border and the tint carry the danger; the label stays the primary text role, legible on the tint at rest\n // and hovered over every surface (`text-fg-danger` is for text set directly on a surface).\n danger: \"border border-danger/50 bg-danger/10 text-fg hover:border-danger hover:bg-danger/20\",\n },\n size: { sm: \"h-8 px-3 text-xs\", md: \"h-10 px-4 text-sm\", lg: \"h-12 px-6 text-sm\", icon: \"h-10 w-10\" },\n // One clean shape (krizaka-ui#393): an 8 px radius (`--kz-radius-sm`, the radius of a field — a button beside an\n // input lines up with it), the pill on demand. `cut` (the retired chamfer) is a deprecated alias of `rounded`.\n shape: { rounded: \"rounded-sm\", pill: \"rounded-full\", cut: \"rounded-sm\" },\n },\n defaultVariants: { variant: \"secondary\", size: \"md\", shape: \"rounded\" },\n});\n"],"mappings":"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","names":["React","tv","jsx","jsxs","Fragment","jsx","jsxs","tv"]}
@@ -0,0 +1,67 @@
1
+ import * as React from 'react';
2
+
3
+ /** Where a file is: waiting, being read, being checked, finished well, or not. */
4
+ declare const QueueStatus: {
5
+ readonly Queued: "queued";
6
+ readonly Reading: "reading";
7
+ readonly Checking: "checking";
8
+ readonly Done: "done";
9
+ readonly Failed: "failed";
10
+ };
11
+ type QueueStatus = (typeof QueueStatus)[keyof typeof QueueStatus];
12
+ /** A row while it works: the shimmer and the live progress belong to these states only. */
13
+ declare function isWorking(status: QueueStatus): boolean;
14
+ interface QueueTally {
15
+ total: number;
16
+ working: number;
17
+ queued: number;
18
+ done: number;
19
+ failed: number;
20
+ }
21
+ /** What the tray's header says: how many are working, finished, failed. */
22
+ declare function tally(items: readonly {
23
+ status: QueueStatus;
24
+ }[]): QueueTally;
25
+ /** The progress of a row clamped to 0–1, or `undefined` when it has none (an indeterminate wait). */
26
+ declare function progressOf(value: number | undefined): number | undefined;
27
+
28
+ interface UploadQueueItem {
29
+ /** Its key: stable while the row updates. */
30
+ id: string;
31
+ /** The file's name. */
32
+ name: string;
33
+ /** Where it is: queued, reading, checking, done, failed. */
34
+ status: QueueStatus;
35
+ /** What it is doing, in words (“Reading…”, “Checking the rules…”) — passed translated, from the live events. */
36
+ statusText: string;
37
+ /** 0–1 while the work reports it; absent for an indeterminate wait. */
38
+ progress?: number;
39
+ /** The result, when done: a chip (`Chip`, `Badge`) or a line. */
40
+ result?: React.ReactNode;
41
+ /** The row's one quiet action: a « keep » toggle, a retry, a dismiss. */
42
+ trailing?: React.ReactNode;
43
+ }
44
+ type UploadQueueProps = Omit<React.ComponentProps<"div">, "children"> & {
45
+ /** The list's accessible name (“Checks in progress”) — passed translated. */
46
+ label: string;
47
+ /** The rows, in the order they were added. */
48
+ items: readonly UploadQueueItem[];
49
+ };
50
+ /** The files being checked, one row each. Changes are announced politely. */
51
+ declare function UploadQueue({ label, items, className, ...props }: UploadQueueProps): React.JSX.Element;
52
+ type TaskTrayProps = Omit<React.ComponentProps<"section">, "title"> & {
53
+ /** The tray's name (“Checks”). */
54
+ title: string;
55
+ /** The tally in words (“1 running · 2 done”). */
56
+ summary: string;
57
+ /** The fold button's name while open (“Collapse”). */
58
+ toggleLabel: string;
59
+ /** … and while folded (“Show”). */
60
+ openLabel: string;
61
+ /** Starts open. @default true */
62
+ defaultOpen?: boolean;
63
+ };
64
+ /** A non-blocking tray for a queue: the user keeps browsing. Bottom edge on a phone, bottom-right from `sm`. */
65
+ declare function TaskTray({ title, summary, toggleLabel, openLabel, defaultOpen, className, children, ...props }: TaskTrayProps): React.JSX.Element;
66
+
67
+ export { QueueStatus, type QueueTally, TaskTray, type TaskTrayProps, UploadQueue, type UploadQueueItem, type UploadQueueProps, isWorking, progressOf, tally };
@@ -0,0 +1,76 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-S6NYGOJT.js";
4
+
5
+ // src/upload-queue/model.ts
6
+ var QueueStatus = { Queued: "queued", Reading: "reading", Checking: "checking", Done: "done", Failed: "failed" };
7
+ function isWorking(status) {
8
+ return status === QueueStatus.Reading || status === QueueStatus.Checking;
9
+ }
10
+ function tally(items) {
11
+ const count = (test) => items.filter((item) => test(item.status)).length;
12
+ return {
13
+ total: items.length,
14
+ working: count(isWorking),
15
+ queued: count((s) => s === QueueStatus.Queued),
16
+ done: count((s) => s === QueueStatus.Done),
17
+ failed: count((s) => s === QueueStatus.Failed)
18
+ };
19
+ }
20
+ function progressOf(value) {
21
+ return value === void 0 || Number.isNaN(value) ? void 0 : Math.min(1, Math.max(0, value));
22
+ }
23
+
24
+ // src/upload-queue/upload-queue.tsx
25
+ import * as React from "react";
26
+ import { jsx, jsxs } from "react/jsx-runtime";
27
+ function UploadQueue({ label, items, className, ...props }) {
28
+ return /* @__PURE__ */ jsx("div", { "aria-live": "polite", className, ...props, children: /* @__PURE__ */ jsx("ul", { "aria-label": label, className: "m-0 flex list-none flex-col divide-y divide-border-subtle p-0", children: items.map((item) => {
29
+ const working = isWorking(item.status);
30
+ const progress = progressOf(item.progress);
31
+ return /* @__PURE__ */ jsxs("li", { "data-status": item.status, "data-working": working ? "" : void 0, className: "kz-queue-row flex flex-col gap-1.5 py-3", children: [
32
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
33
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium text-fg", children: item.name }),
34
+ item.result ? /* @__PURE__ */ jsx("span", { className: "shrink-0", children: item.result }) : null,
35
+ item.trailing ? /* @__PURE__ */ jsx("span", { className: "shrink-0", children: item.trailing }) : null
36
+ ] }),
37
+ /* @__PURE__ */ jsx("span", { className: cn("text-xs", item.status === QueueStatus.Failed ? "text-fg-danger" : "text-fg-secondary"), children: item.statusText }),
38
+ working && progress !== void 0 ? /* @__PURE__ */ jsx("div", { role: "progressbar", "aria-label": item.name, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(progress * 100), className: "h-1 overflow-hidden rounded-full bg-surface-3", children: /* @__PURE__ */ jsx("div", { className: "kz-working h-full rounded-full bg-accent", style: { width: `${progress * 100}%` } }) }) : null
39
+ ] }, item.id);
40
+ }) }) });
41
+ }
42
+ function TaskTray({ title, summary, toggleLabel, openLabel, defaultOpen = true, className, children, ...props }) {
43
+ const [open, setOpen] = React.useState(defaultOpen);
44
+ const id = React.useId();
45
+ return /* @__PURE__ */ jsxs(
46
+ "section",
47
+ {
48
+ "aria-labelledby": `${id}-title`,
49
+ "data-state": open ? "open" : "closed",
50
+ className: cn(
51
+ "kz-tray fixed inset-x-0 bottom-0 z-40 flex max-h-[70dvh] flex-col rounded-t-2xl border border-border-default bg-surface-1 shadow-lg sm:inset-x-auto sm:bottom-4 sm:right-4 sm:w-96 sm:rounded-2xl",
52
+ className
53
+ ),
54
+ ...props,
55
+ children: [
56
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center gap-3 px-4 py-3", children: [
57
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
58
+ /* @__PURE__ */ jsx("h2", { id: `${id}-title`, className: "m-0 text-sm font-semibold text-fg", children: title }),
59
+ /* @__PURE__ */ jsx("p", { className: "m-0 truncate text-xs text-fg-secondary", children: summary })
60
+ ] }),
61
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-expanded": open, "aria-controls": `${id}-body`, onClick: () => setOpen(!open), className: "rounded-md px-2 py-1 text-sm text-fg-secondary hover:text-fg focus-visible:focus-ring", children: open ? toggleLabel : openLabel })
62
+ ] }),
63
+ open ? /* @__PURE__ */ jsx("div", { id: `${id}-body`, className: "kz-tray-body min-h-0 overflow-y-auto px-4 pb-2", children }) : null
64
+ ]
65
+ }
66
+ );
67
+ }
68
+ export {
69
+ QueueStatus,
70
+ TaskTray,
71
+ UploadQueue,
72
+ isWorking,
73
+ progressOf,
74
+ tally
75
+ };
76
+ //# sourceMappingURL=upload-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/upload-queue/model.ts","../src/upload-queue/upload-queue.tsx"],"sourcesContent":["// Headless: the vocabulary and the arithmetic of an upload queue (what each row is doing, what the tray says).\n\n/** Where a file is: waiting, being read, being checked, finished well, or not. */\nexport const QueueStatus = { Queued: \"queued\", Reading: \"reading\", Checking: \"checking\", Done: \"done\", Failed: \"failed\" } as const;\nexport type QueueStatus = (typeof QueueStatus)[keyof typeof QueueStatus];\n\n/** A row while it works: the shimmer and the live progress belong to these states only. */\nexport function isWorking(status: QueueStatus): boolean {\n return status === QueueStatus.Reading || status === QueueStatus.Checking;\n}\n\nexport interface QueueTally {\n total: number;\n working: number;\n queued: number;\n done: number;\n failed: number;\n}\n\n/** What the tray's header says: how many are working, finished, failed. */\nexport function tally(items: readonly { status: QueueStatus }[]): QueueTally {\n const count = (test: (status: QueueStatus) => boolean) => items.filter((item) => test(item.status)).length;\n return {\n total: items.length,\n working: count(isWorking),\n queued: count((s) => s === QueueStatus.Queued),\n done: count((s) => s === QueueStatus.Done),\n failed: count((s) => s === QueueStatus.Failed),\n };\n}\n\n/** The progress of a row clamped to 0–1, or `undefined` when it has none (an indeterminate wait). */\nexport function progressOf(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : Math.min(1, Math.max(0, value));\n}\n","// Client (the tray's fold). A queue of files that stays on screen: each row appears at once, says in words what it is doing,\n// shows live progress while it works (a shimmer only then), and ends in a result chip; a row may carry one quiet action (a\n// « keep » toggle). `TaskTray` is the non-blocking container: bottom-right on a desktop, a sheet on a phone's bottom edge.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { isWorking, progressOf, QueueStatus } from \"./model\";\n\nexport interface UploadQueueItem {\n /** Its key: stable while the row updates. */\n id: string;\n /** The file's name. */\n name: string;\n /** Where it is: queued, reading, checking, done, failed. */\n status: QueueStatus;\n /** What it is doing, in words (“Reading…”, “Checking the rules…”) — passed translated, from the live events. */\n statusText: string;\n /** 0–1 while the work reports it; absent for an indeterminate wait. */\n progress?: number;\n /** The result, when done: a chip (`Chip`, `Badge`) or a line. */\n result?: React.ReactNode;\n /** The row's one quiet action: a « keep » toggle, a retry, a dismiss. */\n trailing?: React.ReactNode;\n}\n\nexport type UploadQueueProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n /** The list's accessible name (“Checks in progress”) — passed translated. */\n label: string;\n /** The rows, in the order they were added. */\n items: readonly UploadQueueItem[];\n};\n\n/** The files being checked, one row each. Changes are announced politely. */\nexport function UploadQueue({ label, items, className, ...props }: UploadQueueProps) {\n return (\n <div aria-live=\"polite\" className={className} {...props}>\n <ul aria-label={label} className=\"m-0 flex list-none flex-col divide-y divide-border-subtle p-0\">\n {items.map((item) => {\n const working = isWorking(item.status);\n const progress = progressOf(item.progress);\n return (\n <li key={item.id} data-status={item.status} data-working={working ? \"\" : undefined} className=\"kz-queue-row flex flex-col gap-1.5 py-3\">\n <div className=\"flex items-center gap-3\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-fg\">{item.name}</span>\n {item.result ? <span className=\"shrink-0\">{item.result}</span> : null}\n {item.trailing ? <span className=\"shrink-0\">{item.trailing}</span> : null}\n </div>\n <span className={cn(\"text-xs\", item.status === QueueStatus.Failed ? \"text-fg-danger\" : \"text-fg-secondary\")}>{item.statusText}</span>\n {working && progress !== undefined ? (\n <div role=\"progressbar\" aria-label={item.name} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(progress * 100)} className=\"h-1 overflow-hidden rounded-full bg-surface-3\">\n <div className=\"kz-working h-full rounded-full bg-accent\" style={{ width: `${progress * 100}%` }} />\n </div>\n ) : null}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\n\nexport type TaskTrayProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n /** The tray's name (“Checks”). */\n title: string;\n /** The tally in words (“1 running · 2 done”). */\n summary: string;\n /** The fold button's name while open (“Collapse”). */\n toggleLabel: string;\n /** … and while folded (“Show”). */\n openLabel: string;\n /** Starts open. @default true */\n defaultOpen?: boolean;\n};\n\n/** A non-blocking tray for a queue: the user keeps browsing. Bottom edge on a phone, bottom-right from `sm`. */\nexport function TaskTray({ title, summary, toggleLabel, openLabel, defaultOpen = true, className, children, ...props }: TaskTrayProps) {\n const [open, setOpen] = React.useState(defaultOpen);\n const id = React.useId();\n return (\n <section\n aria-labelledby={`${id}-title`}\n data-state={open ? \"open\" : \"closed\"}\n className={cn(\n \"kz-tray fixed inset-x-0 bottom-0 z-40 flex max-h-[70dvh] flex-col rounded-t-2xl border border-border-default bg-surface-1 shadow-lg sm:inset-x-auto sm:bottom-4 sm:right-4 sm:w-96 sm:rounded-2xl\",\n className,\n )}\n {...props}\n >\n <header className=\"flex items-center gap-3 px-4 py-3\">\n <div className=\"min-w-0 flex-1\">\n <h2 id={`${id}-title`} className=\"m-0 text-sm font-semibold text-fg\">\n {title}\n </h2>\n <p className=\"m-0 truncate text-xs text-fg-secondary\">{summary}</p>\n </div>\n <button type=\"button\" aria-expanded={open} aria-controls={`${id}-body`} onClick={() => setOpen(!open)} className=\"rounded-md px-2 py-1 text-sm text-fg-secondary hover:text-fg focus-visible:focus-ring\">\n {open ? toggleLabel : openLabel}\n </button>\n </header>\n {open ? (\n <div id={`${id}-body`} className=\"kz-tray-body min-h-0 overflow-y-auto px-4 pb-2\">\n {children}\n </div>\n ) : null}\n </section>\n );\n}\n"],"mappings":"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","names":[]}