@blurrow/ui 0.1.0 → 0.2.1

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 (272) hide show
  1. package/README.md +60 -20
  2. package/dist/Accordion.d.ts +17 -0
  3. package/dist/Accordion.js +138 -0
  4. package/dist/Accordion.js.map +1 -0
  5. package/dist/Alert.d.ts +13 -0
  6. package/dist/Alert.js +69 -0
  7. package/dist/Alert.js.map +1 -0
  8. package/dist/AuthorCard.d.ts +12 -0
  9. package/dist/AuthorCard.js +57 -0
  10. package/dist/AuthorCard.js.map +1 -0
  11. package/dist/Avatar.d.ts +21 -0
  12. package/dist/Avatar.js +96 -0
  13. package/dist/Avatar.js.map +1 -0
  14. package/dist/Badge.d.ts +12 -0
  15. package/dist/Badge.js +46 -0
  16. package/dist/Badge.js.map +1 -0
  17. package/dist/Banner.d.ts +15 -0
  18. package/dist/Banner.js +68 -0
  19. package/dist/Banner.js.map +1 -0
  20. package/dist/Breadcrumb.d.ts +13 -0
  21. package/dist/Breadcrumb.js +45 -0
  22. package/dist/Breadcrumb.js.map +1 -0
  23. package/dist/Button.d.ts +19 -0
  24. package/dist/Button.js +282 -0
  25. package/dist/Button.js.map +1 -0
  26. package/dist/ButtonGroup.d.ts +19 -0
  27. package/dist/ButtonGroup.js +62 -0
  28. package/dist/ButtonGroup.js.map +1 -0
  29. package/dist/Card.d.ts +40 -0
  30. package/dist/Card.js +175 -0
  31. package/dist/Card.js.map +1 -0
  32. package/dist/CardShell.d.ts +18 -0
  33. package/dist/CardShell.js +58 -0
  34. package/dist/CardShell.js.map +1 -0
  35. package/dist/Carousel.d.ts +22 -0
  36. package/dist/Carousel.js +247 -0
  37. package/dist/Carousel.js.map +1 -0
  38. package/dist/Checkbox.d.ts +15 -0
  39. package/dist/Checkbox.js +84 -0
  40. package/dist/Checkbox.js.map +1 -0
  41. package/dist/CloseButton.d.ts +12 -0
  42. package/dist/CloseButton.js +63 -0
  43. package/dist/CloseButton.js.map +1 -0
  44. package/dist/CodeBlock.d.ts +12 -0
  45. package/dist/CodeBlock.js +221 -0
  46. package/dist/CodeBlock.js.map +1 -0
  47. package/dist/ColorPicker.d.ts +24 -0
  48. package/dist/ColorPicker.js +366 -0
  49. package/dist/ColorPicker.js.map +1 -0
  50. package/dist/Combobox.d.ts +34 -0
  51. package/dist/Combobox.js +255 -0
  52. package/dist/Combobox.js.map +1 -0
  53. package/dist/CommandPalette.d.ts +25 -0
  54. package/dist/CommandPalette.js +304 -0
  55. package/dist/CommandPalette.js.map +1 -0
  56. package/dist/ContextMenu.d.ts +16 -0
  57. package/dist/ContextMenu.js +209 -0
  58. package/dist/ContextMenu.js.map +1 -0
  59. package/dist/DataList.d.ts +15 -0
  60. package/dist/DataList.js +42 -0
  61. package/dist/DataList.js.map +1 -0
  62. package/dist/DatePicker.d.ts +65 -0
  63. package/dist/DatePicker.js +391 -0
  64. package/dist/DatePicker.js.map +1 -0
  65. package/dist/DateRangePicker.d.ts +34 -0
  66. package/dist/DateRangePicker.js +180 -0
  67. package/dist/DateRangePicker.js.map +1 -0
  68. package/dist/Drawer.d.ts +17 -0
  69. package/dist/Drawer.js +132 -0
  70. package/dist/Drawer.js.map +1 -0
  71. package/dist/DropdownMenu.d.ts +23 -0
  72. package/dist/DropdownMenu.js +185 -0
  73. package/dist/DropdownMenu.js.map +1 -0
  74. package/dist/EmptyState.d.ts +13 -0
  75. package/dist/EmptyState.js +55 -0
  76. package/dist/EmptyState.js.map +1 -0
  77. package/dist/FieldMessage.d.ts +26 -0
  78. package/dist/FieldMessage.js +95 -0
  79. package/dist/FieldMessage.js.map +1 -0
  80. package/dist/FileUpload.d.ts +32 -0
  81. package/dist/FileUpload.js +253 -0
  82. package/dist/FileUpload.js.map +1 -0
  83. package/dist/Floating.d.ts +32 -0
  84. package/dist/Floating.js +95 -0
  85. package/dist/Floating.js.map +1 -0
  86. package/dist/HoverCard.d.ts +18 -0
  87. package/dist/HoverCard.js +90 -0
  88. package/dist/HoverCard.js.map +1 -0
  89. package/dist/Input.d.ts +17 -0
  90. package/dist/Input.js +96 -0
  91. package/dist/Input.js.map +1 -0
  92. package/dist/Kbd.d.ts +9 -0
  93. package/dist/Kbd.js +24 -0
  94. package/dist/Kbd.js.map +1 -0
  95. package/dist/Modal.d.ts +15 -0
  96. package/dist/Modal.js +109 -0
  97. package/dist/Modal.js.map +1 -0
  98. package/dist/NumberInput.d.ts +23 -0
  99. package/dist/NumberInput.js +162 -0
  100. package/dist/NumberInput.js.map +1 -0
  101. package/dist/OtpInput.d.ts +26 -0
  102. package/dist/OtpInput.js +173 -0
  103. package/dist/OtpInput.js.map +1 -0
  104. package/dist/Pagination.d.ts +11 -0
  105. package/dist/Pagination.js +148 -0
  106. package/dist/Pagination.js.map +1 -0
  107. package/dist/Popover.d.ts +13 -0
  108. package/dist/Popover.js +98 -0
  109. package/dist/Popover.js.map +1 -0
  110. package/dist/Progress.d.ts +12 -0
  111. package/dist/Progress.js +61 -0
  112. package/dist/Progress.js.map +1 -0
  113. package/dist/QuoteCard.d.ts +13 -0
  114. package/dist/QuoteCard.js +92 -0
  115. package/dist/QuoteCard.js.map +1 -0
  116. package/dist/Radio.d.ts +19 -0
  117. package/dist/Radio.js +161 -0
  118. package/dist/Radio.js.map +1 -0
  119. package/dist/RadioCards.d.ts +27 -0
  120. package/dist/RadioCards.js +205 -0
  121. package/dist/RadioCards.js.map +1 -0
  122. package/dist/ReactionBar.d.ts +15 -0
  123. package/dist/ReactionBar.js +92 -0
  124. package/dist/ReactionBar.js.map +1 -0
  125. package/dist/ReadingProgress.d.ts +8 -0
  126. package/dist/ReadingProgress.js +51 -0
  127. package/dist/ReadingProgress.js.map +1 -0
  128. package/dist/ReadingTime.d.ts +9 -0
  129. package/dist/ReadingTime.js +39 -0
  130. package/dist/ReadingTime.js.map +1 -0
  131. package/dist/Select.d.ts +20 -0
  132. package/dist/Select.js +244 -0
  133. package/dist/Select.js.map +1 -0
  134. package/dist/Separator.d.ts +14 -0
  135. package/dist/Separator.js +33 -0
  136. package/dist/Separator.js.map +1 -0
  137. package/dist/ShareButtons.d.ts +10 -0
  138. package/dist/ShareButtons.js +107 -0
  139. package/dist/ShareButtons.js.map +1 -0
  140. package/dist/Sidebar.d.ts +30 -0
  141. package/dist/Sidebar.js +140 -0
  142. package/dist/Sidebar.js.map +1 -0
  143. package/dist/Skeleton.d.ts +15 -0
  144. package/dist/Skeleton.js +67 -0
  145. package/dist/Skeleton.js.map +1 -0
  146. package/dist/Slider.d.ts +27 -0
  147. package/dist/Slider.js +182 -0
  148. package/dist/Slider.js.map +1 -0
  149. package/dist/Spinner.d.ts +12 -0
  150. package/dist/Spinner.js +24 -0
  151. package/dist/Spinner.js.map +1 -0
  152. package/dist/SpotlightCard.d.ts +17 -0
  153. package/dist/SpotlightCard.js +87 -0
  154. package/dist/SpotlightCard.js.map +1 -0
  155. package/dist/Stat.d.ts +19 -0
  156. package/dist/Stat.js +71 -0
  157. package/dist/Stat.js.map +1 -0
  158. package/dist/StatCard.d.ts +24 -0
  159. package/dist/StatCard.js +212 -0
  160. package/dist/StatCard.js.map +1 -0
  161. package/dist/Steps.d.ts +14 -0
  162. package/dist/Steps.js +94 -0
  163. package/dist/Steps.js.map +1 -0
  164. package/dist/Table.d.ts +21 -0
  165. package/dist/Table.js +93 -0
  166. package/dist/Table.js.map +1 -0
  167. package/dist/TableOfContents.d.ts +17 -0
  168. package/dist/TableOfContents.js +92 -0
  169. package/dist/TableOfContents.js.map +1 -0
  170. package/dist/Tabs.d.ts +17 -0
  171. package/dist/Tabs.js +123 -0
  172. package/dist/Tabs.js.map +1 -0
  173. package/dist/TagInput.d.ts +16 -0
  174. package/dist/TagInput.js +169 -0
  175. package/dist/TagInput.js.map +1 -0
  176. package/dist/Textarea.d.ts +13 -0
  177. package/dist/Textarea.js +74 -0
  178. package/dist/Textarea.js.map +1 -0
  179. package/dist/TimePicker.d.ts +28 -0
  180. package/dist/TimePicker.js +264 -0
  181. package/dist/TimePicker.js.map +1 -0
  182. package/dist/Timeline.d.ts +19 -0
  183. package/dist/Timeline.js +76 -0
  184. package/dist/Timeline.js.map +1 -0
  185. package/dist/Toast.d.ts +18 -0
  186. package/dist/Toast.js +318 -0
  187. package/dist/Toast.js.map +1 -0
  188. package/dist/Toggle.d.ts +13 -0
  189. package/dist/Toggle.js +84 -0
  190. package/dist/Toggle.js.map +1 -0
  191. package/dist/Tooltip.d.ts +13 -0
  192. package/dist/Tooltip.js +138 -0
  193. package/dist/Tooltip.js.map +1 -0
  194. package/dist/cli.js +1644 -0
  195. package/dist/index.d.ts +68 -631
  196. package/dist/index.js +67 -5558
  197. package/dist/index.js.map +1 -1
  198. package/dist/registry/Accordion.tsx +140 -0
  199. package/dist/registry/Alert.tsx +95 -0
  200. package/dist/registry/AuthorCard.tsx +67 -0
  201. package/dist/registry/Avatar.tsx +116 -0
  202. package/dist/registry/Badge.tsx +55 -0
  203. package/dist/registry/Banner.tsx +86 -0
  204. package/dist/registry/Breadcrumb.tsx +60 -0
  205. package/dist/registry/Button.tsx +361 -0
  206. package/dist/registry/ButtonGroup.tsx +82 -0
  207. package/dist/registry/Card.tsx +241 -0
  208. package/dist/registry/CardShell.tsx +78 -0
  209. package/dist/registry/Carousel.tsx +296 -0
  210. package/dist/registry/Checkbox.tsx +104 -0
  211. package/dist/registry/CloseButton.tsx +68 -0
  212. package/dist/registry/CodeBlock.tsx +202 -0
  213. package/dist/registry/ColorPicker.tsx +404 -0
  214. package/dist/registry/Combobox.tsx +318 -0
  215. package/dist/registry/CommandPalette.tsx +356 -0
  216. package/dist/registry/ContextMenu.tsx +241 -0
  217. package/dist/registry/DataList.tsx +49 -0
  218. package/dist/registry/DatePicker.tsx +494 -0
  219. package/dist/registry/DateRangePicker.tsx +232 -0
  220. package/dist/registry/Drawer.tsx +166 -0
  221. package/dist/registry/DropdownMenu.tsx +219 -0
  222. package/dist/registry/EmptyState.tsx +55 -0
  223. package/dist/registry/FieldMessage.tsx +123 -0
  224. package/dist/registry/FileUpload.tsx +318 -0
  225. package/dist/registry/Floating.tsx +134 -0
  226. package/dist/registry/HoverCard.tsx +113 -0
  227. package/dist/registry/Input.tsx +108 -0
  228. package/dist/registry/Kbd.tsx +23 -0
  229. package/dist/registry/Modal.tsx +130 -0
  230. package/dist/registry/NumberInput.tsx +183 -0
  231. package/dist/registry/OtpInput.tsx +219 -0
  232. package/dist/registry/Pagination.tsx +160 -0
  233. package/dist/registry/Popover.tsx +116 -0
  234. package/dist/registry/Progress.tsx +66 -0
  235. package/dist/registry/QuoteCard.tsx +93 -0
  236. package/dist/registry/Radio.tsx +205 -0
  237. package/dist/registry/RadioCards.tsx +247 -0
  238. package/dist/registry/ReactionBar.tsx +101 -0
  239. package/dist/registry/ReadingProgress.tsx +51 -0
  240. package/dist/registry/ReadingTime.tsx +35 -0
  241. package/dist/registry/Select.tsx +265 -0
  242. package/dist/registry/Separator.tsx +52 -0
  243. package/dist/registry/ShareButtons.tsx +134 -0
  244. package/dist/registry/Sidebar.tsx +182 -0
  245. package/dist/registry/Skeleton.tsx +83 -0
  246. package/dist/registry/Slider.tsx +229 -0
  247. package/dist/registry/Spinner.tsx +32 -0
  248. package/dist/registry/SpotlightCard.tsx +106 -0
  249. package/dist/registry/Stat.tsx +82 -0
  250. package/dist/registry/StatCard.tsx +256 -0
  251. package/dist/registry/Steps.tsx +109 -0
  252. package/dist/registry/Table.tsx +123 -0
  253. package/dist/registry/TableOfContents.tsx +116 -0
  254. package/dist/registry/Tabs.tsx +136 -0
  255. package/dist/registry/TagInput.tsx +191 -0
  256. package/dist/registry/Textarea.tsx +84 -0
  257. package/dist/registry/TimePicker.tsx +315 -0
  258. package/dist/registry/Timeline.tsx +102 -0
  259. package/dist/registry/Toast.tsx +388 -0
  260. package/dist/registry/Toggle.tsx +96 -0
  261. package/dist/registry/Tooltip.tsx +160 -0
  262. package/dist/registry/styles.css +1207 -0
  263. package/dist/registry/useFloatingPanel.ts +112 -0
  264. package/dist/registry/useModalBehavior.ts +77 -0
  265. package/dist/styles.css +868 -62
  266. package/dist/useFloatingPanel.d.ts +24 -0
  267. package/dist/useFloatingPanel.js +89 -0
  268. package/dist/useFloatingPanel.js.map +1 -0
  269. package/dist/useModalBehavior.d.ts +15 -0
  270. package/dist/useModalBehavior.js +57 -0
  271. package/dist/useModalBehavior.js.map +1 -0
  272. package/package.json +23 -6
