@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,24 @@
1
+ import { RefObject, CSSProperties } from 'react';
2
+ import { Side, Align } from './Floating.js';
3
+
4
+ interface FloatingPanelOptions {
5
+ side?: Side;
6
+ align?: Align;
7
+ gap?: number;
8
+ matchWidth?: boolean;
9
+ /** Called after Escape closes the panel, to hand focus back */
10
+ onEscape?: () => void;
11
+ }
12
+ declare function useFloatingPanel(anchor: RefObject<HTMLElement | null>, options?: FloatingPanelOptions): {
13
+ open: boolean;
14
+ mounted: boolean;
15
+ entered: boolean;
16
+ show: () => void;
17
+ close: () => void;
18
+ toggle: () => void;
19
+ side: Side;
20
+ setFloating: (el: HTMLElement | null) => void;
21
+ panelStyle: CSSProperties;
22
+ };
23
+
24
+ export { useFloatingPanel };
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ // ../../components/ui/useFloatingPanel.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { ENTER_FROM, LAYER, ORIGIN, useAnchoredPosition } from "./Floating.js";
6
+ function useFloatingPanel(anchor, options = {}) {
7
+ const { side = "bottom", align = "start", gap = 8, matchWidth = false, onEscape } = options;
8
+ const [open, setOpen] = useState(false);
9
+ const [mounted, setMounted] = useState(false);
10
+ const [entered, setEntered] = useState(false);
11
+ const timer = useRef(null);
12
+ const escape = useRef(onEscape);
13
+ useEffect(() => {
14
+ escape.current = onEscape;
15
+ });
16
+ const position = useAnchoredPosition(anchor, { open: mounted, side, align, gap, matchWidth });
17
+ const { floatingRef } = position;
18
+ const close = useCallback(() => {
19
+ setOpen(false);
20
+ setEntered(false);
21
+ if (timer.current) clearTimeout(timer.current);
22
+ timer.current = setTimeout(() => setMounted(false), 200);
23
+ }, []);
24
+ const show = useCallback(() => {
25
+ if (timer.current) clearTimeout(timer.current);
26
+ setOpen(true);
27
+ setMounted(true);
28
+ requestAnimationFrame(() => requestAnimationFrame(() => setEntered(true)));
29
+ }, []);
30
+ useEffect(() => () => {
31
+ if (timer.current) clearTimeout(timer.current);
32
+ }, []);
33
+ useEffect(() => {
34
+ if (!open) return;
35
+ const onDown = (e) => {
36
+ const t = e.target;
37
+ if (anchor.current?.contains(t) || floatingRef.current?.contains(t)) return;
38
+ close();
39
+ };
40
+ const onKey = (e) => {
41
+ if (e.key === "Escape") {
42
+ e.stopPropagation();
43
+ close();
44
+ escape.current?.();
45
+ }
46
+ };
47
+ document.addEventListener("pointerdown", onDown);
48
+ document.addEventListener("keydown", onKey, true);
49
+ return () => {
50
+ document.removeEventListener("pointerdown", onDown);
51
+ document.removeEventListener("keydown", onKey, true);
52
+ };
53
+ }, [open, anchor, floatingRef, close]);
54
+ const s = position.side;
55
+ const hidden = {
56
+ bottom: "inset(-48px -48px 55% -48px)",
57
+ top: "inset(55% -48px -48px -48px)",
58
+ left: "inset(-48px -48px -48px 55%)",
59
+ right: "inset(-48px 55% -48px -48px)"
60
+ };
61
+ const panelStyle = {
62
+ position: "fixed",
63
+ top: 0,
64
+ left: 0,
65
+ zIndex: LAYER.floating,
66
+ background: "var(--panel-bezel)",
67
+ boxShadow: "var(--overlay-shadow)",
68
+ transformOrigin: ORIGIN[s],
69
+ opacity: entered ? 1 : 0,
70
+ transform: entered ? "none" : `${ENTER_FROM[s]} scale(0.98)`,
71
+ clipPath: entered ? "inset(-48px -48px -48px -48px)" : hidden[s],
72
+ transition: entered ? "opacity 180ms ease-out, transform 360ms var(--ease-out-soft), clip-path 360ms var(--ease-out-soft)" : "opacity 160ms ease-in, transform 200ms ease-in, clip-path 200ms ease-in"
73
+ };
74
+ return {
75
+ open,
76
+ mounted,
77
+ entered,
78
+ show,
79
+ close,
80
+ toggle: open ? close : show,
81
+ side: s,
82
+ setFloating: position.setFloating,
83
+ panelStyle
84
+ };
85
+ }
86
+ export {
87
+ useFloatingPanel
88
+ };
89
+ //# sourceMappingURL=useFloatingPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../components/ui/useFloatingPanel.ts"],"sourcesContent":["\"use client\";\n\nimport { CSSProperties, RefObject, useCallback, useEffect, useRef, useState } from \"react\";\nimport { Align, ENTER_FROM, LAYER, ORIGIN, Side, useAnchoredPosition } from \"./Floating\";\n\ninterface FloatingPanelOptions {\n side?: Side;\n align?: Align;\n gap?: number;\n matchWidth?: boolean;\n /** Called after Escape closes the panel, to hand focus back */\n onEscape?: () => void;\n}\n\n/* The open/close choreography every field-anchored panel shares (select,\n date picker, colour picker, combobox): the panel is portalled to the\n body and pinned next to its anchor, stays mounted while it folds away,\n unfolds only once painted, and a click outside or Escape puts it back.\n Render the panel inside <Portal> with ref={setFloating} and\n style={panelStyle}. */\nexport function useFloatingPanel(anchor: RefObject<HTMLElement | null>, options: FloatingPanelOptions = {}) {\n const { side = \"bottom\", align = \"start\", gap = 8, matchWidth = false, onEscape } = options;\n const [open, setOpen] = useState(false);\n const [mounted, setMounted] = useState(false);\n const [entered, setEntered] = useState(false);\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const escape = useRef(onEscape);\n useEffect(() => {\n escape.current = onEscape;\n });\n\n const position = useAnchoredPosition(anchor, { open: mounted, side, align, gap, matchWidth });\n const { floatingRef } = position;\n\n const close = useCallback(() => {\n setOpen(false);\n setEntered(false);\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => setMounted(false), 200);\n }, []);\n\n const show = useCallback(() => {\n if (timer.current) clearTimeout(timer.current);\n setOpen(true);\n setMounted(true);\n requestAnimationFrame(() => requestAnimationFrame(() => setEntered(true)));\n }, []);\n\n useEffect(() => () => {\n if (timer.current) clearTimeout(timer.current);\n }, []);\n\n useEffect(() => {\n if (!open) return;\n const onDown = (e: PointerEvent) => {\n const t = e.target as Node;\n if (anchor.current?.contains(t) || floatingRef.current?.contains(t)) return;\n close();\n };\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.stopPropagation();\n close();\n escape.current?.();\n }\n };\n document.addEventListener(\"pointerdown\", onDown);\n document.addEventListener(\"keydown\", onKey, true);\n return () => {\n document.removeEventListener(\"pointerdown\", onDown);\n document.removeEventListener(\"keydown\", onKey, true);\n };\n }, [open, anchor, floatingRef, close]);\n\n /* Unfolds out of the field: a clip reveals it from the anchor's side\n while it fades and settles. The clip reaches past the panel so its\n shadow is never cut off. */\n const s = position.side;\n const hidden: Record<Side, string> = {\n bottom: \"inset(-48px -48px 55% -48px)\",\n top: \"inset(55% -48px -48px -48px)\",\n left: \"inset(-48px -48px -48px 55%)\",\n right: \"inset(-48px 55% -48px -48px)\",\n };\n const panelStyle: CSSProperties = {\n position: \"fixed\",\n top: 0,\n left: 0,\n zIndex: LAYER.floating,\n background: \"var(--panel-bezel)\",\n boxShadow: \"var(--overlay-shadow)\",\n transformOrigin: ORIGIN[s],\n opacity: entered ? 1 : 0,\n transform: entered ? \"none\" : `${ENTER_FROM[s]} scale(0.98)`,\n clipPath: entered ? \"inset(-48px -48px -48px -48px)\" : hidden[s],\n transition: entered\n ? \"opacity 180ms ease-out, transform 360ms var(--ease-out-soft), clip-path 360ms var(--ease-out-soft)\"\n : \"opacity 160ms ease-in, transform 200ms ease-in, clip-path 200ms ease-in\",\n };\n\n return {\n open,\n mounted,\n entered,\n show,\n close,\n toggle: open ? close : show,\n side: s,\n setFloating: position.setFloating,\n panelStyle,\n };\n}\n"],"mappings":";;;AAEA,SAAmC,aAAa,WAAW,QAAQ,gBAAgB;AACnF,SAAgB,YAAY,OAAO,QAAc,2BAA2B;AAiBrE,SAAS,iBAAiB,QAAuC,UAAgC,CAAC,GAAG;AAC1G,QAAM,EAAE,OAAO,UAAU,QAAQ,SAAS,MAAM,GAAG,aAAa,OAAO,SAAS,IAAI;AACpF,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,QAAQ,OAA6C,IAAI;AAC/D,QAAM,SAAS,OAAO,QAAQ;AAC9B,YAAU,MAAM;AACd,WAAO,UAAU;AAAA,EACnB,CAAC;AAED,QAAM,WAAW,oBAAoB,QAAQ,EAAE,MAAM,SAAS,MAAM,OAAO,KAAK,WAAW,CAAC;AAC5F,QAAM,EAAE,YAAY,IAAI;AAExB,QAAM,QAAQ,YAAY,MAAM;AAC9B,YAAQ,KAAK;AACb,eAAW,KAAK;AAChB,QAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAC7C,UAAM,UAAU,WAAW,MAAM,WAAW,KAAK,GAAG,GAAG;AAAA,EACzD,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO,YAAY,MAAM;AAC7B,QAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAC7C,YAAQ,IAAI;AACZ,eAAW,IAAI;AACf,0BAAsB,MAAM,sBAAsB,MAAM,WAAW,IAAI,CAAC,CAAC;AAAA,EAC3E,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM,MAAM;AACpB,QAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAAA,EAC/C,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,CAAC,MAAoB;AAClC,YAAM,IAAI,EAAE;AACZ,UAAI,OAAO,SAAS,SAAS,CAAC,KAAK,YAAY,SAAS,SAAS,CAAC,EAAG;AACrE,YAAM;AAAA,IACR;AACA,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,UAAU;AACtB,UAAE,gBAAgB;AAClB,cAAM;AACN,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AACA,aAAS,iBAAiB,eAAe,MAAM;AAC/C,aAAS,iBAAiB,WAAW,OAAO,IAAI;AAChD,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,MAAM;AAClD,eAAS,oBAAoB,WAAW,OAAO,IAAI;AAAA,IACrD;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,aAAa,KAAK,CAAC;AAKrC,QAAM,IAAI,SAAS;AACnB,QAAM,SAA+B;AAAA,IACnC,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,EACT;AACA,QAAM,aAA4B;AAAA,IAChC,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,MAAM;AAAA,IACd,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,iBAAiB,OAAO,CAAC;AAAA,IACzB,SAAS,UAAU,IAAI;AAAA,IACvB,WAAW,UAAU,SAAS,GAAG,WAAW,CAAC,CAAC;AAAA,IAC9C,UAAU,UAAU,mCAAmC,OAAO,CAAC;AAAA,IAC/D,YAAY,UACR,uGACA;AAAA,EACN;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,OAAO,QAAQ;AAAA,IACvB,MAAM;AAAA,IACN,aAAa,SAAS;AAAA,IACtB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,15 @@
1
+ import { RefObject } from 'react';
2
+
3
+ interface ModalBehaviorOptions {
4
+ /** The dialog is logically open */
5
+ open: boolean;
6
+ /** The dialog is mounted and painted — focus can move into it */
7
+ ready: boolean;
8
+ ref: RefObject<HTMLElement | null>;
9
+ onClose: () => void;
10
+ /** Element to focus on open; defaults to the first focusable */
11
+ initialFocus?: RefObject<HTMLElement | null>;
12
+ }
13
+ declare function useModalBehavior({ open, ready, ref, onClose, initialFocus }: ModalBehaviorOptions): void;
14
+
15
+ export { useModalBehavior };
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ // ../../components/ui/useModalBehavior.ts
4
+ import { useEffect } from "react";
5
+ var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
6
+ function useModalBehavior({ open, ready, ref, onClose, initialFocus }) {
7
+ useEffect(() => {
8
+ if (!open) return;
9
+ const onKey = (e) => {
10
+ if (e.key === "Escape") {
11
+ e.stopPropagation();
12
+ onClose();
13
+ }
14
+ };
15
+ document.addEventListener("keydown", onKey);
16
+ return () => document.removeEventListener("keydown", onKey);
17
+ }, [open, onClose]);
18
+ useEffect(() => {
19
+ if (!open) return;
20
+ const prev = document.body.style.overflow;
21
+ document.body.style.overflow = "hidden";
22
+ return () => {
23
+ document.body.style.overflow = prev;
24
+ };
25
+ }, [open]);
26
+ useEffect(() => {
27
+ if (!open || !ready) return;
28
+ const el = ref.current;
29
+ if (!el) return;
30
+ const opener = document.activeElement;
31
+ const target = initialFocus?.current ?? el.querySelector(FOCUSABLE);
32
+ target?.focus();
33
+ const trap = (e) => {
34
+ if (e.key !== "Tab") return;
35
+ const focusable = el.querySelectorAll(FOCUSABLE);
36
+ if (focusable.length === 0) return;
37
+ const first = focusable[0];
38
+ const last = focusable[focusable.length - 1];
39
+ if (e.shiftKey && document.activeElement === first) {
40
+ e.preventDefault();
41
+ last.focus();
42
+ } else if (!e.shiftKey && document.activeElement === last) {
43
+ e.preventDefault();
44
+ first.focus();
45
+ }
46
+ };
47
+ el.addEventListener("keydown", trap);
48
+ return () => {
49
+ el.removeEventListener("keydown", trap);
50
+ opener?.focus?.();
51
+ };
52
+ }, [open, ready, ref, initialFocus]);
53
+ }
54
+ export {
55
+ useModalBehavior
56
+ };
57
+ //# sourceMappingURL=useModalBehavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../components/ui/useModalBehavior.ts"],"sourcesContent":["\"use client\";\n\nimport { RefObject, useEffect } from \"react\";\n\ninterface ModalBehaviorOptions {\n /** The dialog is logically open */\n open: boolean;\n /** The dialog is mounted and painted — focus can move into it */\n ready: boolean;\n ref: RefObject<HTMLElement | null>;\n onClose: () => void;\n /** Element to focus on open; defaults to the first focusable */\n initialFocus?: RefObject<HTMLElement | null>;\n}\n\nconst FOCUSABLE =\n 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/* Everything a modal surface owes the person using it: Escape closes,\n focus is trapped inside, the page behind doesn't scroll, and focus\n returns to whatever opened it. */\nexport function useModalBehavior({ open, ready, ref, onClose, initialFocus }: ModalBehaviorOptions) {\n // Escape\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.stopPropagation();\n onClose();\n }\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n // Scroll lock\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // Focus trap + restore\n useEffect(() => {\n if (!open || !ready) return;\n const el = ref.current;\n if (!el) return;\n\n const opener = document.activeElement as HTMLElement | null;\n const target = initialFocus?.current ?? el.querySelector<HTMLElement>(FOCUSABLE);\n target?.focus();\n\n const trap = (e: KeyboardEvent) => {\n if (e.key !== \"Tab\") return;\n const focusable = el.querySelectorAll<HTMLElement>(FOCUSABLE);\n if (focusable.length === 0) return;\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n };\n\n el.addEventListener(\"keydown\", trap);\n return () => {\n el.removeEventListener(\"keydown\", trap);\n opener?.focus?.();\n };\n }, [open, ready, ref, initialFocus]);\n}\n"],"mappings":";;;AAEA,SAAoB,iBAAiB;AAarC,IAAM,YACJ;AAKK,SAAS,iBAAiB,EAAE,MAAM,OAAO,KAAK,SAAS,aAAa,GAAyB;AAElG,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,UAAU;AACtB,UAAE,gBAAgB;AAClB,gBAAQ;AAAA,MACV;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,MAAM,OAAO,CAAC;AAGlB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,SAAS,KAAK,MAAM;AACjC,aAAS,KAAK,MAAM,WAAW;AAC/B,WAAO,MAAM;AACX,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAGT,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,MAAO;AACrB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AAET,UAAM,SAAS,SAAS;AACxB,UAAM,SAAS,cAAc,WAAW,GAAG,cAA2B,SAAS;AAC/E,YAAQ,MAAM;AAEd,UAAM,OAAO,CAAC,MAAqB;AACjC,UAAI,EAAE,QAAQ,MAAO;AACrB,YAAM,YAAY,GAAG,iBAA8B,SAAS;AAC5D,UAAI,UAAU,WAAW,EAAG;AAC5B,YAAM,QAAQ,UAAU,CAAC;AACzB,YAAM,OAAO,UAAU,UAAU,SAAS,CAAC;AAC3C,UAAI,EAAE,YAAY,SAAS,kBAAkB,OAAO;AAClD,UAAE,eAAe;AACjB,aAAK,MAAM;AAAA,MACb,WAAW,CAAC,EAAE,YAAY,SAAS,kBAAkB,MAAM;AACzD,UAAE,eAAe;AACjB,cAAM,MAAM;AAAA,MACd;AAAA,IACF;AAEA,OAAG,iBAAiB,WAAW,IAAI;AACnC,WAAO,MAAM;AACX,SAAG,oBAAoB,WAAW,IAAI;AACtC,cAAQ,QAAQ;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,MAAM,OAAO,KAAK,YAAY,CAAC;AACrC;","names":[]}
package/package.json CHANGED
@@ -1,12 +1,22 @@
1
1
  {
2
2
  "name": "@blurrow/ui",
3
- "version": "0.1.0",
4
- "description": "tactile react components: buttons you want to press, fields you want to fill in.",
3
+ "version": "0.2.1",
4
+ "description": "tactile react components: buttons you want to press, fields you want to fill in. comes with a cli: npx @blurrow/ui init",
5
5
  "license": "MIT",
6
6
  "author": "manyports (https://manyports.com)",
7
7
  "homepage": "https://manyports.com",
8
- "keywords": ["react", "components", "ui", "tailwindcss", "design-system"],
8
+ "keywords": [
9
+ "react",
10
+ "components",
11
+ "ui",
12
+ "tailwindcss",
13
+ "design-system",
14
+ "cli"
15
+ ],
9
16
  "type": "module",
17
+ "bin": {
18
+ "blurrow": "./dist/cli.js"
19
+ },
10
20
  "main": "./dist/index.js",
11
21
  "module": "./dist/index.js",
12
22
  "types": "./dist/index.d.ts",
@@ -18,10 +28,16 @@
18
28
  "./styles.css": "./dist/styles.css",
19
29
  "./package.json": "./package.json"
20
30
  },
21
- "files": ["dist", "README.md", "LICENSE"],
22
- "sideEffects": ["**/*.css"],
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "sideEffects": [
37
+ "**/*.css"
38
+ ],
23
39
  "scripts": {
24
- "build": "tsup && cp ../../components/ui/styles.css dist/styles.css",
40
+ "build": "tsup && node scripts/copy-sources.mjs",
25
41
  "prepublishOnly": "npm run build"
26
42
  },
27
43
  "peerDependencies": {
@@ -30,6 +46,7 @@
30
46
  "tailwindcss": "^4.0.0"
31
47
  },
32
48
  "devDependencies": {
49
+ "@types/node": "^20.19.43",
33
50
  "@types/react": "^19",
34
51
  "@types/react-dom": "^19",
35
52
  "tsup": "^8.5.0",