@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 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Floating.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { RefObject, useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n/* ── Portal ───────────────────────────────────────────────────────── */\n/* Anything that floats over the page is rendered at the end of <body>.\n Left where it was declared, a panel lives inside whatever stacking\n context its parents made (a transform, a filter, an isolate), and a\n later sibling on the page can paint straight over it, whatever its\n z-index says. */\nconst noop = () => () => {};\n\nexport function Portal({ children }: { children: React.ReactNode }) {\n const client = useSyncExternalStore(noop, () => true, () => false);\n return client ? createPortal(children, document.body) : null;\n}\n\n/* Layers, lowest first: dialogs, then toasts, then anything anchored to\n a control (so a select inside a modal opens over the modal), then\n tooltips over all of it. */\nexport const LAYER = { dialog: 90, toast: 95, floating: 100, tooltip: 110 } as const;\n\n/* ── Anchored position ────────────────────────────────────────────── */\nexport type Side = \"top\" | \"bottom\" | \"left\" | \"right\";\nexport type Align = \"start\" | \"center\" | \"end\";\n\nconst OPPOSITE: Record<Side, Side> = { top: \"bottom\", bottom: \"top\", left: \"right\", right: \"left\" };\nconst PAD = 8;\n\ninterface AnchorOptions {\n open: boolean;\n side?: Side;\n align?: Align;\n /** Space between the anchor and the panel */\n gap?: number;\n /** Make the panel at least as wide as the anchor (selects, comboboxes) */\n matchWidth?: boolean;\n}\n\n/* Places a fixed-position panel next to its anchor: on the side asked\n for unless the other side has more room, then nudged back inside the\n viewport. Follows the anchor while the page scrolls or resizes. The\n position is written straight onto the element, so scrolling never\n re-renders the component. */\nexport function useAnchoredPosition(\n anchor: RefObject<HTMLElement | null>,\n { open, side = \"bottom\", align = \"start\", gap = 8, matchWidth = false }: AnchorOptions,\n) {\n const [placed, setPlaced] = useState<Side>(side);\n const floating = useRef<HTMLElement | null>(null);\n\n const update = useCallback(() => {\n const a = anchor.current?.getBoundingClientRect();\n const el = floating.current;\n if (!a || !el) return;\n if (matchWidth) el.style.minWidth = `${a.width}px`;\n\n // offset sizes ignore the enter transform, so these are the settled size\n const w = el.offsetWidth;\n const h = el.offsetHeight;\n const vw = document.documentElement.clientWidth;\n const vh = window.innerHeight;\n const room: Record<Side, number> = { top: a.top, bottom: vh - a.bottom, left: a.left, right: vw - a.right };\n const vertical = side === \"top\" || side === \"bottom\";\n const need = (vertical ? h : w) + gap + PAD;\n\n let s = side;\n if (room[s] < need && room[OPPOSITE[s]] > room[s]) s = OPPOSITE[s];\n\n let top: number;\n let left: number;\n if (s === \"top\" || s === \"bottom\") {\n top = s === \"bottom\" ? a.bottom + gap : a.top - gap - h;\n left = align === \"start\" ? a.left : align === \"end\" ? a.right - w : a.left + (a.width - w) / 2;\n } else {\n left = s === \"right\" ? a.right + gap : a.left - gap - w;\n top = align === \"start\" ? a.top : align === \"end\" ? a.bottom - h : a.top + (a.height - h) / 2;\n }\n left = Math.max(PAD, Math.min(left, vw - w - PAD));\n top = Math.max(PAD, Math.min(top, vh - h - PAD));\n\n el.style.top = `${Math.round(top)}px`;\n el.style.left = `${Math.round(left)}px`;\n el.dataset.side = s;\n setPlaced(s);\n }, [anchor, side, align, gap, matchWidth]);\n\n /* Attach this as the panel's ref: it's placed before its first paint */\n const setFloating = useCallback(\n (el: HTMLElement | null) => {\n floating.current = el;\n if (el) update();\n },\n [update],\n );\n\n useEffect(() => {\n if (!open) return;\n let frame = 0;\n const schedule = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(update);\n };\n window.addEventListener(\"scroll\", schedule, true);\n window.addEventListener(\"resize\", schedule);\n // The panel can change size while open (a filtered list, a new month)\n const ro = new ResizeObserver(schedule);\n if (floating.current) ro.observe(floating.current);\n if (anchor.current) ro.observe(anchor.current);\n return () => {\n cancelAnimationFrame(frame);\n window.removeEventListener(\"scroll\", schedule, true);\n window.removeEventListener(\"resize\", schedule);\n ro.disconnect();\n };\n }, [open, update, anchor]);\n\n return { setFloating, side: placed, floatingRef: floating, update };\n}\n\n/* Where the panel grows from, and where it slides in from, for each side */\nexport const ORIGIN: Record<Side, string> = {\n top: \"bottom\",\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n};\nexport const ENTER_FROM: Record<Side, string> = {\n top: \"translateY(4px)\",\n bottom: \"translateY(-4px)\",\n left: \"translateX(4px)\",\n right: \"translateX(-4px)\",\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Side } from './Floating.js';
|
|
4
|
+
|
|
5
|
+
interface HoverCardProps {
|
|
6
|
+
/** What you hover: a link, a name, an avatar */
|
|
7
|
+
trigger: React.ReactNode;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
side?: Side;
|
|
10
|
+
align?: "start" | "center" | "end";
|
|
11
|
+
/** Milliseconds to wait before opening, and before closing */
|
|
12
|
+
openDelay?: number;
|
|
13
|
+
closeDelay?: number;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function HoverCard({ trigger, children, side, align, openDelay, closeDelay, className, }: HoverCardProps): react_jsx_runtime.JSX.Element;
|
|
17
|
+
|
|
18
|
+
export { HoverCard };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/HoverCard.tsx
|
|
4
|
+
import React, { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
import { ENTER_FROM, LAYER, ORIGIN, Portal, useAnchoredPosition } from "./Floating.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function HoverCard({
|
|
8
|
+
trigger,
|
|
9
|
+
children,
|
|
10
|
+
side = "bottom",
|
|
11
|
+
align = "center",
|
|
12
|
+
openDelay = 450,
|
|
13
|
+
closeDelay = 200,
|
|
14
|
+
className = ""
|
|
15
|
+
}) {
|
|
16
|
+
const uid = useId();
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
19
|
+
const [mounted, setMounted] = useState(false);
|
|
20
|
+
const [visible, setVisible] = useState(false);
|
|
21
|
+
const timer = useRef(null);
|
|
22
|
+
const { setFloating, side: placed } = useAnchoredPosition(ref, { open: mounted, side, align, gap: 10 });
|
|
23
|
+
if (open && !mounted) setMounted(true);
|
|
24
|
+
if (!open && visible) setVisible(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open) {
|
|
27
|
+
const id = requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true)));
|
|
28
|
+
return () => cancelAnimationFrame(id);
|
|
29
|
+
}
|
|
30
|
+
const t = setTimeout(() => setMounted(false), 180);
|
|
31
|
+
return () => clearTimeout(t);
|
|
32
|
+
}, [open]);
|
|
33
|
+
useEffect(() => () => {
|
|
34
|
+
if (timer.current) clearTimeout(timer.current);
|
|
35
|
+
}, []);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!open) return;
|
|
38
|
+
const onKey = (e) => e.key === "Escape" && setOpen(false);
|
|
39
|
+
document.addEventListener("keydown", onKey);
|
|
40
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
41
|
+
}, [open]);
|
|
42
|
+
const later = (next, ms) => {
|
|
43
|
+
if (timer.current) clearTimeout(timer.current);
|
|
44
|
+
timer.current = setTimeout(() => setOpen(next), ms);
|
|
45
|
+
};
|
|
46
|
+
const enter = (e) => e.pointerType !== "touch" && later(true, openDelay);
|
|
47
|
+
const leave = (e) => e.pointerType !== "touch" && later(false, closeDelay);
|
|
48
|
+
return /* @__PURE__ */ jsxs(
|
|
49
|
+
"span",
|
|
50
|
+
{
|
|
51
|
+
ref,
|
|
52
|
+
className: ["inline-flex", className].join(" "),
|
|
53
|
+
onPointerEnter: enter,
|
|
54
|
+
onPointerLeave: leave,
|
|
55
|
+
onFocus: () => later(true, 0),
|
|
56
|
+
onBlur: () => later(false, closeDelay),
|
|
57
|
+
children: [
|
|
58
|
+
React.isValidElement(trigger) ? React.cloneElement(trigger, {
|
|
59
|
+
"aria-describedby": mounted ? `${uid}-card` : void 0
|
|
60
|
+
}) : trigger,
|
|
61
|
+
mounted && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
|
|
62
|
+
"div",
|
|
63
|
+
{
|
|
64
|
+
ref: setFloating,
|
|
65
|
+
id: `${uid}-card`,
|
|
66
|
+
onPointerEnter: enter,
|
|
67
|
+
onPointerLeave: leave,
|
|
68
|
+
className: "w-[300px] max-w-[calc(100vw-16px)] rounded-[18px] p-1.5 transition-[opacity,transform] duration-200 ease-[var(--ease-spring)]",
|
|
69
|
+
style: {
|
|
70
|
+
position: "fixed",
|
|
71
|
+
top: 0,
|
|
72
|
+
left: 0,
|
|
73
|
+
zIndex: LAYER.floating,
|
|
74
|
+
transformOrigin: ORIGIN[placed],
|
|
75
|
+
opacity: visible ? 1 : 0,
|
|
76
|
+
transform: visible ? "none" : `${ENTER_FROM[placed]} scale(0.96)`,
|
|
77
|
+
background: "var(--panel-bezel)",
|
|
78
|
+
boxShadow: "var(--overlay-shadow)"
|
|
79
|
+
},
|
|
80
|
+
children: /* @__PURE__ */ jsx("div", { className: "rounded-[13px] bg-panel-face p-4 text-[13px] text-ink-body", style: { boxShadow: "var(--panel-face-shadow)" }, children })
|
|
81
|
+
}
|
|
82
|
+
) })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
HoverCard
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=HoverCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/HoverCard.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useId, useRef, useState } from \"react\";\nimport { ENTER_FROM, LAYER, ORIGIN, Portal, Side, useAnchoredPosition } from \"./Floating\";\n\ninterface HoverCardProps {\n /** What you hover: a link, a name, an avatar */\n trigger: React.ReactNode;\n children: React.ReactNode;\n side?: Side;\n align?: \"start\" | \"center\" | \"end\";\n /** Milliseconds to wait before opening, and before closing */\n openDelay?: number;\n closeDelay?: number;\n className?: string;\n}\n\n/* A preview that floats up when you rest the pointer on something, like\n a profile behind a username. It waits a beat before opening, stays\n open while the pointer crosses into it, and opens on keyboard focus\n too. It's a preview: anything in it should also be reachable by\n following the trigger. */\nexport function HoverCard({\n trigger,\n children,\n side = \"bottom\",\n align = \"center\",\n openDelay = 450,\n closeDelay = 200,\n className = \"\",\n}: HoverCardProps) {\n const uid = useId();\n const ref = useRef<HTMLSpanElement>(null);\n const [open, setOpen] = useState(false);\n const [mounted, setMounted] = useState(false);\n const [visible, setVisible] = useState(false);\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const { setFloating, side: placed } = useAnchoredPosition(ref, { open: mounted, side, align, gap: 10 });\n\n if (open && !mounted) setMounted(true);\n if (!open && visible) setVisible(false);\n\n useEffect(() => {\n if (open) {\n const id = requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true)));\n return () => cancelAnimationFrame(id);\n }\n const t = setTimeout(() => setMounted(false), 180);\n return () => clearTimeout(t);\n }, [open]);\n\n useEffect(() => () => {\n if (timer.current) clearTimeout(timer.current);\n }, []);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open]);\n\n const later = (next: boolean, ms: number) => {\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => setOpen(next), ms);\n };\n // Touch has no hover; a tap goes through to the trigger instead\n const enter = (e: React.PointerEvent) => e.pointerType !== \"touch\" && later(true, openDelay);\n const leave = (e: React.PointerEvent) => e.pointerType !== \"touch\" && later(false, closeDelay);\n\n return (\n <span\n ref={ref}\n className={[\"inline-flex\", className].join(\" \")}\n onPointerEnter={enter}\n onPointerLeave={leave}\n onFocus={() => later(true, 0)}\n onBlur={() => later(false, closeDelay)}\n >\n {React.isValidElement(trigger)\n ? React.cloneElement(trigger as React.ReactElement<Record<string, unknown>>, {\n \"aria-describedby\": mounted ? `${uid}-card` : undefined,\n })\n : trigger}\n {mounted && (\n <Portal>\n <div\n ref={setFloating}\n id={`${uid}-card`}\n onPointerEnter={enter}\n onPointerLeave={leave}\n className=\"w-[300px] max-w-[calc(100vw-16px)] rounded-[18px] p-1.5 transition-[opacity,transform] duration-200 ease-[var(--ease-spring)]\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n zIndex: LAYER.floating,\n transformOrigin: ORIGIN[placed],\n opacity: visible ? 1 : 0,\n transform: visible ? \"none\" : `${ENTER_FROM[placed]} scale(0.96)`,\n background: \"var(--panel-bezel)\",\n boxShadow: \"var(--overlay-shadow)\",\n }}\n >\n <div className=\"rounded-[13px] bg-panel-face p-4 text-[13px] text-ink-body\" style={{ boxShadow: \"var(--panel-face-shadow)\" }}>\n {children}\n </div>\n </div>\n </Portal>\n )}\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/Input.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
label?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
fullWidth?: boolean;
|
|
9
|
+
iconLeft?: React.ReactNode;
|
|
10
|
+
iconRight?: React.ReactNode;
|
|
11
|
+
/** Seat the well in the same bezel as a `size="lg"` button, so a field and
|
|
12
|
+
a button side by side (a newsletter, a waitlist) share one height. */
|
|
13
|
+
layered?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function Input({ label, hint, error, fullWidth, iconLeft, iconRight, layered, className, id, ...rest }: InputProps): react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
export { Input };
|
package/dist/Input.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ../../components/ui/Input.tsx
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
function Input({
|
|
4
|
+
label,
|
|
5
|
+
hint,
|
|
6
|
+
error,
|
|
7
|
+
fullWidth = false,
|
|
8
|
+
iconLeft,
|
|
9
|
+
iconRight,
|
|
10
|
+
layered = false,
|
|
11
|
+
className = "",
|
|
12
|
+
id,
|
|
13
|
+
...rest
|
|
14
|
+
}) {
|
|
15
|
+
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
|
|
16
|
+
const messageId = inputId && (error || hint) ? `${inputId}-message` : void 0;
|
|
17
|
+
return /* @__PURE__ */ jsxs("div", { className: ["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" "), children: [
|
|
18
|
+
label && /* @__PURE__ */ jsx(
|
|
19
|
+
"label",
|
|
20
|
+
{
|
|
21
|
+
htmlFor: inputId,
|
|
22
|
+
className: "text-[12.5px] font-medium text-ink-label tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-ink-strong",
|
|
23
|
+
children: label
|
|
24
|
+
}
|
|
25
|
+
),
|
|
26
|
+
/* @__PURE__ */ jsx(
|
|
27
|
+
"div",
|
|
28
|
+
{
|
|
29
|
+
className: layered ? "relative rounded-[15px] p-[3.5px]" : "contents",
|
|
30
|
+
style: layered ? { background: "var(--bezel)", boxShadow: "var(--bezel-shadow)" } : void 0,
|
|
31
|
+
children: /* @__PURE__ */ jsxs("div", { className: "relative group/input isolate", children: [
|
|
32
|
+
iconLeft && /* @__PURE__ */ jsx(
|
|
33
|
+
"span",
|
|
34
|
+
{
|
|
35
|
+
className: "absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-faint transition-all duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)] group-focus-within/input:text-ink-soft group-focus-within/input:scale-110 pointer-events-none z-20 flex items-center justify-center",
|
|
36
|
+
children: iconLeft
|
|
37
|
+
}
|
|
38
|
+
),
|
|
39
|
+
/* @__PURE__ */ jsx(
|
|
40
|
+
"input",
|
|
41
|
+
{
|
|
42
|
+
id: inputId,
|
|
43
|
+
className: [
|
|
44
|
+
"field relative z-10 w-full text-ink caret-ink",
|
|
45
|
+
layered ? "rounded-[11.5px] py-3 text-[15px]" : "rounded-xl py-3 text-[13.5px]",
|
|
46
|
+
"placeholder:text-ink-faint placeholder:transition-colors placeholder:duration-200 focus:placeholder:text-ink-ghost",
|
|
47
|
+
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
48
|
+
iconLeft ? "pl-10" : "pl-4",
|
|
49
|
+
iconRight ? "pr-10" : "pr-4",
|
|
50
|
+
className
|
|
51
|
+
].filter(Boolean).join(" "),
|
|
52
|
+
"aria-invalid": error ? true : void 0,
|
|
53
|
+
"aria-describedby": messageId,
|
|
54
|
+
...rest
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
"span",
|
|
59
|
+
{
|
|
60
|
+
"aria-hidden": true,
|
|
61
|
+
className: ["pointer-events-none absolute inset-0 z-15 overflow-hidden", layered ? "rounded-[11.5px]" : "rounded-xl"].join(" "),
|
|
62
|
+
children: /* @__PURE__ */ jsx(
|
|
63
|
+
"span",
|
|
64
|
+
{
|
|
65
|
+
className: "block absolute inset-y-0 w-[30%] -left-[30%] opacity-0 group-focus-within/input:animate-input-shimmer",
|
|
66
|
+
style: {
|
|
67
|
+
background: "linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
iconRight && /* @__PURE__ */ jsx(
|
|
74
|
+
"span",
|
|
75
|
+
{
|
|
76
|
+
className: "absolute right-3.5 top-1/2 -translate-y-1/2 text-ink-faint transition-all duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)] group-focus-within/input:text-ink-soft group-focus-within/input:scale-110 pointer-events-none z-20 flex items-center justify-center",
|
|
77
|
+
children: iconRight
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
] })
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
error && /* @__PURE__ */ jsxs("p", { id: messageId, role: "alert", className: "text-[11.5px] text-danger font-medium flex items-center gap-1 mt-0.5", children: [
|
|
84
|
+
/* @__PURE__ */ jsxs("svg", { width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: [
|
|
85
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
86
|
+
/* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M12 8v4m0 4h.01" })
|
|
87
|
+
] }),
|
|
88
|
+
error
|
|
89
|
+
] }),
|
|
90
|
+
hint && !error && /* @__PURE__ */ jsx("p", { id: messageId, className: "text-[11.5px] text-ink-muted mt-0.5", children: hint })
|
|
91
|
+
] });
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
Input
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=Input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Input.tsx"],"sourcesContent":["import React from \"react\";\n\ninterface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {\n label?: string;\n hint?: string;\n error?: string;\n fullWidth?: boolean;\n iconLeft?: React.ReactNode;\n iconRight?: React.ReactNode;\n /** Seat the well in the same bezel as a `size=\"lg\"` button, so a field and\n a button side by side (a newsletter, a waitlist) share one height. */\n layered?: boolean;\n}\n\n/* Fields are sunken wells (see --field tokens in globals.css). Raised\n means \"press me\", sunken means \"fill me\" — that is the whole system.\n Layered keeps the well sunken; only the bezel around it is the button's. */\n\nexport function Input({\n label,\n hint,\n error,\n fullWidth = false,\n iconLeft,\n iconRight,\n layered = false,\n className = \"\",\n id,\n ...rest\n}: InputProps) {\n const inputId = id ?? label?.toLowerCase().replace(/\\s+/g, \"-\");\n const messageId = inputId && (error || hint) ? `${inputId}-message` : undefined;\n\n return (\n <div className={[\"group/field flex flex-col gap-1.5\", fullWidth ? \"w-full\" : \"\"].join(\" \")}>\n {label && (\n <label\n htmlFor={inputId}\n className=\"text-[12.5px] font-medium text-ink-label tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-ink-strong\"\n >\n {label}\n </label>\n )}\n <div\n className={layered ? \"relative rounded-[15px] p-[3.5px]\" : \"contents\"}\n style={layered ? { background: \"var(--bezel)\", boxShadow: \"var(--bezel-shadow)\" } : undefined}\n >\n <div className=\"relative group/input isolate\">\n {iconLeft && (\n <span\n className=\"absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-faint transition-all duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)] group-focus-within/input:text-ink-soft group-focus-within/input:scale-110 pointer-events-none z-20 flex items-center justify-center\"\n >\n {iconLeft}\n </span>\n )}\n <input\n id={inputId}\n className={[\n \"field relative z-10 w-full text-ink caret-ink\",\n layered ? \"rounded-[11.5px] py-3 text-[15px]\" : \"rounded-xl py-3 text-[13.5px]\",\n \"placeholder:text-ink-faint placeholder:transition-colors placeholder:duration-200 focus:placeholder:text-ink-ghost\",\n \"disabled:opacity-40 disabled:cursor-not-allowed\",\n iconLeft ? \"pl-10\" : \"pl-4\",\n iconRight ? \"pr-10\" : \"pr-4\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-invalid={error ? true : undefined}\n aria-describedby={messageId}\n {...rest}\n />\n <span\n aria-hidden\n className={[\"pointer-events-none absolute inset-0 z-15 overflow-hidden\", layered ? \"rounded-[11.5px]\" : \"rounded-xl\"].join(\" \")}\n >\n <span\n className=\"block absolute inset-y-0 w-[30%] -left-[30%] opacity-0 group-focus-within/input:animate-input-shimmer\"\n style={{\n background:\n \"linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)\",\n }}\n />\n </span>\n {iconRight && (\n <span\n className=\"absolute right-3.5 top-1/2 -translate-y-1/2 text-ink-faint transition-all duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)] group-focus-within/input:text-ink-soft group-focus-within/input:scale-110 pointer-events-none z-20 flex items-center justify-center\"\n >\n {iconRight}\n </span>\n )}\n </div>\n </div>\n {error && (\n <p id={messageId} role=\"alert\" className=\"text-[11.5px] text-danger font-medium flex items-center gap-1 mt-0.5\">\n <svg 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 {hint && !error && (\n <p id={messageId} className=\"text-[11.5px] text-ink-muted mt-0.5\">{hint}</p>\n )}\n </div>\n );\n}\n"],"mappings":";AAoCQ,cAWA,YAXA;AAlBD,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAe;AACb,QAAM,UAAU,MAAM,OAAO,YAAY,EAAE,QAAQ,QAAQ,GAAG;AAC9D,QAAM,YAAY,YAAY,SAAS,QAAQ,GAAG,OAAO,aAAa;AAEtE,SACE,qBAAC,SAAI,WAAW,CAAC,qCAAqC,YAAY,WAAW,EAAE,EAAE,KAAK,GAAG,GACtF;AAAA,aACC;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,WAAU;AAAA,QAET;AAAA;AAAA,IACH;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,UAAU,sCAAsC;AAAA,QAC3D,OAAO,UAAU,EAAE,YAAY,gBAAgB,WAAW,sBAAsB,IAAI;AAAA,QAEpF,+BAAC,SAAI,WAAU,gCACZ;AAAA,sBACC;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAW;AAAA,gBACT;AAAA,gBACA,UAAU,sCAAsC;AAAA,gBAChD;AAAA,gBACA;AAAA,gBACA,WAAW,UAAU;AAAA,gBACrB,YAAY,UAAU;AAAA,gBACtB;AAAA,cACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,cACX,gBAAc,QAAQ,OAAO;AAAA,cAC7B,oBAAkB;AAAA,cACjB,GAAG;AAAA;AAAA,UACN;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW,CAAC,6DAA6D,UAAU,qBAAqB,YAAY,EAAE,KAAK,GAAG;AAAA,cAE9H;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,OAAO;AAAA,oBACL,YACE;AAAA,kBACJ;AAAA;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UACC,aACC;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA,WAEJ;AAAA;AAAA,IACF;AAAA,IACC,SACC,qBAAC,OAAE,IAAI,WAAW,MAAK,SAAQ,WAAU,wEACvC;AAAA,2BAAC,SAAI,OAAM,MAAK,QAAO,MAAK,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,SAAQ,aACpF;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,IAED,QAAQ,CAAC,SACR,oBAAC,OAAE,IAAI,WAAW,WAAU,uCAAuC,gBAAK;AAAA,KAE5E;AAEJ;","names":[]}
|
package/dist/Kbd.d.ts
ADDED
package/dist/Kbd.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// ../../components/ui/Kbd.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
function Kbd({
|
|
4
|
+
children,
|
|
5
|
+
className = ""
|
|
6
|
+
}) {
|
|
7
|
+
return /* @__PURE__ */ jsx(
|
|
8
|
+
"kbd",
|
|
9
|
+
{
|
|
10
|
+
className: `inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-md text-[11px] font-semibold text-ink-muted ${className}`,
|
|
11
|
+
style: {
|
|
12
|
+
/* A keycap: lit top, solid 1.5px skirt below so it reads as pressable */
|
|
13
|
+
background: "var(--face)",
|
|
14
|
+
boxShadow: "var(--key-shadow)",
|
|
15
|
+
textShadow: "var(--emboss)"
|
|
16
|
+
},
|
|
17
|
+
children
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
Kbd
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=Kbd.js.map
|
package/dist/Kbd.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Kbd.tsx"],"sourcesContent":["import { ReactNode } from \"react\";\n\nexport function Kbd({\n children,\n className = \"\",\n}: {\n children: ReactNode;\n className?: string;\n}) {\n return (\n <kbd\n className={`inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-md text-[11px] font-semibold text-ink-muted ${className}`}\n style={{\n /* A keycap: lit top, solid 1.5px skirt below so it reads as pressable */\n background: \"var(--face)\",\n boxShadow: \"var(--key-shadow)\",\n textShadow: \"var(--emboss)\",\n }}\n >\n {children}\n </kbd>\n );\n}\n"],"mappings":";AAUI;AARG,SAAS,IAAI;AAAA,EAClB;AAAA,EACA,YAAY;AACd,GAGG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,4HAA4H,SAAS;AAAA,MAChJ,OAAO;AAAA;AAAA,QAEL,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,YAAY;AAAA,MACd;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
package/dist/Modal.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface ModalProps {
|
|
5
|
+
open: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
actions?: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function Modal({ open, onClose, title, description, children, actions, className, }: ModalProps): react_jsx_runtime.JSX.Element | null;
|
|
14
|
+
|
|
15
|
+
export { Modal };
|
package/dist/Modal.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/Modal.tsx
|
|
4
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { Portal } from "./Floating.js";
|
|
6
|
+
import { CloseButton } from "./CloseButton.js";
|
|
7
|
+
import { useModalBehavior } from "./useModalBehavior.js";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var CLOSE_MS = 240;
|
|
10
|
+
function Modal({
|
|
11
|
+
open,
|
|
12
|
+
onClose,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
children,
|
|
16
|
+
actions,
|
|
17
|
+
className = ""
|
|
18
|
+
}) {
|
|
19
|
+
const [mounted, setMounted] = useState(false);
|
|
20
|
+
const [visible, setVisible] = useState(false);
|
|
21
|
+
const dialogRef = useRef(null);
|
|
22
|
+
const uid = useId();
|
|
23
|
+
if (open && !mounted) setMounted(true);
|
|
24
|
+
if (!open && visible) setVisible(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open || !mounted) return;
|
|
27
|
+
const t = setTimeout(() => setMounted(false), CLOSE_MS);
|
|
28
|
+
return () => clearTimeout(t);
|
|
29
|
+
}, [open, mounted]);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
if (!mounted || !open || visible) return;
|
|
32
|
+
void dialogRef.current?.getBoundingClientRect();
|
|
33
|
+
const id = requestAnimationFrame(() => setVisible(true));
|
|
34
|
+
return () => cancelAnimationFrame(id);
|
|
35
|
+
}, [mounted, open, visible]);
|
|
36
|
+
useModalBehavior({ open, ready: visible, ref: dialogRef, onClose });
|
|
37
|
+
if (!mounted) return null;
|
|
38
|
+
return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-90 flex items-center justify-center p-4", children: [
|
|
39
|
+
/* @__PURE__ */ jsx(
|
|
40
|
+
"div",
|
|
41
|
+
{
|
|
42
|
+
"aria-hidden": true,
|
|
43
|
+
className: [
|
|
44
|
+
"absolute inset-0 backdrop-blur-[6px]",
|
|
45
|
+
"transition-opacity ease-out",
|
|
46
|
+
visible ? "opacity-100 duration-300" : "opacity-0 duration-200"
|
|
47
|
+
].join(" "),
|
|
48
|
+
style: { background: "var(--scrim)" },
|
|
49
|
+
onClick: onClose
|
|
50
|
+
}
|
|
51
|
+
),
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
ref: dialogRef,
|
|
56
|
+
role: "dialog",
|
|
57
|
+
"aria-modal": "true",
|
|
58
|
+
"aria-labelledby": title ? `${uid}-title` : void 0,
|
|
59
|
+
"aria-describedby": description ? `${uid}-desc` : void 0,
|
|
60
|
+
className: [
|
|
61
|
+
"relative z-10 w-full max-w-lg rounded-[22px] p-1.5",
|
|
62
|
+
"transition-[opacity,transform,scale,translate] ease-[var(--ease-spring)]",
|
|
63
|
+
visible ? "opacity-100 scale-100 translate-y-0 duration-300" : "opacity-0 scale-[0.94] translate-y-3 duration-200",
|
|
64
|
+
className
|
|
65
|
+
].filter(Boolean).join(" "),
|
|
66
|
+
style: { background: "var(--panel-bezel)", boxShadow: "var(--modal-shadow)" },
|
|
67
|
+
children: /* @__PURE__ */ jsxs(
|
|
68
|
+
"div",
|
|
69
|
+
{
|
|
70
|
+
className: "bg-panel-face rounded-[16px] overflow-hidden",
|
|
71
|
+
style: { boxShadow: "var(--panel-face-shadow)" },
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ jsxs("div", { className: "pl-6 pr-4 pt-5 pb-4 flex items-start gap-4", children: [
|
|
74
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-1.5 pt-1", children: [
|
|
75
|
+
title && /* @__PURE__ */ jsx(
|
|
76
|
+
"h2",
|
|
77
|
+
{
|
|
78
|
+
id: `${uid}-title`,
|
|
79
|
+
className: "text-[16px] font-semibold text-ink tracking-[-0.015em] leading-snug",
|
|
80
|
+
children: title
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
description && /* @__PURE__ */ jsx("p", { id: `${uid}-desc`, className: "text-[13px] text-ink-muted leading-relaxed", children: description })
|
|
84
|
+
] }),
|
|
85
|
+
/* @__PURE__ */ jsx(CloseButton, { label: "Close dialog", onClick: onClose })
|
|
86
|
+
] }),
|
|
87
|
+
children && /* @__PURE__ */ jsx("div", { className: "px-6 pb-5 text-[13.5px] text-ink-body leading-relaxed", children }),
|
|
88
|
+
actions && /* @__PURE__ */ jsx(
|
|
89
|
+
"div",
|
|
90
|
+
{
|
|
91
|
+
className: "px-5 py-3.5 flex items-center justify-end gap-2",
|
|
92
|
+
style: {
|
|
93
|
+
background: "var(--panel-tray)",
|
|
94
|
+
boxShadow: "inset 0 1px 0 var(--hairline)"
|
|
95
|
+
},
|
|
96
|
+
children: actions
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
] }) });
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
Modal
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=Modal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Modal.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { Portal } from \"./Floating\";\nimport { CloseButton } from \"./CloseButton\";\nimport { useModalBehavior } from \"./useModalBehavior\";\n\ninterface ModalProps {\n open: boolean;\n onClose: () => void;\n title?: string;\n description?: string;\n children?: React.ReactNode;\n actions?: React.ReactNode;\n className?: string;\n}\n\nconst CLOSE_MS = 240;\n\nexport function Modal({\n open,\n onClose,\n title,\n description,\n children,\n actions,\n className = \"\",\n}: ModalProps) {\n const [mounted, setMounted] = useState(false);\n const [visible, setVisible] = useState(false);\n const dialogRef = useRef<HTMLDivElement>(null);\n const uid = useId();\n\n /* Mounting on open and fading on close happen during render, so\n neither waits for an extra commit; only the unmount is delayed */\n if (open && !mounted) setMounted(true);\n if (!open && visible) setVisible(false);\n\n useEffect(() => {\n if (open || !mounted) return;\n const t = setTimeout(() => setMounted(false), CLOSE_MS);\n return () => clearTimeout(t);\n }, [open, mounted]);\n\n useLayoutEffect(() => {\n if (!mounted || !open || visible) return;\n void dialogRef.current?.getBoundingClientRect();\n const id = requestAnimationFrame(() => setVisible(true));\n return () => cancelAnimationFrame(id);\n }, [mounted, open, visible]);\n\n // Escape, focus trap + restore, scroll lock\n useModalBehavior({ open, ready: visible, ref: dialogRef, onClose });\n\n if (!mounted) return null;\n\n return (\n <Portal>\n <div className=\"fixed inset-0 z-90 flex items-center justify-center p-4\">\n <div\n aria-hidden\n className={[\n \"absolute inset-0 backdrop-blur-[6px]\",\n \"transition-opacity ease-out\",\n visible ? \"opacity-100 duration-300\" : \"opacity-0 duration-200\",\n ].join(\" \")}\n style={{ background: \"var(--scrim)\" }}\n onClick={onClose}\n />\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? `${uid}-title` : undefined}\n aria-describedby={description ? `${uid}-desc` : undefined}\n className={[\n \"relative z-10 w-full max-w-lg rounded-[22px] p-1.5\",\n \"transition-[opacity,transform,scale,translate] ease-[var(--ease-spring)]\",\n visible\n ? \"opacity-100 scale-100 translate-y-0 duration-300\"\n : \"opacity-0 scale-[0.94] translate-y-3 duration-200\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={{ background: \"var(--panel-bezel)\", boxShadow: \"var(--modal-shadow)\" }}\n >\n <div\n className=\"bg-panel-face rounded-[16px] overflow-hidden\"\n style={{ boxShadow: \"var(--panel-face-shadow)\" }}\n >\n <div className=\"pl-6 pr-4 pt-5 pb-4 flex items-start gap-4\">\n <div className=\"flex-1 min-w-0 flex flex-col gap-1.5 pt-1\">\n {title && (\n <h2\n id={`${uid}-title`}\n className=\"text-[16px] font-semibold text-ink tracking-[-0.015em] leading-snug\"\n >\n {title}\n </h2>\n )}\n {description && (\n <p id={`${uid}-desc`} className=\"text-[13px] text-ink-muted leading-relaxed\">\n {description}\n </p>\n )}\n </div>\n <CloseButton label=\"Close dialog\" onClick={onClose} />\n </div>\n\n {children && <div className=\"px-6 pb-5 text-[13.5px] text-ink-body leading-relaxed\">{children}</div>}\n\n {/* Footer sits in the bezel tone so actions read as a separate tray */}\n {actions && (\n <div\n className=\"px-5 py-3.5 flex items-center justify-end gap-2\"\n style={{\n background: \"var(--panel-tray)\",\n boxShadow: \"inset 0 1px 0 var(--hairline)\",\n }}\n >\n {actions}\n </div>\n )}\n </div>\n </div>\n </div>\n </Portal>\n );\n}\n"],"mappings":";;;AAEA,SAAgB,WAAW,OAAO,iBAAiB,QAAQ,gBAAgB;AAC3E,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AAsD3B,cAiCM,YAjCN;AA1CN,IAAM,WAAW;AAEV,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAAe;AACb,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,YAAY,OAAuB,IAAI;AAC7C,QAAM,MAAM,MAAM;AAIlB,MAAI,QAAQ,CAAC,QAAS,YAAW,IAAI;AACrC,MAAI,CAAC,QAAQ,QAAS,YAAW,KAAK;AAEtC,YAAU,MAAM;AACd,QAAI,QAAQ,CAAC,QAAS;AACtB,UAAM,IAAI,WAAW,MAAM,WAAW,KAAK,GAAG,QAAQ;AACtD,WAAO,MAAM,aAAa,CAAC;AAAA,EAC7B,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,kBAAgB,MAAM;AACpB,QAAI,CAAC,WAAW,CAAC,QAAQ,QAAS;AAClC,SAAK,UAAU,SAAS,sBAAsB;AAC9C,UAAM,KAAK,sBAAsB,MAAM,WAAW,IAAI,CAAC;AACvD,WAAO,MAAM,qBAAqB,EAAE;AAAA,EACtC,GAAG,CAAC,SAAS,MAAM,OAAO,CAAC;AAG3B,mBAAiB,EAAE,MAAM,OAAO,SAAS,KAAK,WAAW,QAAQ,CAAC;AAElE,MAAI,CAAC,QAAS,QAAO;AAErB,SACE,oBAAC,UACD,+BAAC,SAAI,WAAU,2DACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,UAAU,6BAA6B;AAAA,QACzC,EAAE,KAAK,GAAG;AAAA,QACV,OAAO,EAAE,YAAY,eAAe;AAAA,QACpC,SAAS;AAAA;AAAA,IACX;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QACX,mBAAiB,QAAQ,GAAG,GAAG,WAAW;AAAA,QAC1C,oBAAkB,cAAc,GAAG,GAAG,UAAU;AAAA,QAChD,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,UACI,qDACA;AAAA,UACJ;AAAA,QACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,QACX,OAAO,EAAE,YAAY,sBAAsB,WAAW,sBAAsB;AAAA,QAE5E;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,WAAW,2BAA2B;AAAA,YAE/C;AAAA,mCAAC,SAAI,WAAU,8CACb;AAAA,qCAAC,SAAI,WAAU,6CACZ;AAAA,2BACC;AAAA,oBAAC;AAAA;AAAA,sBACC,IAAI,GAAG,GAAG;AAAA,sBACV,WAAU;AAAA,sBAET;AAAA;AAAA,kBACH;AAAA,kBAED,eACC,oBAAC,OAAE,IAAI,GAAG,GAAG,SAAS,WAAU,8CAC7B,uBACH;AAAA,mBAEJ;AAAA,gBACA,oBAAC,eAAY,OAAM,gBAAe,SAAS,SAAS;AAAA,iBACtD;AAAA,cAEC,YAAY,oBAAC,SAAI,WAAU,yDAAyD,UAAS;AAAA,cAG7F,WACC;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,OAAO;AAAA,oBACL,YAAY;AAAA,oBACZ,WAAW;AAAA,kBACb;AAAA,kBAEC;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QAEJ;AAAA;AAAA,IACF;AAAA,KACF,GACA;AAEJ;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type" | "size" | "prefix"> {
|
|
5
|
+
value?: number | null;
|
|
6
|
+
defaultValue?: number | null;
|
|
7
|
+
onChange?: (value: number | null) => void;
|
|
8
|
+
min?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
step?: number;
|
|
11
|
+
/** Digits after the decimal point; inferred from `step` when left out */
|
|
12
|
+
precision?: number;
|
|
13
|
+
label?: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
/** A unit shown before or after the number, like "$" or "kg" */
|
|
17
|
+
prefix?: React.ReactNode;
|
|
18
|
+
suffix?: React.ReactNode;
|
|
19
|
+
fullWidth?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare function NumberInput({ value, defaultValue, onChange, min, max, step, precision, label, hint, error, prefix, suffix, fullWidth, disabled, className, id, onBlur, onKeyDown, ...rest }: NumberInputProps): react_jsx_runtime.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { NumberInput };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/NumberInput.tsx
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
import { FieldKey, FieldLabel, FieldMessage } from "./FieldMessage.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var decimals = (n) => (String(n).split(".")[1] ?? "").length;
|
|
8
|
+
function NumberInput({
|
|
9
|
+
value,
|
|
10
|
+
defaultValue = null,
|
|
11
|
+
onChange,
|
|
12
|
+
min = -Infinity,
|
|
13
|
+
max = Infinity,
|
|
14
|
+
step = 1,
|
|
15
|
+
precision,
|
|
16
|
+
label,
|
|
17
|
+
hint,
|
|
18
|
+
error,
|
|
19
|
+
prefix,
|
|
20
|
+
suffix,
|
|
21
|
+
fullWidth = false,
|
|
22
|
+
disabled,
|
|
23
|
+
className = "",
|
|
24
|
+
id,
|
|
25
|
+
onBlur,
|
|
26
|
+
onKeyDown,
|
|
27
|
+
...rest
|
|
28
|
+
}) {
|
|
29
|
+
const uid = useId();
|
|
30
|
+
const inputId = id ?? `${uid}-input`;
|
|
31
|
+
const messageId = error || hint ? `${inputId}-message` : void 0;
|
|
32
|
+
const digits = precision ?? decimals(step);
|
|
33
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
34
|
+
const current = value !== void 0 ? value : internal;
|
|
35
|
+
const format = (n) => n === null ? "" : n.toFixed(digits);
|
|
36
|
+
const [draft, setDraft] = useState(format(current));
|
|
37
|
+
const editing = useRef(false);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!editing.current) setDraft(format(current));
|
|
40
|
+
}, [current, digits]);
|
|
41
|
+
const clamp = (n) => Math.min(max, Math.max(min, Number(n.toFixed(digits))));
|
|
42
|
+
const latest = useRef(current);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
latest.current = current;
|
|
45
|
+
}, [current]);
|
|
46
|
+
const commit = (n) => {
|
|
47
|
+
const next = n === null || Number.isNaN(n) ? null : clamp(n);
|
|
48
|
+
const prev = latest.current;
|
|
49
|
+
latest.current = next;
|
|
50
|
+
setInternal(next);
|
|
51
|
+
setDraft(format(next));
|
|
52
|
+
if (next !== prev) onChange?.(next);
|
|
53
|
+
};
|
|
54
|
+
const nudge = (dir, times = 1) => {
|
|
55
|
+
const base = latest.current;
|
|
56
|
+
commit(base === null ? clamp(0) : base + dir * step * times);
|
|
57
|
+
};
|
|
58
|
+
const hold = useRef({});
|
|
59
|
+
const stopHold = () => {
|
|
60
|
+
clearTimeout(hold.current.t);
|
|
61
|
+
clearInterval(hold.current.i);
|
|
62
|
+
};
|
|
63
|
+
useEffect(() => stopHold, []);
|
|
64
|
+
const startHold = (dir) => {
|
|
65
|
+
stopHold();
|
|
66
|
+
nudge(dir);
|
|
67
|
+
hold.current.t = setTimeout(() => {
|
|
68
|
+
hold.current.i = setInterval(() => nudge(dir), 60);
|
|
69
|
+
}, 400);
|
|
70
|
+
};
|
|
71
|
+
const atMin = current !== null && current <= min;
|
|
72
|
+
const atMax = current !== null && current >= max;
|
|
73
|
+
return /* @__PURE__ */ jsxs("div", { className: ["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" "), children: [
|
|
74
|
+
label && /* @__PURE__ */ jsx(FieldLabel, { htmlFor: inputId, children: label }),
|
|
75
|
+
/* @__PURE__ */ jsxs("div", { className: ["relative isolate", fullWidth ? "w-full" : "w-40", className].join(" "), children: [
|
|
76
|
+
prefix && /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute left-3.5 top-1/2 z-20 -translate-y-1/2 text-[13.5px] text-ink-faint transition-colors duration-200 group-focus-within/field:text-ink-label", children: prefix }),
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
"input",
|
|
79
|
+
{
|
|
80
|
+
...rest,
|
|
81
|
+
id: inputId,
|
|
82
|
+
type: "text",
|
|
83
|
+
inputMode: digits > 0 || min < 0 ? "decimal" : "numeric",
|
|
84
|
+
autoComplete: "off",
|
|
85
|
+
role: "spinbutton",
|
|
86
|
+
"aria-valuenow": current ?? void 0,
|
|
87
|
+
"aria-valuemin": Number.isFinite(min) ? min : void 0,
|
|
88
|
+
"aria-valuemax": Number.isFinite(max) ? max : void 0,
|
|
89
|
+
"aria-invalid": error ? true : void 0,
|
|
90
|
+
"aria-describedby": messageId,
|
|
91
|
+
disabled,
|
|
92
|
+
value: draft,
|
|
93
|
+
onChange: (e) => {
|
|
94
|
+
editing.current = true;
|
|
95
|
+
setDraft(e.target.value.replace(/[^\d.,\-]/g, ""));
|
|
96
|
+
},
|
|
97
|
+
onBlur: (e) => {
|
|
98
|
+
editing.current = false;
|
|
99
|
+
const raw = draft.replace(",", ".").trim();
|
|
100
|
+
commit(raw === "" || raw === "-" ? null : parseFloat(raw));
|
|
101
|
+
onBlur?.(e);
|
|
102
|
+
},
|
|
103
|
+
onKeyDown: (e) => {
|
|
104
|
+
onKeyDown?.(e);
|
|
105
|
+
if (e.defaultPrevented) return;
|
|
106
|
+
const big = e.shiftKey ? 10 : 1;
|
|
107
|
+
if (e.key === "ArrowUp") {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
editing.current = false;
|
|
110
|
+
nudge(1, big);
|
|
111
|
+
} else if (e.key === "ArrowDown") {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
editing.current = false;
|
|
114
|
+
nudge(-1, big);
|
|
115
|
+
} else if (e.key === "PageUp") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
nudge(1, 10);
|
|
118
|
+
} else if (e.key === "PageDown") {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
nudge(-1, 10);
|
|
121
|
+
} else if (e.key === "Home" && Number.isFinite(min)) {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
commit(min);
|
|
124
|
+
} else if (e.key === "End" && Number.isFinite(max)) {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
commit(max);
|
|
127
|
+
} else if (e.key === "Enter") {
|
|
128
|
+
e.target.blur();
|
|
129
|
+
e.target.focus();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
className: [
|
|
133
|
+
"field relative z-10 w-full rounded-xl py-3 text-[13.5px] tabular-nums text-ink caret-ink",
|
|
134
|
+
"placeholder:text-ink-faint focus:placeholder:text-ink-ghost",
|
|
135
|
+
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
136
|
+
prefix ? "pl-8" : "pl-4",
|
|
137
|
+
suffix ? "pr-[92px]" : "pr-[64px]"
|
|
138
|
+
].join(" ")
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
suffix && /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute right-[64px] top-1/2 z-20 -translate-y-1/2 text-[12.5px] text-ink-faint", children: suffix }),
|
|
142
|
+
/* @__PURE__ */ jsxs(
|
|
143
|
+
"div",
|
|
144
|
+
{
|
|
145
|
+
className: [
|
|
146
|
+
"absolute right-1.5 top-1/2 z-20 flex -translate-y-1/2 gap-1",
|
|
147
|
+
disabled ? "opacity-40 pointer-events-none" : ""
|
|
148
|
+
].join(" "),
|
|
149
|
+
children: [
|
|
150
|
+
/* @__PURE__ */ jsx(FieldKey, { label: "decrease", focusable: false, disabled: atMin, onPress: () => startHold(-1), onRelease: stopHold, children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) }),
|
|
151
|
+
/* @__PURE__ */ jsx(FieldKey, { label: "increase", focusable: false, disabled: atMax, onPress: () => startHold(1), onRelease: stopHold, children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" }) })
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
] }),
|
|
156
|
+
/* @__PURE__ */ jsx(FieldMessage, { id: messageId, error, hint })
|
|
157
|
+
] });
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
NumberInput
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=NumberInput.js.map
|