@@ -0,0 +1,55 @@
1
+ // ../../components/ui/EmptyState.tsx
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ function EmptyState({
4
+ icon,
5
+ title,
6
+ description,
7
+ action,
8
+ className = ""
9
+ }) {
10
+ return /* @__PURE__ */ jsx(
11
+ "div",
12
+ {
13
+ className: ["rounded-[20px] p-1.5", className].filter(Boolean).join(" "),
14
+ style: { background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" },
15
+ children: /* @__PURE__ */ jsxs(
16
+ "div",
17
+ {
18
+ className: "relative bg-panel-face rounded-[15px] overflow-hidden flex flex-col items-center justify-center text-center py-14 px-6",
19
+ style: { boxShadow: "var(--panel-face-shadow)" },
20
+ children: [
21
+ /* @__PURE__ */ jsx(
22
+ "div",
23
+ {
24
+ "aria-hidden": true,
25
+ className: "pointer-events-none absolute inset-0",
26
+ style: {
27
+ backgroundImage: "radial-gradient(var(--dots) 1px, transparent 1px)",
28
+ backgroundSize: "14px 14px",
29
+ maskImage: "radial-gradient(60% 70% at 50% 40%, black 0%, transparent 100%)",
30
+ WebkitMaskImage: "radial-gradient(60% 70% at 50% 40%, black 0%, transparent 100%)"
31
+ }
32
+ }
33
+ ),
34
+ icon && /* @__PURE__ */ jsx(
35
+ "div",
36
+ {
37
+ "aria-hidden": true,
38
+ className: "relative w-12 h-12 rounded-[14px] flex items-center justify-center text-ink-muted mb-5",
39
+ style: { background: "var(--face)", boxShadow: "var(--face-shadow), var(--face-drop)" },
40
+ children: icon
41
+ }
42
+ ),
43
+ /* @__PURE__ */ jsx("p", { className: "relative text-[15px] font-semibold text-ink tracking-[-0.015em]", children: title }),
44
+ description && /* @__PURE__ */ jsx("p", { className: "relative text-[13px] text-ink-muted mt-1.5 max-w-xs leading-relaxed", children: description }),
45
+ action && /* @__PURE__ */ jsx("div", { className: "relative mt-6", children: action })
46
+ ]
47
+ }
48
+ )
49
+ }
50
+ );
51
+ }
52
+ export {
53
+ EmptyState
54
+ };
55
+ //# sourceMappingURL=EmptyState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../components/ui/EmptyState.tsx"],"sourcesContent":["import React from \"react\";\n\ninterface EmptyStateProps {\n icon?: React.ReactNode;\n title: string;\n description?: string;\n action?: React.ReactNode;\n className?: string;\n}\n\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n className = \"\",\n}: EmptyStateProps) {\n return (\n <div\n className={[\"rounded-[20px] p-1.5\", className].filter(Boolean).join(\" \")}\n style={{ background: \"var(--panel-bezel)\", boxShadow: \"var(--panel-shadow)\" }}\n >\n <div\n className=\"relative bg-panel-face rounded-[15px] overflow-hidden flex flex-col items-center justify-center text-center py-14 px-6\"\n style={{ boxShadow: \"var(--panel-face-shadow)\" }}\n >\n {/* A faint dotted floor: the space is empty, not broken */}\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-0\"\n style={{\n backgroundImage: \"radial-gradient(var(--dots) 1px, transparent 1px)\",\n backgroundSize: \"14px 14px\",\n maskImage: \"radial-gradient(60% 70% at 50% 40%, black 0%, transparent 100%)\",\n WebkitMaskImage: \"radial-gradient(60% 70% at 50% 40%, black 0%, transparent 100%)\",\n }}\n />\n {icon && (\n <div\n aria-hidden\n className=\"relative w-12 h-12 rounded-[14px] flex items-center justify-center text-ink-muted mb-5\"\n style={{ background: \"var(--face)\", boxShadow: \"var(--face-shadow), var(--face-drop)\" }}\n >\n {icon}\n </div>\n )}\n <p className=\"relative text-[15px] font-semibold text-ink tracking-[-0.015em]\">{title}</p>\n {description && (\n <p className=\"relative text-[13px] text-ink-muted mt-1.5 max-w-xs leading-relaxed\">{description}</p>\n )}\n {action && <div className=\"relative mt-6\">{action}</div>}\n </div>\n </div>\n );\n}\n"],"mappings":";AAsBM,SAKE,KALF;AAZC,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAAoB;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,CAAC,wBAAwB,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,MACvE,OAAO,EAAE,YAAY,sBAAsB,WAAW,sBAAsB;AAAA,MAE5E;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,WAAW,2BAA2B;AAAA,UAG/C;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,eAAW;AAAA,gBACX,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,iBAAiB;AAAA,kBACjB,gBAAgB;AAAA,kBAChB,WAAW;AAAA,kBACX,iBAAiB;AAAA,gBACnB;AAAA;AAAA,YACF;AAAA,YACC,QACC;AAAA,cAAC;AAAA;AAAA,gBACC,eAAW;AAAA,gBACX,WAAU;AAAA,gBACV,OAAO,EAAE,YAAY,eAAe,WAAW,uCAAuC;AAAA,gBAErF;AAAA;AAAA,YACH;AAAA,YAEF,oBAAC,OAAE,WAAU,mEAAmE,iBAAM;AAAA,YACrF,eACC,oBAAC,OAAE,WAAU,uEAAuE,uBAAY;AAAA,YAEjG,UAAU,oBAAC,SAAI,WAAU,iBAAiB,kBAAO;AAAA;AAAA;AAAA,MACpD;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,26 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+
4
+ declare function FieldLabel({ id, htmlFor, children, }: {
5
+ id?: string;
6
+ htmlFor?: string;
7
+ children: React.ReactNode;
8
+ }): react_jsx_runtime.JSX.Element;
9
+ declare function FieldMessage({ id, error, hint }: {
10
+ id?: string;
11
+ error?: string;
12
+ hint?: string;
13
+ }): react_jsx_runtime.JSX.Element | null;
14
+ declare function FieldKey({ label, disabled, onClick, onPress, onRelease, focusable, size, children, }: {
15
+ label: string;
16
+ disabled?: boolean;
17
+ onClick?: () => void;
18
+ /** Pointer went down (for hold-to-repeat) */
19
+ onPress?: () => void;
20
+ onRelease?: () => void;
21
+ focusable?: boolean;
22
+ size?: number;
23
+ children: React.ReactNode;
24
+ }): react_jsx_runtime.JSX.Element;
25
+
26
+ export { FieldKey, FieldLabel, FieldMessage };
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ // ../../components/ui/FieldMessage.tsx
4
+ import React from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ function FieldLabel({
7
+ id,
8
+ htmlFor,
9
+ children
10
+ }) {
11
+ const className = "text-[12.5px] font-medium text-ink-label tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-ink-strong";
12
+ return htmlFor ? /* @__PURE__ */ jsx("label", { id, htmlFor, className, children }) : /* @__PURE__ */ jsx("span", { id, className, children });
13
+ }
14
+ function FieldMessage({ id, error, hint }) {
15
+ if (error) {
16
+ return /* @__PURE__ */ jsxs("p", { id, role: "alert", className: "text-[11.5px] text-danger font-medium flex items-center gap-1 mt-0.5", children: [
17
+ /* @__PURE__ */ jsxs("svg", { "aria-hidden": true, width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: [
18
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
19
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M12 8v4m0 4h.01" })
20
+ ] }),
21
+ error
22
+ ] });
23
+ }
24
+ if (hint) {
25
+ return /* @__PURE__ */ jsx("p", { id, className: "text-[11.5px] text-ink-muted mt-0.5", children: hint });
26
+ }
27
+ return null;
28
+ }
29
+ function FieldKey({
30
+ label,
31
+ disabled,
32
+ onClick,
33
+ onPress,
34
+ onRelease,
35
+ focusable = true,
36
+ size = 28,
37
+ children
38
+ }) {
39
+ const [pressed, setPressed] = React.useState(false);
40
+ const release = () => {
41
+ setPressed(false);
42
+ onRelease?.();
43
+ };
44
+ return /* @__PURE__ */ jsx(
45
+ "button",
46
+ {
47
+ type: "button",
48
+ "aria-label": label,
49
+ tabIndex: focusable ? void 0 : -1,
50
+ disabled,
51
+ onClick,
52
+ onPointerDown: (e) => {
53
+ if (e.button !== 0) return;
54
+ if (!focusable) e.preventDefault();
55
+ setPressed(true);
56
+ onPress?.();
57
+ },
58
+ onPointerUp: release,
59
+ onPointerLeave: () => pressed && release(),
60
+ onPointerCancel: release,
61
+ className: "grid shrink-0 cursor-pointer place-items-center rounded-[9px] text-ink-muted transition-[box-shadow,color,opacity] duration-150 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ink disabled:opacity-35 disabled:pointer-events-none",
62
+ style: {
63
+ width: size,
64
+ height: size,
65
+ background: "var(--face)",
66
+ boxShadow: pressed ? "var(--face-pressed)" : "var(--key-shadow)"
67
+ },
68
+ children: /* @__PURE__ */ jsx(
69
+ "svg",
70
+ {
71
+ "aria-hidden": true,
72
+ width: "12",
73
+ height: "12",
74
+ fill: "none",
75
+ stroke: "currentColor",
76
+ strokeWidth: "2.25",
77
+ strokeLinecap: "round",
78
+ strokeLinejoin: "round",
79
+ viewBox: "0 0 24 24",
80
+ style: {
81
+ transform: pressed ? "translateY(0.5px) scale(0.92)" : "none",
82
+ transition: "transform 150ms var(--ease-spring)"
83
+ },
84
+ children
85
+ }
86
+ )
87
+ }
88
+ );
89
+ }
90
+ export {
91
+ FieldKey,
92
+ FieldLabel,
93
+ FieldMessage
94
+ };
95
+ //# sourceMappingURL=FieldMessage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../components/ui/FieldMessage.tsx"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\n\n/* The label above a field and the line under it, shared by every form\n control so they read as one family. */\n\nexport function FieldLabel({\n id,\n htmlFor,\n children,\n}: {\n id?: string;\n htmlFor?: string;\n children: React.ReactNode;\n}) {\n const className =\n \"text-[12.5px] font-medium text-ink-label tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-ink-strong\";\n return htmlFor ? (\n <label id={id} htmlFor={htmlFor} className={className}>\n {children}\n </label>\n ) : (\n <span id={id} className={className}>\n {children}\n </span>\n );\n}\n\n/* An error wins over a hint; either is announced through `id` */\nexport function FieldMessage({ id, error, hint }: { id?: string; error?: string; hint?: string }) {\n if (error) {\n return (\n <p id={id} role=\"alert\" className=\"text-[11.5px] text-danger font-medium flex items-center gap-1 mt-0.5\">\n <svg aria-hidden width=\"11\" height=\"11\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path strokeLinecap=\"round\" d=\"M12 8v4m0 4h.01\" />\n </svg>\n {error}\n </p>\n );\n }\n if (hint) {\n return (\n <p id={id} className=\"text-[11.5px] text-ink-muted mt-0.5\">\n {hint}\n </p>\n );\n }\n return null;\n}\n\n/* A small raised key that sits inside a field or a panel: steppers,\n month arrows, clear buttons. It sinks flush while held. */\nexport function FieldKey({\n label,\n disabled,\n onClick,\n onPress,\n onRelease,\n focusable = true,\n size = 28,\n children,\n}: {\n label: string;\n disabled?: boolean;\n onClick?: () => void;\n /** Pointer went down (for hold-to-repeat) */\n onPress?: () => void;\n onRelease?: () => void;\n focusable?: boolean;\n size?: number;\n children: React.ReactNode;\n}) {\n const [pressed, setPressed] = React.useState(false);\n const release = () => {\n setPressed(false);\n onRelease?.();\n };\n return (\n <button\n type=\"button\"\n aria-label={label}\n tabIndex={focusable ? undefined : -1}\n disabled={disabled}\n onClick={onClick}\n onPointerDown={(e) => {\n if (e.button !== 0) return;\n if (!focusable) e.preventDefault(); // keep focus in the field\n setPressed(true);\n onPress?.();\n }}\n onPointerUp={release}\n onPointerLeave={() => pressed && release()}\n onPointerCancel={release}\n className=\"grid shrink-0 cursor-pointer place-items-center rounded-[9px] text-ink-muted transition-[box-shadow,color,opacity] duration-150 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ink disabled:opacity-35 disabled:pointer-events-none\"\n style={{\n width: size,\n height: size,\n background: \"var(--face)\",\n boxShadow: pressed ? \"var(--face-pressed)\" : \"var(--key-shadow)\",\n }}\n >\n <svg\n aria-hidden\n width=\"12\"\n height=\"12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2.25\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n viewBox=\"0 0 24 24\"\n style={{\n transform: pressed ? \"translateY(0.5px) scale(0.92)\" : \"none\",\n transition: \"transform 150ms var(--ease-spring)\",\n }}\n >\n {children}\n </svg>\n </button>\n );\n}\n"],"mappings":";;;AAEA,OAAO,WAAW;AAiBd,cAeI,YAfJ;AAZG,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,YACJ;AACF,SAAO,UACL,oBAAC,WAAM,IAAQ,SAAkB,WAC9B,UACH,IAEA,oBAAC,UAAK,IAAQ,WACX,UACH;AAEJ;AAGO,SAAS,aAAa,EAAE,IAAI,OAAO,KAAK,GAAmD;AAChG,MAAI,OAAO;AACT,WACE,qBAAC,OAAE,IAAQ,MAAK,SAAQ,WAAU,wEAChC;AAAA,2BAAC,SAAI,eAAW,MAAC,OAAM,MAAK,QAAO,MAAK,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,SAAQ,aAChG;AAAA,4BAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK;AAAA,QAC/B,oBAAC,UAAK,eAAc,SAAQ,GAAE,mBAAkB;AAAA,SAClD;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AACA,MAAI,MAAM;AACR,WACE,oBAAC,OAAE,IAAQ,WAAU,uCAClB,gBACH;AAAA,EAEJ;AACA,SAAO;AACT;AAIO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,OAAO;AAAA,EACP;AACF,GAUG;AACD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,UAAU,MAAM;AACpB,eAAW,KAAK;AAChB,gBAAY;AAAA,EACd;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,UAAU,YAAY,SAAY;AAAA,MAClC;AAAA,MACA;AAAA,MACA,eAAe,CAAC,MAAM;AACpB,YAAI,EAAE,WAAW,EAAG;AACpB,YAAI,CAAC,UAAW,GAAE,eAAe;AACjC,mBAAW,IAAI;AACf,kBAAU;AAAA,MACZ;AAAA,MACA,aAAa;AAAA,MACb,gBAAgB,MAAM,WAAW,QAAQ;AAAA,MACzC,iBAAiB;AAAA,MACjB,WAAU;AAAA,MACV,OAAO;AAAA,QACL,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,WAAW,UAAU,wBAAwB;AAAA,MAC/C;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,eAAW;AAAA,UACX,OAAM;AAAA,UACN,QAAO;AAAA,UACP,MAAK;AAAA,UACL,QAAO;AAAA,UACP,aAAY;AAAA,UACZ,eAAc;AAAA,UACd,gBAAe;AAAA,UACf,SAAQ;AAAA,UACR,OAAO;AAAA,YACL,WAAW,UAAU,kCAAkC;AAAA,YACvD,YAAY;AAAA,UACd;AAAA,UAEC;AAAA;AAAA,MACH;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,32 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface UploadItem {
4
+ id: string;
5
+ file: File;
6
+ /** 0–100 while uploading */
7
+ progress: number;
8
+ status: "ready" | "uploading" | "done" | "error";
9
+ error?: string;
10
+ }
11
+ interface FileUploadProps {
12
+ /** Same as the input's accept: "image/*", ".pdf,.docx" */
13
+ accept?: string;
14
+ multiple?: boolean;
15
+ /** In bytes; bigger files land with an error instead of uploading */
16
+ maxSize?: number;
17
+ maxFiles?: number;
18
+ /** Upload each file as it lands. Call onProgress with 0–100; throw to fail it */
19
+ upload?: (file: File, onProgress: (pct: number) => void) => Promise<void>;
20
+ /** Every change to the list: added, progressed, finished, removed */
21
+ onChange?: (items: UploadItem[]) => void;
22
+ label?: string;
23
+ hint?: string;
24
+ /** The words in the empty tray */
25
+ prompt?: string;
26
+ disabled?: boolean;
27
+ className?: string;
28
+ }
29
+ declare function formatBytes(n: number): string;
30
+ declare function FileUpload({ accept, multiple, maxSize, maxFiles, upload, onChange, label, hint, prompt, disabled, className, }: FileUploadProps): react_jsx_runtime.JSX.Element;
31
+
32
+ export { FileUpload, type UploadItem, formatBytes };
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ // ../../components/ui/FileUpload.tsx
4
+ import { useEffect, useId, useRef, useState } from "react";
5
+ import { Button } from "./Button.js";
6
+ import { CloseButton } from "./CloseButton.js";
7
+ import { FieldLabel, FieldMessage } from "./FieldMessage.js";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ function formatBytes(n) {
10
+ if (n < 1024) return `${n} b`;
11
+ const units = ["kb", "mb", "gb"];
12
+ let i = -1;
13
+ do {
14
+ n /= 1024;
15
+ i++;
16
+ } while (n >= 1024 && i < units.length - 1);
17
+ return `${n.toFixed(n < 10 ? 1 : 0)} ${units[i]}`;
18
+ }
19
+ function accepts(file, accept) {
20
+ if (!accept) return true;
21
+ return accept.split(",").some((rule) => {
22
+ const r = rule.trim().toLowerCase();
23
+ if (r.startsWith(".")) return file.name.toLowerCase().endsWith(r);
24
+ if (r.endsWith("/*")) return file.type.startsWith(r.slice(0, -1));
25
+ return file.type === r;
26
+ });
27
+ }
28
+ var seq = 0;
29
+ function FileUpload({
30
+ accept,
31
+ multiple = true,
32
+ maxSize,
33
+ maxFiles,
34
+ upload,
35
+ onChange,
36
+ label,
37
+ hint,
38
+ prompt,
39
+ disabled = false,
40
+ className = ""
41
+ }) {
42
+ const uid = useId();
43
+ const inputRef = useRef(null);
44
+ const [items, setItems] = useState([]);
45
+ const [previews, setPreviews] = useState({});
46
+ const [over, setOver] = useState(false);
47
+ const [notice, setNotice] = useState(null);
48
+ const depth = useRef(0);
49
+ const report = useRef(onChange);
50
+ useEffect(() => {
51
+ report.current = onChange;
52
+ });
53
+ useEffect(() => {
54
+ report.current?.(items);
55
+ }, [items]);
56
+ const urls = useRef(previews);
57
+ useEffect(() => {
58
+ urls.current = previews;
59
+ });
60
+ useEffect(() => () => Object.values(urls.current).forEach(URL.revokeObjectURL), []);
61
+ const patch = (id, p) => setItems((list) => list.map((it) => it.id === id ? { ...it, ...p } : it));
62
+ const start = (item) => {
63
+ if (!upload) return;
64
+ patch(item.id, { status: "uploading", progress: 0 });
65
+ upload(item.file, (pct) => patch(item.id, { progress: Math.max(0, Math.min(100, pct)) })).then(() => patch(item.id, { status: "done", progress: 100 })).catch(
66
+ (err) => patch(item.id, { status: "error", error: err instanceof Error ? err.message : "upload failed" })
67
+ );
68
+ };
69
+ const add = (files) => {
70
+ if (disabled) return;
71
+ let list = Array.from(files);
72
+ if (!multiple) list = list.slice(0, 1);
73
+ const room = maxFiles ? Math.max(0, maxFiles - (multiple ? items.length : 0)) : Infinity;
74
+ setNotice(list.length > room ? `only ${maxFiles} file${maxFiles === 1 ? "" : "s"} can go in` : null);
75
+ const next = list.slice(0, room).map((file) => {
76
+ const id = `${uid}-${++seq}`;
77
+ if (!accepts(file, accept)) return { id, file, progress: 0, status: "error", error: "not a type this takes" };
78
+ if (maxSize && file.size > maxSize)
79
+ return { id, file, progress: 0, status: "error", error: `over ${formatBytes(maxSize)}` };
80
+ return { id, file, progress: 0, status: "ready" };
81
+ });
82
+ const shots = {};
83
+ next.forEach((it) => {
84
+ if (it.file.type.startsWith("image/") && it.status !== "error") shots[it.id] = URL.createObjectURL(it.file);
85
+ });
86
+ if (!multiple) Object.values(previews).forEach(URL.revokeObjectURL);
87
+ setPreviews((p) => multiple ? { ...p, ...shots } : shots);
88
+ setItems((cur) => multiple ? [...cur, ...next] : next);
89
+ next.filter((it) => it.status === "ready").forEach(start);
90
+ };
91
+ const remove = (id) => {
92
+ if (previews[id]) URL.revokeObjectURL(previews[id]);
93
+ setPreviews((p) => {
94
+ const rest = { ...p };
95
+ delete rest[id];
96
+ return rest;
97
+ });
98
+ setItems((list) => list.filter((it) => it.id !== id));
99
+ };
100
+ const browse = () => !disabled && inputRef.current?.click();
101
+ const full = !!maxFiles && multiple && items.length >= maxFiles;
102
+ const empty = items.length === 0;
103
+ return /* @__PURE__ */ jsxs("div", { className: ["group/field flex w-full flex-col gap-1.5", className].join(" "), children: [
104
+ label && /* @__PURE__ */ jsx(FieldLabel, { id: `${uid}-label`, children: label }),
105
+ /* @__PURE__ */ jsxs(
106
+ "div",
107
+ {
108
+ role: "group",
109
+ "aria-labelledby": label ? `${uid}-label` : void 0,
110
+ "aria-describedby": hint ? `${uid}-hint` : void 0,
111
+ "data-open": over || void 0,
112
+ onDragEnter: (e) => {
113
+ e.preventDefault();
114
+ if (disabled) return;
115
+ depth.current++;
116
+ setOver(true);
117
+ },
118
+ onDragOver: (e) => e.preventDefault(),
119
+ onDragLeave: () => {
120
+ if (--depth.current <= 0) setOver(false);
121
+ },
122
+ onDrop: (e) => {
123
+ e.preventDefault();
124
+ depth.current = 0;
125
+ setOver(false);
126
+ add(e.dataTransfer.files);
127
+ },
128
+ className: [
129
+ "field relative flex flex-col rounded-[18px] p-2",
130
+ disabled ? "pointer-events-none opacity-40" : ""
131
+ ].join(" "),
132
+ children: [
133
+ empty ? /* @__PURE__ */ jsxs(
134
+ "div",
135
+ {
136
+ className: "flex cursor-pointer flex-col items-center gap-3.5 px-6 py-9 text-center",
137
+ onClick: (e) => e.target === e.currentTarget && browse(),
138
+ children: [
139
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-1", children: [
140
+ /* @__PURE__ */ jsx("span", { className: "text-[14px] font-medium text-ink tracking-[-0.01em]", children: over ? "let go to drop them in" : prompt ?? (multiple ? "drop files here" : "drop a file here") }),
141
+ hint && /* @__PURE__ */ jsx("span", { id: `${uid}-hint`, className: "text-[12px] text-ink-muted", children: hint })
142
+ ] }),
143
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: browse, disabled, children: multiple ? "choose files" : "choose a file" })
144
+ ]
145
+ }
146
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
147
+ /* @__PURE__ */ jsx("ul", { "aria-label": "files", className: "flex flex-col gap-1.5", children: items.map((it) => /* @__PURE__ */ jsx(FileChip, { item: it, preview: previews[it.id], onRemove: () => remove(it.id) }, it.id)) }),
148
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 pl-2.5 pr-0.5 pt-2.5 pb-0.5", children: [
149
+ /* @__PURE__ */ jsx("span", { id: `${uid}-hint`, className: "truncate text-[12px] text-ink-muted", children: over ? "let go to add them" : hint ?? `${items.length} file${items.length === 1 ? "" : "s"}` }),
150
+ !full && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: browse, disabled, children: multiple ? "add more" : "replace" })
151
+ ] })
152
+ ] }),
153
+ /* @__PURE__ */ jsx(
154
+ "input",
155
+ {
156
+ ref: inputRef,
157
+ type: "file",
158
+ accept,
159
+ multiple,
160
+ disabled,
161
+ tabIndex: -1,
162
+ "aria-hidden": true,
163
+ className: "sr-only",
164
+ onChange: (e) => {
165
+ if (e.target.files) add(e.target.files);
166
+ e.target.value = "";
167
+ }
168
+ }
169
+ )
170
+ ]
171
+ }
172
+ ),
173
+ notice && /* @__PURE__ */ jsx(FieldMessage, { error: notice })
174
+ ] });
175
+ }
176
+ function FileChip({ item, preview, onRemove }) {
177
+ const failed = item.status === "error";
178
+ const meta = failed ? item.error : item.status === "uploading" ? `${formatBytes(item.file.size)} \xB7 ${Math.round(item.progress)}%` : item.status === "done" ? `${formatBytes(item.file.size)} \xB7 uploaded` : formatBytes(item.file.size);
179
+ return /* @__PURE__ */ jsxs(
180
+ "li",
181
+ {
182
+ className: "relative flex items-center gap-3 overflow-hidden rounded-[12px] pt-2.5 pb-3.5 pl-2.5 pr-2",
183
+ style: {
184
+ background: "var(--face)",
185
+ boxShadow: "var(--thumb-shadow)",
186
+ animation: "select-option-in 320ms var(--ease-out-soft) both"
187
+ },
188
+ children: [
189
+ preview && // eslint-disable-next-line @next/next/no-img-element -- a local object URL, nothing to optimise
190
+ /* @__PURE__ */ jsx(
191
+ "img",
192
+ {
193
+ src: preview,
194
+ alt: "",
195
+ className: "h-9 w-9 shrink-0 rounded-[8px] object-cover",
196
+ style: { boxShadow: "inset 0 0 0 1px var(--hairline), var(--key-shadow)" }
197
+ }
198
+ ),
199
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
200
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-ink", children: item.file.name }),
201
+ /* @__PURE__ */ jsxs(
202
+ "span",
203
+ {
204
+ className: [
205
+ "flex items-center gap-1 truncate text-[11.5px] tabular-nums",
206
+ failed ? "text-danger" : "text-ink-muted"
207
+ ].join(" "),
208
+ children: [
209
+ item.status === "done" && /* @__PURE__ */ jsx("svg", { "aria-hidden": true, width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "2.75", viewBox: "0 0 24 24", className: "text-success", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }),
210
+ meta
211
+ ]
212
+ }
213
+ )
214
+ ] }),
215
+ /* @__PURE__ */ jsx(CloseButton, { size: "sm", label: `remove ${item.file.name}`, onClick: onRemove }),
216
+ /* @__PURE__ */ jsx(
217
+ "span",
218
+ {
219
+ "aria-hidden": item.status !== "uploading",
220
+ role: item.status === "uploading" ? "progressbar" : void 0,
221
+ "aria-label": item.status === "uploading" ? `uploading ${item.file.name}` : void 0,
222
+ "aria-valuenow": item.status === "uploading" ? Math.round(item.progress) : void 0,
223
+ "aria-valuemin": 0,
224
+ "aria-valuemax": 100,
225
+ className: "absolute inset-x-2.5 bottom-[6px] h-[3px] rounded-full transition-opacity duration-500",
226
+ style: {
227
+ background: "var(--track)",
228
+ boxShadow: "var(--track-shadow)",
229
+ opacity: item.status === "uploading" ? 1 : 0,
230
+ transitionDelay: item.status === "done" ? "400ms" : "0ms"
231
+ },
232
+ children: /* @__PURE__ */ jsx(
233
+ "span",
234
+ {
235
+ className: "block h-full rounded-full",
236
+ style: {
237
+ width: `${item.progress}%`,
238
+ background: "var(--face-inverse)",
239
+ transition: "width 300ms var(--ease-out-soft)"
240
+ }
241
+ }
242
+ )
243
+ }
244
+ )
245
+ ]
246
+ }
247
+ );
248
+ }
249
+ export {
250
+ FileUpload,
251
+ formatBytes
252
+ };
253
+ //# sourceMappingURL=FileUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../components/ui/FileUpload.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"./Button\";\nimport { CloseButton } from \"./CloseButton\";\nimport { FieldLabel, FieldMessage } from \"./FieldMessage\";\n\nexport interface UploadItem {\n id: string;\n file: File;\n /** 0–100 while uploading */\n progress: number;\n status: \"ready\" | \"uploading\" | \"done\" | \"error\";\n error?: string;\n}\n\ninterface FileUploadProps {\n /** Same as the input's accept: \"image/*\", \".pdf,.docx\" */\n accept?: string;\n multiple?: boolean;\n /** In bytes; bigger files land with an error instead of uploading */\n maxSize?: number;\n maxFiles?: number;\n /** Upload each file as it lands. Call onProgress with 0–100; throw to fail it */\n upload?: (file: File, onProgress: (pct: number) => void) => Promise<void>;\n /** Every change to the list: added, progressed, finished, removed */\n onChange?: (items: UploadItem[]) => void;\n label?: string;\n hint?: string;\n /** The words in the empty tray */\n prompt?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport function formatBytes(n: number) {\n if (n < 1024) return `${n} b`;\n const units = [\"kb\", \"mb\", \"gb\"];\n let i = -1;\n do {\n n /= 1024;\n i++;\n } while (n >= 1024 && i < units.length - 1);\n return `${n.toFixed(n < 10 ? 1 : 0)} ${units[i]}`;\n}\n\n/* Matches a file against an accept string the way the browser's picker does */\nfunction accepts(file: File, accept?: string) {\n if (!accept) return true;\n return accept.split(\",\").some((rule) => {\n const r = rule.trim().toLowerCase();\n if (r.startsWith(\".\")) return file.name.toLowerCase().endsWith(r);\n if (r.endsWith(\"/*\")) return file.type.startsWith(r.slice(0, -1));\n return file.type === r;\n });\n}\n\nlet seq = 0;\n\n/* Something you fill, so it's built as a field: a wide sunken tray. Drag\n files over it and it lifts flush and draws its border, the way an\n Input does when it takes focus. What you drop lands inside it as\n raised chips; each has a groove along its foot that fills while it\n uploads, then fades once it's done. */\nexport function FileUpload({\n accept,\n multiple = true,\n maxSize,\n maxFiles,\n upload,\n onChange,\n label,\n hint,\n prompt,\n disabled = false,\n className = \"\",\n}: FileUploadProps) {\n const uid = useId();\n const inputRef = useRef<HTMLInputElement>(null);\n const [items, setItems] = useState<UploadItem[]>([]);\n const [previews, setPreviews] = useState<Record<string, string>>({});\n const [over, setOver] = useState(false);\n const [notice, setNotice] = useState<string | null>(null);\n const depth = useRef(0);\n\n // Report every change to the list, after it renders\n const report = useRef(onChange);\n useEffect(() => {\n report.current = onChange;\n });\n useEffect(() => {\n report.current?.(items);\n }, [items]);\n\n // Image previews are object URLs; give them back when they're gone\n const urls = useRef(previews);\n useEffect(() => {\n urls.current = previews;\n });\n useEffect(() => () => Object.values(urls.current).forEach(URL.revokeObjectURL), []);\n\n const patch = (id: string, p: Partial<UploadItem>) =>\n setItems((list) => list.map((it) => (it.id === id ? { ...it, ...p } : it)));\n\n const start = (item: UploadItem) => {\n if (!upload) return;\n patch(item.id, { status: \"uploading\", progress: 0 });\n upload(item.file, (pct) => patch(item.id, { progress: Math.max(0, Math.min(100, pct)) }))\n .then(() => patch(item.id, { status: \"done\", progress: 100 }))\n .catch((err: unknown) =>\n patch(item.id, { status: \"error\", error: err instanceof Error ? err.message : \"upload failed\" }),\n );\n };\n\n const add = (files: FileList | File[]) => {\n if (disabled) return;\n let list = Array.from(files);\n if (!multiple) list = list.slice(0, 1);\n const room = maxFiles ? Math.max(0, maxFiles - (multiple ? items.length : 0)) : Infinity;\n setNotice(list.length > room ? `only ${maxFiles} file${maxFiles === 1 ? \"\" : \"s\"} can go in` : null);\n const next: UploadItem[] = list.slice(0, room).map((file) => {\n const id = `${uid}-${++seq}`;\n if (!accepts(file, accept)) return { id, file, progress: 0, status: \"error\", error: \"not a type this takes\" };\n if (maxSize && file.size > maxSize)\n return { id, file, progress: 0, status: \"error\", error: `over ${formatBytes(maxSize)}` };\n return { id, file, progress: 0, status: \"ready\" };\n });\n const shots: Record<string, string> = {};\n next.forEach((it) => {\n if (it.file.type.startsWith(\"image/\") && it.status !== \"error\") shots[it.id] = URL.createObjectURL(it.file);\n });\n if (!multiple) Object.values(previews).forEach(URL.revokeObjectURL);\n setPreviews((p) => (multiple ? { ...p, ...shots } : shots));\n setItems((cur) => (multiple ? [...cur, ...next] : next));\n next.filter((it) => it.status === \"ready\").forEach(start);\n };\n\n const remove = (id: string) => {\n if (previews[id]) URL.revokeObjectURL(previews[id]);\n setPreviews((p) => {\n const rest = { ...p };\n delete rest[id];\n return rest;\n });\n setItems((list) => list.filter((it) => it.id !== id));\n };\n\n const browse = () => !disabled && inputRef.current?.click();\n const full = !!maxFiles && multiple && items.length >= maxFiles;\n const empty = items.length === 0;\n\n return (\n <div className={[\"group/field flex w-full flex-col gap-1.5\", className].join(\" \")}>\n {label && <FieldLabel id={`${uid}-label`}>{label}</FieldLabel>}\n\n {/* The tray: a field, so data-open gives it Input's focused well */}\n <div\n role=\"group\"\n aria-labelledby={label ? `${uid}-label` : undefined}\n aria-describedby={hint ? `${uid}-hint` : undefined}\n data-open={over || undefined}\n onDragEnter={(e) => {\n e.preventDefault();\n if (disabled) return;\n depth.current++;\n setOver(true);\n }}\n onDragOver={(e) => e.preventDefault()}\n onDragLeave={() => {\n // dragleave fires for every child crossed; only the last one counts\n if (--depth.current <= 0) setOver(false);\n }}\n onDrop={(e) => {\n e.preventDefault();\n depth.current = 0;\n setOver(false);\n add(e.dataTransfer.files);\n }}\n className={[\n \"field relative flex flex-col rounded-[18px] p-2\",\n disabled ? \"pointer-events-none opacity-40\" : \"\",\n ].join(\" \")}\n >\n {empty ? (\n <div\n className=\"flex cursor-pointer flex-col items-center gap-3.5 px-6 py-9 text-center\"\n onClick={(e) => e.target === e.currentTarget && browse()}\n >\n <span className=\"flex flex-col gap-1\">\n <span className=\"text-[14px] font-medium text-ink tracking-[-0.01em]\">\n {over ? \"let go to drop them in\" : prompt ?? (multiple ? \"drop files here\" : \"drop a file here\")}\n </span>\n {hint && (\n <span id={`${uid}-hint`} className=\"text-[12px] text-ink-muted\">\n {hint}\n </span>\n )}\n </span>\n <Button variant=\"secondary\" size=\"sm\" onClick={browse} disabled={disabled}>\n {multiple ? \"choose files\" : \"choose a file\"}\n </Button>\n </div>\n ) : (\n <>\n <ul aria-label=\"files\" className=\"flex flex-col gap-1.5\">\n {items.map((it) => (\n <FileChip key={it.id} item={it} preview={previews[it.id]} onRemove={() => remove(it.id)} />\n ))}\n </ul>\n <div className=\"flex items-center justify-between gap-3 pl-2.5 pr-0.5 pt-2.5 pb-0.5\">\n <span id={`${uid}-hint`} className=\"truncate text-[12px] text-ink-muted\">\n {over ? \"let go to add them\" : hint ?? `${items.length} file${items.length === 1 ? \"\" : \"s\"}`}\n </span>\n {!full && (\n <Button variant=\"secondary\" size=\"sm\" onClick={browse} disabled={disabled}>\n {multiple ? \"add more\" : \"replace\"}\n </Button>\n )}\n </div>\n </>\n )}\n\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n onChange={(e) => {\n if (e.target.files) add(e.target.files);\n e.target.value = \"\"; // so picking the same file again still fires\n }}\n />\n </div>\n\n {notice && <FieldMessage error={notice} />}\n </div>\n );\n}\n\n/* A raised chip sitting in the tray. The groove along its foot is a\n Progress track in miniature; it fills, then fades once the file is up. */\nfunction FileChip({ item, preview, onRemove }: { item: UploadItem; preview?: string; onRemove: () => void }) {\n const failed = item.status === \"error\";\n const meta = failed\n ? item.error\n : item.status === \"uploading\"\n ? `${formatBytes(item.file.size)} · ${Math.round(item.progress)}%`\n : item.status === \"done\"\n ? `${formatBytes(item.file.size)} · uploaded`\n : formatBytes(item.file.size);\n return (\n <li\n className=\"relative flex items-center gap-3 overflow-hidden rounded-[12px] pt-2.5 pb-3.5 pl-2.5 pr-2\"\n style={{\n background: \"var(--face)\",\n boxShadow: \"var(--thumb-shadow)\",\n animation: \"select-option-in 320ms var(--ease-out-soft) both\",\n }}\n >\n {preview && (\n // eslint-disable-next-line @next/next/no-img-element -- a local object URL, nothing to optimise\n <img\n src={preview}\n alt=\"\"\n className=\"h-9 w-9 shrink-0 rounded-[8px] object-cover\"\n style={{ boxShadow: \"inset 0 0 0 1px var(--hairline), var(--key-shadow)\" }}\n />\n )}\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"truncate text-[13px] font-medium text-ink\">{item.file.name}</span>\n <span\n className={[\n \"flex items-center gap-1 truncate text-[11.5px] tabular-nums\",\n failed ? \"text-danger\" : \"text-ink-muted\",\n ].join(\" \")}\n >\n {item.status === \"done\" && (\n <svg aria-hidden width=\"11\" height=\"11\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.75\" viewBox=\"0 0 24 24\" className=\"text-success\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n {meta}\n </span>\n </span>\n <CloseButton size=\"sm\" label={`remove ${item.file.name}`} onClick={onRemove} />\n\n {/* The groove along the chip's foot */}\n <span\n aria-hidden={item.status !== \"uploading\"}\n role={item.status === \"uploading\" ? \"progressbar\" : undefined}\n aria-label={item.status === \"uploading\" ? `uploading ${item.file.name}` : undefined}\n aria-valuenow={item.status === \"uploading\" ? Math.round(item.progress) : undefined}\n aria-valuemin={0}\n aria-valuemax={100}\n className=\"absolute inset-x-2.5 bottom-[6px] h-[3px] rounded-full transition-opacity duration-500\"\n style={{\n background: \"var(--track)\",\n boxShadow: \"var(--track-shadow)\",\n opacity: item.status === \"uploading\" ? 1 : 0,\n transitionDelay: item.status === \"done\" ? \"400ms\" : \"0ms\",\n }}\n >\n <span\n className=\"block h-full rounded-full\"\n style={{\n width: `${item.progress}%`,\n background: \"var(--face-inverse)\",\n transition: \"width 300ms var(--ease-out-soft)\",\n }}\n />\n </span>\n </li>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,32 @@
1
+ import React, { RefObject } from 'react';
2
+
3
+ declare function Portal({ children }: {
4
+ children: React.ReactNode;
5
+ }): React.ReactPortal | null;
6
+ declare const LAYER: {
7
+ readonly dialog: 90;
8
+ readonly toast: 95;
9
+ readonly floating: 100;
10
+ readonly tooltip: 110;
11
+ };
12
+ type Side = "top" | "bottom" | "left" | "right";
13
+ type Align = "start" | "center" | "end";
14
+ interface AnchorOptions {
15
+ open: boolean;
16
+ side?: Side;
17
+ align?: Align;
18
+ /** Space between the anchor and the panel */
19
+ gap?: number;
20
+ /** Make the panel at least as wide as the anchor (selects, comboboxes) */
21
+ matchWidth?: boolean;
22
+ }
23
+ declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null>, { open, side, align, gap, matchWidth }: AnchorOptions): {
24
+ setFloating: (el: HTMLElement | null) => void;
25
+ side: Side;
26
+ floatingRef: React.RefObject<HTMLElement | null>;
27
+ update: () => void;
28
+ };
29
+ declare const ORIGIN: Record<Side, string>;
30
+ declare const ENTER_FROM: Record<Side, string>;
31
+
32
+ export { type Align, ENTER_FROM, LAYER, ORIGIN, Portal, type Side, useAnchoredPosition };
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ // ../../components/ui/Floating.tsx
4
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ import { createPortal } from "react-dom";
6
+ var noop = () => () => {
7
+ };
8
+ function Portal({ children }) {
9
+ const client = useSyncExternalStore(noop, () => true, () => false);
10
+ return client ? createPortal(children, document.body) : null;
11
+ }
12
+ var LAYER = { dialog: 90, toast: 95, floating: 100, tooltip: 110 };
13
+ var OPPOSITE = { top: "bottom", bottom: "top", left: "right", right: "left" };
14
+ var PAD = 8;
15
+ function useAnchoredPosition(anchor, { open, side = "bottom", align = "start", gap = 8, matchWidth = false }) {
16
+ const [placed, setPlaced] = useState(side);
17
+ const floating = useRef(null);
18
+ const update = useCallback(() => {
19
+ const a = anchor.current?.getBoundingClientRect();
20
+ const el = floating.current;
21
+ if (!a || !el) return;
22
+ if (matchWidth) el.style.minWidth = `${a.width}px`;
23
+ const w = el.offsetWidth;
24
+ const h = el.offsetHeight;
25
+ const vw = document.documentElement.clientWidth;
26
+ const vh = window.innerHeight;
27
+ const room = { top: a.top, bottom: vh - a.bottom, left: a.left, right: vw - a.right };
28
+ const vertical = side === "top" || side === "bottom";
29
+ const need = (vertical ? h : w) + gap + PAD;
30
+ let s = side;
31
+ if (room[s] < need && room[OPPOSITE[s]] > room[s]) s = OPPOSITE[s];
32
+ let top;
33
+ let left;
34
+ if (s === "top" || s === "bottom") {
35
+ top = s === "bottom" ? a.bottom + gap : a.top - gap - h;
36
+ left = align === "start" ? a.left : align === "end" ? a.right - w : a.left + (a.width - w) / 2;
37
+ } else {
38
+ left = s === "right" ? a.right + gap : a.left - gap - w;
39
+ top = align === "start" ? a.top : align === "end" ? a.bottom - h : a.top + (a.height - h) / 2;
40
+ }
41
+ left = Math.max(PAD, Math.min(left, vw - w - PAD));
42
+ top = Math.max(PAD, Math.min(top, vh - h - PAD));
43
+ el.style.top = `${Math.round(top)}px`;
44
+ el.style.left = `${Math.round(left)}px`;
45
+ el.dataset.side = s;
46
+ setPlaced(s);
47
+ }, [anchor, side, align, gap, matchWidth]);
48
+ const setFloating = useCallback(
49
+ (el) => {
50
+ floating.current = el;
51
+ if (el) update();
52
+ },
53
+ [update]
54
+ );
55
+ useEffect(() => {
56
+ if (!open) return;
57
+ let frame = 0;
58
+ const schedule = () => {
59
+ cancelAnimationFrame(frame);
60
+ frame = requestAnimationFrame(update);
61
+ };
62
+ window.addEventListener("scroll", schedule, true);
63
+ window.addEventListener("resize", schedule);
64
+ const ro = new ResizeObserver(schedule);
65
+ if (floating.current) ro.observe(floating.current);
66
+ if (anchor.current) ro.observe(anchor.current);
67
+ return () => {
68
+ cancelAnimationFrame(frame);
69
+ window.removeEventListener("scroll", schedule, true);
70
+ window.removeEventListener("resize", schedule);
71
+ ro.disconnect();
72
+ };
73
+ }, [open, update, anchor]);
74
+ return { setFloating, side: placed, floatingRef: floating, update };
75
+ }
76
+ var ORIGIN = {
77
+ top: "bottom",
78
+ bottom: "top",
79
+ left: "right",
80
+ right: "left"
81
+ };
82
+ var ENTER_FROM = {
83
+ top: "translateY(4px)",
84
+ bottom: "translateY(-4px)",
85
+ left: "translateX(4px)",
86
+ right: "translateX(-4px)"
87
+ };
88
+ export {
89
+ ENTER_FROM,
90
+ LAYER,
91
+ ORIGIN,
92
+ Portal,
93
+ useAnchoredPosition
94
+ };
95
+ //# sourceMappingURL=Floating.js.map