@eifi1/ui-kit 0.22.0 → 0.23.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 (205) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +11 -8
  3. package/dist/components/amount-input.d.ts +28 -8
  4. package/dist/components/amount-input.js +3 -1
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +21 -0
  7. package/dist/components/autocomplete.js +128 -103
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +11 -8
  10. package/dist/components/calculator.d.ts +11 -8
  11. package/dist/components/column-mapper.d.ts +182 -0
  12. package/dist/components/column-mapper.js +375 -0
  13. package/dist/components/column-mapper.js.map +1 -0
  14. package/dist/components/combobox-core.d.ts +39 -2
  15. package/dist/components/combobox-core.js +29 -6
  16. package/dist/components/combobox-core.js.map +1 -1
  17. package/dist/components/combobox.d.ts +80 -2
  18. package/dist/components/combobox.js +448 -311
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +11 -8
  21. package/dist/components/confirm-dialog.js +7 -8
  22. package/dist/components/confirm-dialog.js.map +1 -1
  23. package/dist/components/copy-button.d.ts +8 -5
  24. package/dist/components/country-select.d.ts +50 -7
  25. package/dist/components/country-select.js +65 -9
  26. package/dist/components/country-select.js.map +1 -1
  27. package/dist/components/danger-confirm.d.ts +11 -8
  28. package/dist/components/danger-confirm.js +13 -3
  29. package/dist/components/danger-confirm.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +41 -0
  31. package/dist/components/date-picker.js +269 -8
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/entity-combobox.d.ts +35 -1
  34. package/dist/components/entity-combobox.js +148 -112
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +11 -8
  37. package/dist/components/field-parts.d.ts +116 -6
  38. package/dist/components/field-parts.js +84 -0
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +105 -0
  41. package/dist/components/field-strip.js +46 -0
  42. package/dist/components/field-strip.js.map +1 -0
  43. package/dist/components/file-button.d.ts +11 -8
  44. package/dist/components/file-dropzone.d.ts +11 -8
  45. package/dist/components/form-actions.d.ts +10 -7
  46. package/dist/components/form-actions.js +8 -2
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +11 -8
  49. package/dist/components/icon-picker.d.ts +10 -1
  50. package/dist/components/icon-picker.js +12 -4
  51. package/dist/components/icon-picker.js.map +1 -1
  52. package/dist/components/language-select.d.ts +11 -8
  53. package/dist/components/money-field.d.ts +14 -8
  54. package/dist/components/money-field.js.map +1 -1
  55. package/dist/components/month-picker.js +2 -1
  56. package/dist/components/month-picker.js.map +1 -1
  57. package/dist/components/multi-entity-combobox.d.ts +26 -1
  58. package/dist/components/multi-entity-combobox.js +143 -102
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +11 -8
  61. package/dist/components/number-input.d.ts +11 -8
  62. package/dist/components/numpad-sheet.d.ts +11 -8
  63. package/dist/components/phone-input.d.ts +11 -8
  64. package/dist/components/reauth-dialog.d.ts +2 -2
  65. package/dist/components/reauth-dialog.js +6 -8
  66. package/dist/components/reauth-dialog.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +11 -8
  68. package/dist/components/settings-fields.d.ts +11 -8
  69. package/dist/components/share-card.d.ts +10 -7
  70. package/dist/components/swatch-picker.d.ts +17 -1
  71. package/dist/components/swatch-picker.js +10 -4
  72. package/dist/components/swatch-picker.js.map +1 -1
  73. package/dist/components/text-link.d.ts +10 -7
  74. package/dist/components/tile-radio.d.ts +20 -0
  75. package/dist/components/tile-radio.js +25 -8
  76. package/dist/components/tile-radio.js.map +1 -1
  77. package/dist/components/time-input.d.ts +11 -8
  78. package/dist/components/toggle-group.js +2 -2
  79. package/dist/components/toggle-group.js.map +1 -1
  80. package/dist/components/trigger-aria.d.ts +3 -0
  81. package/dist/components/trigger-aria.js +3 -1
  82. package/dist/components/trigger-aria.js.map +1 -1
  83. package/dist/components/ui.d.ts +8 -5
  84. package/dist/components/ui.js +5 -11
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +1 -1
  87. package/dist/feedback/feedback-attachment.js +156 -56
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +1 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +3 -2
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +68 -287
  94. package/dist/feedback/feedback-thread.js +8 -1
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-BxeQVzwq.d.ts} +405 -22
  97. package/dist/{feedback-attachment-NhUm7Zga.d.ts → feedback-attachment-fGAzZPf0.d.ts} +112 -10
  98. package/dist/feedback.d.ts +66 -2
  99. package/dist/hooks/use-file-drop.d.ts +11 -8
  100. package/dist/i18n/defaults.d.ts +11 -8
  101. package/dist/i18n/defaults.js +3 -1
  102. package/dist/i18n/defaults.js.map +1 -1
  103. package/dist/i18n/german.d.ts +11 -8
  104. package/dist/i18n/german.js +36 -2
  105. package/dist/i18n/german.js.map +1 -1
  106. package/dist/i18n/kit-labels.d.ts +7 -4
  107. package/dist/i18n/kit-labels.js.map +1 -1
  108. package/dist/i18n/languages.d.ts +11 -8
  109. package/dist/i18n/locales/de-CH.d.ts +11 -8
  110. package/dist/i18n/locales/en.d.ts +11 -8
  111. package/dist/i18n/locales/en.js +7 -0
  112. package/dist/i18n/locales/en.js.map +1 -1
  113. package/dist/i18n/locales/es.d.ts +11 -8
  114. package/dist/i18n/locales/es.js +37 -2
  115. package/dist/i18n/locales/es.js.map +1 -1
  116. package/dist/i18n/locales/fr.d.ts +11 -8
  117. package/dist/i18n/locales/fr.js +36 -2
  118. package/dist/i18n/locales/fr.js.map +1 -1
  119. package/dist/i18n/locales/hu.d.ts +11 -8
  120. package/dist/i18n/locales/hu.js +38 -2
  121. package/dist/i18n/locales/hu.js.map +1 -1
  122. package/dist/i18n/locales/it.d.ts +11 -8
  123. package/dist/i18n/locales/it.js +37 -2
  124. package/dist/i18n/locales/it.js.map +1 -1
  125. package/dist/i18n/locales/zh.d.ts +11 -8
  126. package/dist/i18n/locales/zh.js +34 -2
  127. package/dist/i18n/locales/zh.js.map +1 -1
  128. package/dist/i18n/review.d.ts +11 -8
  129. package/dist/i18n/review.js +20 -1
  130. package/dist/i18n/review.js.map +1 -1
  131. package/dist/index.d.ts +7 -3
  132. package/dist/index.js +16 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/lib/column-mapping.d.ts +81 -0
  135. package/dist/lib/column-mapping.js +108 -0
  136. package/dist/lib/column-mapping.js.map +1 -0
  137. package/dist/lib/table-text.d.ts +109 -1
  138. package/dist/lib/table-text.js +122 -1
  139. package/dist/lib/table-text.js.map +1 -1
  140. package/dist/rhf/fields.d.ts +89 -11
  141. package/dist/rhf/fields.js +124 -0
  142. package/dist/rhf/fields.js.map +1 -1
  143. package/dist/rhf/form.d.ts +11 -8
  144. package/dist/rhf.d.ts +12 -9
  145. package/dist/rhf.js.map +1 -1
  146. package/dist/shell/app-shell.d.ts +10 -7
  147. package/dist/shell/top-bar-brand.d.ts +11 -8
  148. package/dist/shell/topbar-action-menu.d.ts +36 -3
  149. package/dist/shell/topbar-action-menu.js +74 -33
  150. package/dist/shell/topbar-action-menu.js.map +1 -1
  151. package/dist/shell.d.ts +10 -7
  152. package/dist/table-text.d.ts +1 -1
  153. package/dist/wizard/stepper-nav.d.ts +41 -10
  154. package/dist/wizard/stepper-nav.js +4 -0
  155. package/dist/wizard/stepper-nav.js.map +1 -1
  156. package/dist/wizard.d.ts +11 -8
  157. package/package.json +1 -1
  158. package/src/components/amount-input.tsx +20 -1
  159. package/src/components/autocomplete.tsx +172 -111
  160. package/src/components/column-mapper.tsx +666 -0
  161. package/src/components/combobox-core.tsx +82 -9
  162. package/src/components/combobox.tsx +614 -332
  163. package/src/components/confirm-dialog.tsx +12 -8
  164. package/src/components/country-select.tsx +135 -22
  165. package/src/components/danger-confirm.tsx +91 -12
  166. package/src/components/date-picker.tsx +423 -10
  167. package/src/components/entity-combobox.tsx +217 -126
  168. package/src/components/field-parts.tsx +224 -5
  169. package/src/components/field-strip.tsx +149 -0
  170. package/src/components/form-actions.tsx +32 -4
  171. package/src/components/icon-picker.tsx +23 -4
  172. package/src/components/money-field.tsx +3 -0
  173. package/src/components/month-picker.tsx +2 -1
  174. package/src/components/multi-entity-combobox.tsx +207 -120
  175. package/src/components/reauth-dialog.tsx +17 -18
  176. package/src/components/swatch-picker.tsx +29 -5
  177. package/src/components/tile-radio.tsx +74 -13
  178. package/src/components/toggle-group.tsx +2 -2
  179. package/src/components/trigger-aria.ts +5 -0
  180. package/src/components/ui.tsx +7 -33
  181. package/src/feedback/feedback-attachment.tsx +308 -61
  182. package/src/feedback/feedback-dialog.tsx +7 -3
  183. package/src/feedback/feedback-inbox.tsx +3 -2
  184. package/src/feedback/feedback-thread.tsx +47 -4
  185. package/src/i18n/defaults.ts +2 -0
  186. package/src/i18n/german.ts +42 -0
  187. package/src/i18n/kit-labels.tsx +4 -0
  188. package/src/i18n/locales/en.ts +27 -5
  189. package/src/i18n/locales/es.ts +41 -0
  190. package/src/i18n/locales/fr.ts +42 -0
  191. package/src/i18n/locales/hu.ts +37 -0
  192. package/src/i18n/locales/it.ts +41 -0
  193. package/src/i18n/locales/zh.ts +32 -0
  194. package/src/i18n/review.ts +19 -0
  195. package/src/index.ts +18 -0
  196. package/src/lib/column-mapping.ts +234 -0
  197. package/src/lib/table-text.ts +271 -0
  198. package/src/rhf/fields.tsx +254 -0
  199. package/src/rhf.ts +2 -1
  200. package/src/shell/topbar-action-menu.tsx +134 -38
  201. package/src/wizard/stepper-nav.tsx +35 -1
  202. package/dist/components/field-anatomy.d.ts +0 -95
  203. package/dist/components/field-anatomy.js +0 -84
  204. package/dist/components/field-anatomy.js.map +0 -1
  205. package/src/components/field-anatomy.tsx +0 -190
