@vitavision/ui 0.8.0 → 0.9.0
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 +2 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +184 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/Toaster.tsx +148 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/ui/toastStore.ts +128 -0
- package/src/styles.css +19 -2
package/README.md
CHANGED
|
@@ -127,7 +127,8 @@ keep independent preferences.
|
|
|
127
127
|
`SkeletonRows` · `Field` · `Input`, `NumberInput` (with an optional `unit` written in the
|
|
128
128
|
field), `Textarea` · `VectorInput`, `PoseInput` · `PageHeader`, `Panel`,
|
|
129
129
|
`ReadoutStrip`, `Section` · `SegmentedControl` · `Select` · `Slider` · `Table` ·
|
|
130
|
-
`Checkbox`, `Switch` · `ToggleChip` · `
|
|
130
|
+
`Checkbox`, `Switch` · `ToggleChip` · `Toaster`, `toast()` (notifications from anywhere; mount one `<Toaster />` near the root) ·
|
|
131
|
+
`InfoHint`, `Tooltip`, `TooltipProvider` ·
|
|
131
132
|
`ThemeToggle` · plus `cn`, `focusRing`, `focusRingInset`.
|
|
132
133
|
|
|
133
134
|
`PoseInput` edits an SE(3) pose in the wire form `{ rotation: [qx, qy, qz, qw], translation:
|
package/dist/index.d.ts
CHANGED
|
@@ -880,6 +880,97 @@ export declare function ToggleChip({ checked, onCheckedChange, children, swatch,
|
|
|
880
880
|
className?: string | undefined;
|
|
881
881
|
}): import("react").JSX.Element;
|
|
882
882
|
//#endregion
|
|
883
|
+
//#region src/components/ui/toastStore.d.ts
|
|
884
|
+
/** A toast's kind: a fact (`info`), a completed action (`success`), a caveat (`warn`), a failure (`error`). */
|
|
885
|
+
type ToastTone = "info" | "success" | "warn" | "error";
|
|
886
|
+
/** What `toast()` takes. */
|
|
887
|
+
interface ToastOptions {
|
|
888
|
+
/** One line: what happened. */
|
|
889
|
+
title: string;
|
|
890
|
+
/** More detail, and what to do about it. */
|
|
891
|
+
description?: string | undefined;
|
|
892
|
+
/** Defaults to `info`. */
|
|
893
|
+
tone?: ToastTone | undefined;
|
|
894
|
+
/**
|
|
895
|
+
* How long it stays, in milliseconds, while not hovered or focused. Defaults to 5000
|
|
896
|
+
* (8000 for `error`); `Infinity` keeps it until dismissed.
|
|
897
|
+
*/
|
|
898
|
+
duration?: number | undefined;
|
|
899
|
+
/** Replace the toast with this id instead of adding one — for progress that updates in place. */
|
|
900
|
+
id?: string | undefined;
|
|
901
|
+
}
|
|
902
|
+
/** A toast in the store. */
|
|
903
|
+
interface ToastRecord {
|
|
904
|
+
/** Identifies the toast to `dismiss` and to `id` replacement. */
|
|
905
|
+
id: string;
|
|
906
|
+
/** One line: what happened. */
|
|
907
|
+
title: string;
|
|
908
|
+
/** More detail, if any. */
|
|
909
|
+
description: string | undefined;
|
|
910
|
+
/** Its kind. */
|
|
911
|
+
tone: ToastTone;
|
|
912
|
+
/** Milliseconds before it dismisses itself (`Infinity`: never). */
|
|
913
|
+
duration: number;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* A toast store: what `toast()` writes to and `<Toaster />` reads. Every member may be called
|
|
917
|
+
* detached.
|
|
918
|
+
*/
|
|
919
|
+
interface ToastStore {
|
|
920
|
+
/** Add a toast (or replace the one with `options.id`), returning its id. */
|
|
921
|
+
readonly toast: (options: ToastOptions) => string;
|
|
922
|
+
/** Remove one toast, or every toast when `id` is omitted. */
|
|
923
|
+
readonly dismiss: (id?: string) => void;
|
|
924
|
+
/** The toasts, oldest first. The same array until the store changes. */
|
|
925
|
+
readonly getSnapshot: () => readonly ToastRecord[];
|
|
926
|
+
/** Be told when the toasts change; returns the unsubscribe function. */
|
|
927
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* A new, empty toast store. The package keeps one (behind `toast()` and the default
|
|
931
|
+
* `<Toaster />`); create another only to isolate a region or a test.
|
|
932
|
+
*
|
|
933
|
+
* @returns The store.
|
|
934
|
+
*/
|
|
935
|
+
export declare function createToastStore(): ToastStore;
|
|
936
|
+
/** The store behind `toast()` and a `<Toaster />` given no `store`. */
|
|
937
|
+
export declare const defaultToastStore: ToastStore;
|
|
938
|
+
/**
|
|
939
|
+
* Raise a notification, from anywhere — shown by the mounted `<Toaster />`.
|
|
940
|
+
*
|
|
941
|
+
* `toast({ title: "Scenario loaded", tone: "success" })`. Pass `id` to update a toast in
|
|
942
|
+
* place (a progress message that becomes a result). `toast.dismiss(id)` removes one early,
|
|
943
|
+
* `toast.dismiss()` all of them.
|
|
944
|
+
*
|
|
945
|
+
* @param options - The message, its tone and its duration.
|
|
946
|
+
* @returns The toast's id.
|
|
947
|
+
*/
|
|
948
|
+
export declare const toast: ((options: ToastOptions) => string) & {
|
|
949
|
+
/** Remove one toast, or every toast when `id` is omitted. */
|
|
950
|
+
dismiss: (id?: string) => void;
|
|
951
|
+
};
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/components/ui/Toaster.d.ts
|
|
954
|
+
/** Props of `Toaster`. */
|
|
955
|
+
interface ToasterProps {
|
|
956
|
+
/** The store to show. Defaults to the one behind `toast()`. */
|
|
957
|
+
store?: ToastStore | undefined;
|
|
958
|
+
/** How many toasts are shown at once; older ones wait. Defaults to 5. */
|
|
959
|
+
limit?: number | undefined;
|
|
960
|
+
/** Merged with the stack's own classes through `cn` (it is fixed to the bottom-right corner). */
|
|
961
|
+
className?: string | undefined;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* The stack of notifications raised with `toast()`. Mount it once, near the root.
|
|
965
|
+
*
|
|
966
|
+
* Fixed to the bottom-right corner, newest at the bottom. Each toast dismisses itself after
|
|
967
|
+
* its `duration`; the countdown pauses while the pointer is over the stack or focus is inside
|
|
968
|
+
* it. A `section` landmark ("Notifications") holding a polite live region, so new toasts are
|
|
969
|
+
* announced; an `error` toast is a `role="alert"`, announced at once. Each toast carries
|
|
970
|
+
* `data-tone` and a "Dismiss" button.
|
|
971
|
+
*/
|
|
972
|
+
export declare function Toaster({ store, limit, className }: ToasterProps): import("react").JSX.Element;
|
|
973
|
+
//#endregion
|
|
883
974
|
//#region src/components/ui/Tooltip.d.ts
|
|
884
975
|
/**
|
|
885
976
|
* Shares one open delay (200 ms) across the tooltips below it. Mount it once near the root:
|
|
@@ -999,5 +1090,5 @@ export declare function Tabs<Id extends string>({ items, active, onSelect, label
|
|
|
999
1090
|
className?: string | undefined;
|
|
1000
1091
|
}): import("react").JSX.Element;
|
|
1001
1092
|
//#endregion
|
|
1002
|
-
export type { BackLink, ButtonSize, ButtonVariant, CalloutTone, Column, Density, MeasureTone, NumberInputProps, PoseInputProps, PoseValue, Quaternion, ReadoutItem, RotationView, SelectOption, TabItem, ThemeChoice, Tone, Vec3, VectorInputProps };
|
|
1093
|
+
export type { BackLink, ButtonSize, ButtonVariant, CalloutTone, Column, Density, MeasureTone, NumberInputProps, PoseInputProps, PoseValue, Quaternion, ReadoutItem, RotationView, SelectOption, TabItem, ThemeChoice, ToastOptions, ToastRecord, ToastStore, ToastTone, ToasterProps, Tone, Vec3, VectorInputProps };
|
|
1003
1094
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AlertTriangle, Check, CheckCircle2, ChevronDown, ChevronRight, HelpCircle, Info, Loader2, Minus, Moon, Sun, SunMoon, XCircle } from "lucide-react";
|
|
2
|
-
import { createContext, isValidElement, use, useCallback, useId, useMemo, useState, useSyncExternalStore } from "react";
|
|
1
|
+
import { AlertTriangle, Check, CheckCircle2, ChevronDown, ChevronRight, CircleCheck, CircleX, HelpCircle, Info, Loader2, Minus, Moon, Sun, SunMoon, TriangleAlert, X, XCircle } from "lucide-react";
|
|
2
|
+
import { createContext, isValidElement, use, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import * as RadixTooltip from "@radix-ui/react-tooltip";
|
|
4
4
|
import { clsx } from "clsx";
|
|
5
5
|
import { twMerge } from "tailwind-merge";
|
|
@@ -1558,6 +1558,187 @@ function ToggleChip({ checked, onCheckedChange, children, swatch, disabled = fal
|
|
|
1558
1558
|
});
|
|
1559
1559
|
}
|
|
1560
1560
|
//#endregion
|
|
1561
|
+
//#region src/components/ui/toastStore.ts
|
|
1562
|
+
const DEFAULT_DURATION = 5e3;
|
|
1563
|
+
const ERROR_DURATION = 8e3;
|
|
1564
|
+
/**
|
|
1565
|
+
* A new, empty toast store. The package keeps one (behind `toast()` and the default
|
|
1566
|
+
* `<Toaster />`); create another only to isolate a region or a test.
|
|
1567
|
+
*
|
|
1568
|
+
* @returns The store.
|
|
1569
|
+
*/
|
|
1570
|
+
function createToastStore() {
|
|
1571
|
+
let toasts = [];
|
|
1572
|
+
let sequence = 0;
|
|
1573
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1574
|
+
const commit = (next) => {
|
|
1575
|
+
toasts = next;
|
|
1576
|
+
for (const listener of [...listeners]) listener();
|
|
1577
|
+
};
|
|
1578
|
+
return {
|
|
1579
|
+
toast: (options) => {
|
|
1580
|
+
const tone = options.tone ?? "info";
|
|
1581
|
+
sequence += 1;
|
|
1582
|
+
const record = {
|
|
1583
|
+
id: options.id ?? `toast-${sequence}`,
|
|
1584
|
+
title: options.title,
|
|
1585
|
+
description: options.description,
|
|
1586
|
+
tone,
|
|
1587
|
+
duration: options.duration ?? (tone === "error" ? ERROR_DURATION : DEFAULT_DURATION)
|
|
1588
|
+
};
|
|
1589
|
+
const existing = toasts.findIndex((t) => t.id === record.id);
|
|
1590
|
+
commit(existing === -1 ? [...toasts, record] : toasts.map((t, i) => i === existing ? record : t));
|
|
1591
|
+
return record.id;
|
|
1592
|
+
},
|
|
1593
|
+
dismiss: (id) => {
|
|
1594
|
+
if (id === void 0) {
|
|
1595
|
+
if (toasts.length > 0) commit([]);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (toasts.some((t) => t.id === id)) commit(toasts.filter((t) => t.id !== id));
|
|
1599
|
+
},
|
|
1600
|
+
getSnapshot: () => toasts,
|
|
1601
|
+
subscribe: (listener) => {
|
|
1602
|
+
listeners.add(listener);
|
|
1603
|
+
return () => {
|
|
1604
|
+
listeners.delete(listener);
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
/** The store behind `toast()` and a `<Toaster />` given no `store`. */
|
|
1610
|
+
const defaultToastStore = createToastStore();
|
|
1611
|
+
/**
|
|
1612
|
+
* Raise a notification, from anywhere — shown by the mounted `<Toaster />`.
|
|
1613
|
+
*
|
|
1614
|
+
* `toast({ title: "Scenario loaded", tone: "success" })`. Pass `id` to update a toast in
|
|
1615
|
+
* place (a progress message that becomes a result). `toast.dismiss(id)` removes one early,
|
|
1616
|
+
* `toast.dismiss()` all of them.
|
|
1617
|
+
*
|
|
1618
|
+
* @param options - The message, its tone and its duration.
|
|
1619
|
+
* @returns The toast's id.
|
|
1620
|
+
*/
|
|
1621
|
+
const toast = Object.assign((options) => defaultToastStore.toast(options), { dismiss: (id) => defaultToastStore.dismiss(id) });
|
|
1622
|
+
//#endregion
|
|
1623
|
+
//#region src/components/ui/Toaster.tsx
|
|
1624
|
+
const TONE = {
|
|
1625
|
+
info: {
|
|
1626
|
+
border: "border-signal/30",
|
|
1627
|
+
icon: "text-signal",
|
|
1628
|
+
Icon: Info
|
|
1629
|
+
},
|
|
1630
|
+
success: {
|
|
1631
|
+
border: "border-normal/30",
|
|
1632
|
+
icon: "text-normal",
|
|
1633
|
+
Icon: CircleCheck
|
|
1634
|
+
},
|
|
1635
|
+
warn: {
|
|
1636
|
+
border: "border-warn/30",
|
|
1637
|
+
icon: "text-warn",
|
|
1638
|
+
Icon: TriangleAlert
|
|
1639
|
+
},
|
|
1640
|
+
error: {
|
|
1641
|
+
border: "border-defect/30",
|
|
1642
|
+
icon: "text-defect",
|
|
1643
|
+
Icon: CircleX
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
const NO_TOASTS = [];
|
|
1647
|
+
/**
|
|
1648
|
+
* The stack of notifications raised with `toast()`. Mount it once, near the root.
|
|
1649
|
+
*
|
|
1650
|
+
* Fixed to the bottom-right corner, newest at the bottom. Each toast dismisses itself after
|
|
1651
|
+
* its `duration`; the countdown pauses while the pointer is over the stack or focus is inside
|
|
1652
|
+
* it. A `section` landmark ("Notifications") holding a polite live region, so new toasts are
|
|
1653
|
+
* announced; an `error` toast is a `role="alert"`, announced at once. Each toast carries
|
|
1654
|
+
* `data-tone` and a "Dismiss" button.
|
|
1655
|
+
*/
|
|
1656
|
+
function Toaster({ store = defaultToastStore, limit = 5, className }) {
|
|
1657
|
+
const toasts = useSyncExternalStore(store.subscribe, store.getSnapshot, () => NO_TOASTS);
|
|
1658
|
+
const [hovered, setHovered] = useState(false);
|
|
1659
|
+
const [focused, setFocused] = useState(false);
|
|
1660
|
+
const shown = toasts.slice(-Math.max(1, limit));
|
|
1661
|
+
const onBlur = (event) => {
|
|
1662
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false);
|
|
1663
|
+
};
|
|
1664
|
+
return /* @__PURE__ */ jsx("section", {
|
|
1665
|
+
"aria-label": "Notifications",
|
|
1666
|
+
onPointerEnter: () => setHovered(true),
|
|
1667
|
+
onPointerLeave: () => setHovered(false),
|
|
1668
|
+
onFocus: () => setFocused(true),
|
|
1669
|
+
onBlur,
|
|
1670
|
+
className: cn("pointer-events-none fixed right-4 bottom-4 z-50 w-80 max-w-[calc(100vw-2rem)]", className),
|
|
1671
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
1672
|
+
"aria-live": "polite",
|
|
1673
|
+
className: "flex flex-col gap-2",
|
|
1674
|
+
children: shown.map((record) => /* @__PURE__ */ jsx(ToastItem, {
|
|
1675
|
+
record,
|
|
1676
|
+
paused: hovered || focused,
|
|
1677
|
+
onDismiss: () => store.dismiss(record.id)
|
|
1678
|
+
}, record.id))
|
|
1679
|
+
})
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
function ToastItem({ record, paused, onDismiss }) {
|
|
1683
|
+
const { border, icon, Icon } = TONE[record.tone];
|
|
1684
|
+
const remainingRef = useRef({
|
|
1685
|
+
record,
|
|
1686
|
+
ms: record.duration
|
|
1687
|
+
});
|
|
1688
|
+
const dismissRef = useRef(onDismiss);
|
|
1689
|
+
useEffect(() => {
|
|
1690
|
+
dismissRef.current = onDismiss;
|
|
1691
|
+
});
|
|
1692
|
+
useEffect(() => {
|
|
1693
|
+
if (remainingRef.current.record !== record) remainingRef.current = {
|
|
1694
|
+
record,
|
|
1695
|
+
ms: record.duration
|
|
1696
|
+
};
|
|
1697
|
+
if (paused || !Number.isFinite(remainingRef.current.ms)) return;
|
|
1698
|
+
const started = Date.now();
|
|
1699
|
+
const timer = setTimeout(() => dismissRef.current(), remainingRef.current.ms);
|
|
1700
|
+
return () => {
|
|
1701
|
+
clearTimeout(timer);
|
|
1702
|
+
remainingRef.current.ms = Math.max(0, remainingRef.current.ms - (Date.now() - started));
|
|
1703
|
+
};
|
|
1704
|
+
}, [paused, record]);
|
|
1705
|
+
return /* @__PURE__ */ jsx("li", {
|
|
1706
|
+
className: "pointer-events-auto",
|
|
1707
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1708
|
+
role: record.tone === "error" ? "alert" : void 0,
|
|
1709
|
+
"data-tone": record.tone,
|
|
1710
|
+
className: cn("flex items-start gap-2.5 rounded-panel border bg-overlay px-3 py-2.5 text-sm leading-snug shadow-lg shadow-black/25", "transition-[opacity,translate] duration-200 starting:translate-y-2 starting:opacity-0", border),
|
|
1711
|
+
children: [
|
|
1712
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
1713
|
+
className: cn("mt-0.5 size-4 shrink-0", icon),
|
|
1714
|
+
"aria-hidden": true
|
|
1715
|
+
}),
|
|
1716
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1717
|
+
className: "flex min-w-0 flex-1 flex-col gap-0.5",
|
|
1718
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1719
|
+
className: "font-medium text-fg",
|
|
1720
|
+
children: record.title
|
|
1721
|
+
}), record.description && /* @__PURE__ */ jsx("p", {
|
|
1722
|
+
className: "text-fg-muted",
|
|
1723
|
+
children: record.description
|
|
1724
|
+
})]
|
|
1725
|
+
}),
|
|
1726
|
+
/* @__PURE__ */ jsx("button", {
|
|
1727
|
+
type: "button",
|
|
1728
|
+
"aria-label": "Dismiss",
|
|
1729
|
+
title: "Dismiss",
|
|
1730
|
+
onClick: onDismiss,
|
|
1731
|
+
className: cn("-mr-1 grid size-6 shrink-0 place-items-center rounded-control text-fg-subtle transition-colors hover:bg-raised hover:text-fg", focusRing),
|
|
1732
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
1733
|
+
className: "size-3.5",
|
|
1734
|
+
"aria-hidden": true
|
|
1735
|
+
})
|
|
1736
|
+
})
|
|
1737
|
+
]
|
|
1738
|
+
})
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
//#endregion
|
|
1561
1742
|
//#region src/components/Tabs.tsx
|
|
1562
1743
|
const NEXT_KEYS = {
|
|
1563
1744
|
ArrowRight: (index, length) => (index + 1) % length,
|
|
@@ -1620,6 +1801,6 @@ function Tabs({ items, active, onSelect, label, idPrefix, className }) {
|
|
|
1620
1801
|
});
|
|
1621
1802
|
}
|
|
1622
1803
|
//#endregion
|
|
1623
|
-
export { Badge, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, Empty, ErrorBox, Field, InfoHint, Input, NumberInput, PageHeader, Panel, PoseInput, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, ToggleChip, Tooltip, TooltipProvider, VectorInput, buttonClasses, byDensity, cn, controlClasses, focusRing, focusRingInset, initTheme, inputClasses, readThemeChoice, resolveTheme, setThemeChoice, toneColor, useControlHeight, useDensity };
|
|
1804
|
+
export { Badge, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, Empty, ErrorBox, Field, InfoHint, Input, NumberInput, PageHeader, Panel, PoseInput, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, Toaster, ToggleChip, Tooltip, TooltipProvider, VectorInput, buttonClasses, byDensity, cn, controlClasses, createToastStore, defaultToastStore, focusRing, focusRingInset, initTheme, inputClasses, readThemeChoice, resolveTheme, setThemeChoice, toast, toneColor, useControlHeight, useDensity };
|
|
1624
1805
|
|
|
1625
1806
|
//# sourceMappingURL=index.js.map
|