@nizivo/react 0.1.0 → 0.2.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 (238) hide show
  1. package/THIRD_PARTY_NOTICES +12 -5
  2. package/dist/_engines/react-aria/dialog.client.js +16 -24
  3. package/dist/_engines/react-aria/drawer.client.js +8 -0
  4. package/dist/_engines/react-aria/file-upload.client.js +50 -0
  5. package/dist/_engines/react-aria/menu.client.js +17 -13
  6. package/dist/_engines/react-aria/open-state.client.js +53 -0
  7. package/dist/_engines/react-aria/outside-press.client.js +61 -0
  8. package/dist/_engines/react-aria/overlay.client.js +6 -1
  9. package/dist/_engines/react-aria/popover.client.js +148 -0
  10. package/dist/_engines/react-aria/popup.client.js +31 -5
  11. package/dist/_engines/react-aria/select.client.js +22 -12
  12. package/dist/_engines/react-aria/toast.client.js +38 -0
  13. package/dist/_engines/react-aria/tooltip.client.js +11 -11
  14. package/dist/_internal/data-status.d.ts +3 -0
  15. package/dist/_internal/data-status.js +13 -0
  16. package/dist/_internal/glyphs.js +54 -1
  17. package/dist/_internal/modal.client.js +54 -0
  18. package/dist/_internal/nested-focus.js +33 -0
  19. package/dist/alert/alert-dismiss.client.d.ts +1 -1
  20. package/dist/alert/alert-dismiss.client.js +3 -1
  21. package/dist/app-shell/app-shell.d.ts +24 -0
  22. package/dist/app-shell/app-shell.js +56 -0
  23. package/dist/app-shell/index.d.ts +2 -0
  24. package/dist/app-shell/index.js +1 -0
  25. package/dist/avatar/avatar.d.ts +96 -0
  26. package/dist/avatar/avatar.js +94 -0
  27. package/dist/avatar/index.d.ts +2 -0
  28. package/dist/avatar/index.js +1 -0
  29. package/dist/banner/banner-dismiss.client.d.ts +32 -0
  30. package/dist/banner/banner-dismiss.client.js +38 -0
  31. package/dist/banner/banner.d.ts +34 -0
  32. package/dist/banner/banner.js +47 -0
  33. package/dist/banner/index.d.ts +3 -0
  34. package/dist/banner/index.js +3 -0
  35. package/dist/breadcrumb/breadcrumb.d.ts +46 -0
  36. package/dist/breadcrumb/breadcrumb.js +46 -0
  37. package/dist/breadcrumb/breadcrumb.parts.d.ts +6 -0
  38. package/dist/breadcrumb/breadcrumb.parts.js +9 -0
  39. package/dist/breadcrumb/index.d.ts +3 -0
  40. package/dist/breadcrumb/index.js +3 -0
  41. package/dist/button/button.d.ts +3 -2
  42. package/dist/card/card.d.ts +9 -2
  43. package/dist/chip-group/chip-group.client.d.ts +88 -0
  44. package/dist/chip-group/chip-group.client.js +157 -0
  45. package/dist/chip-group/index.d.ts +2 -0
  46. package/dist/chip-group/index.js +1 -0
  47. package/dist/choice-card/choice-card.client.d.ts +66 -0
  48. package/dist/choice-card/choice-card.client.js +138 -0
  49. package/dist/choice-card/index.d.ts +2 -0
  50. package/dist/choice-card/index.js +1 -0
  51. package/dist/config/config.client.d.ts +16 -1
  52. package/dist/config/config.client.js +18 -3
  53. package/dist/config/index.d.ts +2 -2
  54. package/dist/contracts/index.d.ts +1 -1
  55. package/dist/css/app-shell.css +2 -0
  56. package/dist/css/avatar.css +2 -0
  57. package/dist/css/banner.css +2 -0
  58. package/dist/css/base.css +1 -1
  59. package/dist/css/breadcrumb.css +2 -0
  60. package/dist/css/card.css +1 -1
  61. package/dist/css/chip-group.css +2 -0
  62. package/dist/css/choice-card.css +2 -0
  63. package/dist/css/delta.css +1 -1
  64. package/dist/css/drawer.css +2 -0
  65. package/dist/css/file-upload.css +2 -0
  66. package/dist/css/form.css +2 -0
  67. package/dist/css/heading.css +1 -1
  68. package/dist/css/kpi-tile.css +2 -0
  69. package/dist/css/metric.css +2 -0
  70. package/dist/css/mobile-tab-bar.css +2 -0
  71. package/dist/css/mobile-top-bar.css +2 -0
  72. package/dist/css/motion.css +2 -0
  73. package/dist/css/nav-item.css +2 -0
  74. package/dist/css/number-value.css +2 -0
  75. package/dist/css/page-header.css +2 -0
  76. package/dist/css/pagination.css +2 -0
  77. package/dist/css/password-input.css +2 -0
  78. package/dist/css/popover.css +2 -0
  79. package/dist/css/ranked-list.css +2 -0
  80. package/dist/css/search-field.css +2 -0
  81. package/dist/css/sidebar.css +2 -0
  82. package/dist/css/split.css +2 -0
  83. package/dist/css/sticky-bar.css +2 -0
  84. package/dist/css/table-selection.css +2 -0
  85. package/dist/css/table.css +2 -0
  86. package/dist/css/text.css +1 -1
  87. package/dist/css/toast.css +2 -0
  88. package/dist/css/top-bar.css +2 -0
  89. package/dist/css/trend-card.css +2 -0
  90. package/dist/dialog/dialog.client.d.ts +1 -1
  91. package/dist/dialog/dialog.client.js +15 -37
  92. package/dist/drawer/drawer.client.d.ts +25 -0
  93. package/dist/drawer/drawer.client.js +154 -0
  94. package/dist/drawer/drawer.contract.d.ts +98 -0
  95. package/dist/drawer/drawer.parts.d.ts +6 -0
  96. package/dist/drawer/drawer.parts.js +16 -0
  97. package/dist/drawer/index.d.ts +5 -0
  98. package/dist/drawer/index.js +3 -0
  99. package/dist/field/field-scope.client.js +9 -2
  100. package/dist/file-upload/file-upload.client.d.ts +9 -0
  101. package/dist/file-upload/file-upload.client.js +406 -0
  102. package/dist/file-upload/file-upload.contract.d.ts +112 -0
  103. package/dist/file-upload/index.d.ts +3 -0
  104. package/dist/file-upload/index.js +1 -0
  105. package/dist/file-upload/seam.d.ts +1 -0
  106. package/dist/form/form.client.d.ts +38 -0
  107. package/dist/form/form.client.js +127 -0
  108. package/dist/form/form.d.ts +36 -0
  109. package/dist/form/form.js +29 -0
  110. package/dist/form/index.d.ts +4 -0
  111. package/dist/form/index.js +4 -0
  112. package/dist/form/submit-button.client.d.ts +20 -0
  113. package/dist/form/submit-button.client.js +19 -0
  114. package/dist/heading/heading.d.ts +4 -1
  115. package/dist/i18n/format-label.d.ts +24 -0
  116. package/dist/i18n/format-label.js +19 -0
  117. package/dist/i18n/index.d.ts +2 -0
  118. package/dist/i18n/index.js +1 -0
  119. package/dist/icon-button/icon-button.client.js +1 -1
  120. package/dist/icon-button/icon-button.js +1 -1
  121. package/dist/index.d.ts +84 -5
  122. package/dist/index.js +49 -4
  123. package/dist/kpi-tile/index.d.ts +2 -0
  124. package/dist/kpi-tile/index.js +1 -0
  125. package/dist/kpi-tile/kpi-tile.d.ts +30 -0
  126. package/dist/kpi-tile/kpi-tile.js +48 -0
  127. package/dist/locales/en-US.d.ts +77 -0
  128. package/dist/locales/en-US.js +83 -0
  129. package/dist/menu/menu.contract.d.ts +1 -1
  130. package/dist/metric/index.d.ts +2 -0
  131. package/dist/metric/index.js +1 -0
  132. package/dist/metric/metric.d.ts +72 -0
  133. package/dist/metric/metric.js +116 -0
  134. package/dist/mobile-tab-bar/index.d.ts +4 -0
  135. package/dist/mobile-tab-bar/index.js +4 -0
  136. package/dist/mobile-tab-bar/mobile-tab-bar-inset.client.js +38 -0
  137. package/dist/mobile-tab-bar/mobile-tab-bar-more.client.d.ts +26 -0
  138. package/dist/mobile-tab-bar/mobile-tab-bar-more.client.js +60 -0
  139. package/dist/mobile-tab-bar/mobile-tab-bar.d.ts +39 -0
  140. package/dist/mobile-tab-bar/mobile-tab-bar.js +54 -0
  141. package/dist/mobile-tab-bar/mobile-tab-bar.parts.d.ts +6 -0
  142. package/dist/mobile-tab-bar/mobile-tab-bar.parts.js +9 -0
  143. package/dist/mobile-top-bar/index.d.ts +2 -0
  144. package/dist/mobile-top-bar/index.js +1 -0
  145. package/dist/mobile-top-bar/mobile-top-bar.d.ts +17 -0
  146. package/dist/mobile-top-bar/mobile-top-bar.js +31 -0
  147. package/dist/motion/count-up.client.d.ts +39 -0
  148. package/dist/motion/count-up.client.js +109 -0
  149. package/dist/motion/index.d.ts +2 -0
  150. package/dist/motion/index.js +1 -0
  151. package/dist/nav-item/index.d.ts +2 -0
  152. package/dist/nav-item/index.js +1 -0
  153. package/dist/nav-item/nav-item.d.ts +35 -0
  154. package/dist/nav-item/nav-item.js +50 -0
  155. package/dist/number-value/index.d.ts +2 -0
  156. package/dist/number-value/index.js +1 -0
  157. package/dist/number-value/number-value.d.ts +25 -0
  158. package/dist/number-value/number-value.js +26 -0
  159. package/dist/page-header/index.d.ts +2 -0
  160. package/dist/page-header/index.js +1 -0
  161. package/dist/page-header/page-header.d.ts +19 -0
  162. package/dist/page-header/page-header.js +39 -0
  163. package/dist/pagination/index.d.ts +2 -0
  164. package/dist/pagination/index.js +1 -0
  165. package/dist/pagination/pagination.d.ts +74 -0
  166. package/dist/pagination/pagination.js +136 -0
  167. package/dist/password-input/index.d.ts +2 -0
  168. package/dist/password-input/index.js +1 -0
  169. package/dist/password-input/password-input.client.d.ts +52 -0
  170. package/dist/password-input/password-input.client.js +106 -0
  171. package/dist/popover/index.d.ts +5 -0
  172. package/dist/popover/index.js +3 -0
  173. package/dist/popover/popover.client.d.ts +20 -0
  174. package/dist/popover/popover.client.js +113 -0
  175. package/dist/popover/popover.contract.d.ts +88 -0
  176. package/dist/popover/popover.parts.d.ts +6 -0
  177. package/dist/popover/popover.parts.js +13 -0
  178. package/dist/ranked-list/index.d.ts +3 -0
  179. package/dist/ranked-list/index.js +3 -0
  180. package/dist/ranked-list/ranked-list.d.ts +85 -0
  181. package/dist/ranked-list/ranked-list.js +153 -0
  182. package/dist/ranked-list/ranked-list.parts.d.ts +6 -0
  183. package/dist/ranked-list/ranked-list.parts.js +10 -0
  184. package/dist/search-field/index.d.ts +2 -0
  185. package/dist/search-field/index.js +1 -0
  186. package/dist/search-field/search-field.client.d.ts +55 -0
  187. package/dist/search-field/search-field.client.js +136 -0
  188. package/dist/select/select.contract.d.ts +1 -1
  189. package/dist/sidebar/index.d.ts +4 -0
  190. package/dist/sidebar/index.js +4 -0
  191. package/dist/sidebar/sidebar-item.client.d.ts +10 -0
  192. package/dist/sidebar/sidebar-item.client.js +55 -0
  193. package/dist/sidebar/sidebar.d.ts +45 -0
  194. package/dist/sidebar/sidebar.js +62 -0
  195. package/dist/sidebar/sidebar.parts.d.ts +6 -0
  196. package/dist/sidebar/sidebar.parts.js +11 -0
  197. package/dist/split/index.d.ts +3 -0
  198. package/dist/split/index.js +3 -0
  199. package/dist/split/split.d.ts +28 -0
  200. package/dist/split/split.js +33 -0
  201. package/dist/split/split.parts.d.ts +6 -0
  202. package/dist/split/split.parts.js +10 -0
  203. package/dist/sticky-bar/index.d.ts +2 -0
  204. package/dist/sticky-bar/index.js +1 -0
  205. package/dist/sticky-bar/sticky-bar.d.ts +16 -0
  206. package/dist/sticky-bar/sticky-bar.js +26 -0
  207. package/dist/styles.css +66 -7
  208. package/dist/table/index.d.ts +3 -0
  209. package/dist/table/index.js +3 -0
  210. package/dist/table/table.d.ts +101 -0
  211. package/dist/table/table.js +121 -0
  212. package/dist/table/table.parts.d.ts +6 -0
  213. package/dist/table/table.parts.js +18 -0
  214. package/dist/table-selection/index.d.ts +3 -0
  215. package/dist/table-selection/index.js +3 -0
  216. package/dist/table-selection/table-selection.client.d.ts +14 -0
  217. package/dist/table-selection/table-selection.client.js +84 -0
  218. package/dist/table-selection/table-selection.d.ts +34 -0
  219. package/dist/table-selection/table-selection.js +50 -0
  220. package/dist/text/text.d.ts +13 -2
  221. package/dist/toast/index.d.ts +4 -0
  222. package/dist/toast/index.js +3 -0
  223. package/dist/toast/seam.d.ts +2 -0
  224. package/dist/toast/store.d.ts +19 -0
  225. package/dist/toast/store.js +136 -0
  226. package/dist/toast/toast.contract.d.ts +56 -0
  227. package/dist/toast/toaster.client.d.ts +10 -0
  228. package/dist/toast/toaster.client.js +305 -0
  229. package/dist/tooltip/tooltip.contract.d.ts +1 -1
  230. package/dist/top-bar/index.d.ts +2 -0
  231. package/dist/top-bar/index.js +1 -0
  232. package/dist/top-bar/top-bar.d.ts +17 -0
  233. package/dist/top-bar/top-bar.js +35 -0
  234. package/dist/trend-card/index.d.ts +2 -0
  235. package/dist/trend-card/index.js +1 -0
  236. package/dist/trend-card/trend-card.d.ts +34 -0
  237. package/dist/trend-card/trend-card.js +74 -0
  238. package/package.json +167 -4
