@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.
- package/README.md +60 -20
- package/dist/Accordion.d.ts +17 -0
- package/dist/Accordion.js +138 -0
- package/dist/Accordion.js.map +1 -0
- package/dist/Alert.d.ts +13 -0
- package/dist/Alert.js +69 -0
- package/dist/Alert.js.map +1 -0
- package/dist/AuthorCard.d.ts +12 -0
- package/dist/AuthorCard.js +57 -0
- package/dist/AuthorCard.js.map +1 -0
- package/dist/Avatar.d.ts +21 -0
- package/dist/Avatar.js +96 -0
- package/dist/Avatar.js.map +1 -0
- package/dist/Badge.d.ts +12 -0
- package/dist/Badge.js +46 -0
- package/dist/Badge.js.map +1 -0
- package/dist/Banner.d.ts +15 -0
- package/dist/Banner.js +68 -0
- package/dist/Banner.js.map +1 -0
- package/dist/Breadcrumb.d.ts +13 -0
- package/dist/Breadcrumb.js +45 -0
- package/dist/Breadcrumb.js.map +1 -0
- package/dist/Button.d.ts +19 -0
- package/dist/Button.js +282 -0
- package/dist/Button.js.map +1 -0
- package/dist/ButtonGroup.d.ts +19 -0
- package/dist/ButtonGroup.js +62 -0
- package/dist/ButtonGroup.js.map +1 -0
- package/dist/Card.d.ts +40 -0
- package/dist/Card.js +175 -0
- package/dist/Card.js.map +1 -0
- package/dist/CardShell.d.ts +18 -0
- package/dist/CardShell.js +58 -0
- package/dist/CardShell.js.map +1 -0
- package/dist/Carousel.d.ts +22 -0
- package/dist/Carousel.js +247 -0
- package/dist/Carousel.js.map +1 -0
- package/dist/Checkbox.d.ts +15 -0
- package/dist/Checkbox.js +84 -0
- package/dist/Checkbox.js.map +1 -0
- package/dist/CloseButton.d.ts +12 -0
- package/dist/CloseButton.js +63 -0
- package/dist/CloseButton.js.map +1 -0
- package/dist/CodeBlock.d.ts +12 -0
- package/dist/CodeBlock.js +221 -0
- package/dist/CodeBlock.js.map +1 -0
- package/dist/ColorPicker.d.ts +24 -0
- package/dist/ColorPicker.js +366 -0
- package/dist/ColorPicker.js.map +1 -0
- package/dist/Combobox.d.ts +34 -0
- package/dist/Combobox.js +255 -0
- package/dist/Combobox.js.map +1 -0
- package/dist/CommandPalette.d.ts +25 -0
- package/dist/CommandPalette.js +304 -0
- package/dist/CommandPalette.js.map +1 -0
- package/dist/ContextMenu.d.ts +16 -0
- package/dist/ContextMenu.js +209 -0
- package/dist/ContextMenu.js.map +1 -0
- package/dist/DataList.d.ts +15 -0
- package/dist/DataList.js +42 -0
- package/dist/DataList.js.map +1 -0
- package/dist/DatePicker.d.ts +65 -0
- package/dist/DatePicker.js +391 -0
- package/dist/DatePicker.js.map +1 -0
- package/dist/DateRangePicker.d.ts +34 -0
- package/dist/DateRangePicker.js +180 -0
- package/dist/DateRangePicker.js.map +1 -0
- package/dist/Drawer.d.ts +17 -0
- package/dist/Drawer.js +132 -0
- package/dist/Drawer.js.map +1 -0
- package/dist/DropdownMenu.d.ts +23 -0
- package/dist/DropdownMenu.js +185 -0
- package/dist/DropdownMenu.js.map +1 -0
- package/dist/EmptyState.d.ts +13 -0
- package/dist/EmptyState.js +55 -0
- package/dist/EmptyState.js.map +1 -0
- package/dist/FieldMessage.d.ts +26 -0
- package/dist/FieldMessage.js +95 -0
- package/dist/FieldMessage.js.map +1 -0
- package/dist/FileUpload.d.ts +32 -0
- package/dist/FileUpload.js +253 -0
- package/dist/FileUpload.js.map +1 -0
- package/dist/Floating.d.ts +32 -0
- package/dist/Floating.js +95 -0
- package/dist/Floating.js.map +1 -0
- package/dist/HoverCard.d.ts +18 -0
- package/dist/HoverCard.js +90 -0
- package/dist/HoverCard.js.map +1 -0
- package/dist/Input.d.ts +17 -0
- package/dist/Input.js +96 -0
- package/dist/Input.js.map +1 -0
- package/dist/Kbd.d.ts +9 -0
- package/dist/Kbd.js +24 -0
- package/dist/Kbd.js.map +1 -0
- package/dist/Modal.d.ts +15 -0
- package/dist/Modal.js +109 -0
- package/dist/Modal.js.map +1 -0
- package/dist/NumberInput.d.ts +23 -0
- package/dist/NumberInput.js +162 -0
- package/dist/NumberInput.js.map +1 -0
- package/dist/OtpInput.d.ts +26 -0
- package/dist/OtpInput.js +173 -0
- package/dist/OtpInput.js.map +1 -0
- package/dist/Pagination.d.ts +11 -0
- package/dist/Pagination.js +148 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/Popover.d.ts +13 -0
- package/dist/Popover.js +98 -0
- package/dist/Popover.js.map +1 -0
- package/dist/Progress.d.ts +12 -0
- package/dist/Progress.js +61 -0
- package/dist/Progress.js.map +1 -0
- package/dist/QuoteCard.d.ts +13 -0
- package/dist/QuoteCard.js +92 -0
- package/dist/QuoteCard.js.map +1 -0
- package/dist/Radio.d.ts +19 -0
- package/dist/Radio.js +161 -0
- package/dist/Radio.js.map +1 -0
- package/dist/RadioCards.d.ts +27 -0
- package/dist/RadioCards.js +205 -0
- package/dist/RadioCards.js.map +1 -0
- package/dist/ReactionBar.d.ts +15 -0
- package/dist/ReactionBar.js +92 -0
- package/dist/ReactionBar.js.map +1 -0
- package/dist/ReadingProgress.d.ts +8 -0
- package/dist/ReadingProgress.js +51 -0
- package/dist/ReadingProgress.js.map +1 -0
- package/dist/ReadingTime.d.ts +9 -0
- package/dist/ReadingTime.js +39 -0
- package/dist/ReadingTime.js.map +1 -0
- package/dist/Select.d.ts +20 -0
- package/dist/Select.js +244 -0
- package/dist/Select.js.map +1 -0
- package/dist/Separator.d.ts +14 -0
- package/dist/Separator.js +33 -0
- package/dist/Separator.js.map +1 -0
- package/dist/ShareButtons.d.ts +10 -0
- package/dist/ShareButtons.js +107 -0
- package/dist/ShareButtons.js.map +1 -0
- package/dist/Sidebar.d.ts +30 -0
- package/dist/Sidebar.js +140 -0
- package/dist/Sidebar.js.map +1 -0
- package/dist/Skeleton.d.ts +15 -0
- package/dist/Skeleton.js +67 -0
- package/dist/Skeleton.js.map +1 -0
- package/dist/Slider.d.ts +27 -0
- package/dist/Slider.js +182 -0
- package/dist/Slider.js.map +1 -0
- package/dist/Spinner.d.ts +12 -0
- package/dist/Spinner.js +24 -0
- package/dist/Spinner.js.map +1 -0
- package/dist/SpotlightCard.d.ts +17 -0
- package/dist/SpotlightCard.js +87 -0
- package/dist/SpotlightCard.js.map +1 -0
- package/dist/Stat.d.ts +19 -0
- package/dist/Stat.js +71 -0
- package/dist/Stat.js.map +1 -0
- package/dist/StatCard.d.ts +24 -0
- package/dist/StatCard.js +212 -0
- package/dist/StatCard.js.map +1 -0
- package/dist/Steps.d.ts +14 -0
- package/dist/Steps.js +94 -0
- package/dist/Steps.js.map +1 -0
- package/dist/Table.d.ts +21 -0
- package/dist/Table.js +93 -0
- package/dist/Table.js.map +1 -0
- package/dist/TableOfContents.d.ts +17 -0
- package/dist/TableOfContents.js +92 -0
- package/dist/TableOfContents.js.map +1 -0
- package/dist/Tabs.d.ts +17 -0
- package/dist/Tabs.js +123 -0
- package/dist/Tabs.js.map +1 -0
- package/dist/TagInput.d.ts +16 -0
- package/dist/TagInput.js +169 -0
- package/dist/TagInput.js.map +1 -0
- package/dist/Textarea.d.ts +13 -0
- package/dist/Textarea.js +74 -0
- package/dist/Textarea.js.map +1 -0
- package/dist/TimePicker.d.ts +28 -0
- package/dist/TimePicker.js +264 -0
- package/dist/TimePicker.js.map +1 -0
- package/dist/Timeline.d.ts +19 -0
- package/dist/Timeline.js +76 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/Toast.d.ts +18 -0
- package/dist/Toast.js +318 -0
- package/dist/Toast.js.map +1 -0
- package/dist/Toggle.d.ts +13 -0
- package/dist/Toggle.js +84 -0
- package/dist/Toggle.js.map +1 -0
- package/dist/Tooltip.d.ts +13 -0
- package/dist/Tooltip.js +138 -0
- package/dist/Tooltip.js.map +1 -0
- package/dist/cli.js +1644 -0
- package/dist/index.d.ts +68 -631
- package/dist/index.js +67 -5558
- package/dist/index.js.map +1 -1
- package/dist/registry/Accordion.tsx +140 -0
- package/dist/registry/Alert.tsx +95 -0
- package/dist/registry/AuthorCard.tsx +67 -0
- package/dist/registry/Avatar.tsx +116 -0
- package/dist/registry/Badge.tsx +55 -0
- package/dist/registry/Banner.tsx +86 -0
- package/dist/registry/Breadcrumb.tsx +60 -0
- package/dist/registry/Button.tsx +361 -0
- package/dist/registry/ButtonGroup.tsx +82 -0
- package/dist/registry/Card.tsx +241 -0
- package/dist/registry/CardShell.tsx +78 -0
- package/dist/registry/Carousel.tsx +296 -0
- package/dist/registry/Checkbox.tsx +104 -0
- package/dist/registry/CloseButton.tsx +68 -0
- package/dist/registry/CodeBlock.tsx +202 -0
- package/dist/registry/ColorPicker.tsx +404 -0
- package/dist/registry/Combobox.tsx +318 -0
- package/dist/registry/CommandPalette.tsx +356 -0
- package/dist/registry/ContextMenu.tsx +241 -0
- package/dist/registry/DataList.tsx +49 -0
- package/dist/registry/DatePicker.tsx +494 -0
- package/dist/registry/DateRangePicker.tsx +232 -0
- package/dist/registry/Drawer.tsx +166 -0
- package/dist/registry/DropdownMenu.tsx +219 -0
- package/dist/registry/EmptyState.tsx +55 -0
- package/dist/registry/FieldMessage.tsx +123 -0
- package/dist/registry/FileUpload.tsx +318 -0
- package/dist/registry/Floating.tsx +134 -0
- package/dist/registry/HoverCard.tsx +113 -0
- package/dist/registry/Input.tsx +108 -0
- package/dist/registry/Kbd.tsx +23 -0
- package/dist/registry/Modal.tsx +130 -0
- package/dist/registry/NumberInput.tsx +183 -0
- package/dist/registry/OtpInput.tsx +219 -0
- package/dist/registry/Pagination.tsx +160 -0
- package/dist/registry/Popover.tsx +116 -0
- package/dist/registry/Progress.tsx +66 -0
- package/dist/registry/QuoteCard.tsx +93 -0
- package/dist/registry/Radio.tsx +205 -0
- package/dist/registry/RadioCards.tsx +247 -0
- package/dist/registry/ReactionBar.tsx +101 -0
- package/dist/registry/ReadingProgress.tsx +51 -0
- package/dist/registry/ReadingTime.tsx +35 -0
- package/dist/registry/Select.tsx +265 -0
- package/dist/registry/Separator.tsx +52 -0
- package/dist/registry/ShareButtons.tsx +134 -0
- package/dist/registry/Sidebar.tsx +182 -0
- package/dist/registry/Skeleton.tsx +83 -0
- package/dist/registry/Slider.tsx +229 -0
- package/dist/registry/Spinner.tsx +32 -0
- package/dist/registry/SpotlightCard.tsx +106 -0
- package/dist/registry/Stat.tsx +82 -0
- package/dist/registry/StatCard.tsx +256 -0
- package/dist/registry/Steps.tsx +109 -0
- package/dist/registry/Table.tsx +123 -0
- package/dist/registry/TableOfContents.tsx +116 -0
- package/dist/registry/Tabs.tsx +136 -0
- package/dist/registry/TagInput.tsx +191 -0
- package/dist/registry/Textarea.tsx +84 -0
- package/dist/registry/TimePicker.tsx +315 -0
- package/dist/registry/Timeline.tsx +102 -0
- package/dist/registry/Toast.tsx +388 -0
- package/dist/registry/Toggle.tsx +96 -0
- package/dist/registry/Tooltip.tsx +160 -0
- package/dist/registry/styles.css +1207 -0
- package/dist/registry/useFloatingPanel.ts +112 -0
- package/dist/registry/useModalBehavior.ts +77 -0
- package/dist/styles.css +868 -62
- package/dist/useFloatingPanel.d.ts +24 -0
- package/dist/useFloatingPanel.js +89 -0
- package/dist/useFloatingPanel.js.map +1 -0
- package/dist/useModalBehavior.d.ts +15 -0
- package/dist/useModalBehavior.js +57 -0
- package/dist/useModalBehavior.js.map +1 -0
- 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
|
|
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": [
|
|
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": [
|
|
22
|
-
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"sideEffects": [
|
|
37
|
+
"**/*.css"
|
|
38
|
+
],
|
|
23
39
|
"scripts": {
|
|
24
|
-
"build": "tsup &&
|
|
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",
|