@blurrow/ui 0.2.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 +48 -36
- 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 +172 -5
- 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 +6 -6
- package/dist/registry/Alert.tsx +13 -10
- package/dist/registry/AuthorCard.tsx +7 -7
- package/dist/registry/Avatar.tsx +5 -5
- package/dist/registry/Badge.tsx +12 -15
- package/dist/registry/Banner.tsx +15 -18
- package/dist/registry/Breadcrumb.tsx +3 -3
- package/dist/registry/Button.tsx +58 -69
- package/dist/registry/ButtonGroup.tsx +9 -9
- package/dist/registry/Card.tsx +15 -15
- package/dist/registry/CardShell.tsx +6 -6
- package/dist/registry/Carousel.tsx +19 -15
- package/dist/registry/Checkbox.tsx +14 -9
- package/dist/registry/CloseButton.tsx +2 -2
- package/dist/registry/CodeBlock.tsx +15 -15
- package/dist/registry/ColorPicker.tsx +404 -0
- package/dist/registry/Combobox.tsx +318 -0
- package/dist/registry/CommandPalette.tsx +31 -24
- package/dist/registry/ContextMenu.tsx +241 -0
- package/dist/registry/DataList.tsx +4 -4
- package/dist/registry/DatePicker.tsx +494 -0
- package/dist/registry/DateRangePicker.tsx +232 -0
- package/dist/registry/Drawer.tsx +17 -15
- package/dist/registry/DropdownMenu.tsx +39 -77
- package/dist/registry/EmptyState.tsx +8 -8
- 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 +54 -43
- package/dist/registry/Kbd.tsx +4 -5
- package/dist/registry/Modal.tsx +17 -15
- package/dist/registry/NumberInput.tsx +183 -0
- package/dist/registry/OtpInput.tsx +219 -0
- package/dist/registry/Pagination.tsx +15 -18
- package/dist/registry/Popover.tsx +40 -45
- package/dist/registry/Progress.tsx +4 -4
- package/dist/registry/QuoteCard.tsx +7 -7
- package/dist/registry/Radio.tsx +24 -21
- package/dist/registry/RadioCards.tsx +247 -0
- package/dist/registry/ReactionBar.tsx +5 -5
- package/dist/registry/ReadingProgress.tsx +2 -2
- package/dist/registry/ReadingTime.tsx +2 -2
- package/dist/registry/Select.tsx +28 -53
- package/dist/registry/Separator.tsx +52 -0
- package/dist/registry/ShareButtons.tsx +2 -2
- package/dist/registry/Sidebar.tsx +18 -18
- package/dist/registry/Skeleton.tsx +1 -1
- package/dist/registry/Slider.tsx +229 -0
- package/dist/registry/Spinner.tsx +32 -0
- package/dist/registry/SpotlightCard.tsx +6 -6
- package/dist/registry/Stat.tsx +4 -4
- package/dist/registry/StatCard.tsx +19 -19
- package/dist/registry/Steps.tsx +7 -7
- package/dist/registry/Table.tsx +12 -10
- package/dist/registry/TableOfContents.tsx +8 -8
- package/dist/registry/Tabs.tsx +6 -6
- package/dist/registry/TagInput.tsx +9 -9
- package/dist/registry/Textarea.tsx +5 -5
- package/dist/registry/TimePicker.tsx +315 -0
- package/dist/registry/Timeline.tsx +14 -14
- package/dist/registry/Toast.tsx +15 -12
- package/dist/registry/Toggle.tsx +7 -10
- package/dist/registry/Tooltip.tsx +21 -73
- package/dist/registry/styles.css +868 -62
- package/dist/registry/useFloatingPanel.ts +112 -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 +1 -1
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blurrow/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
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)",
|