@@ -0,0 +1,406 @@
1
+ "use client";
2
+ import { cx } from "../_internal/cx.js";
3
+ import { mergeProps } from "../contracts/merge-props.js";
4
+ import { useLabels } from "../config/config.client.js";
5
+ import { CircleCheckGlyph, CircleXGlyph, CloseGlyph } from "../_internal/glyphs.js";
6
+ import { Button } from "../button/button.js";
7
+ import { stateAttrs } from "../contracts/state.js";
8
+ import { joinIds } from "../_internal/field.js";
9
+ import { InlineError } from "../inline-error/inline-error.js";
10
+ import { useFieldControl } from "../field/field-scope.client.js";
11
+ import { iconButtonElement } from "../icon-button/icon-button-static.js";
12
+ import { ProgressBar } from "../progress-bar/progress-bar.js";
13
+ import { useDropTarget } from "../_engines/react-aria/file-upload.client.js";
14
+ import { useEffect, useEffectEvent, useId, useLayoutEffect, useRef, useState } from "react";
15
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
+ //#region src/file-upload/file-upload.client.tsx
17
+ const LABELS = {
18
+ prompt: "Drop files here or {browse}",
19
+ browse: "browse",
20
+ release: "Release to add files",
21
+ summary: "{complete} of {total} uploaded",
22
+ wrongType: "{name} is not an accepted file type.",
23
+ tooLarge: "{name} is larger than {max}.",
24
+ tooMany: "{name} was not added: choose one file.",
25
+ list: "Chosen files",
26
+ remove: "Remove {name}",
27
+ retry: "Retry",
28
+ cancel: "Cancel",
29
+ uploading: "Uploading {name}",
30
+ complete: "Uploaded",
31
+ failed: "Upload failed"
32
+ };
33
+ /** Ids for held files: unique on the page, and never in the server HTML. */
34
+ let nextId = 0;
35
+ /**
36
+ * `className` and `style` go on the root; every other input prop, `ref` included, goes on the
37
+ * native `<input type="file">`.
38
+ */
39
+ function FileUpload({ id, name, accept, multiple = false, maxSize, disabled, required, invalid, hint, icon, items, onFilesChange, onReject, onRemove, onRetry, onCancel, labels: labelsProp, locale = "en-US", className, style, "aria-invalid": ariaInvalid, "aria-describedby": describedByProp, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }) {
40
+ const labels = {
41
+ ...LABELS,
42
+ ...useLabels("fileUpload", labelsProp)
43
+ };
44
+ const base = useId();
45
+ const hintId = `${base}-hint`;
46
+ const messagesId = `${base}-messages`;
47
+ const [files, setFiles] = useState([]);
48
+ const [rejected, setRejected] = useState([]);
49
+ const zone = useRef(null);
50
+ const list = useRef(null);
51
+ const refocus = useRef(null);
52
+ const showHint = hint != null && hint !== false && hint !== "";
53
+ const field = useFieldControl({
54
+ id,
55
+ name,
56
+ required,
57
+ disabled,
58
+ invalid: invalid ?? (ariaInvalid === true || ariaInvalid === "true" ? true : void 0),
59
+ "aria-describedby": joinIds(describedByProp, showHint && hintId, rejected.length > 0 && messagesId),
60
+ labelBehavior: "focus"
61
+ });
62
+ const rows = listRows(files, items);
63
+ const pending = rows.filter((row) => row.item?.status === "uploading").length > 0;
64
+ const complete = rows.filter((row) => row.item?.status === "complete").length;
65
+ /** Writes the list into the input, then into state, then tells the app. */
66
+ const commit = (element, nextFiles) => {
67
+ write(element, nextFiles);
68
+ setFiles(nextFiles);
69
+ onFilesChange?.(nextFiles);
70
+ };
71
+ /** A pick or a drop: check each file, add the ones that pass, report the others. */
72
+ const add = (incoming, element) => {
73
+ const accepted = [];
74
+ const refused = [];
75
+ for (const file of incoming) {
76
+ const reason = !matches(file, accept) ? "type" : maxSize !== void 0 && file.size > maxSize ? "size" : !multiple && accepted.length > 0 ? "count" : void 0;
77
+ if (reason !== void 0) refused.push({
78
+ file,
79
+ reason
80
+ });
81
+ else if (!files.some((held) => same(held.file, file))) {
82
+ nextId += 1;
83
+ accepted.push({
84
+ id: `${base}-${nextId}`,
85
+ file
86
+ });
87
+ }
88
+ }
89
+ setRejected(refused);
90
+ if (refused.length > 0) onReject?.(refused);
91
+ if (accepted.length > 0) commit(element, multiple ? [...files, ...accepted] : accepted);
92
+ else write(element, files);
93
+ };
94
+ const { dropProps, isDropTarget } = useDropTarget({
95
+ disabled: field.disabled,
96
+ onDrop: (dropped, target) => add(dropped, inputIn(target))
97
+ }, zone);
98
+ const adopt = useEffectEvent((element) => {
99
+ if (element.files !== null && element.files.length > 0) add([...element.files], element);
100
+ });
101
+ useLayoutEffect(() => {
102
+ const element = fileInputOf(zone.current);
103
+ if (element !== null) adopt(element);
104
+ }, []);
105
+ useEffect(() => {
106
+ const form = fileInputOf(zone.current)?.form;
107
+ if (form == null) return void 0;
108
+ const onReset = () => {
109
+ setFiles([]);
110
+ setRejected([]);
111
+ onFilesChange?.([]);
112
+ };
113
+ form.addEventListener("reset", onReset);
114
+ return () => form.removeEventListener("reset", onReset);
115
+ }, [onFilesChange]);
116
+ const count = rows.length;
117
+ useLayoutEffect(() => {
118
+ const removed = refocus.current;
119
+ refocus.current = null;
120
+ if (removed === null || count >= removed.count) return;
121
+ const buttons = [...list.current?.children ?? []].map((row) => row.querySelector("button"));
122
+ (buttons[Math.min(removed.index, buttons.length - 1)] ?? fileInputOf(zone.current))?.focus();
123
+ });
124
+ const drop = (row, index, event) => {
125
+ refocus.current = {
126
+ index,
127
+ count
128
+ };
129
+ if (row.held) commit(inputIn(event.currentTarget), files.filter((entry) => entry.id !== row.id));
130
+ };
131
+ const onChange = (event) => {
132
+ const chosen = [...event.currentTarget.files ?? []];
133
+ if (chosen.length > 0) add(chosen, event.currentTarget);
134
+ else write(event.currentTarget, files);
135
+ };
136
+ const prompt = isDropTarget ? labels.release : pending ? fill(labels.summary, {
137
+ complete: String(complete),
138
+ total: String(rows.length)
139
+ }) : browseLine(labels.prompt, labels.browse);
140
+ const invalidShown = field.invalid || rejected.length > 0;
141
+ const removeLabel = (row) => fill(labels.remove, { name: row.name });
142
+ return /* @__PURE__ */ jsxs("div", {
143
+ className: cx("nz-file-upload", className),
144
+ "data-slot": "file-upload",
145
+ ...stateAttrs({
146
+ disabled: field.disabled,
147
+ invalid: invalidShown,
148
+ pending
149
+ }),
150
+ style,
151
+ children: [
152
+ /* @__PURE__ */ jsxs("div", {
153
+ ...dropProps,
154
+ ref: zone,
155
+ className: "nz-file-upload-dropzone",
156
+ "data-slot": "file-upload-dropzone",
157
+ "data-drag-over": isDropTarget ? "" : void 0,
158
+ ...stateAttrs({
159
+ disabled: field.disabled,
160
+ invalid: invalidShown
161
+ }),
162
+ "aria-disabled": field.disabled ? true : void 0,
163
+ children: [
164
+ /* @__PURE__ */ jsx("input", {
165
+ ...mergeProps(field.controlProps, rest),
166
+ onChange,
167
+ type: "file",
168
+ className: "nz-file-upload-input",
169
+ "data-slot": "file-upload-input",
170
+ accept,
171
+ multiple,
172
+ required: field.required && rows.length === 0 ? true : void 0,
173
+ "aria-label": ariaLabel ?? (ariaLabelledBy === void 0 && field.labelledBy === void 0 ? fill(labels.prompt, { browse: labels.browse }) : void 0),
174
+ "aria-labelledby": ariaLabelledBy
175
+ }),
176
+ icon != null && /* @__PURE__ */ jsx("span", {
177
+ className: "nz-file-upload-icon",
178
+ "data-slot": "file-upload-icon",
179
+ "aria-hidden": "true",
180
+ children: icon
181
+ }),
182
+ /* @__PURE__ */ jsx("span", {
183
+ className: "nz-file-upload-prompt",
184
+ "data-slot": "file-upload-prompt",
185
+ children: prompt
186
+ }),
187
+ showHint && /* @__PURE__ */ jsx("span", {
188
+ className: "nz-file-upload-hint",
189
+ "data-slot": "file-upload-hint",
190
+ id: hintId,
191
+ children: hint
192
+ })
193
+ ]
194
+ }),
195
+ /* @__PURE__ */ jsx("div", {
196
+ className: "nz-file-upload-messages",
197
+ "data-slot": "file-upload-messages",
198
+ id: messagesId,
199
+ "aria-live": "polite",
200
+ children: rejected.map(({ file, reason }) => /* @__PURE__ */ jsx(InlineError, { children: rejection(labels, reason, file.name, maxSize, locale) }, `${reason}-${file.name}-${file.size}-${file.lastModified}`))
201
+ }),
202
+ rows.length > 0 && /* @__PURE__ */ jsx("ul", {
203
+ ref: list,
204
+ className: "nz-file-upload-list",
205
+ "data-slot": "file-upload-list",
206
+ "aria-label": labels.list,
207
+ children: rows.map((row, index) => {
208
+ const status = row.item?.status;
209
+ const nameId = `${base}-name-${row.id}`;
210
+ return /* @__PURE__ */ jsxs("li", {
211
+ className: "nz-file-upload-item",
212
+ "data-slot": "file-upload-item",
213
+ ...stateAttrs({
214
+ pending: status === "uploading",
215
+ error: status === "error"
216
+ }),
217
+ children: [
218
+ /* @__PURE__ */ jsx("span", {
219
+ className: "nz-file-upload-item-name",
220
+ "data-slot": "file-upload-item-name",
221
+ id: nameId,
222
+ children: row.name
223
+ }),
224
+ /* @__PURE__ */ jsxs("span", {
225
+ className: "nz-file-upload-item-status",
226
+ "data-slot": "file-upload-item-status",
227
+ children: [row.size !== void 0 && /* @__PURE__ */ jsx("span", {
228
+ className: "nz-file-upload-item-size",
229
+ "data-slot": "file-upload-item-size",
230
+ children: formatBytes(row.size, locale)
231
+ }), itemStatus(row, labels)]
232
+ }),
233
+ /* @__PURE__ */ jsxs("span", {
234
+ className: "nz-file-upload-item-actions",
235
+ "data-slot": "file-upload-item-actions",
236
+ children: [status === "error" && /* @__PURE__ */ jsx(Button, {
237
+ variant: "tertiary",
238
+ size: "sm",
239
+ "aria-describedby": nameId,
240
+ disabled: field.disabled,
241
+ onClick: () => onRetry?.(row.id),
242
+ children: labels.retry
243
+ }), status === "uploading" ? /* @__PURE__ */ jsx(Button, {
244
+ variant: "tertiary",
245
+ size: "sm",
246
+ "aria-describedby": nameId,
247
+ disabled: field.disabled,
248
+ onClick: (event) => {
249
+ drop(row, index, event);
250
+ onCancel?.(row.id);
251
+ },
252
+ children: labels.cancel
253
+ }) : /* @__PURE__ */ jsx(RemoveButton, {
254
+ label: removeLabel(row),
255
+ disabled: field.disabled,
256
+ onClick: (event) => {
257
+ drop(row, index, event);
258
+ onRemove?.(row.id);
259
+ }
260
+ })]
261
+ })
262
+ ]
263
+ }, row.id);
264
+ })
265
+ })
266
+ ]
267
+ });
268
+ }
269
+ /** IconButton's element with no tooltip, as Toast's close button: the row's name is visible. */
270
+ function RemoveButton(props) {
271
+ return iconButtonElement({
272
+ label: props.label,
273
+ icon: /* @__PURE__ */ jsx(CloseGlyph, {}),
274
+ variant: "tertiary",
275
+ size: "sm",
276
+ disabled: props.disabled,
277
+ onClick: props.onClick
278
+ });
279
+ }
280
+ /** The file input of the FileUpload that holds `element`. */
281
+ function inputIn(element) {
282
+ return fileInputOf(element.closest("[data-slot=\"file-upload\"]"));
283
+ }
284
+ function fileInputOf(scope) {
285
+ return scope?.querySelector("[data-slot=\"file-upload-input\"]") ?? null;
286
+ }
287
+ /** The held files in order, each with its item, then the items that match no held file. */
288
+ function listRows(files, items) {
289
+ const byId = new Map((items ?? []).map((item) => [item.id, item]));
290
+ const rows = files.map(({ id, file }) => {
291
+ const item = byId.get(id);
292
+ byId.delete(id);
293
+ return {
294
+ id,
295
+ name: item?.name ?? file.name,
296
+ size: item?.size ?? file.size,
297
+ item,
298
+ held: true
299
+ };
300
+ });
301
+ for (const item of byId.values()) rows.push({
302
+ id: item.id,
303
+ name: item.name ?? item.id,
304
+ size: item.size,
305
+ item,
306
+ held: false
307
+ });
308
+ return rows;
309
+ }
310
+ function itemStatus(row, labels) {
311
+ const item = row.item;
312
+ switch (item?.status) {
313
+ case "uploading": return /* @__PURE__ */ jsx(ProgressBar, {
314
+ value: item.progress ?? 0,
315
+ indeterminate: item.progress === void 0,
316
+ showValue: false,
317
+ "aria-label": fill(labels.uploading, { name: row.name })
318
+ });
319
+ case "complete": return /* @__PURE__ */ jsxs("span", {
320
+ className: "nz-file-upload-item-complete",
321
+ "data-slot": "file-upload-item-complete",
322
+ children: [/* @__PURE__ */ jsx(CircleCheckGlyph, {}), labels.complete]
323
+ });
324
+ case "error": return /* @__PURE__ */ jsxs("span", {
325
+ className: "nz-file-upload-item-error",
326
+ "data-slot": "file-upload-item-error",
327
+ children: [/* @__PURE__ */ jsx(CircleXGlyph, {}), item.error ?? labels.failed]
328
+ });
329
+ default: return null;
330
+ }
331
+ }
332
+ function rejection(labels, reason, name, maxSize, locale) {
333
+ if (reason === "type") return fill(labels.wrongType, { name });
334
+ if (reason === "count") return fill(labels.tooMany, { name });
335
+ return fill(labels.tooLarge, {
336
+ name,
337
+ max: formatBytes(maxSize ?? 0, locale)
338
+ });
339
+ }
340
+ /** The prompt with the browse word in its own span, where `{browse}` is (at the end if absent). */
341
+ function browseLine(template, browse) {
342
+ const at = template.indexOf("{browse}");
343
+ const word = /* @__PURE__ */ jsx("span", {
344
+ className: "nz-file-upload-browse",
345
+ "data-slot": "file-upload-browse",
346
+ children: browse
347
+ });
348
+ if (at === -1) return template;
349
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
350
+ template.slice(0, at),
351
+ word,
352
+ template.slice(at + 8)
353
+ ] });
354
+ }
355
+ function fill(template, values) {
356
+ let out = template;
357
+ for (const [key, value] of Object.entries(values)) out = out.replaceAll(`{${key}}`, value);
358
+ return out;
359
+ }
360
+ const UNITS = [
361
+ "byte",
362
+ "kilobyte",
363
+ "megabyte",
364
+ "gigabyte",
365
+ "terabyte"
366
+ ];
367
+ /** Decimal units (1 MB = 1,000,000 bytes), as macOS and the `maxSize` docs write them. */
368
+ function formatBytes(bytes, locale) {
369
+ let value = Math.max(bytes, 0);
370
+ let unit = 0;
371
+ while (value >= 1e3 && unit < UNITS.length - 1) {
372
+ value /= 1e3;
373
+ unit += 1;
374
+ }
375
+ return new Intl.NumberFormat(locale, {
376
+ style: "unit",
377
+ unit: UNITS[unit],
378
+ unitDisplay: unit === 0 ? "long" : "short",
379
+ maximumFractionDigits: unit > 0 && value < 10 ? 1 : 0
380
+ }).format(value);
381
+ }
382
+ /** The native `accept` rule: a MIME type, a `type/*` wildcard or a `.ext`, any of them. */
383
+ function matches(file, accept) {
384
+ const rules = (accept ?? "").split(",").map((rule) => rule.trim().toLowerCase()).filter((rule) => rule !== "");
385
+ if (rules.length === 0) return true;
386
+ const fileName = file.name.toLowerCase();
387
+ const type = file.type.toLowerCase();
388
+ return rules.some((rule) => rule.startsWith(".") ? fileName.endsWith(rule) : rule.endsWith("/*") ? type.startsWith(rule.slice(0, -1)) : type === rule);
389
+ }
390
+ function same(a, b) {
391
+ return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
392
+ }
393
+ /**
394
+ * Puts the held files in the input, as a FileList (the only kind `input.files` takes), and fires
395
+ * `input` so a Field withdraws a "choose a file" message. Not `change`: React's onChange on a file
396
+ * input is the native `change`, which would run the pick handler again.
397
+ */
398
+ function write(element, files) {
399
+ if (element === null) return;
400
+ const transfer = new DataTransfer();
401
+ for (const { file } of files) transfer.items.add(file);
402
+ element.files = transfer.files;
403
+ element.dispatchEvent(new Event("input", { bubbles: true }));
404
+ }
405
+ //#endregion
406
+ export { FileUpload };
@@ -0,0 +1,112 @@
1
+ import "./seam.js";
2
+ import { ComponentPropsWithRef, ReactNode, RefObject } from "react";
3
+ //#region src/file-upload/file-upload.contract.d.ts
4
+ /** Where an item's upload is. Absent on an item: chosen, not sent yet. */
5
+ export type FileUploadStatus = "uploading" | "complete" | "error";
6
+ /** A file the component holds (and its input submits), with the id the component gave it. */
7
+ export interface FileUploadFile {
8
+ /** Stays the same while the file is listed. Key your `items` and upload requests by it. */
9
+ id: string;
10
+ file: File;
11
+ }
12
+ /**
13
+ * An item's upload state, from the app, which does the uploading. Matched to a held file by `id`;
14
+ * an item whose id matches no held file is listed too, after the held files (a file uploaded
15
+ * earlier, shown on an edit form).
16
+ */
17
+ export interface FileUploadItem {
18
+ id: string;
19
+ /**
20
+ * Shown in the list. Defaults to the held file's name; an item that matches no held file needs
21
+ * one, or its `id` is shown.
22
+ */
23
+ name?: string;
24
+ /** In bytes, shown beside the name. Defaults to the held file's size. */
25
+ size?: number;
26
+ status?: FileUploadStatus;
27
+ /** 0 to 100 while `uploading`; absent, the bar is indeterminate. */
28
+ progress?: number;
29
+ /** The message while `status` is `error`. @default labels.failed */
30
+ error?: string;
31
+ }
32
+ /** Why a chosen or dropped file was not added. */
33
+ export type FileUploadRejectionReason = "type" | "size" | "count";
34
+ export interface FileUploadRejection {
35
+ file: File;
36
+ reason: FileUploadRejectionReason;
37
+ }
38
+ /**
39
+ * Strings FileUpload renders (§16.4): serialisable, English defaults. `{name}` is a file's name,
40
+ * `{max}` the formatted `maxSize`, `{browse}` where the browse word goes, `{complete}` and `{total}`
41
+ * counts.
42
+ */
43
+ export interface FileUploadLabels {
44
+ /** The dropzone's line. @default 'Drop files here or {browse}' */
45
+ prompt: string;
46
+ /** The word that reads as the picker's button, in the brand colour. @default 'browse' */
47
+ browse: string;
48
+ /** Replaces the prompt while a drag with files is over the dropzone. @default 'Release to add files' */
49
+ release: string;
50
+ /** Replaces the prompt while an item is uploading. @default '{complete} of {total} uploaded' */
51
+ summary: string;
52
+ /** A file `accept` does not match. @default '{name} is not an accepted file type.' */
53
+ wrongType: string;
54
+ /** A file over `maxSize`. @default '{name} is larger than {max}.' */
55
+ tooLarge: string;
56
+ /** Without `multiple`, every file of a drop after the first. @default '{name} was not added: choose one file.' */
57
+ tooMany: string;
58
+ /** The list's accessible name. @default 'Chosen files' */
59
+ list: string;
60
+ /** Each item's remove button. @default 'Remove {name}' */
61
+ remove: string;
62
+ /** An item in error. @default 'Retry' */
63
+ retry: string;
64
+ /** An item uploading. @default 'Cancel' */
65
+ cancel: string;
66
+ /** An uploading item's progress bar. @default 'Uploading {name}' */
67
+ uploading: string;
68
+ /** A complete item's status. @default 'Uploaded' */
69
+ complete: string;
70
+ /** An item in error with no `error` text. @default 'Upload failed' */
71
+ failed: string;
72
+ }
73
+ export interface FileUploadProps extends Omit<ComponentPropsWithRef<"input">, "type" | "size" | "children" | "value" | "defaultValue" | "onChange" | "accept" | "multiple"> {
74
+ /**
75
+ * The native `accept`: MIME types (`image/png`), wildcards (`image/*`) and extensions (`.pdf`),
76
+ * comma-separated. The picker filters by it, and a dropped or picked file that does not match is
77
+ * not added.
78
+ */
79
+ accept?: string;
80
+ /** More than one file; each pick or drop adds to the list. Without it, a new file replaces the old. @default false */
81
+ multiple?: boolean;
82
+ /** Bytes. A larger file is not added. 1 MB is 1,000,000 bytes, as `{max}` is written. */
83
+ maxSize?: number;
84
+ /**
85
+ * An error message is presented for this control (§8.3). Inside a Field, the Field sets it; an
86
+ * explicit value here wins.
87
+ */
88
+ invalid?: boolean;
89
+ /** The line under the prompt, such as "PNG, JPG or WebP, up to 20 MB". */
90
+ hint?: ReactNode;
91
+ /** Decorative mark above the prompt, such as your icon set's `upload` glyph (`aria-hidden`). */
92
+ icon?: ReactNode;
93
+ /**
94
+ * Upload state per file, keyed by the ids `onFilesChange` gives. Leave it out when the form
95
+ * submits the files itself.
96
+ */
97
+ items?: readonly FileUploadItem[];
98
+ /** The held files changed: a pick, a drop, a remove, a cancel or the form's reset. Every held file, in order. */
99
+ onFilesChange?: (files: FileUploadFile[]) => void;
100
+ /** Files of a pick or drop that were not added; their messages show under the dropzone. */
101
+ onReject?: (rejections: FileUploadRejection[]) => void;
102
+ /** An item's remove button. A held file has already left the list and the input. */
103
+ onRemove?: (id: string) => void;
104
+ /** An item's retry button, on an item in `error`. */
105
+ onRetry?: (id: string) => void;
106
+ /** An item's cancel button, on an item `uploading`. The held file has already left the list. */
107
+ onCancel?: (id: string) => void;
108
+ labels?: Partial<FileUploadLabels>;
109
+ /** BCP 47 tag for sizes. @default 'en-US' */
110
+ locale?: string;
111
+ }
112
+ //#endregion
@@ -0,0 +1,3 @@
1
+ import { FileUploadFile, FileUploadItem, FileUploadLabels, FileUploadProps, FileUploadRejection, FileUploadRejectionReason, FileUploadStatus } from "./file-upload.contract.js";
2
+ import { FileUpload } from "./file-upload.client.js";
3
+ export { FileUpload, type FileUploadFile, type FileUploadItem, type FileUploadLabels, type FileUploadProps, type FileUploadRejection, type FileUploadRejectionReason, type FileUploadStatus };
@@ -0,0 +1 @@
1
+ export { FileUpload } from "./file-upload.client.js";
@@ -0,0 +1 @@
1
+ import "react";
@@ -0,0 +1,38 @@
1
+ import { RenderProp } from "../contracts/render.js";
2
+ import { ComponentPropsWithRef, ReactElement, ReactNode } from "react";
3
+ //#region src/form/form.client.d.ts
4
+ /** One Field's errors: a message, or several (the first is shown), or none. */
5
+ export type FormErrorValue = string | readonly string[] | null | undefined;
6
+ /**
7
+ * Errors keyed by the Field's `name`, as a server action returns them (Zod's
8
+ * `z.flattenError(error).fieldErrors`, or Conform's and TanStack Form's issues mapped to text). A
9
+ * nested name is one key, written as the Field's `name` is: `items.0.title`. Serialisable.
10
+ */
11
+ export type FormErrors = Readonly<Record<string, FormErrorValue>>;
12
+ /** Form's own strings (§16.4): serialisable, English defaults. */
13
+ export interface FormLabels {
14
+ /** The error summary's title. @default 'There is a problem' */
15
+ summaryTitle: string;
16
+ }
17
+ export interface FormProps extends Omit<ComponentPropsWithRef<"form">, "children"> {
18
+ /**
19
+ * Errors by Field `name`, typically a server action's reply. Each one is presented by the Field
20
+ * with that `name` (the Field's own `error` prop wins over it) until the user edits that control.
21
+ * A new object (by identity) presents them all again and moves focus: to the error summary with
22
+ * `summary`, else to the first invalid control. The first render moves no focus.
23
+ */
24
+ errors?: FormErrors | undefined;
25
+ /**
26
+ * Renders an error summary at the start of the form while `errors` presents a message: a title
27
+ * and one link per error to its control, in page order. New errors focus the summary instead of
28
+ * the first invalid control (GOV.UK's error summary). For long forms. @default false
29
+ */
30
+ summary?: boolean;
31
+ /** Overrides Form's strings, for example `labels={{ summaryTitle: 'Es gibt ein Problem' }}`. */
32
+ labels?: Partial<FormLabels>;
33
+ /** Composition of the `<form>`. */
34
+ render?: RenderProp<Record<string, never>>;
35
+ children?: ReactNode;
36
+ }
37
+ export declare function Form({ errors, summary, labels: labelsProp, render, className, children, ...rest }: FormProps): ReactElement;
38
+ //#endregion