@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/NumberInput.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useId, useRef, useState } from \"react\";\nimport { FieldKey, FieldLabel, FieldMessage } from \"./FieldMessage\";\n\ninterface NumberInputProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"size\" | \"prefix\"> {\n value?: number | null;\n defaultValue?: number | null;\n onChange?: (value: number | null) => void;\n min?: number;\n max?: number;\n step?: number;\n /** Digits after the decimal point; inferred from `step` when left out */\n precision?: number;\n label?: string;\n hint?: string;\n error?: string;\n /** A unit shown before or after the number, like \"$\" or \"kg\" */\n prefix?: React.ReactNode;\n suffix?: React.ReactNode;\n fullWidth?: boolean;\n}\n\nconst decimals = (n: number) => (String(n).split(\".\")[1] ?? \"\").length;\n\n/* A sunken field with two small raised keys at its right edge. The keys\n repeat while held, arrow keys step (shift for ten at a time), and\n whatever you type is clamped and rounded when you leave the field. */\nexport function NumberInput({\n value,\n defaultValue = null,\n onChange,\n min = -Infinity,\n max = Infinity,\n step = 1,\n precision,\n label,\n hint,\n error,\n prefix,\n suffix,\n fullWidth = false,\n disabled,\n className = \"\",\n id,\n onBlur,\n onKeyDown,\n ...rest\n}: NumberInputProps) {\n const uid = useId();\n const inputId = id ?? `${uid}-input`;\n const messageId = error || hint ? `${inputId}-message` : undefined;\n const digits = precision ?? decimals(step);\n\n const [internal, setInternal] = useState<number | null>(defaultValue);\n const current = value !== undefined ? value : internal;\n const format = (n: number | null) => (n === null ? \"\" : n.toFixed(digits));\n const [draft, setDraft] = useState(format(current));\n const editing = useRef(false);\n\n // Follow the controlled value unless someone is mid-way through typing\n useEffect(() => {\n if (!editing.current) setDraft(format(current));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [current, digits]);\n\n const clamp = (n: number) => Math.min(max, Math.max(min, Number(n.toFixed(digits))));\n\n // The hold-to-repeat timer reads the newest value from here, not a stale render\n const latest = useRef(current);\n useEffect(() => {\n latest.current = current;\n }, [current]);\n\n const commit = (n: number | null) => {\n const next = n === null || Number.isNaN(n) ? null : clamp(n);\n const prev = latest.current;\n latest.current = next;\n setInternal(next);\n setDraft(format(next));\n if (next !== prev) onChange?.(next);\n };\n\n // An empty field starts from zero, or from the nearest edge of the range\n const nudge = (dir: 1 | -1, times = 1) => {\n const base = latest.current;\n commit(base === null ? clamp(0) : base + dir * step * times);\n };\n\n const hold = useRef<{ t?: ReturnType<typeof setTimeout>; i?: ReturnType<typeof setInterval> }>({});\n const stopHold = () => {\n clearTimeout(hold.current.t);\n clearInterval(hold.current.i);\n };\n useEffect(() => stopHold, []);\n const startHold = (dir: 1 | -1) => {\n stopHold();\n nudge(dir);\n hold.current.t = setTimeout(() => {\n hold.current.i = setInterval(() => nudge(dir), 60);\n }, 400);\n };\n\n const atMin = current !== null && current <= min;\n const atMax = current !== null && current >= max;\n\n return (\n <div className={[\"group/field flex flex-col gap-1.5\", fullWidth ? \"w-full\" : \"\"].join(\" \")}>\n {label && <FieldLabel htmlFor={inputId}>{label}</FieldLabel>}\n <div className={[\"relative isolate\", fullWidth ? \"w-full\" : \"w-40\", className].join(\" \")}>\n {prefix && (\n <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\">\n {prefix}\n </span>\n )}\n <input\n {...rest}\n id={inputId}\n type=\"text\"\n inputMode={digits > 0 || min < 0 ? \"decimal\" : \"numeric\"}\n autoComplete=\"off\"\n role=\"spinbutton\"\n aria-valuenow={current ?? undefined}\n aria-valuemin={Number.isFinite(min) ? min : undefined}\n aria-valuemax={Number.isFinite(max) ? max : undefined}\n aria-invalid={error ? true : undefined}\n aria-describedby={messageId}\n disabled={disabled}\n value={draft}\n onChange={(e) => {\n editing.current = true;\n setDraft(e.target.value.replace(/[^\\d.,\\-]/g, \"\"));\n }}\n onBlur={(e) => {\n editing.current = false;\n const raw = draft.replace(\",\", \".\").trim();\n commit(raw === \"\" || raw === \"-\" ? null : parseFloat(raw));\n onBlur?.(e);\n }}\n onKeyDown={(e) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n const big = e.shiftKey ? 10 : 1;\n if (e.key === \"ArrowUp\") { e.preventDefault(); editing.current = false; nudge(1, big); }\n else if (e.key === \"ArrowDown\") { e.preventDefault(); editing.current = false; nudge(-1, big); }\n else if (e.key === \"PageUp\") { e.preventDefault(); nudge(1, 10); }\n else if (e.key === \"PageDown\") { e.preventDefault(); nudge(-1, 10); }\n else if (e.key === \"Home\" && Number.isFinite(min)) { e.preventDefault(); commit(min); }\n else if (e.key === \"End\" && Number.isFinite(max)) { e.preventDefault(); commit(max); }\n else if (e.key === \"Enter\") { (e.target as HTMLInputElement).blur(); (e.target as HTMLInputElement).focus(); }\n }}\n className={[\n \"field relative z-10 w-full rounded-xl py-3 text-[13.5px] tabular-nums text-ink caret-ink\",\n \"placeholder:text-ink-faint focus:placeholder:text-ink-ghost\",\n \"disabled:opacity-40 disabled:cursor-not-allowed\",\n prefix ? \"pl-8\" : \"pl-4\",\n suffix ? \"pr-[92px]\" : \"pr-[64px]\",\n ].join(\" \")}\n />\n {suffix && (\n <span className=\"pointer-events-none absolute right-[64px] top-1/2 z-20 -translate-y-1/2 text-[12.5px] text-ink-faint\">\n {suffix}\n </span>\n )}\n <div\n className={[\n \"absolute right-1.5 top-1/2 z-20 flex -translate-y-1/2 gap-1\",\n disabled ? \"opacity-40 pointer-events-none\" : \"\",\n ].join(\" \")}\n >\n <FieldKey label=\"decrease\" focusable={false} disabled={atMin} onPress={() => startHold(-1)} onRelease={stopHold}>\n <path d=\"M5 12h14\" />\n </FieldKey>\n <FieldKey label=\"increase\" focusable={false} disabled={atMax} onPress={() => startHold(1)} onRelease={stopHold}>\n <path d=\"M12 5v14M5 12h14\" />\n </FieldKey>\n </div>\n </div>\n <FieldMessage id={messageId} error={error} hint={hint} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface OtpInputProps {
|
|
4
|
+
/** How many characters */
|
|
5
|
+
length?: number;
|
|
6
|
+
value?: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
onChange?: (code: string) => void;
|
|
9
|
+
/** Fires once every cell is filled, with the whole code */
|
|
10
|
+
onComplete?: (code: string) => void;
|
|
11
|
+
/** Digits only, or letters and digits */
|
|
12
|
+
type?: "numeric" | "alphanumeric";
|
|
13
|
+
/** Split the cells into wells of this many, like 123 456 */
|
|
14
|
+
groupSize?: number;
|
|
15
|
+
/** Show dots instead of the characters */
|
|
16
|
+
mask?: boolean;
|
|
17
|
+
label?: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
autoFocus?: boolean;
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
declare function OtpInput({ length, value, defaultValue, onChange, onComplete, type, groupSize, mask, label, hint, error, disabled, autoFocus, name, }: OtpInputProps): react_jsx_runtime.JSX.Element;
|
|
25
|
+
|
|
26
|
+
export { OtpInput };
|
package/dist/OtpInput.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/OtpInput.tsx
|
|
4
|
+
import React, { useId, useRef, useState } from "react";
|
|
5
|
+
import { FieldLabel, FieldMessage } from "./FieldMessage.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var CELL = 44;
|
|
8
|
+
function OtpInput({
|
|
9
|
+
length = 6,
|
|
10
|
+
value,
|
|
11
|
+
defaultValue = "",
|
|
12
|
+
onChange,
|
|
13
|
+
onComplete,
|
|
14
|
+
type = "numeric",
|
|
15
|
+
groupSize,
|
|
16
|
+
mask = false,
|
|
17
|
+
label,
|
|
18
|
+
hint,
|
|
19
|
+
error,
|
|
20
|
+
disabled = false,
|
|
21
|
+
autoFocus = false,
|
|
22
|
+
name
|
|
23
|
+
}) {
|
|
24
|
+
const uid = useId();
|
|
25
|
+
const inputRef = useRef(null);
|
|
26
|
+
const [internal, setInternal] = useState(defaultValue.slice(0, length));
|
|
27
|
+
const code = (value !== void 0 ? value : internal).slice(0, length);
|
|
28
|
+
const [focused, setFocused] = useState(false);
|
|
29
|
+
const [cursor, setCursor] = useState(0);
|
|
30
|
+
const messageId = error || hint ? `${uid}-message` : void 0;
|
|
31
|
+
const at = Math.min(cursor, code.length, length - 1);
|
|
32
|
+
const pattern = type === "numeric" ? /[^0-9]/g : /[^0-9a-z]/gi;
|
|
33
|
+
const selectCell = (i) => {
|
|
34
|
+
const el = inputRef.current;
|
|
35
|
+
if (!el) return;
|
|
36
|
+
const n = el.value.length;
|
|
37
|
+
const pos = Math.max(0, Math.min(i, n));
|
|
38
|
+
el.setSelectionRange(pos, pos < n ? pos + 1 : pos);
|
|
39
|
+
setCursor(pos);
|
|
40
|
+
};
|
|
41
|
+
const groups = groupSize && groupSize < length ? Math.ceil(length / groupSize) : 1;
|
|
42
|
+
const per = groups > 1 ? groupSize : length;
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", { className: "group/field flex flex-col gap-1.5", children: [
|
|
44
|
+
label && /* @__PURE__ */ jsx(FieldLabel, { id: `${uid}-label`, htmlFor: `${uid}-input`, children: label }),
|
|
45
|
+
/* @__PURE__ */ jsxs(
|
|
46
|
+
"div",
|
|
47
|
+
{
|
|
48
|
+
className: ["relative flex w-fit items-center gap-3", disabled ? "opacity-40" : ""].join(" "),
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
"input",
|
|
52
|
+
{
|
|
53
|
+
ref: inputRef,
|
|
54
|
+
id: `${uid}-input`,
|
|
55
|
+
type: mask ? "password" : "text",
|
|
56
|
+
inputMode: type === "numeric" ? "numeric" : "text",
|
|
57
|
+
autoComplete: "one-time-code",
|
|
58
|
+
autoCapitalize: "characters",
|
|
59
|
+
autoFocus,
|
|
60
|
+
spellCheck: false,
|
|
61
|
+
maxLength: length,
|
|
62
|
+
disabled,
|
|
63
|
+
value: code,
|
|
64
|
+
"aria-invalid": error ? true : void 0,
|
|
65
|
+
"aria-describedby": messageId,
|
|
66
|
+
"aria-label": label ? void 0 : `${length}-character code`,
|
|
67
|
+
onClick: (e) => {
|
|
68
|
+
const cell = document.elementsFromPoint(e.clientX, e.clientY).find((el) => el instanceof HTMLElement && el.dataset.cell !== void 0);
|
|
69
|
+
selectCell(cell ? Number(cell.dataset.cell) : code.length);
|
|
70
|
+
},
|
|
71
|
+
onFocus: () => {
|
|
72
|
+
setFocused(true);
|
|
73
|
+
requestAnimationFrame(() => selectCell(code.length));
|
|
74
|
+
},
|
|
75
|
+
onBlur: () => setFocused(false),
|
|
76
|
+
onChange: (e) => {
|
|
77
|
+
const next = e.target.value.replace(pattern, "").toUpperCase().slice(0, length);
|
|
78
|
+
setInternal(next);
|
|
79
|
+
onChange?.(next);
|
|
80
|
+
if (next.length === length && next !== code) onComplete?.(next);
|
|
81
|
+
const pos = Math.min(e.target.selectionStart ?? next.length, next.length);
|
|
82
|
+
requestAnimationFrame(() => selectCell(pos));
|
|
83
|
+
},
|
|
84
|
+
onKeyDown: (e) => {
|
|
85
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
selectCell(at + (e.key === "ArrowLeft" ? -1 : 1));
|
|
88
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
selectCell(e.key === "Home" ? 0 : code.length);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
onSelect: (e) => {
|
|
94
|
+
const el = e.currentTarget;
|
|
95
|
+
setCursor(Math.min(el.selectionStart ?? 0, length - 1));
|
|
96
|
+
},
|
|
97
|
+
className: "absolute inset-0 z-30 h-full w-full cursor-text opacity-0 disabled:cursor-not-allowed",
|
|
98
|
+
style: {
|
|
99
|
+
fontSize: 16
|
|
100
|
+
/* stops iOS zooming in on focus */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
Array.from({ length: groups }, (_, g) => {
|
|
105
|
+
const start = g * per;
|
|
106
|
+
const cells = Math.min(per, length - start);
|
|
107
|
+
const caretHere = focused && at >= start && at < start + cells;
|
|
108
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
109
|
+
g > 0 && /* @__PURE__ */ jsx(
|
|
110
|
+
"span",
|
|
111
|
+
{
|
|
112
|
+
"aria-hidden": true,
|
|
113
|
+
className: "h-[2px] w-2.5 shrink-0 rounded-full",
|
|
114
|
+
style: { background: "var(--groove)", boxShadow: "var(--groove-lip-below)" }
|
|
115
|
+
}
|
|
116
|
+
),
|
|
117
|
+
/* @__PURE__ */ jsxs(
|
|
118
|
+
"div",
|
|
119
|
+
{
|
|
120
|
+
"aria-hidden": true,
|
|
121
|
+
"data-open": caretHere || void 0,
|
|
122
|
+
"data-invalid": error ? true : void 0,
|
|
123
|
+
className: "field relative z-10 flex h-[52px] overflow-hidden rounded-xl",
|
|
124
|
+
children: [
|
|
125
|
+
Array.from({ length: cells }, (_2, k) => {
|
|
126
|
+
const i = start + k;
|
|
127
|
+
const ch = code[i];
|
|
128
|
+
return /* @__PURE__ */ jsxs("div", { "data-cell": i, className: "relative grid place-items-center", style: { width: CELL }, children: [
|
|
129
|
+
k > 0 && /* @__PURE__ */ jsx(
|
|
130
|
+
"span",
|
|
131
|
+
{
|
|
132
|
+
className: "absolute inset-y-3 left-0 w-px",
|
|
133
|
+
style: { background: "var(--groove)", boxShadow: "var(--groove-lip)" }
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
ch && (mask ? /* @__PURE__ */ jsx("span", { className: "animate-checkbox-in h-2.5 w-2.5 rounded-full bg-ink" }, ch + i) : /* @__PURE__ */ jsx("span", { className: "animate-checkbox-in text-[20px] font-semibold tabular-nums text-ink", children: ch }, ch + i)),
|
|
137
|
+
caretHere && i === at && ch && /* @__PURE__ */ jsx("span", { className: "absolute inset-x-1.5 bottom-2 h-[2px] rounded-full bg-ink animate-otp-caret" })
|
|
138
|
+
] }, i);
|
|
139
|
+
}),
|
|
140
|
+
caretHere && !code[at] && /* @__PURE__ */ jsx(
|
|
141
|
+
"span",
|
|
142
|
+
{
|
|
143
|
+
className: "pointer-events-none absolute top-1/2 h-[22px] w-[2px] -translate-y-1/2",
|
|
144
|
+
style: {
|
|
145
|
+
left: (at - start) * CELL + CELL / 2 - 1,
|
|
146
|
+
transition: "left 260ms var(--ease-spring)"
|
|
147
|
+
},
|
|
148
|
+
children: /* @__PURE__ */ jsx("span", { className: "block h-full w-full animate-otp-caret rounded-full bg-ink" })
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
caretHere && /* @__PURE__ */ jsx(
|
|
152
|
+
"span",
|
|
153
|
+
{
|
|
154
|
+
className: "pointer-events-none absolute inset-y-0 -left-[30%] block w-[30%] animate-input-shimmer",
|
|
155
|
+
style: { background: "linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)" }
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
] }, g);
|
|
162
|
+
})
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: code }),
|
|
167
|
+
/* @__PURE__ */ jsx(FieldMessage, { id: messageId, error, hint })
|
|
168
|
+
] });
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
OtpInput
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=OtpInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/OtpInput.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useId, useRef, useState } from \"react\";\nimport { FieldLabel, FieldMessage } from \"./FieldMessage\";\n\ninterface OtpInputProps {\n /** How many characters */\n length?: number;\n value?: string;\n defaultValue?: string;\n onChange?: (code: string) => void;\n /** Fires once every cell is filled, with the whole code */\n onComplete?: (code: string) => void;\n /** Digits only, or letters and digits */\n type?: \"numeric\" | \"alphanumeric\";\n /** Split the cells into wells of this many, like 123 456 */\n groupSize?: number;\n /** Show dots instead of the characters */\n mask?: boolean;\n label?: string;\n hint?: string;\n error?: string;\n disabled?: boolean;\n autoFocus?: boolean;\n name?: string;\n}\n\nconst CELL = 44;\n\n/* One real input sits underneath, so typing, backspace, paste and the\n phone filling the code from a text all behave natively. What you see\n is drawn over it: each group is a single sunken field, cut into cells\n by scored grooves, that lifts flush and draws its border while you're\n in it, exactly like Input. A caret glides from cell to cell on a\n spring as you type, and characters press into place. */\nexport function OtpInput({\n length = 6,\n value,\n defaultValue = \"\",\n onChange,\n onComplete,\n type = \"numeric\",\n groupSize,\n mask = false,\n label,\n hint,\n error,\n disabled = false,\n autoFocus = false,\n name,\n}: OtpInputProps) {\n const uid = useId();\n const inputRef = useRef<HTMLInputElement>(null);\n const [internal, setInternal] = useState(defaultValue.slice(0, length));\n const code = (value !== undefined ? value : internal).slice(0, length);\n const [focused, setFocused] = useState(false);\n // The cell the caret is in: the selection start, kept within the code\n const [cursor, setCursor] = useState(0);\n const messageId = error || hint ? `${uid}-message` : undefined;\n const at = Math.min(cursor, code.length, length - 1);\n\n const pattern = type === \"numeric\" ? /[^0-9]/g : /[^0-9a-z]/gi;\n\n /* A filled cell is selected rather than sitting behind a caret, so\n typing over it replaces it in place */\n const selectCell = (i: number) => {\n const el = inputRef.current;\n if (!el) return;\n const n = el.value.length;\n const pos = Math.max(0, Math.min(i, n));\n el.setSelectionRange(pos, pos < n ? pos + 1 : pos);\n setCursor(pos);\n };\n\n const groups = groupSize && groupSize < length ? Math.ceil(length / groupSize) : 1;\n const per = groups > 1 ? groupSize! : length;\n\n return (\n <div className=\"group/field flex flex-col gap-1.5\">\n {label && (\n <FieldLabel id={`${uid}-label`} htmlFor={`${uid}-input`}>\n {label}\n </FieldLabel>\n )}\n\n <div\n className={[\"relative flex w-fit items-center gap-3\", disabled ? \"opacity-40\" : \"\"].join(\" \")}\n >\n <input\n ref={inputRef}\n id={`${uid}-input`}\n type={mask ? \"password\" : \"text\"}\n inputMode={type === \"numeric\" ? \"numeric\" : \"text\"}\n autoComplete=\"one-time-code\"\n autoCapitalize=\"characters\"\n autoFocus={autoFocus}\n spellCheck={false}\n maxLength={length}\n disabled={disabled}\n value={code}\n aria-invalid={error ? true : undefined}\n aria-describedby={messageId}\n aria-label={label ? undefined : `${length}-character code`}\n // The input lies over the cells; send the caret to whichever was clicked\n onClick={(e) => {\n const cell = document\n .elementsFromPoint(e.clientX, e.clientY)\n .find((el): el is HTMLElement => el instanceof HTMLElement && el.dataset.cell !== undefined);\n selectCell(cell ? Number(cell.dataset.cell) : code.length);\n }}\n onFocus={() => {\n setFocused(true);\n requestAnimationFrame(() => selectCell(code.length));\n }}\n onBlur={() => setFocused(false)}\n onChange={(e) => {\n const next = e.target.value.replace(pattern, \"\").toUpperCase().slice(0, length);\n setInternal(next);\n onChange?.(next);\n if (next.length === length && next !== code) onComplete?.(next);\n const pos = Math.min(e.target.selectionStart ?? next.length, next.length);\n requestAnimationFrame(() => selectCell(pos));\n }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n selectCell(at + (e.key === \"ArrowLeft\" ? -1 : 1));\n } else if (e.key === \"Home\" || e.key === \"End\") {\n e.preventDefault();\n selectCell(e.key === \"Home\" ? 0 : code.length);\n }\n }}\n onSelect={(e) => {\n const el = e.currentTarget;\n setCursor(Math.min(el.selectionStart ?? 0, length - 1));\n }}\n // Invisible, but it's what a tap lands on, so phones raise their keyboard\n className=\"absolute inset-0 z-30 h-full w-full cursor-text opacity-0 disabled:cursor-not-allowed\"\n style={{ fontSize: 16 /* stops iOS zooming in on focus */ }}\n />\n\n {Array.from({ length: groups }, (_, g) => {\n const start = g * per;\n const cells = Math.min(per, length - start);\n const caretHere = focused && at >= start && at < start + cells;\n return (\n <React.Fragment key={g}>\n {g > 0 && (\n <span\n aria-hidden\n className=\"h-[2px] w-2.5 shrink-0 rounded-full\"\n style={{ background: \"var(--groove)\", boxShadow: \"var(--groove-lip-below)\" }}\n />\n )}\n {/* One field per group: data-open gives it the focused well */}\n <div\n aria-hidden\n data-open={caretHere || undefined}\n data-invalid={error ? true : undefined}\n className=\"field relative z-10 flex h-[52px] overflow-hidden rounded-xl\"\n >\n {Array.from({ length: cells }, (_, k) => {\n const i = start + k;\n const ch = code[i];\n return (\n <div key={i} data-cell={i} className=\"relative grid place-items-center\" style={{ width: CELL }}>\n {k > 0 && (\n <span\n className=\"absolute inset-y-3 left-0 w-px\"\n style={{ background: \"var(--groove)\", boxShadow: \"var(--groove-lip)\" }}\n />\n )}\n {ch &&\n (mask ? (\n <span key={ch + i} className=\"animate-checkbox-in h-2.5 w-2.5 rounded-full bg-ink\" />\n ) : (\n <span key={ch + i} className=\"animate-checkbox-in text-[20px] font-semibold tabular-nums text-ink\">\n {ch}\n </span>\n ))}\n {/* A filled cell under the caret is picked out, ready to be typed over */}\n {caretHere && i === at && ch && (\n <span className=\"absolute inset-x-1.5 bottom-2 h-[2px] rounded-full bg-ink animate-otp-caret\" />\n )}\n </div>\n );\n })}\n\n {/* The caret glides between the empty cells of this group */}\n {caretHere && !code[at] && (\n <span\n className=\"pointer-events-none absolute top-1/2 h-[22px] w-[2px] -translate-y-1/2\"\n style={{\n left: (at - start) * CELL + CELL / 2 - 1,\n transition: \"left 260ms var(--ease-spring)\",\n }}\n >\n <span className=\"block h-full w-full animate-otp-caret rounded-full bg-ink\" />\n </span>\n )}\n\n {/* Input's sweep of light as the well comes up */}\n {caretHere && (\n <span\n className=\"pointer-events-none absolute inset-y-0 -left-[30%] block w-[30%] animate-input-shimmer\"\n style={{ background: \"linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)\" }}\n />\n )}\n </div>\n </React.Fragment>\n );\n })}\n </div>\n\n {name && <input type=\"hidden\" name={name} value={code} />}\n <FieldMessage id={messageId} error={error} hint={hint} />\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface PaginationProps {
|
|
4
|
+
currentPage: number;
|
|
5
|
+
totalPages: number;
|
|
6
|
+
onPageChange: (page: number) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
declare function Pagination({ currentPage, totalPages, onPageChange, className, }: PaginationProps): react_jsx_runtime.JSX.Element;
|
|
10
|
+
|
|
11
|
+
export { Pagination };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/Pagination.tsx
|
|
4
|
+
import { useRef, useState, useEffect, useLayoutEffect } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function Pagination({
|
|
7
|
+
currentPage,
|
|
8
|
+
totalPages,
|
|
9
|
+
onPageChange,
|
|
10
|
+
className = ""
|
|
11
|
+
}) {
|
|
12
|
+
const slots = buildSlots(currentPage, totalPages);
|
|
13
|
+
const [pill, setPill] = useState({ left: 0, width: 0, ready: false });
|
|
14
|
+
const containerRef = useRef(null);
|
|
15
|
+
const btnRefs = useRef({});
|
|
16
|
+
const measurePill = (page) => {
|
|
17
|
+
const btn = btnRefs.current[String(page)];
|
|
18
|
+
const container = containerRef.current;
|
|
19
|
+
if (!btn || !container) return;
|
|
20
|
+
const cr = container.getBoundingClientRect();
|
|
21
|
+
const br = btn.getBoundingClientRect();
|
|
22
|
+
setPill({ left: br.left - cr.left, width: br.width, ready: true });
|
|
23
|
+
};
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
measurePill(currentPage);
|
|
26
|
+
}, [currentPage]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const ro = new ResizeObserver(() => measurePill(currentPage));
|
|
29
|
+
if (containerRef.current) ro.observe(containerRef.current);
|
|
30
|
+
return () => ro.disconnect();
|
|
31
|
+
}, [currentPage]);
|
|
32
|
+
return /* @__PURE__ */ jsxs(
|
|
33
|
+
"nav",
|
|
34
|
+
{
|
|
35
|
+
"aria-label": "Pagination",
|
|
36
|
+
className: [
|
|
37
|
+
"inline-flex items-center gap-0.5 rounded-[13px] p-1",
|
|
38
|
+
className
|
|
39
|
+
].join(" "),
|
|
40
|
+
style: { background: "var(--well)", boxShadow: "var(--well-shadow)" },
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsxs(
|
|
43
|
+
"button",
|
|
44
|
+
{
|
|
45
|
+
onClick: () => onPageChange(currentPage - 1),
|
|
46
|
+
disabled: currentPage <= 1,
|
|
47
|
+
"aria-label": "Previous page",
|
|
48
|
+
className: "group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-ink-body hover:text-ink transition-colors cursor-pointer disabled:opacity-30 disabled:pointer-events-none",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face)] [box-shadow:var(--thumb-shadow)]" }),
|
|
51
|
+
/* @__PURE__ */ jsx("svg", { className: "relative z-10", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19l-7-7 7-7" }) }),
|
|
52
|
+
/* @__PURE__ */ jsx("span", { className: "relative z-10 hidden sm:inline", children: "prev" })
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
/* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative flex items-center", children: [
|
|
57
|
+
pill.ready && /* @__PURE__ */ jsx(
|
|
58
|
+
"span",
|
|
59
|
+
{
|
|
60
|
+
"aria-hidden": true,
|
|
61
|
+
className: "absolute top-0 bottom-0 rounded-[9px] pointer-events-none z-0",
|
|
62
|
+
style: {
|
|
63
|
+
left: pill.left,
|
|
64
|
+
width: pill.width,
|
|
65
|
+
background: "var(--face)",
|
|
66
|
+
boxShadow: "var(--thumb-shadow)",
|
|
67
|
+
transition: "left 340ms var(--ease-out-soft), width 340ms var(--ease-out-soft)"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
slots.map(
|
|
72
|
+
(slot, i) => slot === "..." ? /* @__PURE__ */ jsx(
|
|
73
|
+
"span",
|
|
74
|
+
{
|
|
75
|
+
"aria-hidden": true,
|
|
76
|
+
className: "w-8 h-8 flex items-center justify-center text-[11px] text-ink-faint select-none relative z-10",
|
|
77
|
+
children: "\xB7\xB7\xB7"
|
|
78
|
+
},
|
|
79
|
+
`dot-${i}`
|
|
80
|
+
) : /* @__PURE__ */ jsx(
|
|
81
|
+
"button",
|
|
82
|
+
{
|
|
83
|
+
ref: (el) => {
|
|
84
|
+
btnRefs.current[String(slot)] = el;
|
|
85
|
+
},
|
|
86
|
+
onClick: () => slot !== currentPage && onPageChange(slot),
|
|
87
|
+
"aria-label": `Page ${slot}`,
|
|
88
|
+
"aria-current": slot === currentPage ? "page" : void 0,
|
|
89
|
+
className: [
|
|
90
|
+
"relative z-10 min-w-8 h-8 px-1 rounded-[9px] flex items-center justify-center text-[13px] font-medium transition-colors duration-200 cursor-pointer tabular-nums",
|
|
91
|
+
slot === currentPage ? "text-ink" : "text-ink-muted hover:text-ink"
|
|
92
|
+
].join(" "),
|
|
93
|
+
children: slot
|
|
94
|
+
},
|
|
95
|
+
`slot-${i}`
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
] }),
|
|
99
|
+
/* @__PURE__ */ jsxs(
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
onClick: () => onPageChange(currentPage + 1),
|
|
103
|
+
disabled: currentPage >= totalPages,
|
|
104
|
+
"aria-label": "Next page",
|
|
105
|
+
className: "group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-ink-body hover:text-ink transition-colors cursor-pointer disabled:opacity-30 disabled:pointer-events-none",
|
|
106
|
+
children: [
|
|
107
|
+
/* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face)] [box-shadow:var(--thumb-shadow)]" }),
|
|
108
|
+
/* @__PURE__ */ jsx("span", { className: "relative z-10 hidden sm:inline", children: "next" }),
|
|
109
|
+
/* @__PURE__ */ jsx("svg", { className: "relative z-10", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) })
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
function buildSlots(current, total) {
|
|
118
|
+
if (total <= 7) {
|
|
119
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
120
|
+
}
|
|
121
|
+
const slots = new Array(7);
|
|
122
|
+
slots[0] = 1;
|
|
123
|
+
slots[6] = total;
|
|
124
|
+
if (current <= 3) {
|
|
125
|
+
slots[1] = 2;
|
|
126
|
+
slots[2] = 3;
|
|
127
|
+
slots[3] = 4;
|
|
128
|
+
slots[4] = 5;
|
|
129
|
+
slots[5] = "...";
|
|
130
|
+
} else if (current >= total - 2) {
|
|
131
|
+
slots[1] = "...";
|
|
132
|
+
slots[2] = total - 4;
|
|
133
|
+
slots[3] = total - 3;
|
|
134
|
+
slots[4] = total - 2;
|
|
135
|
+
slots[5] = total - 1;
|
|
136
|
+
} else {
|
|
137
|
+
slots[1] = "...";
|
|
138
|
+
slots[2] = current - 1;
|
|
139
|
+
slots[3] = current;
|
|
140
|
+
slots[4] = current + 1;
|
|
141
|
+
slots[5] = "...";
|
|
142
|
+
}
|
|
143
|
+
return slots;
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
Pagination
|
|
147
|
+
};
|
|
148
|
+
//# sourceMappingURL=Pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Pagination.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useRef, useState, useEffect, useLayoutEffect } from \"react\";\n\ninterface PaginationProps {\n currentPage: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n className?: string;\n}\n\nexport function Pagination({\n currentPage,\n totalPages,\n onPageChange,\n className = \"\",\n}: PaginationProps) {\n const slots = buildSlots(currentPage, totalPages);\n const [pill, setPill] = useState({ left: 0, width: 0, ready: false });\n const containerRef = useRef<HTMLDivElement>(null);\n const btnRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n\n const measurePill = (page: number) => {\n const btn = btnRefs.current[String(page)];\n const container = containerRef.current;\n if (!btn || !container) return;\n const cr = container.getBoundingClientRect();\n const br = btn.getBoundingClientRect();\n setPill({ left: br.left - cr.left, width: br.width, ready: true });\n };\n\n /* Only the thumb moves: it glides to the new page without overshoot,\n and the numbers themselves stay still. Measured before paint, so it\n is never seen out of place. */\n useLayoutEffect(() => {\n measurePill(currentPage);\n }, [currentPage]);\n\n useEffect(() => {\n const ro = new ResizeObserver(() => measurePill(currentPage));\n if (containerRef.current) ro.observe(containerRef.current);\n return () => ro.disconnect();\n }, [currentPage]);\n\n return (\n <nav\n aria-label=\"Pagination\"\n className={[\n \"inline-flex items-center gap-0.5 rounded-[13px] p-1\",\n className,\n ].join(\" \")}\n style={{ background: \"var(--well)\", boxShadow: \"var(--well-shadow)\" }}\n >\n <button\n onClick={() => onPageChange(currentPage - 1)}\n disabled={currentPage <= 1}\n aria-label=\"Previous page\"\n className=\"group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-ink-body hover:text-ink transition-colors cursor-pointer disabled:opacity-30 disabled:pointer-events-none\"\n >\n <span className=\"pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face)] [box-shadow:var(--thumb-shadow)]\" />\n <svg className=\"relative z-10\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n <span className=\"relative z-10 hidden sm:inline\">prev</span>\n </button>\n\n <div ref={containerRef} className=\"relative flex items-center\">\n {pill.ready && (\n <span\n aria-hidden\n className=\"absolute top-0 bottom-0 rounded-[9px] pointer-events-none z-0\"\n style={{\n left: pill.left,\n width: pill.width,\n background: \"var(--face)\",\n boxShadow: \"var(--thumb-shadow)\",\n transition: \"left 340ms var(--ease-out-soft), width 340ms var(--ease-out-soft)\",\n }}\n />\n )}\n\n {slots.map((slot, i) =>\n slot === \"...\" ? (\n <span\n key={`dot-${i}`}\n aria-hidden\n className=\"w-8 h-8 flex items-center justify-center text-[11px] text-ink-faint select-none relative z-10\"\n >\n ···\n </span>\n ) : (\n <button\n key={`slot-${i}`}\n ref={(el) => {\n btnRefs.current[String(slot)] = el;\n }}\n onClick={() => slot !== currentPage && onPageChange(slot as number)}\n aria-label={`Page ${slot}`}\n aria-current={slot === currentPage ? \"page\" : undefined}\n className={[\n \"relative z-10 min-w-8 h-8 px-1 rounded-[9px] flex items-center justify-center text-[13px] font-medium transition-colors duration-200 cursor-pointer tabular-nums\",\n slot === currentPage\n ? \"text-ink\"\n : \"text-ink-muted hover:text-ink\",\n ].join(\" \")}\n >\n {slot}\n </button>\n ),\n )}\n </div>\n\n <button\n onClick={() => onPageChange(currentPage + 1)}\n disabled={currentPage >= totalPages}\n aria-label=\"Next page\"\n className=\"group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-ink-body hover:text-ink transition-colors cursor-pointer disabled:opacity-30 disabled:pointer-events-none\"\n >\n <span className=\"pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face)] [box-shadow:var(--thumb-shadow)]\" />\n <span className=\"relative z-10 hidden sm:inline\">next</span>\n <svg className=\"relative z-10\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </nav>\n );\n}\n\nfunction buildSlots(current: number, total: number): (number | \"...\")[] {\n if (total <= 7) {\n return Array.from({ length: total }, (_, i) => i + 1);\n }\n\n const slots: (number | \"...\")[] = new Array(7);\n\n slots[0] = 1;\n slots[6] = total;\n\n if (current <= 3) {\n slots[1] = 2;\n slots[2] = 3;\n slots[3] = 4;\n slots[4] = 5;\n slots[5] = \"...\";\n } else if (current >= total - 2) {\n slots[1] = \"...\";\n slots[2] = total - 4;\n slots[3] = total - 3;\n slots[4] = total - 2;\n slots[5] = total - 1;\n } else {\n slots[1] = \"...\";\n slots[2] = current - 1;\n slots[3] = current;\n slots[4] = current + 1;\n slots[5] = \"...\";\n }\n\n return slots;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
type PopoverPosition = "top" | "bottom" | "left" | "right";
|
|
5
|
+
interface PopoverProps {
|
|
6
|
+
trigger: React.ReactNode;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
position?: PopoverPosition;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function Popover({ trigger, children, position, className, }: PopoverProps): react_jsx_runtime.JSX.Element;
|
|
12
|
+
|
|
13
|
+
export { Popover };
|
package/dist/Popover.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/Popover.tsx
|
|
4
|
+
import React, { useState, useRef, useEffect, useId } from "react";
|
|
5
|
+
import { ENTER_FROM, LAYER, ORIGIN, Portal, useAnchoredPosition } from "./Floating.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function Popover({
|
|
8
|
+
trigger,
|
|
9
|
+
children,
|
|
10
|
+
position = "bottom",
|
|
11
|
+
className = ""
|
|
12
|
+
}) {
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const [visible, setVisible] = useState(false);
|
|
15
|
+
const [mounted, setMounted] = useState(false);
|
|
16
|
+
const ref = useRef(null);
|
|
17
|
+
const uid = useId();
|
|
18
|
+
const { setFloating, floatingRef, side } = useAnchoredPosition(ref, { open: mounted, side: position, align: "center" });
|
|
19
|
+
if (open && !mounted) setMounted(true);
|
|
20
|
+
if (!open && visible) setVisible(false);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (open) {
|
|
23
|
+
requestAnimationFrame(
|
|
24
|
+
() => requestAnimationFrame(() => {
|
|
25
|
+
setVisible(true);
|
|
26
|
+
const panel = floatingRef.current;
|
|
27
|
+
const first = panel?.querySelector(
|
|
28
|
+
'button:not([disabled]), [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
29
|
+
);
|
|
30
|
+
(first ?? panel)?.focus({ preventScroll: true });
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
} else {
|
|
34
|
+
const t = setTimeout(() => setMounted(false), 180);
|
|
35
|
+
return () => clearTimeout(t);
|
|
36
|
+
}
|
|
37
|
+
}, [open, floatingRef]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
function onOutside(e) {
|
|
40
|
+
const t = e.target;
|
|
41
|
+
if (ref.current?.contains(t) || floatingRef.current?.contains(t)) return;
|
|
42
|
+
setOpen(false);
|
|
43
|
+
}
|
|
44
|
+
document.addEventListener("mousedown", onOutside);
|
|
45
|
+
return () => document.removeEventListener("mousedown", onOutside);
|
|
46
|
+
}, [floatingRef]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!open) return;
|
|
49
|
+
const onKey = (e) => {
|
|
50
|
+
if (e.key === "Escape") {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
ref.current?.querySelector("[data-popover-trigger] button, [data-popover-trigger] a")?.focus();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener("keydown", onKey);
|
|
56
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
57
|
+
}, [open]);
|
|
58
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: ["relative inline-flex", className].join(" "), children: [
|
|
59
|
+
/* @__PURE__ */ jsx("div", { "data-popover-trigger": true, onClick: () => setOpen((v) => !v), className: "inline-flex cursor-pointer", children: React.isValidElement(trigger) ? React.cloneElement(trigger, {
|
|
60
|
+
"aria-haspopup": "dialog",
|
|
61
|
+
"aria-expanded": open,
|
|
62
|
+
"aria-controls": open ? `${uid}-popover` : void 0
|
|
63
|
+
}) : trigger }),
|
|
64
|
+
mounted && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
|
|
65
|
+
"div",
|
|
66
|
+
{
|
|
67
|
+
ref: setFloating,
|
|
68
|
+
id: `${uid}-popover`,
|
|
69
|
+
role: "dialog",
|
|
70
|
+
tabIndex: -1,
|
|
71
|
+
className: "outline-none min-w-[220px] max-w-[calc(100vw-16px)] rounded-[16px] p-1.5 transition-[opacity,transform] duration-200 ease-[var(--ease-spring)]",
|
|
72
|
+
style: {
|
|
73
|
+
position: "fixed",
|
|
74
|
+
top: 0,
|
|
75
|
+
left: 0,
|
|
76
|
+
zIndex: LAYER.floating,
|
|
77
|
+
transformOrigin: ORIGIN[side],
|
|
78
|
+
opacity: visible ? 1 : 0,
|
|
79
|
+
transform: visible ? "none" : `${ENTER_FROM[side]} scale(0.96)`,
|
|
80
|
+
background: "var(--panel-bezel)",
|
|
81
|
+
boxShadow: "var(--overlay-shadow)"
|
|
82
|
+
},
|
|
83
|
+
children: /* @__PURE__ */ jsx(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
className: "bg-panel-face rounded-[11px] p-3.5 text-[13px] text-ink-body leading-relaxed",
|
|
87
|
+
style: { boxShadow: "var(--panel-face-shadow)" },
|
|
88
|
+
children
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
) })
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
Popover
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=Popover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Popover.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useRef, useEffect, useId } from \"react\";\nimport { ENTER_FROM, LAYER, ORIGIN, Portal, useAnchoredPosition } from \"./Floating\";\n\ntype PopoverPosition = \"top\" | \"bottom\" | \"left\" | \"right\";\n\ninterface PopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n position?: PopoverPosition;\n className?: string;\n}\n\nexport function Popover({\n trigger,\n children,\n position = \"bottom\",\n className = \"\",\n}: PopoverProps) {\n const [open, setOpen] = useState(false);\n const [visible, setVisible] = useState(false);\n const [mounted, setMounted] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n const uid = useId();\n // Portalled and pinned beside the trigger, flipping if there's no room\n const { setFloating, floatingRef, side } = useAnchoredPosition(ref, { open: mounted, side: position, align: \"center\" });\n\n /* Mounted and faded out during render; the effect only times the rest */\n if (open && !mounted) setMounted(true);\n if (!open && visible) setVisible(false);\n\n useEffect(() => {\n if (open) {\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n setVisible(true);\n // The panel lives at the end of <body>; bring focus to it so Tab reaches its contents\n const panel = floatingRef.current;\n const first = panel?.querySelector<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n );\n (first ?? panel)?.focus({ preventScroll: true });\n }),\n );\n } else {\n const t = setTimeout(() => setMounted(false), 180);\n return () => clearTimeout(t);\n }\n }, [open, floatingRef]);\n\n useEffect(() => {\n function onOutside(e: MouseEvent) {\n const t = e.target as Node;\n if (ref.current?.contains(t) || floatingRef.current?.contains(t)) return;\n setOpen(false);\n }\n document.addEventListener(\"mousedown\", onOutside);\n return () => document.removeEventListener(\"mousedown\", onOutside);\n }, [floatingRef]);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n setOpen(false);\n ref.current?.querySelector<HTMLElement>(\"[data-popover-trigger] button, [data-popover-trigger] a\")?.focus();\n }\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open]);\n\n return (\n <div ref={ref} className={[\"relative inline-flex\", className].join(\" \")}>\n <div data-popover-trigger onClick={() => setOpen((v) => !v)} className=\"inline-flex cursor-pointer\">\n {React.isValidElement(trigger)\n ? React.cloneElement(trigger as React.ReactElement<Record<string, unknown>>, {\n \"aria-haspopup\": \"dialog\",\n \"aria-expanded\": open,\n \"aria-controls\": open ? `${uid}-popover` : undefined,\n })\n : trigger}\n </div>\n {mounted && (\n <Portal>\n <div\n ref={setFloating}\n id={`${uid}-popover`}\n role=\"dialog\"\n tabIndex={-1}\n className=\"outline-none min-w-[220px] max-w-[calc(100vw-16px)] rounded-[16px] 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[side],\n opacity: visible ? 1 : 0,\n transform: visible ? \"none\" : `${ENTER_FROM[side]} scale(0.96)`,\n background: \"var(--panel-bezel)\",\n boxShadow: \"var(--overlay-shadow)\",\n }}\n >\n <div\n className=\"bg-panel-face rounded-[11px] p-3.5 text-[13px] text-ink-body leading-relaxed\"\n style={{ boxShadow: \"var(--panel-face-shadow)\" }}\n >\n {children}\n </div>\n </div>\n </Portal>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface ProgressProps {
|
|
4
|
+
value: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
label?: string;
|
|
7
|
+
showValue?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function Progress({ value, max, label, showValue, className, }: ProgressProps): react_jsx_runtime.JSX.Element;
|
|
11
|
+
|
|
12
|
+
export { Progress };
|
package/dist/Progress.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/Progress.tsx
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function Progress({
|
|
7
|
+
value,
|
|
8
|
+
max = 100,
|
|
9
|
+
label,
|
|
10
|
+
showValue = false,
|
|
11
|
+
className = ""
|
|
12
|
+
}) {
|
|
13
|
+
const pct = Math.min(Math.max(value / max * 100, 0), 100);
|
|
14
|
+
const [displayed, setDisplayed] = useState(0);
|
|
15
|
+
const t = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
t.current = setTimeout(() => setDisplayed(pct), 80);
|
|
18
|
+
return () => {
|
|
19
|
+
if (t.current) clearTimeout(t.current);
|
|
20
|
+
};
|
|
21
|
+
}, [pct]);
|
|
22
|
+
return /* @__PURE__ */ jsxs("div", { className: ["flex flex-col gap-2", className].join(" "), children: [
|
|
23
|
+
(label || showValue) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
|
|
24
|
+
label && /* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium text-ink-label truncate", children: label }),
|
|
25
|
+
showValue && /* @__PURE__ */ jsxs("span", { "aria-hidden": true, className: "text-[12px] tabular-nums text-ink-muted shrink-0", children: [
|
|
26
|
+
Math.round(pct),
|
|
27
|
+
"%"
|
|
28
|
+
] })
|
|
29
|
+
] }),
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
className: "w-full h-2 rounded-full p-px overflow-hidden",
|
|
34
|
+
role: "progressbar",
|
|
35
|
+
"aria-label": label,
|
|
36
|
+
"aria-valuenow": value,
|
|
37
|
+
"aria-valuemin": 0,
|
|
38
|
+
"aria-valuemax": max,
|
|
39
|
+
style: { background: "var(--well)", boxShadow: "var(--well-shadow)" },
|
|
40
|
+
children: /* @__PURE__ */ jsx(
|
|
41
|
+
"div",
|
|
42
|
+
{
|
|
43
|
+
className: "h-full rounded-full relative overflow-hidden",
|
|
44
|
+
style: {
|
|
45
|
+
width: `${displayed}%`,
|
|
46
|
+
/* Never collapses below a pill, so 1% still reads as started */
|
|
47
|
+
minWidth: displayed > 0 ? 6 : 0,
|
|
48
|
+
background: "var(--face-inverse)",
|
|
49
|
+
boxShadow: "var(--fill-shadow)",
|
|
50
|
+
transition: "width 700ms var(--ease-out-soft)"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
] });
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
Progress
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=Progress.js.map
|