@devalok/shilp-sutra 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/AGENTS.md +7 -1
  2. package/BREAKING.json +29 -0
  3. package/MIGRATION.md +55 -0
  4. package/dist/composed/avatar-group.d.ts +11 -9
  5. package/dist/composed/avatar-group.d.ts.map +1 -1
  6. package/dist/composed/avatar-group.js +108 -140
  7. package/dist/composed/avatar-group.js.map +1 -1
  8. package/dist/composed/bulk-action-bar.d.ts +19 -11
  9. package/dist/composed/bulk-action-bar.d.ts.map +1 -1
  10. package/dist/composed/bulk-action-bar.js +81 -110
  11. package/dist/composed/bulk-action-bar.js.map +1 -1
  12. package/dist/composed/error-boundary.d.ts +15 -3
  13. package/dist/composed/error-boundary.d.ts.map +1 -1
  14. package/dist/composed/error-boundary.js +43 -26
  15. package/dist/composed/error-boundary.js.map +1 -1
  16. package/dist/composed/loading-skeleton.d.ts +2 -0
  17. package/dist/composed/loading-skeleton.d.ts.map +1 -1
  18. package/dist/composed/loading-skeleton.js +79 -56
  19. package/dist/composed/loading-skeleton.js.map +1 -1
  20. package/dist/composed/master-detail.d.ts +21 -8
  21. package/dist/composed/master-detail.d.ts.map +1 -1
  22. package/dist/composed/master-detail.js +91 -77
  23. package/dist/composed/master-detail.js.map +1 -1
  24. package/dist/composed/page-skeletons.d.ts +6 -0
  25. package/dist/composed/page-skeletons.d.ts.map +1 -1
  26. package/dist/composed/page-skeletons.js +93 -71
  27. package/dist/composed/page-skeletons.js.map +1 -1
  28. package/dist/shell/index.d.ts +5 -3
  29. package/dist/shell/index.d.ts.map +1 -1
  30. package/dist/shell/index.js +2 -3
  31. package/dist/shell/notification-preferences.d.ts.map +1 -1
  32. package/dist/shell/notification-preferences.js +2 -0
  33. package/dist/shell/notification-preferences.js.map +1 -1
  34. package/dist/ui/combobox.d.ts +1 -1
  35. package/dist/ui/combobox.d.ts.map +1 -1
  36. package/dist/ui/combobox.js +133 -128
  37. package/dist/ui/combobox.js.map +1 -1
  38. package/dist/ui/file-upload.d.ts.map +1 -1
  39. package/dist/ui/file-upload.js +124 -138
  40. package/dist/ui/file-upload.js.map +1 -1
  41. package/dist/ui/form.d.ts +7 -0
  42. package/dist/ui/form.d.ts.map +1 -1
  43. package/dist/ui/form.js +4 -2
  44. package/dist/ui/form.js.map +1 -1
  45. package/dist/ui/icon-button.d.ts +2 -1
  46. package/dist/ui/icon-button.d.ts.map +1 -1
  47. package/dist/ui/icon-button.js +1 -1
  48. package/dist/ui/icon-button.js.map +1 -1
  49. package/dist/ui/label.d.ts.map +1 -1
  50. package/dist/ui/label.js +7 -6
  51. package/dist/ui/label.js.map +1 -1
  52. package/dist/ui/spinner.d.ts.map +1 -1
  53. package/dist/ui/spinner.js +50 -44
  54. package/dist/ui/spinner.js.map +1 -1
  55. package/dist/ui/switch.d.ts.map +1 -1
  56. package/dist/ui/switch.js +24 -24
  57. package/dist/ui/switch.js.map +1 -1
  58. package/dist/ui/table.js +1 -1
  59. package/dist/ui/table.js.map +1 -1
  60. package/docs/components/composed/bulk-action-bar.md +9 -3
  61. package/docs/components/composed/error-boundary.md +18 -2
  62. package/docs/components/composed/master-detail.md +12 -2
  63. package/docs/components/composed/page-skeletons.md +2 -2
  64. package/docs/components/composed/rich-chat-input.md +18 -6
  65. package/docs/components/composed/simple-tooltip.md +1 -1
  66. package/docs/components/shell/app-command-palette.md +2 -1
  67. package/docs/components/shell/bottom-navbar.md +4 -4
  68. package/docs/components/shell/command-registry.md +2 -2
  69. package/docs/components/shell/link-context.md +2 -2
  70. package/docs/components/shell/top-bar.md +1 -1
  71. package/docs/components/ui/search-input.md +2 -2
  72. package/docs/components/ui/slider.md +2 -2
  73. package/docs/components/ui/split-button.md +3 -3
  74. package/llms.txt +2 -2
  75. package/make-kit/Guidelines.md +1 -1
  76. package/make-kit/components/overview.md +2 -2
  77. package/make-kit/foundations/surfaces.md +1 -1
  78. package/mcp-manifest.json +403 -147
  79. package/package.json +1 -6
  80. package/skill/SKILL.md +1 -1
  81. package/skill/chat-skill/README.md +18 -0
  82. package/skill/chat-skill/SKILL.md +78 -0
  83. package/skill/chat-skill/chatgpt-instructions.md +26 -0
  84. package/skill/chat-skill/gemini-instructions.md +25 -0
  85. package/skill/references/components.md +2 -2
  86. package/dist/shell/sidebar.d.ts +0 -113
  87. package/dist/shell/sidebar.d.ts.map +0 -1
  88. package/dist/shell/sidebar.js +0 -293
  89. package/dist/shell/sidebar.js.map +0 -1
  90. package/docs/components/shell/sidebar.md +0 -103
@@ -1,127 +1,127 @@
1
1
  "use client";
2
- import { durations as e, springs as t, tweens as n } from "./lib/motion.js";
3
- import { cn as r } from "./lib/utils.js";
4
- import { Spinner as i } from "./spinner.js";
5
- import { Icon as a } from "./icon.js";
6
- import * as o from "react";
7
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
8
- import { IconCheck as l, IconPaperclip as u, IconUpload as d } from "@tabler/icons-react";
9
- import { AnimatePresence as f, motion as p } from "framer-motion";
2
+ import { springs as e, tweens as t } from "./lib/motion.js";
3
+ import { cn as n } from "./lib/utils.js";
4
+ import { Spinner as r } from "./spinner.js";
5
+ import { Icon as i } from "./icon.js";
6
+ import * as a from "react";
7
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
8
+ import { IconCheck as c, IconPaperclip as l, IconUpload as u } from "@tabler/icons-react";
9
+ import { AnimatePresence as d, motion as f } from "framer-motion";
10
10
  //#region src/ui/file-upload.tsx