@@ -1,2 +1,2 @@
1
1
  import 'react';
2
- export { D as DEFAULT_ATTACHMENT_ACCEPT, a as DEFAULT_FEEDBACK_ATTACHMENT_LABELS, c as DEFAULT_MAX_ATTACHMENTS, d as DEFAULT_MAX_ATTACHMENT_BYTES, F as FeedbackAttachmentError, e as FeedbackAttachmentField, f as FeedbackAttachmentFieldLabels, g as FeedbackAttachmentFieldMultipleProps, h as FeedbackAttachmentFieldProps, i as FeedbackAttachmentFieldRefsProps, j as FeedbackAttachmentFieldSingleProps, l as FeedbackAttachmentRef, m as FeedbackAttachmentRefsError, w as pastedName } from '../feedback-attachment-NhUm7Zga.js';
2
+ export { D as DEFAULT_ATTACHMENT_ACCEPT, a as DEFAULT_FEEDBACK_ATTACHMENT_LABELS, c as DEFAULT_MAX_ATTACHMENTS, d as DEFAULT_MAX_ATTACHMENT_BYTES, F as FeedbackAttachmentError, e as FeedbackAttachmentErrorInfo, f as FeedbackAttachmentField, g as FeedbackAttachmentFieldLabels, h as FeedbackAttachmentFieldMultipleProps, i as FeedbackAttachmentFieldProps, j as FeedbackAttachmentFieldRefsProps, k as FeedbackAttachmentFieldSingleProps, m as FeedbackAttachmentRef, n as FeedbackAttachmentRefsError, x as pastedName } from '../feedback-attachment-fGAzZPf0.js';
@@ -1,9 +1,16 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { cn } from "../lib/cn.js";
4
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import {
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState
9
+ } from "react";
5
10
  import { Camera, FileText, Image as ImageIcon, Paperclip, X } from "lucide-react";