11
- var m = 10 * 1024 * 1024;
12
- function h(e, t) {
11
+ var p = 10 * 1024 * 1024;
12
+ function m(e, t) {
13
13
  return t.split(",").map((e) => e.trim()).some((t) => t.startsWith(".") ? e.name.toLowerCase().endsWith(t.toLowerCase()) : t.endsWith("/*") ? e.type.startsWith(t.slice(0, t.indexOf("/") + 1)) : e.type === t);
14
14
  }
15
- function g(e) {
15
+ function h(e) {
16
16
  return e < 1024 ? `${e} B` : e < 1024 * 1024 ? `${(e / 1024).toFixed(1)} KB` : `${(e / (1024 * 1024)).toFixed(1)} MB`;
17
17
  }
18
- var _ = o.forwardRef(({ accept: _, maxSize: v = m, multiple: y = !1, uploading: b = !1, progress: x = 0, onFiles: S, error: C, compact: w = !1, disabled: T = !1, className: E, label: D, sublabel: O, ...k }, A) => {
19
- let j = o.useRef(null), [M, N] = o.useState(!1), [P, F] = o.useState(null), I = o.useRef(!1), L = o.useRef(!1), R = C || P, z = o.useCallback(() => {
20
- T || b || j.current?.click();
21
- }, [T, b]), B = o.useCallback((e) => {
22
- F(null);
18
+ var g = a.forwardRef(({ accept: g, maxSize: _ = p, multiple: v = !1, uploading: y = !1, progress: b = 0, onFiles: x, error: S, compact: C = !1, disabled: w = !1, className: T, label: E, sublabel: D, ...O }, k) => {
19
+ let A = a.useRef(null), [j, M] = a.useState(!1), [N, P] = a.useState(null), F = a.useRef(!1), I = a.useRef(!1), L = S || N, R = a.useCallback(() => {
20
+ w || y || A.current?.click();
21
+ }, [w, y]), z = a.useCallback((e) => {
22
+ P(null);
23
23
  for (let t of e) {
24
- if (t.size > v) {
25
- F(`File "${t.name}" exceeds the maximum size of ${g(v)}`);
24
+ if (t.size > _) {
25
+ P(`File "${t.name}" exceeds the maximum size of ${h(_)}`);
26
26
  return;
27
27
  }
28
- if (_ && !h(t, _)) {
29
- F(`File "${t.name}" is not an accepted file type`);
28
+ if (g && !m(t, g)) {
29
+ P(`File "${t.name}" is not an accepted file type`);
30
30
  return;
31
31
  }
32
32
  }
33
- S(e);
33
+ x(e);
34
34
  }, [
35
- v,
36
35
  _,
37
- S
38
- ]), V = o.useCallback((e) => {
39
- L.current = !0;
36
+ g,
37
+ x
38
+ ]), B = a.useCallback((e) => {
39
+ I.current = !0;
40
40
  let t = e.target.files;
41
- !t || t.length === 0 || (B(Array.from(t)), e.target.value = "");
42
- }, [B]), H = o.useCallback(() => {
43
- I.current = !0, L.current = !1;
44
- }, []), U = o.useCallback(() => {
45
- I.current && !L.current && _ && F("File type is not accepted"), I.current = !1;
46
- }, [_]), W = o.useCallback((e) => {
47
- e.preventDefault(), e.stopPropagation(), !T && !b && N(!0);
48
- }, [T, b]), G = o.useCallback((e) => {
49
- e.preventDefault(), e.stopPropagation(), !T && !b && N(!0);
50
- }, [T, b]), K = o.useCallback((e) => {
51
- e.preventDefault(), e.stopPropagation(), N(!1);
52
- }, []), q = o.useCallback((e) => {
53
- if (e.preventDefault(), e.stopPropagation(), N(!1), T || b) return;
41
+ !t || t.length === 0 || (z(Array.from(t)), e.target.value = "");
42
+ }, [z]), V = a.useCallback(() => {
43
+ F.current = !0, I.current = !1;
44
+ }, []), H = a.useCallback(() => {
45
+ F.current && !I.current && g && P("File type is not accepted"), F.current = !1;
46
+ }, [g]), U = a.useCallback((e) => {
47
+ e.preventDefault(), e.stopPropagation(), !w && !y && M(!0);
48
+ }, [w, y]), W = a.useCallback((e) => {
49
+ e.preventDefault(), e.stopPropagation(), !w && !y && M(!0);
50
+ }, [w, y]), G = a.useCallback((e) => {
51
+ e.preventDefault(), e.stopPropagation(), M(!1);
52
+ }, []), K = a.useCallback((e) => {
53
+ if (e.preventDefault(), e.stopPropagation(), M(!1), w || y) return;
54
54
  let t = e.dataTransfer.files;
55
- !t || t.length === 0 || B(Array.from(t));
55
+ !t || t.length === 0 || z(Array.from(t));
56
56
  }, [
57
- T,
58
- b,
59
- B
60
- ]), J = D ?? "Drop files here or click to browse", Y = O ?? `Max file size: ${g(v)}`, X = o.useId();
61
- return w ? /* @__PURE__ */ c("div", {
62
- ref: A,
63
- className: r("inline-flex flex-col", E),
64
- ...k,
57
+ w,
58
+ y,
59
+ z
60
+ ]), q = E ?? "Drop files here or click to browse", J = D ?? `Max file size: ${h(_)}`, Y = a.useId();
61
+ return C ? /* @__PURE__ */ s("div", {
62
+ ref: k,
63
+ className: n("inline-flex flex-col", T),
64
+ ...O,
65
65
  children: [
66
- /* @__PURE__ */ c("button", {
66
+ /* @__PURE__ */ s("button", {
67
67
  type: "button",
68
- onClick: z,
69
- disabled: T,
70
- className: r("inline-flex items-center gap-ds-02 rounded-control px-ds-03 py-ds-02", "text-body-sm font-medium", "border border-surface-border-strong", "bg-surface-raised-hover text-surface-fg-muted", "hover:bg-accent-2", "disabled:opacity-action-disabled disabled:cursor-not-allowed", "transition-colors"),
71
- children: [b ? /* @__PURE__ */ s(i, { size: "sm" }) : /* @__PURE__ */ s(a, {
72
- icon: u,
68
+ onClick: R,
69
+ disabled: w,
70
+ className: n("inline-flex items-center gap-ds-02 rounded-control px-ds-03 py-ds-02", "text-body-sm font-medium", "border border-surface-border-strong", "bg-surface-raised-hover text-surface-fg-muted", "hover:bg-accent-2", "disabled:opacity-action-disabled disabled:cursor-not-allowed", "transition-colors"),
71
+ children: [y ? /* @__PURE__ */ o(r, { size: "sm" }) : /* @__PURE__ */ o(i, {
72
+ icon: l,
73
73
  size: "sm"
74
- }), D ?? "Attach files"]
74
+ }), E ?? "Attach files"]
75
75
  }),
76
- /* @__PURE__ */ s("input", {
77
- ref: j,
78
- id: X,
76
+ /* @__PURE__ */ o("input", {
77
+ ref: A,
78
+ id: Y,
79
79
  type: "file",
80
80
  className: "sr-only",
81
81
  style: { visibility: "hidden" },
82
82
  "aria-hidden": "true",
83
- "aria-label": D ?? "Attach files",
84
- accept: _,
85
- multiple: y,
86
- disabled: T,
87
- onChange: V,
88
- onClick: H,
89
- onFocus: U,
83
+ "aria-label": E ?? "Attach files",
84
+ accept: g,
85
+ multiple: v,
86
+ disabled: w,
87
+ onChange: B,
88
+ onClick: V,
89
+ onFocus: H,
90
90
  tabIndex: -1
91
91
  }),
92
- R && /* @__PURE__ */ s("p", {
92
+ L && /* @__PURE__ */ o("p", {
93
93
  role: "alert",
94
94
  "aria-live": "polite",
95
95
  className: "mt-ds-02 text-body-xs text-error-11",
96
- children: R
96
+ children: L
97
97
  })
98
98
  ]
99
- }) : /* @__PURE__ */ c("div", {
100
- ref: A,
101
- ...k,
99
+ }) : /* @__PURE__ */ s("div", {
100
+ ref: k,
101
+ ...O,
102
102
  role: "presentation",
103
- className: r("flex flex-col", E),
104
- "data-drag-active": M ? "true" : void 0,
105
- onDragEnter: W,
106
- onDragOver: G,
107
- onDragLeave: K,
108
- onDrop: q,
103
+ className: n("flex flex-col", T),
104
+ "data-drag-active": j ? "true" : void 0,
105
+ onDragEnter: U,
106
+ onDragOver: W,
107
+ onDragLeave: G,
108
+ onDrop: K,
109
109
  children: [
110
- /* @__PURE__ */ c(p.div, {
110
+ /* @__PURE__ */ s(f.div, {
111
111
  role: "button",
112
- tabIndex: 0,
113
- "aria-disabled": T || void 0,
114
- onClick: T ? void 0 : z,
112
+ tabIndex: w ? -1 : 0,
113
+ "aria-disabled": w || void 0,
114
+ onClick: w ? void 0 : R,
115
115
  onKeyDown: (e) => {
116
- !T && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), z());
116
+ !w && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), R());
117
117
  },
118
- className: r("flex flex-col items-center justify-center gap-ds-03 rounded-surface", "border-2 border-dashed p-ds-08", "cursor-pointer", "border-surface-border-strong bg-surface-raised-hover", M && "border-accent-7 bg-accent-2", T && "opacity-action-disabled cursor-not-allowed"),
119
- animate: { scale: M ? 1.02 : 1 },
120
- transition: t.snappy,
118
+ className: n("flex flex-col items-center justify-center gap-ds-03 rounded-surface", "border-2 border-dashed p-ds-08", "focus-ring transition-colors duration-fast-02 ease-productive-standard", "border-surface-border-strong bg-surface-base", w ? "cursor-not-allowed opacity-action-disabled" : "cursor-pointer hover:bg-surface-raised-hover", j && "border-accent-7 bg-accent-2"),
119
+ animate: { scale: j ? 1.02 : 1 },
120
+ transition: e.snappy,
121
121
  children: [
122
- /* @__PURE__ */ s(f, {
122
+ /* @__PURE__ */ o(d, {
123
123
  mode: "wait",
124
- children: b ? /* @__PURE__ */ s(p.div, {
124
+ children: y ? /* @__PURE__ */ o(f.div, {
125
125
  initial: {
126
126
  opacity: 0,
127
127
  scale: .5
@@ -134,9 +134,9 @@ var _ = o.forwardRef(({ accept: _, maxSize: v = m, multiple: y = !1, uploading:
134
134
  opacity: 0,
135
135
  scale: .5
136
136
  },
137
- transition: n.fade,
138
- children: /* @__PURE__ */ s(i, { size: "md" })
139
- }, "spinner") : x === 100 && !C ? /* @__PURE__ */ s(p.div, {
137
+ transition: t.fade,
138
+ children: /* @__PURE__ */ o(r, { size: "md" })
139
+ }, "spinner") : b === 100 && !S ? /* @__PURE__ */ o(f.div, {
140
140
  initial: {
141
141
  opacity: 0,
142
142
  scale: .5
@@ -149,97 +149,83 @@ var _ = o.forwardRef(({ accept: _, maxSize: v = m, multiple: y = !1, uploading:
149
149
  opacity: 0,
150
150
  scale: .5
151
151
  },
152
- transition: t.bouncy,
153
- children: /* @__PURE__ */ s(a, {
154
- icon: l,
152
+ transition: e.bouncy,
153
+ children: /* @__PURE__ */ o(i, {
154
+ icon: c,
155
155
  size: "2xl",
156
156
  className: "text-success-11"
157
157
  })
158
- }, "complete") : /* @__PURE__ */ s(p.div, {
158
+ }, "complete") : /* @__PURE__ */ o(f.div, {
159
159
  initial: { opacity: 0 },
160
160
  animate: { opacity: 1 },
161
161
  exit: { opacity: 0 },
162
- transition: n.fade,
163
- children: /* @__PURE__ */ s(a, {
164
- icon: d,
162
+ transition: t.fade,
163
+ children: /* @__PURE__ */ o(i, {
164
+ icon: u,
165
165
  size: "2xl",
166
166
  className: "text-surface-fg-subtle"
167
167
  })
168
168
  }, "upload")
169
169
  }),
170
- /* @__PURE__ */ s("span", {
171
- id: X + "-label",
170
+ /* @__PURE__ */ o("span", {
171
+ id: Y + "-label",
172
172
  className: "text-body-sm text-surface-fg-muted",
173
- children: J
173
+ children: q
174
174
  }),
175
- b ? /* @__PURE__ */ s("div", {
175
+ y ? /* @__PURE__ */ o("div", {
176
176
  className: "w-full max-w-xs",
177
- children: /* @__PURE__ */ s("div", {
177
+ children: /* @__PURE__ */ o("div", {
178
178
  role: "progressbar",
179
- "aria-valuenow": x,
179
+ "aria-valuenow": b,
180
180
  "aria-valuemin": 0,
181
181
  "aria-valuemax": 100,
182
182
  className: "h-2 w-full overflow-hidden rounded-pill bg-surface-raised-hover",
183
- children: /* @__PURE__ */ s(p.div, {
184
- className: "h-full rounded-pill bg-accent-9",
185
- animate: { width: `${x}%` },
186
- transition: t.smooth
183
+ children: /* @__PURE__ */ o(f.div, {
184
+ className: "h-full w-full origin-left rounded-pill bg-accent-9",
185
+ animate: { scaleX: Math.max(0, Math.min(100, b)) / 100 },
186
+ transition: e.smooth
187
187
  })
188
188
  })
189
- }) : /* @__PURE__ */ s("span", {
189
+ }) : /* @__PURE__ */ o("span", {
190
190
  className: "text-caption text-surface-fg-subtle",
191
- children: Y
191
+ children: J
192
192
  })
193
193
  ]
194
194
  }),
195
- /* @__PURE__ */ s("input", {
196
- ref: j,
197
- id: X,
195
+ /* @__PURE__ */ o("input", {
196
+ ref: A,
197
+ id: Y,
198
198
  type: "file",
199
199
  className: "sr-only",
200
200
  style: { visibility: "hidden" },
201
201
  "aria-hidden": "true",
202
- "aria-label": J,
203
- accept: _,
204
- multiple: y,
205
- disabled: T,
206
- onChange: V,
207
- onClick: H,
208
- onFocus: U,
202
+ "aria-label": q,
203
+ accept: g,
204
+ multiple: v,
205
+ disabled: w,
206
+ onChange: B,
207
+ onClick: V,
208
+ onFocus: H,
209
209
  tabIndex: -1
210
210
  }),
211
- /* @__PURE__ */ s(f, { children: R && /* @__PURE__ */ s(p.p, {
211
+ /* @__PURE__ */ o(d, { children: L && /* @__PURE__ */ o(f.p, {
212
212
  role: "alert",
213
213
  className: "mt-ds-02 text-body-xs text-error-11",
214
214
  initial: {
215
215
  opacity: 0,
216
- x: 0
216
+ y: -2
217
217
  },
218
218
  animate: {
219
219
  opacity: 1,
220
- x: [
221
- 0,
222
- -4,
223
- 4,
224
- -4,
225
- 4,
226
- 0
227
- ]
220
+ y: 0
228
221
  },
229
222
  exit: { opacity: 0 },
230
- transition: {
231
- opacity: n.fade,
232
- x: {
233
- type: "tween",
234
- duration: e.slow01,
235
- ease: "easeOut"
236
- }
237
- },
238
- children: R
223
+ transition: t.fade,
224
+ children: L
239
225
  }) })
240
226
  ]
241
227
  });
242
228
  });
243
- _.displayName = "FileUpload";
229
+ g.displayName = "FileUpload";
244
230
  //#endregion
245
- export { _ as FileUpload };
231
+ export { g as FileUpload };
@@ -1 +1 @@
1
- {"version":3,"file":"file-upload.js","names":[],"sources":["../../src/ui/file-upload.tsx"],"sourcesContent":["'use client'\n\nimport { IconCheck,IconPaperclip, IconUpload } from '@tabler/icons-react'\nimport { AnimatePresence,motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Icon } from './icon'\nimport { durations,springs, tweens } from './lib/motion'\nimport { cn } from './lib/utils'\nimport { Spinner } from './spinner'\n\n/**\n * Props for FileUpload — a drag-and-drop file picker with client-side validation (type + size),\n * an upload progress bar, and a compact inline mode.\n *\n * **Two modes:**\n * - Default: large drop zone with an upload icon, label, and sublabel — good for primary upload areas.\n * - `compact={true}`: a small inline button with an attachment icon — good for secondary/inline uploads.\n *\n * **Validation:** Pass `accept` (MIME or extension) and `maxSize` (bytes). Errors are shown inline.\n * The `onFiles` callback only fires with valid files — invalid files are rejected with an error message.\n *\n * **`onFiles` required:** This is the only required prop. Integrating with a real uploader means\n * calling your upload API from this callback and driving `uploading` + `progress` externally.\n *\n * @example\n * // Avatar upload with image-only restriction:\n * <FileUpload\n * accept=\"image/*\"\n * maxSize={2 * 1024 * 1024}\n * onFiles={(files) => uploadAvatar(files[0])}\n * label=\"Upload profile photo\"\n * sublabel=\"PNG, JPG up to 2MB\"\n * />\n *\n * @example\n * // Document multi-upload with progress:\n * <FileUpload\n * multiple\n * accept=\".pdf,.doc,.docx\"\n * uploading={isUploading}\n * progress={uploadPercent}\n * onFiles={handleFiles}\n * />\n *\n * @example\n * // Compact inline attachment button in a comment form:\n * <FileUpload compact onFiles={(files) => attachFiles(files)} label=\"Attach files\" />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface FileUploadProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {\n /** Accepted file types (e.g., \"image/*\", \".pdf,.doc\") */\n accept?: string\n /** Max file size in bytes (default: 10MB) */\n maxSize?: number\n /** Allow multiple files */\n multiple?: boolean\n /** Currently uploading */\n uploading?: boolean\n /** Upload progress (0-100) */\n progress?: number\n /** Callback when files are selected/dropped */\n onFiles: (files: File[]) => void\n /** Error message to display */\n error?: string\n /** Compact mode (inline button instead of drop zone) */\n compact?: boolean\n /** Disabled */\n disabled?: boolean\n /** Custom label */\n label?: string\n /** Custom sublabel */\n sublabel?: string\n}\n\nconst DEFAULT_MAX_SIZE = 10 * 1024 * 1024 // 10MB\n\nfunction fileMatchesAccept(file: File, accept: string): boolean {\n return accept\n .split(',')\n .map((t) => t.trim())\n .some((type) => {\n if (type.startsWith('.'))\n return file.name.toLowerCase().endsWith(type.toLowerCase())\n if (type.endsWith('/*'))\n return file.type.startsWith(type.slice(0, type.indexOf('/') + 1))\n return file.type === type\n })\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nconst FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(\n (\n {\n accept,\n maxSize = DEFAULT_MAX_SIZE,\n multiple = false,\n uploading = false,\n progress = 0,\n onFiles,\n error,\n compact = false,\n disabled = false,\n className,\n label,\n sublabel,\n ...props\n },\n ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [isDragActive, setIsDragActive] = React.useState(false)\n const [validationError, setValidationError] = React.useState<string | null>(\n null,\n )\n // Track whether a file dialog was opened, so we can detect when the\n // browser (or testing library) filters out all files via the accept attribute\n const dialogOpenRef = React.useRef(false)\n const filesAcceptedRef = React.useRef(false)\n\n const displayError = error || validationError\n\n const openPicker = React.useCallback(() => {\n if (disabled || uploading) return\n inputRef.current?.click()\n }, [disabled, uploading])\n\n const validateAndEmit = React.useCallback(\n (files: File[]) => {\n setValidationError(null)\n\n for (const file of files) {\n if (file.size > maxSize) {\n setValidationError(\n `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}`,\n )\n return\n }\n if (accept && !fileMatchesAccept(file, accept)) {\n setValidationError(\n `File \"${file.name}\" is not an accepted file type`,\n )\n return\n }\n }\n\n onFiles(files)\n },\n [maxSize, accept, onFiles],\n )\n\n const handleInputChange = React.useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n filesAcceptedRef.current = true\n const fileList = e.target.files\n if (!fileList || fileList.length === 0) return\n validateAndEmit(Array.from(fileList))\n // Reset input value so the same file can be selected again\n e.target.value = ''\n },\n [validateAndEmit],\n )\n\n // When the file input is clicked, the browser opens a dialog and blurs the input.\n // We track this to detect when the dialog closes without selecting valid files.\n const handleInputClick = React.useCallback(() => {\n dialogOpenRef.current = true\n filesAcceptedRef.current = false\n }, [])\n\n // After the file dialog closes, the browser re-focuses the input.\n // If no files were accepted (e.g., the browser/test library filtered by accept),\n // and the accept prop is set, show a validation error.\n const handleInputFocus = React.useCallback(() => {\n if (dialogOpenRef.current && !filesAcceptedRef.current && accept) {\n setValidationError('File type is not accepted')\n }\n dialogOpenRef.current = false\n }, [accept])\n\n const handleDragEnter = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n if (!disabled && !uploading) {\n setIsDragActive(true)\n }\n },\n [disabled, uploading],\n )\n\n const handleDragOver = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n if (!disabled && !uploading) {\n setIsDragActive(true)\n }\n },\n [disabled, uploading],\n )\n\n const handleDragLeave = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setIsDragActive(false)\n },\n [],\n )\n\n const handleDrop = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setIsDragActive(false)\n\n if (disabled || uploading) return\n\n const fileList = e.dataTransfer.files\n if (!fileList || fileList.length === 0) return\n validateAndEmit(Array.from(fileList))\n },\n [disabled, uploading, validateAndEmit],\n )\n\n const defaultLabel = label ?? 'Drop files here or click to browse'\n const defaultSublabel = sublabel ?? `Max file size: ${formatBytes(maxSize)}`\n const inputId = React.useId()\n\n if (compact) {\n return (\n <div ref={ref} className={cn('inline-flex flex-col', className)} {...props}>\n <button\n type=\"button\"\n onClick={openPicker}\n disabled={disabled}\n className={cn(\n 'inline-flex items-center gap-ds-02 rounded-control px-ds-03 py-ds-02',\n 'text-body-sm font-medium',\n 'border border-surface-border-strong',\n 'bg-surface-raised-hover text-surface-fg-muted',\n 'hover:bg-accent-2',\n 'disabled:opacity-action-disabled disabled:cursor-not-allowed',\n 'transition-colors',\n )}\n >\n {uploading ? (\n <Spinner size=\"sm\" />\n ) : (\n <Icon icon={IconPaperclip} size=\"sm\" />\n )}\n {label ?? 'Attach files'}\n </button>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n className=\"sr-only\"\n style={{ visibility: 'hidden' }}\n aria-hidden=\"true\"\n aria-label={label ?? 'Attach files'}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n onChange={handleInputChange}\n onClick={handleInputClick}\n onFocus={handleInputFocus}\n tabIndex={-1}\n />\n {displayError && (\n <p\n role=\"alert\"\n aria-live=\"polite\"\n className=\"mt-ds-02 text-body-xs text-error-11\"\n >\n {displayError}\n </p>\n )}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n {...props}\n role=\"presentation\"\n className={cn('flex flex-col', className)}\n data-drag-active={isDragActive ? 'true' : undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <motion.div\n role=\"button\"\n tabIndex={0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : openPicker}\n onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!disabled && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault()\n openPicker()\n }\n }}\n className={cn(\n 'flex flex-col items-center justify-center gap-ds-03 rounded-surface',\n 'border-2 border-dashed p-ds-08',\n 'cursor-pointer',\n 'border-surface-border-strong bg-surface-raised-hover',\n isDragActive &&\n 'border-accent-7 bg-accent-2',\n disabled && 'opacity-action-disabled cursor-not-allowed',\n )}\n animate={{\n scale: isDragActive ? 1.02 : 1,\n }}\n transition={springs.snappy}\n >\n {/* Icon — animates between upload ↔ spinner ↔ checkmark */}\n <AnimatePresence mode=\"wait\">\n {uploading ? (\n <motion.div\n key=\"spinner\"\n initial={{ opacity: 0, scale: 0.5 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.5 }}\n transition={tweens.fade}\n >\n <Spinner size=\"md\" />\n </motion.div>\n ) : progress === 100 && !error ? (\n <motion.div\n key=\"complete\"\n initial={{ opacity: 0, scale: 0.5 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.5 }}\n transition={springs.bouncy}\n >\n <Icon icon={IconCheck} size=\"2xl\" className=\"text-success-11\" />\n </motion.div>\n ) : (\n <motion.div\n key=\"upload\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={tweens.fade}\n >\n <Icon icon={IconUpload} size=\"2xl\" className=\"text-surface-fg-subtle\" />\n </motion.div>\n )}\n </AnimatePresence>\n <span id={inputId + '-label'} className=\"text-body-sm text-surface-fg-muted\">\n {defaultLabel}\n </span>\n {uploading ? (\n <div className=\"w-full max-w-xs\">\n <div\n role=\"progressbar\"\n aria-valuenow={progress}\n aria-valuemin={0}\n aria-valuemax={100}\n className=\"h-2 w-full overflow-hidden rounded-pill bg-surface-raised-hover\"\n >\n <motion.div\n className=\"h-full rounded-pill bg-accent-9\"\n animate={{ width: `${progress}%` }}\n transition={springs.smooth}\n />\n </div>\n </div>\n ) : (\n <span className=\"text-caption text-surface-fg-subtle\">\n {defaultSublabel}\n </span>\n )}\n </motion.div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n className=\"sr-only\"\n style={{ visibility: 'hidden' }}\n aria-hidden=\"true\"\n aria-label={defaultLabel}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n onChange={handleInputChange}\n onClick={handleInputClick}\n onFocus={handleInputFocus}\n tabIndex={-1}\n />\n {/* Error message with shake animation */}\n <AnimatePresence>\n {displayError && (\n <motion.p\n role=\"alert\"\n className=\"mt-ds-02 text-body-xs text-error-11\"\n initial={{ opacity: 0, x: 0 }}\n animate={{ opacity: 1, x: [0, -4, 4, -4, 4, 0] }}\n exit={{ opacity: 0 }}\n transition={{ opacity: tweens.fade, x: { type: 'tween', duration: durations.slow01, ease: 'easeOut' } }}\n >\n {displayError}\n </motion.p>\n )}\n </AnimatePresence>\n </div>\n )\n },\n)\nFileUpload.displayName = 'FileUpload'\n\nexport { FileUpload }\n"],"mappings":";;;;;;;;;;AA4EA,IAAM,IAAmB,KAAK,OAAO;AAErC,SAAS,EAAkB,GAAY,GAAyB;CAC9D,OAAO,EACJ,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,MAAM,MACD,EAAK,WAAW,GAAG,IACd,EAAK,KAAK,YAAY,CAAC,CAAC,SAAS,EAAK,YAAY,CAAC,IACxD,EAAK,SAAS,IAAI,IACb,EAAK,KAAK,WAAW,EAAK,MAAM,GAAG,EAAK,QAAQ,GAAG,IAAI,CAAC,CAAC,IAC3D,EAAK,SAAS,CACtB;AACL;AAEA,SAAS,EAAY,GAAuB;CAG1C,OAFI,IAAQ,OAAa,GAAG,EAAM,MAC9B,IAAQ,OAAO,OAAa,IAAI,IAAQ,KAAA,CAAM,QAAQ,CAAC,EAAE,OACtD,IAAI,KAAS,OAAO,MAAA,CAAO,QAAQ,CAAC,EAAE;AAC/C;AAEA,IAAM,IAAa,EAAM,YAErB,EACE,WACA,aAAU,GACV,cAAW,IACX,eAAY,IACZ,cAAW,GACX,YACA,UACA,aAAU,IACV,cAAW,IACX,cACA,UACA,aACA,GAAG,KAEL,MACG;CACH,IAAM,IAAW,EAAM,OAAyB,IAAI,GAC9C,CAAC,GAAc,KAAmB,EAAM,SAAS,EAAK,GACtD,CAAC,GAAiB,KAAsB,EAAM,SAClD,IACF,GAGM,IAAgB,EAAM,OAAO,EAAK,GAClC,IAAmB,EAAM,OAAO,EAAK,GAErC,IAAe,KAAS,GAExB,IAAa,EAAM,kBAAkB;EACrC,KAAY,KAChB,EAAS,SAAS,MAAM;CAC1B,GAAG,CAAC,GAAU,CAAS,CAAC,GAElB,IAAkB,EAAM,aAC3B,MAAkB;EACjB,EAAmB,IAAI;EAEvB,KAAK,IAAM,KAAQ,GAAO;GACxB,IAAI,EAAK,OAAO,GAAS;IACvB,EACE,SAAS,EAAK,KAAK,gCAAgC,EAAY,CAAO,GACxE;IACA;GACF;GACA,IAAI,KAAU,CAAC,EAAkB,GAAM,CAAM,GAAG;IAC9C,EACE,SAAS,EAAK,KAAK,+BACrB;IACA;GACF;EACF;EAEA,EAAQ,CAAK;CACf,GACA;EAAC;EAAS;EAAQ;CAAO,CAC3B,GAEM,IAAoB,EAAM,aAC7B,MAA2C;EAC1C,EAAiB,UAAU;EAC3B,IAAM,IAAW,EAAE,OAAO;EACtB,CAAC,KAAY,EAAS,WAAW,MACrC,EAAgB,MAAM,KAAK,CAAQ,CAAC,GAEpC,EAAE,OAAO,QAAQ;CACnB,GACA,CAAC,CAAe,CAClB,GAIM,IAAmB,EAAM,kBAAkB;EAE/C,AADA,EAAc,UAAU,IACxB,EAAiB,UAAU;CAC7B,GAAG,CAAC,CAAC,GAKC,IAAmB,EAAM,kBAAkB;EAI/C,AAHI,EAAc,WAAW,CAAC,EAAiB,WAAW,KACxD,EAAmB,2BAA2B,GAEhD,EAAc,UAAU;CAC1B,GAAG,CAAC,CAAM,CAAC,GAEL,IAAkB,EAAM,aAC3B,MAAuC;EAGtC,AAFA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GACd,CAAC,KAAY,CAAC,KAChB,EAAgB,EAAI;CAExB,GACA,CAAC,GAAU,CAAS,CACtB,GAEM,IAAiB,EAAM,aAC1B,MAAuC;EAGtC,AAFA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GACd,CAAC,KAAY,CAAC,KAChB,EAAgB,EAAI;CAExB,GACA,CAAC,GAAU,CAAS,CACtB,GAEM,IAAkB,EAAM,aAC3B,MAAuC;EAGtC,AAFA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GAClB,EAAgB,EAAK;CACvB,GACA,CAAC,CACH,GAEM,IAAa,EAAM,aACtB,MAAuC;EAKtC,IAJA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GAClB,EAAgB,EAAK,GAEjB,KAAY,GAAW;EAE3B,IAAM,IAAW,EAAE,aAAa;EAC5B,CAAC,KAAY,EAAS,WAAW,KACrC,EAAgB,MAAM,KAAK,CAAQ,CAAC;CACtC,GACA;EAAC;EAAU;EAAW;CAAe,CACvC,GAEM,IAAe,KAAS,sCACxB,IAAkB,KAAY,kBAAkB,EAAY,CAAO,KACnE,IAAU,EAAM,MAAM;CAuD5B,OArDI,IAEA,kBAAC,OAAD;EAAU;EAAK,WAAW,EAAG,wBAAwB,CAAS;EAAG,GAAI;YAArE;GACE,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACC;IACV,WAAW,EACT,wEACA,4BACA,uCACA,iDACA,qBACA,gEACA,mBACF;cAZF,CAcG,IACC,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA,IAEpB,kBAAC,GAAD;KAAM,MAAM;KAAe,MAAK;IAAM,CAAA,GAEvC,KAAS,cACJ;;GACR,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACL,WAAU;IACV,OAAO,EAAE,YAAY,SAAS;IAC9B,eAAY;IACZ,cAAY,KAAS;IACb;IACE;IACA;IACV,UAAU;IACV,SAAS;IACT,SAAS;IACT,UAAU;GACX,CAAA;GACA,KACC,kBAAC,KAAD;IACE,MAAK;IACL,aAAU;IACV,WAAU;cAET;GACA,CAAA;EAEF;MAKP,kBAAC,OAAD;EACO;EACL,GAAI;EACJ,MAAK;EACL,WAAW,EAAG,iBAAiB,CAAS;EACxC,oBAAkB,IAAe,SAAS,KAAA;EAC1C,aAAa;EACb,YAAY;EACZ,aAAa;EACb,QAAQ;YATV;GAWE,kBAAC,EAAO,KAAR;IACE,MAAK;IACL,UAAU;IACV,iBAAe,KAAY,KAAA;IAC3B,SAAS,IAAW,KAAA,IAAY;IAChC,YAAY,MAA2C;KACrD,AAAI,CAAC,MAAa,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC/C,EAAE,eAAe,GACjB,EAAW;IAEf;IACA,WAAW,EACT,uEACA,kCACA,kBACA,wDACA,KACE,+BACF,KAAY,4CACd;IACA,SAAS,EACP,OAAO,IAAe,OAAO,EAC/B;IACA,YAAY,EAAQ;cAvBtB;KA0BE,kBAAC,GAAD;MAAiB,MAAK;gBACnB,IACC,kBAAC,EAAO,KAAR;OAEE,SAAS;QAAE,SAAS;QAAG,OAAO;OAAI;OAClC,SAAS;QAAE,SAAS;QAAG,OAAO;OAAE;OAChC,MAAM;QAAE,SAAS;QAAG,OAAO;OAAI;OAC/B,YAAY,EAAO;iBAEnB,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;MACV,GAPN,SAOM,IACV,MAAa,OAAO,CAAC,IACvB,kBAAC,EAAO,KAAR;OAEE,SAAS;QAAE,SAAS;QAAG,OAAO;OAAI;OAClC,SAAS;QAAE,SAAS;QAAG,OAAO;OAAE;OAChC,MAAM;QAAE,SAAS;QAAG,OAAO;OAAI;OAC/B,YAAY,EAAQ;iBAEpB,kBAAC,GAAD;QAAM,MAAM;QAAW,MAAK;QAAM,WAAU;OAAmB,CAAA;MACrD,GAPN,UAOM,IAEZ,kBAAC,EAAO,KAAR;OAEE,SAAS,EAAE,SAAS,EAAE;OACtB,SAAS,EAAE,SAAS,EAAE;OACtB,MAAM,EAAE,SAAS,EAAE;OACnB,YAAY,EAAO;iBAEnB,kBAAC,GAAD;QAAM,MAAM;QAAY,MAAK;QAAM,WAAU;OAA0B,CAAA;MAC7D,GAPN,QAOM;KAEC,CAAA;KACjB,kBAAC,QAAD;MAAM,IAAI,IAAU;MAAU,WAAU;gBACrC;KACG,CAAA;KACL,IACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,MAAK;OACL,iBAAe;OACf,iBAAe;OACf,iBAAe;OACf,WAAU;iBAEV,kBAAC,EAAO,KAAR;QACE,WAAU;QACV,SAAS,EAAE,OAAO,GAAG,EAAS,GAAG;QACjC,YAAY,EAAQ;OACrB,CAAA;MACE,CAAA;KACF,CAAA,IAEL,kBAAC,QAAD;MAAM,WAAU;gBACb;KACG,CAAA;IAEE;;GACZ,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACL,WAAU;IACV,OAAO,EAAE,YAAY,SAAS;IAC9B,eAAY;IACZ,cAAY;IACJ;IACE;IACA;IACV,UAAU;IACV,SAAS;IACT,SAAS;IACT,UAAU;GACX,CAAA;GAED,kBAAC,GAAD,EAAA,UACG,KACC,kBAAC,EAAO,GAAR;IACE,MAAK;IACL,WAAU;IACV,SAAS;KAAE,SAAS;KAAG,GAAG;IAAE;IAC5B,SAAS;KAAE,SAAS;KAAG,GAAG;MAAC;MAAG;MAAI;MAAG;MAAI;MAAG;KAAC;IAAE;IAC/C,MAAM,EAAE,SAAS,EAAE;IACnB,YAAY;KAAE,SAAS,EAAO;KAAM,GAAG;MAAE,MAAM;MAAS,UAAU,EAAU;MAAQ,MAAM;KAAU;IAAE;cAErG;GACO,CAAA,EAEG,CAAA;EACd;;AAET,CACF;AACA,EAAW,cAAc"}
1
+ {"version":3,"file":"file-upload.js","names":[],"sources":["../../src/ui/file-upload.tsx"],"sourcesContent":["'use client'\n\nimport { IconCheck,IconPaperclip, IconUpload } from '@tabler/icons-react'\nimport { AnimatePresence,motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Icon } from './icon'\nimport { springs, tweens } from './lib/motion'\nimport { cn } from './lib/utils'\nimport { Spinner } from './spinner'\n\n/**\n * Props for FileUpload — a drag-and-drop file picker with client-side validation (type + size),\n * an upload progress bar, and a compact inline mode.\n *\n * **Two modes:**\n * - Default: large drop zone with an upload icon, label, and sublabel — good for primary upload areas.\n * - `compact={true}`: a small inline button with an attachment icon — good for secondary/inline uploads.\n *\n * **Validation:** Pass `accept` (MIME or extension) and `maxSize` (bytes). Errors are shown inline.\n * The `onFiles` callback only fires with valid files — invalid files are rejected with an error message.\n *\n * **`onFiles` required:** This is the only required prop. Integrating with a real uploader means\n * calling your upload API from this callback and driving `uploading` + `progress` externally.\n *\n * @example\n * // Avatar upload with image-only restriction:\n * <FileUpload\n * accept=\"image/*\"\n * maxSize={2 * 1024 * 1024}\n * onFiles={(files) => uploadAvatar(files[0])}\n * label=\"Upload profile photo\"\n * sublabel=\"PNG, JPG up to 2MB\"\n * />\n *\n * @example\n * // Document multi-upload with progress:\n * <FileUpload\n * multiple\n * accept=\".pdf,.doc,.docx\"\n * uploading={isUploading}\n * progress={uploadPercent}\n * onFiles={handleFiles}\n * />\n *\n * @example\n * // Compact inline attachment button in a comment form:\n * <FileUpload compact onFiles={(files) => attachFiles(files)} label=\"Attach files\" />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface FileUploadProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {\n /** Accepted file types (e.g., \"image/*\", \".pdf,.doc\") */\n accept?: string\n /** Max file size in bytes (default: 10MB) */\n maxSize?: number\n /** Allow multiple files */\n multiple?: boolean\n /** Currently uploading */\n uploading?: boolean\n /** Upload progress (0-100) */\n progress?: number\n /** Callback when files are selected/dropped */\n onFiles: (files: File[]) => void\n /** Error message to display */\n error?: string\n /** Compact mode (inline button instead of drop zone) */\n compact?: boolean\n /** Disabled */\n disabled?: boolean\n /** Custom label */\n label?: string\n /** Custom sublabel */\n sublabel?: string\n}\n\nconst DEFAULT_MAX_SIZE = 10 * 1024 * 1024 // 10MB\n\nfunction fileMatchesAccept(file: File, accept: string): boolean {\n return accept\n .split(',')\n .map((t) => t.trim())\n .some((type) => {\n if (type.startsWith('.'))\n return file.name.toLowerCase().endsWith(type.toLowerCase())\n if (type.endsWith('/*'))\n return file.type.startsWith(type.slice(0, type.indexOf('/') + 1))\n return file.type === type\n })\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nconst FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(\n (\n {\n accept,\n maxSize = DEFAULT_MAX_SIZE,\n multiple = false,\n uploading = false,\n progress = 0,\n onFiles,\n error,\n compact = false,\n disabled = false,\n className,\n label,\n sublabel,\n ...props\n },\n ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [isDragActive, setIsDragActive] = React.useState(false)\n const [validationError, setValidationError] = React.useState<string | null>(\n null,\n )\n // Track whether a file dialog was opened, so we can detect when the\n // browser (or testing library) filters out all files via the accept attribute\n const dialogOpenRef = React.useRef(false)\n const filesAcceptedRef = React.useRef(false)\n\n const displayError = error || validationError\n\n const openPicker = React.useCallback(() => {\n if (disabled || uploading) return\n inputRef.current?.click()\n }, [disabled, uploading])\n\n const validateAndEmit = React.useCallback(\n (files: File[]) => {\n setValidationError(null)\n\n for (const file of files) {\n if (file.size > maxSize) {\n setValidationError(\n `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}`,\n )\n return\n }\n if (accept && !fileMatchesAccept(file, accept)) {\n setValidationError(\n `File \"${file.name}\" is not an accepted file type`,\n )\n return\n }\n }\n\n onFiles(files)\n },\n [maxSize, accept, onFiles],\n )\n\n const handleInputChange = React.useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n filesAcceptedRef.current = true\n const fileList = e.target.files\n if (!fileList || fileList.length === 0) return\n validateAndEmit(Array.from(fileList))\n // Reset input value so the same file can be selected again\n e.target.value = ''\n },\n [validateAndEmit],\n )\n\n // When the file input is clicked, the browser opens a dialog and blurs the input.\n // We track this to detect when the dialog closes without selecting valid files.\n const handleInputClick = React.useCallback(() => {\n dialogOpenRef.current = true\n filesAcceptedRef.current = false\n }, [])\n\n // After the file dialog closes, the browser re-focuses the input.\n // If no files were accepted (e.g., the browser/test library filtered by accept),\n // and the accept prop is set, show a validation error.\n const handleInputFocus = React.useCallback(() => {\n if (dialogOpenRef.current && !filesAcceptedRef.current && accept) {\n setValidationError('File type is not accepted')\n }\n dialogOpenRef.current = false\n }, [accept])\n\n const handleDragEnter = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n if (!disabled && !uploading) {\n setIsDragActive(true)\n }\n },\n [disabled, uploading],\n )\n\n const handleDragOver = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n if (!disabled && !uploading) {\n setIsDragActive(true)\n }\n },\n [disabled, uploading],\n )\n\n const handleDragLeave = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setIsDragActive(false)\n },\n [],\n )\n\n const handleDrop = React.useCallback(\n (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setIsDragActive(false)\n\n if (disabled || uploading) return\n\n const fileList = e.dataTransfer.files\n if (!fileList || fileList.length === 0) return\n validateAndEmit(Array.from(fileList))\n },\n [disabled, uploading, validateAndEmit],\n )\n\n const defaultLabel = label ?? 'Drop files here or click to browse'\n const defaultSublabel = sublabel ?? `Max file size: ${formatBytes(maxSize)}`\n const inputId = React.useId()\n\n if (compact) {\n return (\n <div ref={ref} className={cn('inline-flex flex-col', className)} {...props}>\n <button\n type=\"button\"\n onClick={openPicker}\n disabled={disabled}\n className={cn(\n 'inline-flex items-center gap-ds-02 rounded-control px-ds-03 py-ds-02',\n 'text-body-sm font-medium',\n 'border border-surface-border-strong',\n 'bg-surface-raised-hover text-surface-fg-muted',\n 'hover:bg-accent-2',\n 'disabled:opacity-action-disabled disabled:cursor-not-allowed',\n 'transition-colors',\n )}\n >\n {uploading ? (\n <Spinner size=\"sm\" />\n ) : (\n <Icon icon={IconPaperclip} size=\"sm\" />\n )}\n {label ?? 'Attach files'}\n </button>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n className=\"sr-only\"\n style={{ visibility: 'hidden' }}\n aria-hidden=\"true\"\n aria-label={label ?? 'Attach files'}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n onChange={handleInputChange}\n onClick={handleInputClick}\n onFocus={handleInputFocus}\n tabIndex={-1}\n />\n {displayError && (\n <p\n role=\"alert\"\n aria-live=\"polite\"\n className=\"mt-ds-02 text-body-xs text-error-11\"\n >\n {displayError}\n </p>\n )}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n {...props}\n role=\"presentation\"\n className={cn('flex flex-col', className)}\n data-drag-active={isDragActive ? 'true' : undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <motion.div\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : openPicker}\n onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!disabled && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault()\n openPicker()\n }\n }}\n className={cn(\n 'flex flex-col items-center justify-center gap-ds-03 rounded-surface',\n 'border-2 border-dashed p-ds-08',\n 'focus-ring transition-colors duration-fast-02 ease-productive-standard',\n // Rest on the canvas fill; reserve the hover token for actual hover.\n 'border-surface-border-strong bg-surface-base',\n disabled ? 'cursor-not-allowed opacity-action-disabled' : 'cursor-pointer hover:bg-surface-raised-hover',\n isDragActive && 'border-accent-7 bg-accent-2',\n )}\n animate={{\n scale: isDragActive ? 1.02 : 1,\n }}\n transition={springs.snappy}\n >\n {/* Icon — animates between upload ↔ spinner ↔ checkmark */}\n <AnimatePresence mode=\"wait\">\n {uploading ? (\n <motion.div\n key=\"spinner\"\n initial={{ opacity: 0, scale: 0.5 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.5 }}\n transition={tweens.fade}\n >\n <Spinner size=\"md\" />\n </motion.div>\n ) : progress === 100 && !error ? (\n <motion.div\n key=\"complete\"\n initial={{ opacity: 0, scale: 0.5 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.5 }}\n transition={springs.bouncy}\n >\n <Icon icon={IconCheck} size=\"2xl\" className=\"text-success-11\" />\n </motion.div>\n ) : (\n <motion.div\n key=\"upload\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={tweens.fade}\n >\n <Icon icon={IconUpload} size=\"2xl\" className=\"text-surface-fg-subtle\" />\n </motion.div>\n )}\n </AnimatePresence>\n <span id={inputId + '-label'} className=\"text-body-sm text-surface-fg-muted\">\n {defaultLabel}\n </span>\n {uploading ? (\n <div className=\"w-full max-w-xs\">\n <div\n role=\"progressbar\"\n aria-valuenow={progress}\n aria-valuemin={0}\n aria-valuemax={100}\n className=\"h-2 w-full overflow-hidden rounded-pill bg-surface-raised-hover\"\n >\n <motion.div\n // scaleX (compositor-only) not width (layout) → smooth + honored by reduced-motion.\n className=\"h-full w-full origin-left rounded-pill bg-accent-9\"\n animate={{ scaleX: Math.max(0, Math.min(100, progress)) / 100 }}\n transition={springs.smooth}\n />\n </div>\n </div>\n ) : (\n <span className=\"text-caption text-surface-fg-subtle\">\n {defaultSublabel}\n </span>\n )}\n </motion.div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n className=\"sr-only\"\n style={{ visibility: 'hidden' }}\n aria-hidden=\"true\"\n aria-label={defaultLabel}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n onChange={handleInputChange}\n onClick={handleInputClick}\n onFocus={handleInputFocus}\n tabIndex={-1}\n />\n {/* Error message — a calm fade/slide in (no decorative shake). */}\n <AnimatePresence>\n {displayError && (\n <motion.p\n role=\"alert\"\n className=\"mt-ds-02 text-body-xs text-error-11\"\n initial={{ opacity: 0, y: -2 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0 }}\n transition={tweens.fade}\n >\n {displayError}\n </motion.p>\n )}\n </AnimatePresence>\n </div>\n )\n },\n)\nFileUpload.displayName = 'FileUpload'\n\nexport { FileUpload }\n"],"mappings":"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"}
package/dist/ui/form.d.ts CHANGED
@@ -6,6 +6,13 @@ interface FormFieldContextValue {
6
6
  helperTextId?: string;
7
7
  /** Auto-generated id shared between Label (htmlFor) and the field input (id). */
8
8
  inputId?: string;
9
+ /**
10
+ * Auto-generated id of the Label itself. Non-labellable controls (e.g. the
11
+ * Combobox trigger, a `div[role=combobox]`, which `<label htmlFor>` can't target)
12
+ * reference this via `aria-labelledby` to still adopt the visible label as their
13
+ * accessible name.
14
+ */
15
+ labelId?: string;
9
16
  required?: boolean;
10
17
  }
11
18
  declare const FormFieldContext: React.Context<FormFieldContextValue>;
@@ -1 +1 @@
1
- {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/ui/form.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;AAExE,UAAU,qBAAqB;IAC7B,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AACD,QAAA,MAAM,gBAAgB,sCAAiD,CAAA;AAEvE;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAe,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IAC1E;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kFAAkF;IAClF,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,QAAA,MAAM,SAAS,uFAsBd,CAAA;AAMD,MAAM,WAAW,mBAAoB,SAAQ,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC;IACrF,KAAK,CAAC,EAAE,eAAe,CAAA;CACxB;AASD,QAAA,MAAM,cAAc,kGAoBnB,CAAA;AAGD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,IAAI,qBAAqB,CAEpD;AAED,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/ui/form.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;AAExE,UAAU,qBAAqB;IAC7B,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AACD,QAAA,MAAM,gBAAgB,sCAAiD,CAAA;AAEvE;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAe,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IAC1E;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kFAAkF;IAClF,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,QAAA,MAAM,SAAS,uFAuBd,CAAA;AAMD,MAAM,WAAW,mBAAoB,SAAQ,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC;IACrF,KAAK,CAAC,EAAE,eAAe,CAAA;CACxB;AASD,QAAA,MAAM,cAAc,kGAoBnB,CAAA;AAGD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,IAAI,qBAAqB,CAEpD;AAED,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAA"}
package/dist/ui/form.js CHANGED
@@ -6,19 +6,21 @@ import { jsx as i } from "react/jsx-runtime";
6
6
  import { motion as a } from "framer-motion";
7
7
  //#region src/ui/form.tsx
8
8
  var o = r.createContext({}), s = r.forwardRef(({ className: e, helperTextId: t, inputId: a, state: s = "helper", required: c, children: l, ...u }, d) => {
9
- let f = r.useId(), p = t || `${f}-helper`, m = a || `${f}-input`, h = r.useMemo(() => ({
9
+ let f = r.useId(), p = t || `${f}-helper`, m = a || `${f}-input`, h = `${m}-label`, g = r.useMemo(() => ({
10
10
  state: s,
11
11
  helperTextId: p,
12
12
  inputId: m,
13
+ labelId: h,
13
14
  required: c
14
15
  }), [
15
16
  s,
16
17
  p,
17
18
  m,
19
+ h,
18
20
  c
19
21
  ]);
20
22
  return /* @__PURE__ */ i(o.Provider, {
21
- value: h,
23
+ value: g,
22
24
  children: /* @__PURE__ */ i("div", {
23
25
  ref: d,
24
26
  className: n("flex flex-col gap-ds-02", e),
@@ -1 +1 @@
1
- {"version":3,"file":"form.js","names":[],"sources":["../../src/ui/form.tsx"],"sourcesContent":["'use client'\n\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { motionProps, tweens } from './lib/motion'\nimport { cn } from './lib/utils'\n\nexport type FormHelperState = 'helper' | 'error' | 'warning' | 'success'\n\ninterface FormFieldContextValue {\n state?: FormHelperState\n helperTextId?: string\n /** Auto-generated id shared between Label (htmlFor) and the field input (id). */\n inputId?: string\n required?: boolean\n}\nconst FormFieldContext = React.createContext<FormFieldContextValue>({})\n\n/**\n * FormField — vertical flex container that provides validation state and\n * aria-describedby wiring to children via React context.\n *\n * **Automatic a11y wiring:** FormField generates a stable ID (or uses a provided `helperTextId`)\n * and passes it to `FormHelperText` via context. Use the `useFormField()` hook in custom inputs\n * to read `{ state, helperTextId, required }` and spread onto your input element.\n *\n * @example\n * <FormField state=\"error\">\n * <Label htmlFor=\"email\">Email</Label>\n * <Input id=\"email\" state=\"error\" />\n * <FormHelperText>Please enter a valid email address.</FormHelperText>\n * </FormField>\n */\nexport interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Custom ID for the helper text element. If omitted, an auto-generated ID is used.\n * Pass this to `useFormField()` consumers that need to set `aria-describedby`.\n */\n helperTextId?: string\n /**\n * Custom ID for the field input element. If omitted, an auto-generated ID is used.\n * Label (when inside FormField) reads this via `htmlFor`; Input reads it via `id`.\n * Explicit `htmlFor` / `id` props on children always win.\n */\n inputId?: string\n /** Current validation state — propagated to child `FormHelperText` via context */\n state?: FormHelperState\n /** Whether the field is required — available to children via `useFormField()` */\n required?: boolean\n}\n\nconst FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(\n ({ className, helperTextId, inputId, state = 'helper', required, children, ...props }, ref) => {\n const autoId = React.useId()\n const resolvedHelperId = helperTextId || `${autoId}-helper`\n const resolvedInputId = inputId || `${autoId}-input`\n const contextValue = React.useMemo(\n () => ({ state, helperTextId: resolvedHelperId, inputId: resolvedInputId, required }),\n [state, resolvedHelperId, resolvedInputId, required],\n )\n\n return (\n <FormFieldContext.Provider value={contextValue}>\n <div\n ref={ref}\n className={cn('flex flex-col gap-ds-02', className)}\n {...props}\n >\n {children}\n </div>\n </FormFieldContext.Provider>\n )\n },\n)\nFormField.displayName = 'FormField'\n\n// FormHelperText — small helper/error/warning/success message under a field\n//\n// When used for errors, set an `id` matching the input's `aria-describedby`.\nexport interface FormHelperTextProps extends React.HTMLAttributes<HTMLParagraphElement> {\n state?: FormHelperState\n}\n\nconst helperStateClasses: Record<FormHelperState, string> = {\n helper: 'text-surface-fg-subtle',\n error: 'text-error-11',\n warning: 'text-warning-11',\n success: 'text-success-11',\n}\n\nconst FormHelperText = React.forwardRef<HTMLParagraphElement, FormHelperTextProps>(\n ({ className, id: idProp, state: stateProp, ...props }, ref) => {\n const context = React.useContext(FormFieldContext)\n const state = stateProp ?? context.state ?? 'helper'\n const id = idProp ?? context.helperTextId\n\n return (\n <motion.p\n ref={ref}\n id={id}\n role={state === 'error' ? 'alert' : undefined}\n className={cn('text-body-sm', helperStateClasses[state], className)}\n initial={{ opacity: 0, y: -4 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -4 }}\n transition={tweens.fade}\n {...motionProps(props)}\n />\n )\n },\n)\nFormHelperText.displayName = 'FormHelperText'\n\n/**\n * Hook to read FormField context — returns `{ state, helperTextId, required }`.\n * Use in custom input components to auto-wire `aria-describedby` and `aria-invalid`.\n *\n * @example\n * function MyCustomInput(props) {\n * const { state, helperTextId, required } = useFormField()\n * return (\n * <input\n * aria-describedby={helperTextId}\n * aria-invalid={state === 'error' || undefined}\n * aria-required={required}\n * {...props}\n * />\n * )\n * }\n */\nexport function useFormField(): FormFieldContextValue {\n return React.useContext(FormFieldContext)\n}\n\nexport { FormField, FormFieldContext, FormHelperText }\n"],"mappings":";;;;;;;AAiBA,IAAM,IAAmB,EAAM,cAAqC,CAAC,CAAC,GAmChE,IAAY,EAAM,YACrB,EAAE,cAAW,iBAAc,YAAS,WAAQ,UAAU,aAAU,aAAU,GAAG,KAAS,MAAQ;CAC7F,IAAM,IAAS,EAAM,MAAM,GACrB,IAAmB,KAAgB,GAAG,EAAO,UAC7C,IAAkB,KAAW,GAAG,EAAO,SACvC,IAAe,EAAM,eAClB;EAAE;EAAO,cAAc;EAAkB,SAAS;EAAiB;CAAS,IACnF;EAAC;EAAO;EAAkB;EAAiB;CAAQ,CACrD;CAEA,OACE,kBAAC,EAAiB,UAAlB;EAA2B,OAAO;YAChC,kBAAC,OAAD;GACO;GACL,WAAW,EAAG,2BAA2B,CAAS;GAClD,GAAI;GAEH;EACE,CAAA;CACoB,CAAA;AAE/B,CACF;AACA,EAAU,cAAc;AASxB,IAAM,IAAsD;CAC1D,QAAS;CACT,OAAS;CACT,SAAS;CACT,SAAS;AACX,GAEM,IAAiB,EAAM,YAC1B,EAAE,cAAW,IAAI,GAAQ,OAAO,GAAW,GAAG,KAAS,MAAQ;CAC9D,IAAM,IAAU,EAAM,WAAW,CAAgB,GAC3C,IAAQ,KAAa,EAAQ,SAAS,UACtC,IAAK,KAAU,EAAQ;CAE7B,OACE,kBAAC,EAAO,GAAR;EACO;EACD;EACJ,MAAM,MAAU,UAAU,UAAU,KAAA;EACpC,WAAW,EAAG,gBAAgB,EAAmB,IAAQ,CAAS;EAClE,SAAS;GAAE,SAAS;GAAG,GAAG;EAAG;EAC7B,SAAS;GAAE,SAAS;GAAG,GAAG;EAAE;EAC5B,MAAM;GAAE,SAAS;GAAG,GAAG;EAAG;EAC1B,YAAY,EAAO;EACnB,GAAI,EAAY,CAAK;CACtB,CAAA;AAEL,CACF;AACA,EAAe,cAAc;AAmB7B,SAAgB,IAAsC;CACpD,OAAO,EAAM,WAAW,CAAgB;AAC1C"}
1
+ {"version":3,"file":"form.js","names":[],"sources":["../../src/ui/form.tsx"],"sourcesContent":["'use client'\n\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { motionProps, tweens } from './lib/motion'\nimport { cn } from './lib/utils'\n\nexport type FormHelperState = 'helper' | 'error' | 'warning' | 'success'\n\ninterface FormFieldContextValue {\n state?: FormHelperState\n helperTextId?: string\n /** Auto-generated id shared between Label (htmlFor) and the field input (id). */\n inputId?: string\n /**\n * Auto-generated id of the Label itself. Non-labellable controls (e.g. the\n * Combobox trigger, a `div[role=combobox]`, which `<label htmlFor>` can't target)\n * reference this via `aria-labelledby` to still adopt the visible label as their\n * accessible name.\n */\n labelId?: string\n required?: boolean\n}\nconst FormFieldContext = React.createContext<FormFieldContextValue>({})\n\n/**\n * FormField — vertical flex container that provides validation state and\n * aria-describedby wiring to children via React context.\n *\n * **Automatic a11y wiring:** FormField generates a stable ID (or uses a provided `helperTextId`)\n * and passes it to `FormHelperText` via context. Use the `useFormField()` hook in custom inputs\n * to read `{ state, helperTextId, required }` and spread onto your input element.\n *\n * @example\n * <FormField state=\"error\">\n * <Label htmlFor=\"email\">Email</Label>\n * <Input id=\"email\" state=\"error\" />\n * <FormHelperText>Please enter a valid email address.</FormHelperText>\n * </FormField>\n */\nexport interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Custom ID for the helper text element. If omitted, an auto-generated ID is used.\n * Pass this to `useFormField()` consumers that need to set `aria-describedby`.\n */\n helperTextId?: string\n /**\n * Custom ID for the field input element. If omitted, an auto-generated ID is used.\n * Label (when inside FormField) reads this via `htmlFor`; Input reads it via `id`.\n * Explicit `htmlFor` / `id` props on children always win.\n */\n inputId?: string\n /** Current validation state — propagated to child `FormHelperText` via context */\n state?: FormHelperState\n /** Whether the field is required — available to children via `useFormField()` */\n required?: boolean\n}\n\nconst FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(\n ({ className, helperTextId, inputId, state = 'helper', required, children, ...props }, ref) => {\n const autoId = React.useId()\n const resolvedHelperId = helperTextId || `${autoId}-helper`\n const resolvedInputId = inputId || `${autoId}-input`\n const resolvedLabelId = `${resolvedInputId}-label`\n const contextValue = React.useMemo(\n () => ({ state, helperTextId: resolvedHelperId, inputId: resolvedInputId, labelId: resolvedLabelId, required }),\n [state, resolvedHelperId, resolvedInputId, resolvedLabelId, required],\n )\n\n return (\n <FormFieldContext.Provider value={contextValue}>\n <div\n ref={ref}\n className={cn('flex flex-col gap-ds-02', className)}\n {...props}\n >\n {children}\n </div>\n </FormFieldContext.Provider>\n )\n },\n)\nFormField.displayName = 'FormField'\n\n// FormHelperText — small helper/error/warning/success message under a field\n//\n// When used for errors, set an `id` matching the input's `aria-describedby`.\nexport interface FormHelperTextProps extends React.HTMLAttributes<HTMLParagraphElement> {\n state?: FormHelperState\n}\n\nconst helperStateClasses: Record<FormHelperState, string> = {\n helper: 'text-surface-fg-subtle',\n error: 'text-error-11',\n warning: 'text-warning-11',\n success: 'text-success-11',\n}\n\nconst FormHelperText = React.forwardRef<HTMLParagraphElement, FormHelperTextProps>(\n ({ className, id: idProp, state: stateProp, ...props }, ref) => {\n const context = React.useContext(FormFieldContext)\n const state = stateProp ?? context.state ?? 'helper'\n const id = idProp ?? context.helperTextId\n\n return (\n <motion.p\n ref={ref}\n id={id}\n role={state === 'error' ? 'alert' : undefined}\n className={cn('text-body-sm', helperStateClasses[state], className)}\n initial={{ opacity: 0, y: -4 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -4 }}\n transition={tweens.fade}\n {...motionProps(props)}\n />\n )\n },\n)\nFormHelperText.displayName = 'FormHelperText'\n\n/**\n * Hook to read FormField context — returns `{ state, helperTextId, required }`.\n * Use in custom input components to auto-wire `aria-describedby` and `aria-invalid`.\n *\n * @example\n * function MyCustomInput(props) {\n * const { state, helperTextId, required } = useFormField()\n * return (\n * <input\n * aria-describedby={helperTextId}\n * aria-invalid={state === 'error' || undefined}\n * aria-required={required}\n * {...props}\n * />\n * )\n * }\n */\nexport function useFormField(): FormFieldContextValue {\n return React.useContext(FormFieldContext)\n}\n\nexport { FormField, FormFieldContext, FormHelperText }\n"],"mappings":";;;;;;;AAwBA,IAAM,IAAmB,EAAM,cAAqC,CAAC,CAAC,GAmChE,IAAY,EAAM,YACrB,EAAE,cAAW,iBAAc,YAAS,WAAQ,UAAU,aAAU,aAAU,GAAG,KAAS,MAAQ;CAC7F,IAAM,IAAS,EAAM,MAAM,GACrB,IAAmB,KAAgB,GAAG,EAAO,UAC7C,IAAkB,KAAW,GAAG,EAAO,SACvC,IAAkB,GAAG,EAAgB,SACrC,IAAe,EAAM,eAClB;EAAE;EAAO,cAAc;EAAkB,SAAS;EAAiB,SAAS;EAAiB;CAAS,IAC7G;EAAC;EAAO;EAAkB;EAAiB;EAAiB;CAAQ,CACtE;CAEA,OACE,kBAAC,EAAiB,UAAlB;EAA2B,OAAO;YAChC,kBAAC,OAAD;GACO;GACL,WAAW,EAAG,2BAA2B,CAAS;GAClD,GAAI;GAEH;EACE,CAAA;CACoB,CAAA;AAE/B,CACF;AACA,EAAU,cAAc;AASxB,IAAM,IAAsD;CAC1D,QAAS;CACT,OAAS;CACT,SAAS;CACT,SAAS;AACX,GAEM,IAAiB,EAAM,YAC1B,EAAE,cAAW,IAAI,GAAQ,OAAO,GAAW,GAAG,KAAS,MAAQ;CAC9D,IAAM,IAAU,EAAM,WAAW,CAAgB,GAC3C,IAAQ,KAAa,EAAQ,SAAS,UACtC,IAAK,KAAU,EAAQ;CAE7B,OACE,kBAAC,EAAO,GAAR;EACO;EACD;EACJ,MAAM,MAAU,UAAU,UAAU,KAAA;EACpC,WAAW,EAAG,gBAAgB,EAAmB,IAAQ,CAAS;EAClE,SAAS;GAAE,SAAS;GAAG,GAAG;EAAG;EAC7B,SAAS;GAAE,SAAS;GAAG,GAAG;EAAE;EAC5B,MAAM;GAAE,SAAS;GAAG,GAAG;EAAG;EAC1B,YAAY,EAAO;EACnB,GAAI,EAAY,CAAK;CACtB,CAAA;AAEL,CACF;AACA,EAAe,cAAc;AAmB7B,SAAgB,IAAsC;CACpD,OAAO,EAAM,WAAW,CAAgB;AAC1C"}
@@ -14,7 +14,8 @@ type IconButtonSize = 'sm' | 'md' | 'lg';
14
14
  *
15
15
  * **Size:** `'sm'` | `'md'` (default) | `'lg'` — maps to `icon-sm/md/lg` sizes internally.
16
16
  *
17
- * **All `Button` variants** are supported: `solid`, `outline`, `ghost`, `link` x `default`, `error`.
17
+ * **All `Button` axes** flow through: variants `solid` | `soft` | `outline` | `ghost` | `link`,
18
+ * colors `accent` | `error` | `success` | `warning` | `info` | `neutral` (see `Button`).
18
19
  *
19
20
  * @example
20
21
  * // Ghost toolbar icon button:
@@ -1 +1 @@
1
- {"version":3,"file":"icon-button.d.ts","sourceRoot":"","sources":["../../src/ui/icon-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAWjD,KAAK,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,iBAAiB,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;IACpH;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,CAAA;IACf,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,sCAAsC;IACtC,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,iCAAiC;IACjC,IAAI,CAAC,EAAE,cAAc,CAAA;CACtB;AAED,QAAA,MAAM,UAAU,2FAef,CAAA;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
1
+ {"version":3,"file":"icon-button.d.ts","sourceRoot":"","sources":["../../src/ui/icon-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAWjD,KAAK,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,iBAAiB,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;IACpH;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,CAAA;IACf,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,sCAAsC;IACtC,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,iCAAiC;IACjC,IAAI,CAAC,EAAE,cAAc,CAAA;CACtB;AAED,QAAA,MAAM,UAAU,2FAqBf,CAAA;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
@@ -12,7 +12,7 @@ var a = {
12
12
  }, o = r.forwardRef(({ icon: r, shape: o = "square", size: s = "md", className: c, loading: l, ...u }, d) => /* @__PURE__ */ i(n, {
13
13
  ref: d,
14
14
  size: a[s],
15
- className: e(o === "circle" && "rounded-pill", c),
15
+ className: e(o === "circle" && "rounded-pill", (s === "sm" || s === "md") && "touch-target", c),
16
16
  loading: l,
17
17
  loadingPosition: "center",
18
18
  ...u,
@@ -1 +1 @@
1
- {"version":3,"file":"icon-button.js","names":[],"sources":["../../src/ui/icon-button.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\n\nimport { Button, type ButtonProps } from './button'\nimport type { IconInput } from './lib/icon-input'\nimport { normalizeIcon } from './lib/normalize-icon'\nimport { cn } from './lib/utils'\n\n/** Map friendly sizes to icon-* sizes for the underlying Button */\nconst sizeMap = {\n sm: 'icon-sm',\n md: 'icon-md',\n lg: 'icon-lg',\n} as const\n\ntype IconButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * Props for IconButton — a square or circular icon-only button built on top of `<Button>`,\n * with `aria-label` required for WCAG AA compliance.\n *\n * **`aria-label` is required** — icon-only buttons have no visible text, so a descriptive label\n * is mandatory for screen readers. TypeScript enforces this.\n *\n * **Shape:** `'square'` (default, rounded corners) | `'circle'` (fully round, great for avatar-adjacent actions)\n *\n * **Size:** `'sm'` | `'md'` (default) | `'lg'` — maps to `icon-sm/md/lg` sizes internally.\n *\n * **All `Button` variants** are supported: `solid`, `outline`, `ghost`, `link` x `default`, `error`.\n *\n * @example\n * // Ghost toolbar icon button:\n * <IconButton icon={<Icon icon={IconEdit} />} variant=\"ghost\" aria-label=\"Edit item\" />\n *\n * @example\n * // Circular close button (e.g. in a modal header):\n * <IconButton icon={<Icon icon={IconX} />} shape=\"circle\" variant=\"ghost\" size=\"sm\" aria-label=\"Close dialog\" />\n *\n * @example\n * // Solid floating action button:\n * <IconButton icon={<Icon icon={IconPlus} />} variant=\"solid\" size=\"lg\" aria-label=\"Create new project\" />\n *\n * @example\n * // Error icon button with loading state (e.g. confirm delete):\n * <IconButton icon={<Icon icon={IconTrash} />} variant=\"solid\" color=\"error\" loading={isDeleting} aria-label=\"Delete\" />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface IconButtonProps\n extends Omit<ButtonProps, 'startIcon' | 'endIcon' | 'fullWidth' | 'loadingPosition' | 'children' | 'size' | 'shape'> {\n /**\n * The icon to render. Accepts any `IconInput`:\n * - `<Icon icon={IconPlus} />` (canonical — size flows from context)\n * - `<IconPlus />` (raw Tabler element — passes through)\n * - `IconPlus` (Tabler component ref — auto-wrapped in `<Icon>`)\n * - Custom `<span>$</span>` or any ReactElement.\n */\n icon: IconInput\n /** Accessible label — required for icon-only buttons (WCAG AA) */\n 'aria-label': string\n /** Button shape. Default: 'square' */\n shape?: 'square' | 'circle'\n /** Button size. Default: 'md' */\n size?: IconButtonSize\n}\n\nconst IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n ({ icon, shape = 'square', size = 'md', className, loading, ...props }, ref) => {\n return (\n <Button\n ref={ref}\n size={sizeMap[size]}\n className={cn(shape === 'circle' && 'rounded-pill', className)}\n loading={loading}\n loadingPosition=\"center\"\n {...props}\n >\n {normalizeIcon(icon)}\n </Button>\n )\n },\n)\nIconButton.displayName = 'IconButton'\n\nexport { IconButton }\n"],"mappings":";;;;;;;AAUA,IAAM,IAAU;CACd,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAoDM,IAAa,EAAM,YACtB,EAAE,SAAM,WAAQ,UAAU,UAAO,MAAM,cAAW,YAAS,GAAG,KAAS,MAEpE,kBAAC,GAAD;CACO;CACL,MAAM,EAAQ;CACd,WAAW,EAAG,MAAU,YAAY,gBAAgB,CAAS;CACpD;CACT,iBAAgB;CAChB,GAAI;WAEH,EAAc,CAAI;AACb,CAAA,CAGd;AACA,EAAW,cAAc"}
1
+ {"version":3,"file":"icon-button.js","names":[],"sources":["../../src/ui/icon-button.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\n\nimport { Button, type ButtonProps } from './button'\nimport type { IconInput } from './lib/icon-input'\nimport { normalizeIcon } from './lib/normalize-icon'\nimport { cn } from './lib/utils'\n\n/** Map friendly sizes to icon-* sizes for the underlying Button */\nconst sizeMap = {\n sm: 'icon-sm',\n md: 'icon-md',\n lg: 'icon-lg',\n} as const\n\ntype IconButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * Props for IconButton — a square or circular icon-only button built on top of `<Button>`,\n * with `aria-label` required for WCAG AA compliance.\n *\n * **`aria-label` is required** — icon-only buttons have no visible text, so a descriptive label\n * is mandatory for screen readers. TypeScript enforces this.\n *\n * **Shape:** `'square'` (default, rounded corners) | `'circle'` (fully round, great for avatar-adjacent actions)\n *\n * **Size:** `'sm'` | `'md'` (default) | `'lg'` — maps to `icon-sm/md/lg` sizes internally.\n *\n * **All `Button` axes** flow through: variants `solid` | `soft` | `outline` | `ghost` | `link`,\n * colors `accent` | `error` | `success` | `warning` | `info` | `neutral` (see `Button`).\n *\n * @example\n * // Ghost toolbar icon button:\n * <IconButton icon={<Icon icon={IconEdit} />} variant=\"ghost\" aria-label=\"Edit item\" />\n *\n * @example\n * // Circular close button (e.g. in a modal header):\n * <IconButton icon={<Icon icon={IconX} />} shape=\"circle\" variant=\"ghost\" size=\"sm\" aria-label=\"Close dialog\" />\n *\n * @example\n * // Solid floating action button:\n * <IconButton icon={<Icon icon={IconPlus} />} variant=\"solid\" size=\"lg\" aria-label=\"Create new project\" />\n *\n * @example\n * // Error icon button with loading state (e.g. confirm delete):\n * <IconButton icon={<Icon icon={IconTrash} />} variant=\"solid\" color=\"error\" loading={isDeleting} aria-label=\"Delete\" />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface IconButtonProps\n extends Omit<ButtonProps, 'startIcon' | 'endIcon' | 'fullWidth' | 'loadingPosition' | 'children' | 'size' | 'shape'> {\n /**\n * The icon to render. Accepts any `IconInput`:\n * - `<Icon icon={IconPlus} />` (canonical — size flows from context)\n * - `<IconPlus />` (raw Tabler element — passes through)\n * - `IconPlus` (Tabler component ref — auto-wrapped in `<Icon>`)\n * - Custom `<span>$</span>` or any ReactElement.\n */\n icon: IconInput\n /** Accessible label — required for icon-only buttons (WCAG AA) */\n 'aria-label': string\n /** Button shape. Default: 'square' */\n shape?: 'square' | 'circle'\n /** Button size. Default: 'md' */\n size?: IconButtonSize\n}\n\nconst IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n ({ icon, shape = 'square', size = 'md', className, loading, ...props }, ref) => {\n return (\n <Button\n ref={ref}\n size={sizeMap[size]}\n // sm (32px) / md (40px) are below the 44px touch target — expand the press\n // region with an invisible ::before (visual size unchanged). lg is 48px.\n className={cn(\n shape === 'circle' && 'rounded-pill',\n (size === 'sm' || size === 'md') && 'touch-target',\n className,\n )}\n loading={loading}\n loadingPosition=\"center\"\n {...props}\n >\n {normalizeIcon(icon)}\n </Button>\n )\n },\n)\nIconButton.displayName = 'IconButton'\n\nexport { IconButton }\n"],"mappings":";;;;;;;AAUA,IAAM,IAAU;CACd,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAqDM,IAAa,EAAM,YACtB,EAAE,SAAM,WAAQ,UAAU,UAAO,MAAM,cAAW,YAAS,GAAG,KAAS,MAEpE,kBAAC,GAAD;CACO;CACL,MAAM,EAAQ;CAGd,WAAW,EACT,MAAU,YAAY,iBACrB,MAAS,QAAQ,MAAS,SAAS,gBACpC,CACF;CACS;CACT,iBAAgB;CAChB,GAAI;WAEH,EAAc,CAAI;AACb,CAAA,CAGd;AACA,EAAW,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../src/ui/label.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,WAAW,UACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,QAAA,MAAM,KAAK,qFAwBT,CAAA;AAGF,OAAO,EAAE,KAAK,EAAE,CAAA"}
1
+ {"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../src/ui/label.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,WAAW,UACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,QAAA,MAAM,KAAK,qFA4BT,CAAA;AAGF,OAAO,EAAE,KAAK,EAAE,CAAA"}
package/dist/ui/label.js CHANGED
@@ -5,14 +5,15 @@ import { useFormField as n } from "./form.js";
5
5
  import * as r from "react";
6
6
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
7
7
  //#region src/ui/label.tsx
8
- var o = r.forwardRef(({ className: r, required: o, children: s, htmlFor: c, ...l }, u) => {
9
- let d = n(), f = c ?? d.inputId, p = o ?? d.required;
8
+ var o = r.forwardRef(({ className: r, required: o, children: s, htmlFor: c, id: l, ...u }, d) => {
9
+ let f = n(), p = c ?? f.inputId, m = l ?? f.labelId, h = o ?? f.required;
10
10
  return /* @__PURE__ */ a(e, {
11
- ref: u,
12
- htmlFor: f,
11
+ ref: d,
12
+ id: m,
13
+ htmlFor: p,
13
14
  className: t("font-sans text-body-md font-medium text-surface-fg leading-none transition-opacity duration-fast-01 ease-productive-standard peer-disabled:opacity-action-disabled", r),
14
- ...l,
15
- children: [s, p && /* @__PURE__ */ i("span", {
15
+ ...u,
16
+ children: [s, h && /* @__PURE__ */ i("span", {
16
17
  className: "text-error-11 ml-ds-01",
17
18
  "aria-hidden": "true",
18
19
  children: "*"