6
- import { Button, Spinner } from "../components/ui.js";
11
+ import { Button, IconButton, Spinner } from "../components/ui.js";
12
+ import { DEFAULT_FILE_PICKER_LABELS, formatAccept } from "../components/file-button.js";
13
+ import { useCommitReason } from "../components/write-lock.js";
7
14
  import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels.js";
8
15
  const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
9
16
  const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
@@ -17,7 +24,8 @@ const DEFAULT_FEEDBACK_ATTACHMENT_LABELS = {
17
24
  attachmentScreenshot: "Screenshot",
18
25
  attachmentRemoveFile: (name) => `Remove ${name}`,
19
26
  attachmentLimit: (max) => `Up to ${max} ${max === 1 ? "attachment" : "attachments"} \u2014 remove one to add another.`,
20
- attachmentUploading: "Uploading\u2026"
27
+ attachmentUploading: "Uploading\u2026",
28
+ attachmentUploadFailed: (name) => `\u201C${name}\u201D could not be uploaded`
21
29
  };
22
30
  function FeedbackAttachmentField(props) {
23
31
  if (props.refs) return /* @__PURE__ */ jsx(RefsField, { ...props });
@@ -33,6 +41,7 @@ function SingleField({
33
41
  onCaptureScreenshot,
34
42
  documentPaste = false,
35
43
  pasteFrom,
44
+ disabled = false,
36
45
  className
37
46
  }) {
38
47
  const [previewFor, setPreviewFor] = useState(null);
@@ -40,6 +49,7 @@ function SingleField({
40
49
  const fileInputRef = useRef(null);
41
50
  const fileText = useKitFileLabels();
42
51
  const text = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
52
+ const errorInfo = useErrorInfo(accept, maxBytes, void 0, text);
43
53
  useEffect(() => {
44
54
  if (!value) return;
45
55
  const url = URL.createObjectURL(value);
@@ -50,7 +60,7 @@ function SingleField({
50
60
  if (!file) return;
51
61
  const problem = rejection(file, accept, maxBytes);
52
62
  if (problem) {
53
- onError?.(problem);
63
+ onError?.(problem, errorInfo(problem, [file]));
54
64
  return;
55
65
  }
56
66
  onChange(file);
@@ -61,7 +71,8 @@ function SingleField({
61
71
  pick(new File([file], pastedName(file.type), { type: file.type }));
62
72
  },
63
73
  documentPaste,
64
- pasteFrom
74
+ pasteFrom,
75
+ disabled
65
76
  );
66
77
  return /* @__PURE__ */ jsxs(
67
78
  "div",
@@ -87,24 +98,32 @@ function SingleField({
87
98
  /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-[var(--text-secondary)]", children: value.name }),
88
99
  /* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-muted)]", children: fileText.size(value.size) })
89
100
  ] }),
90
- /* @__PURE__ */ jsx(
91
- "button",
92
- {
93
- type: "button",
94
- onClick: () => onChange(null),
95
- "aria-label": text.attachmentRemove,
96
- className: "rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
97
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
98
- }
99
- )
101
+ /* @__PURE__ */ jsx(RemoveButton, { label: text.attachmentRemove, onClick: () => onChange(null), disabled })
100
102
  ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
101
103
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
102
- /* @__PURE__ */ jsxs(Button, { type: "button", variant: "secondary", onClick: () => fileInputRef.current?.click(), children: [
103
- /* @__PURE__ */ jsx(Paperclip, { className: "size-4" }),
104
- " ",
105
- text.attachmentAdd
106
- ] }),
107
- onCaptureScreenshot && /* @__PURE__ */ jsx(CaptureButton, { capture: onCaptureScreenshot, onFile: pick, label: text.attachmentCapture })
104
+ /* @__PURE__ */ jsxs(
105
+ Button,
106
+ {
107
+ type: "button",
108
+ variant: "secondary",
109
+ disabled,
110
+ onClick: () => fileInputRef.current?.click(),
111
+ children: [
112
+ /* @__PURE__ */ jsx(Paperclip, { className: "size-4" }),
113
+ " ",
114
+ text.attachmentAdd
115
+ ]
116
+ }
117
+ ),
118
+ onCaptureScreenshot && /* @__PURE__ */ jsx(
119
+ CaptureButton,
120
+ {
121
+ capture: onCaptureScreenshot,
122
+ onFile: pick,
123
+ label: text.attachmentCapture,
124
+ disabled
125
+ }
126
+ )
108
127
  ] }),
109
128
  /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: text.attachmentPaste })
110
129
  ] }),
@@ -115,6 +134,7 @@ function SingleField({
115
134
  type: "file",
116
135
  accept: accept.join(","),
117
136
  className: "sr-only",
137
+ disabled,
118
138
  onChange: (e) => {
119
139
  pick(e.target.files?.[0]);
120
140
  e.target.value = "";
@@ -138,6 +158,7 @@ function MultipleField({
138
158
  onCaptureScreenshot,
139
159
  documentPaste = false,
140
160
  pasteFrom,
161
+ disabled = false,
141
162
  className
142
163
  }) {
143
164
  const rootRef = useRef(null);
@@ -147,6 +168,7 @@ function MultipleField({
147
168
  const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile;
148
169
  const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit;
149
170
  const screenshotLabel = text.attachmentScreenshot ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentScreenshot;
171
+ const errorInfo = useErrorInfo(accept, maxBytes, max, text);
150
172
  const latest = useRef(value);
151
173
  useLayoutEffect(() => {
152
174
  latest.current = value;
@@ -163,29 +185,29 @@ function MultipleField({
163
185
  const accepted = [];
164
186
  const taken = new Set([...current, ...screenshot ? [screenshot] : []].map((f) => f.name));
165
187
  for (const raw of incoming) {
166
- const problem = rejection(raw, accept, maxBytes);
188
+ const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
189
+ const problem = rejection(file, accept, maxBytes);
167
190
  if (problem) {
168
- onError?.(problem);
191
+ onError?.(problem, errorInfo(problem, [file]));
169
192
  continue;
170
193
  }
171
- const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
172
194
  taken.add(file.name);
173
195
  if (!current.includes(file)) accepted.push(file);
174
196
  }
175
197
  if (accepted.length === 0) return;
176
198
  const space = Math.max(0, max - current.length);
177
- if (accepted.length > space) onError?.("count");
199
+ if (accepted.length > space) onError?.("count", errorInfo("count", accepted.slice(space)));
178
200
  const kept = accepted.slice(0, space);
179
201
  if (kept.length > 0) commit([...current, ...kept]);
180
202
  };
181
- const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom);
203
+ const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, disabled);
182
204
  const capture = (file) => {
183
205
  if (!hasSlot) {
184
206
  add([file]);
185
207
  return;
186
208
  }
187
209
  const problem = rejection(file, accept, maxBytes);
188
- if (problem) onError?.(problem);
210
+ if (problem) onError?.(problem, errorInfo(problem, [file]));
189
211
  else onScreenshotChange(file);
190
212
  };
191
213
  const pendingFocus = useRef(null);
@@ -224,17 +246,14 @@ function MultipleField({
224
246
  /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-[var(--text-muted)]", children: chip.screenshot ? `${chip.file.name} \xB7 ${fileText.size(chip.file.size)}` : fileText.size(chip.file.size) })
225
247
  ] }),
226
248
  /* @__PURE__ */ jsx(
227
- "button",
249
+ RemoveButton,
228
250
  {
229
- type: "button",
230
- "data-attachment-remove": "",
251
+ label: removeLabel(chip.name),
252
+ disabled,
231
253
  onClick: () => {
232
254
  pendingFocus.current = index;
233
255
  chip.remove();
234
- },
235
- "aria-label": removeLabel(chip.name),
236
- className: "shrink-0 rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
237
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
256
+ }
238
257
  }
239
258
  )
240
259
  ]
@@ -248,6 +267,7 @@ function MultipleField({
248
267
  type: "button",
249
268
  variant: "secondary",
250
269
  "data-attachment-action": "",
270
+ disabled,
251
271
  onClick: () => fileInputRef.current?.click(),
252
272
  children: [
253
273
  /* @__PURE__ */ jsx(Paperclip, { className: "size-4" }),
@@ -262,6 +282,7 @@ function MultipleField({
262
282
  capture: onCaptureScreenshot,
263
283
  onFile: capture,
264
284
  label: text.attachmentCapture,
285
+ disabled,
265
286
  "data-attachment-action": ""
266
287
  }
267
288
  )
@@ -277,6 +298,7 @@ function MultipleField({
277
298
  className: "sr-only",
278
299
  tabIndex: -1,
279
300
  "aria-hidden": true,
301
+ disabled,
280
302
  onChange: (e) => {
281
303
  add(Array.from(e.target.files ?? []));
282
304
  e.target.value = "";
@@ -300,6 +322,9 @@ function RefsField({
300
322
  onCaptureScreenshot,
301
323
  documentPaste = false,
302
324
  pasteFrom,
325
+ disabled = false,
326
+ commit: commits,
327
+ disabledReason: ownDisabledReason,
303
328
  className
304
329
  }) {
305
330
  const rootRef = useRef(null);
@@ -309,6 +334,11 @@ function RefsField({
309
334
  const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile;
310
335
  const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit;
311
336
  const uploadingLabel = text.attachmentUploading ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploading;
337
+ const errorInfo = useErrorInfo(accept, maxBytes, max, text);
338
+ const disabledReason = useCommitReason(commits, ownDisabledReason);
339
+ const locked = hasReason(disabledReason);
340
+ const reason = locked ? disabledReason : void 0;
341
+ const inert = disabled || locked;
312
342
  const latest = useRef(value);
313
343
  useLayoutEffect(() => {
314
344
  latest.current = value;
@@ -355,32 +385,32 @@ function RefsField({
355
385
  },
356
386
  (error) => {
357
387
  settle();
358
- if (mounted.current) onError?.("upload", error);
388
+ if (mounted.current) onError?.("upload", error, errorInfo("upload", [file], error));
359
389
  }
360
390
  );
361
391
  };
362
392
  const add = (incoming, pasted = false) => {
363
393
  let space = Math.max(0, max - latest.current.length - inFlight.current.length);
364
394
  const taken = /* @__PURE__ */ new Set([...latest.current.map((r) => r.name), ...inFlight.current.map((u) => u.file.name)]);
365
- let dropped = false;
395
+ const dropped = [];
366
396
  for (const raw of incoming) {
367
- const problem = rejection(raw, accept, maxBytes);
397
+ const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
398
+ const problem = rejection(file, accept, maxBytes);
368
399
  if (problem) {
369
- onError?.(problem);
400
+ onError?.(problem, void 0, errorInfo(problem, [file]));
370
401
  continue;
371
402
  }
403
+ taken.add(file.name);
372
404
  if (space === 0) {
373
- dropped = true;
405
+ dropped.push(file);
374
406
  continue;
375
407
  }
376
- const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
377
- taken.add(file.name);
378
408
  space -= 1;
379
409
  start(file);
380
410
  }
381
- if (dropped) onError?.("count");
411
+ if (dropped.length > 0) onError?.("count", void 0, errorInfo("count", dropped));
382
412
  };
383
- const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom);
413
+ const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, inert);
384
414
  const pendingFocus = useRef(null);
385
415
  useEffect(() => {
386
416
  const index = pendingFocus.current;
@@ -393,6 +423,7 @@ function RefsField({
393
423
  target?.focus();
394
424
  }, [value]);
395
425
  const remove = (key, index) => {
426
+ if (inert) return;
396
427
  pendingFocus.current = index;
397
428
  commit(latest.current.filter((r) => r.key !== key));
398
429
  onRemove?.(key);
@@ -412,14 +443,12 @@ function RefsField({
412
443
  ref.size !== void 0 && /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-[var(--text-muted)]", children: fileText.size(ref.size) })
413
444
  ] }),
414
445
  /* @__PURE__ */ jsx(
415
- "button",
446
+ RemoveButton,
416
447
  {
417
- type: "button",
418
- "data-attachment-remove": "",
448
+ label: removeLabel(ref.name),
419
449
  onClick: () => remove(ref.key, index),
420
- "aria-label": removeLabel(ref.name),
421
- className: "shrink-0 rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
422
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
450
+ disabled,
451
+ reason
423
452
  }
424
453
  )
425
454
  ]
@@ -450,6 +479,8 @@ function RefsField({
450
479
  type: "button",
451
480
  variant: "secondary",
452
481
  "data-attachment-action": "",
482
+ disabled,
483
+ disabledReason: reason,
453
484
  onClick: () => fileInputRef.current?.click(),
454
485
  children: [
455
486
  /* @__PURE__ */ jsx(Paperclip, { className: "size-4" }),
@@ -464,11 +495,17 @@ function RefsField({
464
495
  capture: onCaptureScreenshot,
465
496
  onFile: (file) => add([file]),
466
497
  label: text.attachmentCapture,
498
+ disabled,
499
+ disabledReason: reason,
467
500
  "data-attachment-action": ""
468
501
  }
469
502
  )
470
503
  ] }),
471
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: room > 0 ? text.attachmentPaste : limitLabel(max) }),
504
+ locked ? (
505
+ // Where the paste hint was: a phone never hovers, so the Tooltip alone would
506
+ // leave a touch user with buttons that do nothing and no word why.
507
+ /* @__PURE__ */ jsx("div", { "data-attachment-reason": "", className: "text-xs text-[var(--text-muted)]", children: disabledReason })
508
+ ) : /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: room > 0 ? text.attachmentPaste : limitLabel(max) }),
472
509
  /* @__PURE__ */ jsx(
473
510
  "input",
474
511
  {
@@ -479,6 +516,7 @@ function RefsField({
479
516
  className: "sr-only",
480
517
  tabIndex: -1,
481
518
  "aria-hidden": true,
519
+ disabled: inert,
482
520
  onChange: (e) => {
483
521
  add(Array.from(e.target.files ?? []));
484
522
  e.target.value = "";
@@ -498,6 +536,8 @@ function CaptureButton({
498
536
  capture,
499
537
  onFile,
500
538
  label,
539
+ disabled = false,
540
+ disabledReason,
501
541
  ...rest
502
542
  }) {
503
543
  const [capturing, setCapturing] = useState(false);
@@ -506,10 +546,11 @@ function CaptureButton({
506
546
  {
507
547
  type: "button",
508
548
  variant: "secondary",
509
- disabled: capturing,
549
+ disabled: capturing || disabled,
550
+ disabledReason,
510
551
  ...rest,
511
552
  onClick: () => {
512
- if (capturing) return;
553
+ if (capturing || disabled) return;
513
554
  setCapturing(true);
514
555
  void capture().then((file) => {
515
556
  if (file) onFile(file);
@@ -523,6 +564,60 @@ function CaptureButton({
523
564
  }
524
565
  );
525
566
  }
567
+ function RemoveButton({
568
+ label,
569
+ onClick,
570
+ disabled,
571
+ reason
572
+ }) {
573
+ return /* @__PURE__ */ jsx(
574
+ IconButton,
575
+ {
576
+ type: "button",
577
+ size: "xs",
578
+ tone: "muted",
579
+ "data-attachment-remove": "",
580
+ "aria-label": label,
581
+ disabled,
582
+ disabledReason: reason,
583
+ onClick,
584
+ className: "shrink-0",
585
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": true })
586
+ }
587
+ );
588
+ }
589
+ function hasReason(reason) {
590
+ return reason !== void 0 && reason !== null && reason !== false && reason !== "";
591
+ }
592
+ function useErrorInfo(accept, maxBytes, max, text) {
593
+ const picker = useKitLabels("filePicker", DEFAULT_FILE_PICKER_LABELS);
594
+ const fileText = useKitFileLabels();
595
+ const uploadFailed = text.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed;
596
+ return (kind, files, error) => {
597
+ const file = files[0];
598
+ let message;
599
+ if (kind === "type") {
600
+ const acceptText = formatAccept(accept.join(","));
601
+ const useOnly = acceptText !== "" && (picker.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly || picker.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);
602
+ message = useOnly ? picker.rejectedTypeOnly(acceptText, file.name) : picker.rejectedType(file.name);
603
+ } else if (kind === "size") {
604
+ message = picker.rejectedSize(file.name, fileText.size(maxBytes));
605
+ } else if (kind === "count") {
606
+ message = files.length === 1 ? picker.rejectedCount(file.name, max ?? 1) : picker.rejectedMany(files.length);
607
+ } else {
608
+ message = uploadFailed(file.name);
609
+ }
610
+ return {
611
+ file,
612
+ files,
613
+ message,
614
+ maxBytes,
615
+ accept,
616
+ ...max !== void 0 ? { max } : null,
617
+ ...kind === "upload" ? { error } : null
618
+ };
619
+ };
620
+ }
526
621
  function ChipPreview({ file, screenshot }) {
527
622
  const isImage = file.type.startsWith("image/");
528
623
  const [previewFor, setPreviewFor] = useState(null);
@@ -565,15 +660,17 @@ function uniqueName(name, taken) {
565
660
  while (taken.has(`${stem}-${n}${ext}`)) n++;
566
661
  return `${stem}-${n}${ext}`;
567
662
  }
568
- function usePaste(onImages, documentPaste, pasteFrom) {
569
- const latest = useRef(onImages);
663
+ function usePaste(onImages, documentPaste, pasteFrom, off) {
664
+ const handler = off ? null : onImages;
665
+ const latest = useRef(handler);
570
666
  useEffect(() => {
571
- latest.current = onImages;
667
+ latest.current = handler;
572
668
  });
573
669
  useEffect(() => {
574
670
  const target = documentPaste ? document : pasteFrom?.current ?? null;
575
671
  if (!target) return;
576
672
  const onPaste = (event) => {
673
+ if (!latest.current) return;
577
674
  const clipboard = event.clipboardData;
578
675
  takeImages(clipboard, () => event.preventDefault(), latest.current);
579
676
  };
@@ -581,7 +678,10 @@ function usePaste(onImages, documentPaste, pasteFrom) {
581
678
  return () => target.removeEventListener("paste", onPaste);
582
679
  }, [documentPaste, pasteFrom]);
583
680
  const listensElsewhere = documentPaste || pasteFrom !== void 0;
584
- return listensElsewhere ? void 0 : (event) => takeImages(event.clipboardData, () => event.preventDefault(), latest.current);
681
+ return listensElsewhere ? void 0 : (event) => {
682
+ if (!latest.current) return;
683
+ takeImages(event.clipboardData, () => event.preventDefault(), latest.current);
684
+ };
585
685
  }
586
686
  function takeImages(clipboard, stop, onImages) {
587
687
  const images = Array.from(clipboard?.items ?? []).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => file !== null);