@carbonid1/design-system 5.2.2 → 5.2.3
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/dist/Badge/Badge.types.js +1 -0
- package/dist/Button/Button.js +34 -0
- package/dist/Button/Button.types.js +1 -0
- package/dist/ContextMenu/ContextMenu.js +40 -0
- package/dist/Kbd/Kbd.js +38 -0
- package/dist/Kbd/Kbd.types.js +1 -0
- package/dist/ProgressRing/ProgressRing.consts.js +4 -0
- package/dist/ProgressRing/ProgressRing.js +12 -0
- package/dist/Select/Select.js +85 -0
- package/dist/Select/Select.types.js +1 -0
- package/dist/Slider/Slider.js +5 -0
- package/dist/ThemeCycler/ThemeCycler.js +30 -0
- package/dist/ThemeProvider/ThemeProvider.js +4 -0
- package/dist/Toaster/Toaster.js +4 -0
- package/dist/Tooltip/Tooltip.js +59 -0
- package/dist/helpers/cn/cn.js +5 -0
- package/dist/helpers/getModKey/getModKey.js +1 -0
- package/dist/index.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../helpers/cn/cn';
|
|
4
|
+
import { Button as ButtonPrimitive } from '@base-ui/react/button';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
import { Loader2 } from 'lucide-react';
|
|
7
|
+
const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-hidden select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
ghost: 'hover:bg-accent aria-expanded:bg-accent dark:hover:bg-accent/50',
|
|
11
|
+
primary: 'bg-primary text-primary-foreground font-medium hover:bg-primary/90',
|
|
12
|
+
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
|
13
|
+
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
|
14
|
+
attention: 'text-attention-foreground hover:bg-attention-muted',
|
|
15
|
+
subtle: 'text-muted-foreground hover:text-primary',
|
|
16
|
+
danger: 'text-muted-foreground hover:text-destructive',
|
|
17
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
18
|
+
},
|
|
19
|
+
size: {
|
|
20
|
+
small: 'h-7 gap-1 px-2.5 text-[0.8rem]',
|
|
21
|
+
default: 'h-8 gap-1.5 px-2.5',
|
|
22
|
+
large: 'h-9 gap-1.5 px-3',
|
|
23
|
+
icon: 'size-8 rounded-full',
|
|
24
|
+
smallIcon: 'size-6 rounded-full',
|
|
25
|
+
largeIcon: 'size-12 rounded-full',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: 'ghost',
|
|
30
|
+
size: 'default',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const Button = ({ className, variant, size, fullWidth, loading, disabled, children, ...props }) => (_jsxs(ButtonPrimitive, { "data-slot": "button", className: cn(buttonVariants({ variant, size }), fullWidth && 'w-full justify-start', className), disabled: disabled ?? loading, "aria-busy": loading ?? undefined, ...props, children: [loading && _jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" }), children] }));
|
|
34
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu';
|
|
4
|
+
import { ChevronRight } from 'lucide-react';
|
|
5
|
+
import { cn } from '../helpers/cn/cn';
|
|
6
|
+
const popupClasses = 'bg-popover text-popover-foreground border-border z-50 min-w-45 origin-[var(--transform-origin)] rounded-lg border py-1 shadow-lg outline-hidden transition-[transform,opacity] data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0';
|
|
7
|
+
const itemBaseClasses = 'relative flex h-7 cursor-default items-center gap-1.5 px-2.5 text-[0.8rem] outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4 data-[disabled]:pointer-events-none data-[disabled]:opacity-50';
|
|
8
|
+
const itemVariantClasses = {
|
|
9
|
+
default: 'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground',
|
|
10
|
+
destructive: 'text-destructive data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive',
|
|
11
|
+
};
|
|
12
|
+
const Positioner = ({ className, sideOffset = 4, ...props }) => (_jsx(BaseContextMenu.Positioner, { sideOffset: sideOffset, className: cn('z-50 outline-hidden', className), ...props }));
|
|
13
|
+
const Popup = ({ className, ...props }) => (_jsx(BaseContextMenu.Popup, { className: cn(popupClasses, className), ...props }));
|
|
14
|
+
const Item = ({ className, variant = 'default', ...props }) => (_jsx(BaseContextMenu.Item, { className: cn(itemBaseClasses, itemVariantClasses[variant], className), ...props }));
|
|
15
|
+
const Separator = ({ className, ...props }) => (_jsx(BaseContextMenu.Separator, { className: cn('bg-border my-1 h-px', className), ...props }));
|
|
16
|
+
const GroupLabel = ({ className, ...props }) => (_jsx(BaseContextMenu.GroupLabel, { className: cn('text-muted-foreground px-2.5 py-1 text-xs', className), ...props }));
|
|
17
|
+
const CheckboxItem = ({ className, children, ...props }) => (_jsxs(BaseContextMenu.CheckboxItem, { className: cn(itemBaseClasses, itemVariantClasses.default, 'pl-7', className), ...props, children: [_jsx("span", { "aria-hidden": true, className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(BaseContextMenu.CheckboxItemIndicator, { className: "size-3.5", children: _jsx(CheckIcon, {}) }) }), children] }));
|
|
18
|
+
const RadioItem = ({ className, children, ...props }) => (_jsxs(BaseContextMenu.RadioItem, { className: cn(itemBaseClasses, itemVariantClasses.default, 'pl-7', className), ...props, children: [_jsx("span", { "aria-hidden": true, className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(BaseContextMenu.RadioItemIndicator, { className: "size-3.5", children: _jsx(DotIcon, {}) }) }), children] }));
|
|
19
|
+
const SubmenuTrigger = ({ className, children, ...props }) => (_jsxs(BaseContextMenu.SubmenuTrigger, { className: cn(itemBaseClasses, itemVariantClasses.default, 'data-popup-open:bg-accent data-popup-open:text-accent-foreground pr-2', className), ...props, children: [children, _jsx(ChevronRight, { className: "ml-auto size-4 opacity-60", "aria-hidden": true })] }));
|
|
20
|
+
const CheckIcon = () => (_jsx("svg", { viewBox: "0 0 14 14", fill: "none", className: "size-3.5", children: _jsx("path", { d: "M3 7.5 6 10.5 11 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
21
|
+
const DotIcon = () => (_jsx("svg", { viewBox: "0 0 14 14", className: "size-3.5", children: _jsx("circle", { cx: "7", cy: "7", r: "3", fill: "currentColor" }) }));
|
|
22
|
+
export const ContextMenu = {
|
|
23
|
+
Root: BaseContextMenu.Root,
|
|
24
|
+
Trigger: BaseContextMenu.Trigger,
|
|
25
|
+
Portal: BaseContextMenu.Portal,
|
|
26
|
+
Backdrop: BaseContextMenu.Backdrop,
|
|
27
|
+
Positioner,
|
|
28
|
+
Popup,
|
|
29
|
+
Arrow: BaseContextMenu.Arrow,
|
|
30
|
+
Group: BaseContextMenu.Group,
|
|
31
|
+
GroupLabel,
|
|
32
|
+
Separator,
|
|
33
|
+
Item,
|
|
34
|
+
LinkItem: BaseContextMenu.LinkItem,
|
|
35
|
+
CheckboxItem,
|
|
36
|
+
RadioGroup: BaseContextMenu.RadioGroup,
|
|
37
|
+
RadioItem,
|
|
38
|
+
SubmenuRoot: BaseContextMenu.SubmenuRoot,
|
|
39
|
+
SubmenuTrigger,
|
|
40
|
+
};
|
package/dist/Kbd/Kbd.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { getModKey } from '../helpers/getModKey/getModKey';
|
|
4
|
+
import { cn } from '../helpers/cn/cn';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
const MOD_SYMBOLS = { Cmd: '⌘', Ctrl: 'Ctrl' };
|
|
7
|
+
const KEY_SYMBOLS = {
|
|
8
|
+
shift: '⇧',
|
|
9
|
+
alt: '⌥',
|
|
10
|
+
enter: '↵',
|
|
11
|
+
backspace: '⌫',
|
|
12
|
+
escape: 'Esc',
|
|
13
|
+
space: '␣',
|
|
14
|
+
left: '←',
|
|
15
|
+
right: '→',
|
|
16
|
+
up: '↑',
|
|
17
|
+
down: '↓',
|
|
18
|
+
};
|
|
19
|
+
const resolveKey = (key) => {
|
|
20
|
+
const lower = key.toLowerCase();
|
|
21
|
+
if (lower === 'mod')
|
|
22
|
+
return MOD_SYMBOLS[getModKey()] ?? 'Ctrl';
|
|
23
|
+
return KEY_SYMBOLS[lower] ?? key;
|
|
24
|
+
};
|
|
25
|
+
const kbdVariants = cva('inline-flex items-center justify-center rounded-sm border font-mono leading-none select-none', {
|
|
26
|
+
variants: {
|
|
27
|
+
size: {
|
|
28
|
+
sm: 'min-w-4 px-1 py-0.5 text-[10px] border-foreground/10 bg-foreground/10',
|
|
29
|
+
default: 'min-w-5 px-1.5 py-1 text-[11px] border-border bg-muted',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: { size: 'default' },
|
|
33
|
+
});
|
|
34
|
+
const Kbd = ({ keys, size, className }) => {
|
|
35
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
36
|
+
return (_jsx("span", { className: "inline-flex items-center gap-0.5", role: "presentation", children: keyList.map((key, i) => (_jsx("kbd", { className: cn(kbdVariants({ size }), className), children: resolveKey(key) }, i))) }));
|
|
37
|
+
};
|
|
38
|
+
export { Kbd, kbdVariants };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CIRCUMFERENCE, RADIUS, RING_SIZE, STROKE_WIDTH } from './ProgressRing.consts';
|
|
3
|
+
const DASH_SEGMENT = CIRCUMFERENCE / 8;
|
|
4
|
+
export const ProgressRing = ({ progress, colorClass, label, animate = false, pendingStyle = 'none', testId, }) => {
|
|
5
|
+
const MIN_VISIBLE = 0.08;
|
|
6
|
+
const visibleProgress = animate ? Math.max(progress, MIN_VISIBLE) : progress;
|
|
7
|
+
const dashoffset = CIRCUMFERENCE * (1 - visibleProgress);
|
|
8
|
+
const shouldSpin = animate && progress < 0.15;
|
|
9
|
+
const isDashed = !animate && pendingStyle === 'dashed';
|
|
10
|
+
const center = RING_SIZE / 2;
|
|
11
|
+
return (_jsxs("svg", { width: RING_SIZE, height: RING_SIZE, viewBox: `0 0 ${RING_SIZE} ${RING_SIZE}`, role: "img", "aria-label": label, className: animate && !shouldSpin ? 'animate-buffer-pulse motion-reduce:animate-none' : '', children: [_jsx("circle", { cx: center, cy: center, r: RADIUS, fill: "none", stroke: "currentColor", strokeWidth: STROKE_WIDTH, className: `text-border ${isDashed ? 'animate-ring-drift motion-reduce:animate-none' : ''}`, strokeDasharray: isDashed ? `${DASH_SEGMENT * 0.6} ${DASH_SEGMENT * 0.4}` : undefined, style: isDashed ? { transformOrigin: `${center}px ${center}px` } : undefined }), _jsx("circle", { "data-testid": testId, cx: center, cy: center, r: RADIUS, fill: "none", stroke: "currentColor", strokeWidth: STROKE_WIDTH, strokeDasharray: CIRCUMFERENCE, strokeDashoffset: dashoffset, strokeLinecap: "round", className: `${colorClass} transition-[stroke-dashoffset,color] duration-500 ${shouldSpin ? 'animate-ring-spin motion-reduce:animate-none' : ''}`, style: { transformOrigin: `${center}px ${center}px` } })] }));
|
|
12
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
export const Select = ({ value, onChange, options, groups, placeholder, id, className, menuClassName, renderOption, onOpenChange, 'aria-label': ariaLabel, }) => {
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
8
|
+
const containerRef = useRef(null);
|
|
9
|
+
const changeOpen = useCallback((next) => {
|
|
10
|
+
setOpen(next);
|
|
11
|
+
onOpenChange?.(next);
|
|
12
|
+
}, [onOpenChange]);
|
|
13
|
+
// Build flat items list with group metadata
|
|
14
|
+
const { items, groupStartIndices } = useMemo(() => {
|
|
15
|
+
const flat = [...(options ?? [])];
|
|
16
|
+
const starts = [];
|
|
17
|
+
for (const group of groups ?? []) {
|
|
18
|
+
starts.push({ index: flat.length, label: group.label });
|
|
19
|
+
flat.push(...group.options);
|
|
20
|
+
}
|
|
21
|
+
return { items: flat, groupStartIndices: starts };
|
|
22
|
+
}, [options, groups]);
|
|
23
|
+
const selectedOption = items.find(o => o.value === value);
|
|
24
|
+
const displayText = selectedOption?.label ?? placeholder ?? value;
|
|
25
|
+
// Click outside to close
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleClickOutside = (e) => {
|
|
28
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
29
|
+
changeOpen(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
33
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
34
|
+
}, [changeOpen]);
|
|
35
|
+
const selectItem = (itemValue) => {
|
|
36
|
+
onChange(itemValue);
|
|
37
|
+
changeOpen(false);
|
|
38
|
+
setHighlightedIndex(-1);
|
|
39
|
+
};
|
|
40
|
+
const handleKeyDown = (e) => {
|
|
41
|
+
if (e.key === 'Escape') {
|
|
42
|
+
changeOpen(false);
|
|
43
|
+
setHighlightedIndex(-1);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!open) {
|
|
47
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
changeOpen(true);
|
|
50
|
+
setHighlightedIndex(0);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (e.key === 'ArrowDown') {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
setHighlightedIndex(prev => (prev + 1) % items.length);
|
|
57
|
+
}
|
|
58
|
+
else if (e.key === 'ArrowUp') {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setHighlightedIndex(prev => (prev - 1 + items.length) % items.length);
|
|
61
|
+
}
|
|
62
|
+
else if (e.key === 'Enter') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
const highlighted = items[highlightedIndex];
|
|
65
|
+
if (highlighted) {
|
|
66
|
+
selectItem(highlighted.value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const itemClassName = (index, itemValue) => {
|
|
71
|
+
const base = 'px-3 py-2.5 cursor-pointer';
|
|
72
|
+
const highlight = index === highlightedIndex ? 'bg-primary-muted' : 'hover:bg-primary-muted';
|
|
73
|
+
const selected = itemValue === value ? 'font-medium' : '';
|
|
74
|
+
return `${base} ${highlight} ${selected}`;
|
|
75
|
+
};
|
|
76
|
+
return (_jsxs("div", { className: "relative", ref: containerRef, onKeyDown: handleKeyDown, children: [_jsxs("button", { type: "button", id: id, className: `flex items-center justify-between gap-2 ${className ?? ''}`, "aria-expanded": open, "aria-haspopup": "listbox", "aria-label": ariaLabel, onClick: () => changeOpen(!open), children: [_jsx("span", { className: "truncate", children: displayText }), _jsx(ChevronDown, { className: `size-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}` })] }), open && (_jsx("ul", { role: "listbox", className: `border-border bg-background absolute z-50 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-lg border shadow-lg ${menuClassName ?? ''}`, children: items.map((item, index) => {
|
|
77
|
+
const groupHeader = groupStartIndices.find(g => g.index === index);
|
|
78
|
+
return (_jsxs("li", { role: "presentation", children: [groupHeader && (_jsx("div", { className: "text-muted-foreground px-3 pt-2 pb-1 text-xs font-medium uppercase", children: groupHeader.label })), _jsx("div", { role: "option", "aria-selected": value === item.value, className: itemClassName(index, item.value), onClick: () => selectItem(item.value), onMouseEnter: () => setHighlightedIndex(index), children: renderOption
|
|
79
|
+
? renderOption(item, {
|
|
80
|
+
highlighted: index === highlightedIndex,
|
|
81
|
+
selected: value === item.value,
|
|
82
|
+
})
|
|
83
|
+
: item.label })] }, item.value));
|
|
84
|
+
}) }))] }));
|
|
85
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../helpers/cn/cn';
|
|
4
|
+
import { Slider as SliderPrimitive } from '@base-ui/react/slider';
|
|
5
|
+
export const Slider = ({ value, onChange, onCommit, min, max, step = 1, disabled, 'aria-label': ariaLabel, className, }) => (_jsx(SliderPrimitive.Root, { value: value, onValueChange: v => onChange(v), onValueCommitted: v => onCommit?.(v), min: min, max: max, step: step, disabled: disabled, "aria-label": ariaLabel, className: cn('relative flex w-full touch-none items-center py-2', className), children: _jsxs(SliderPrimitive.Control, { className: "relative flex h-1.5 w-full items-center", children: [_jsx(SliderPrimitive.Track, { className: "bg-input h-full w-full overflow-hidden rounded-full", children: _jsx(SliderPrimitive.Indicator, { className: "bg-primary h-full rounded-full" }) }), _jsx(SliderPrimitive.Thumb, { className: cn('bg-primary border-background block size-4 rounded-full border-2 shadow-sm', 'focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-hidden', disabled && 'pointer-events-none opacity-50') })] }) }));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useTheme } from 'next-themes';
|
|
3
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
4
|
+
import { useHotkeys } from 'react-hotkeys-hook';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
const THEME_CYCLE = ['system', 'light', 'dark'];
|
|
7
|
+
const THEME_LABELS = {
|
|
8
|
+
system: 'System',
|
|
9
|
+
light: 'Light',
|
|
10
|
+
dark: 'Dark',
|
|
11
|
+
};
|
|
12
|
+
const emptySubscribe = () => () => { };
|
|
13
|
+
const getSnapshot = () => true;
|
|
14
|
+
const getServerSnapshot = () => false;
|
|
15
|
+
export const ThemeCycler = () => {
|
|
16
|
+
const { theme, setTheme } = useTheme();
|
|
17
|
+
const mounted = useSyncExternalStore(emptySubscribe, getSnapshot, getServerSnapshot);
|
|
18
|
+
const cycleTheme = useCallback(() => {
|
|
19
|
+
if (!mounted)
|
|
20
|
+
return;
|
|
21
|
+
const current = theme ?? 'system';
|
|
22
|
+
const currentIndex = THEME_CYCLE.indexOf(current);
|
|
23
|
+
const nextIndex = (currentIndex + 1) % THEME_CYCLE.length;
|
|
24
|
+
const next = THEME_CYCLE[nextIndex] ?? 'system';
|
|
25
|
+
setTheme(next);
|
|
26
|
+
toast(`Theme: ${THEME_LABELS[next]}`, { duration: 2000 });
|
|
27
|
+
}, [theme, setTheme, mounted]);
|
|
28
|
+
useHotkeys('shift+t', cycleTheme, { preventDefault: true });
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
|
4
|
+
export const ThemeProvider = ({ children }) => (_jsx(NextThemesProvider, { attribute: "class", defaultTheme: "system", enableSystem: true, disableTransitionOnChange: true, children: children }));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Kbd } from '../Kbd/Kbd';
|
|
4
|
+
import { cloneElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
const DEFAULT_DELAY = 200;
|
|
6
|
+
export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DELAY, maxWidth, disabled, className, children, }) => {
|
|
7
|
+
const [visible, setVisible] = useState(false);
|
|
8
|
+
const timeoutRef = useRef(null);
|
|
9
|
+
const wrapperRef = useRef(null);
|
|
10
|
+
const show = useCallback(() => {
|
|
11
|
+
if (disabled)
|
|
12
|
+
return;
|
|
13
|
+
timeoutRef.current = setTimeout(() => setVisible(true), delay);
|
|
14
|
+
}, [disabled, delay]);
|
|
15
|
+
const hide = useCallback(() => {
|
|
16
|
+
if (timeoutRef.current) {
|
|
17
|
+
clearTimeout(timeoutRef.current);
|
|
18
|
+
timeoutRef.current = null;
|
|
19
|
+
}
|
|
20
|
+
setVisible(false);
|
|
21
|
+
}, []);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (disabled && timeoutRef.current) {
|
|
24
|
+
clearTimeout(timeoutRef.current);
|
|
25
|
+
timeoutRef.current = null;
|
|
26
|
+
}
|
|
27
|
+
}, [disabled]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
return () => {
|
|
30
|
+
if (timeoutRef.current)
|
|
31
|
+
clearTimeout(timeoutRef.current);
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!visible)
|
|
36
|
+
return;
|
|
37
|
+
let lastCheck = 0;
|
|
38
|
+
const handlePointerMove = (e) => {
|
|
39
|
+
if (e.timeStamp - lastCheck < 100)
|
|
40
|
+
return;
|
|
41
|
+
lastCheck = e.timeStamp;
|
|
42
|
+
const rect = wrapperRef.current?.getBoundingClientRect();
|
|
43
|
+
if (!rect)
|
|
44
|
+
return;
|
|
45
|
+
const inside = e.clientX >= rect.left &&
|
|
46
|
+
e.clientX <= rect.right &&
|
|
47
|
+
e.clientY >= rect.top &&
|
|
48
|
+
e.clientY <= rect.bottom;
|
|
49
|
+
if (!inside)
|
|
50
|
+
hide();
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener('pointermove', handlePointerMove, { passive: true });
|
|
53
|
+
return () => document.removeEventListener('pointermove', handlePointerMove);
|
|
54
|
+
}, [visible, hide]);
|
|
55
|
+
const positionClasses = position === 'top' ? 'bottom-full mb-2' : 'top-full mt-2';
|
|
56
|
+
return (_jsxs("div", { ref: wrapperRef, className: `relative inline-flex${className ? ` ${className}` : ''}`, onMouseEnter: show, onMouseLeave: hide, onPointerDown: hide, onFocus: show, onBlur: hide, children: [cloneElement(children, {
|
|
57
|
+
'aria-label': children.props['aria-label'] ?? label,
|
|
58
|
+
}), visible && !disabled && (_jsxs("div", { role: "tooltip", style: maxWidth ? { maxWidth } : undefined, className: `absolute left-1/2 z-50 -translate-x-1/2 ${maxWidth ? 'w-max whitespace-normal' : 'whitespace-nowrap'} bg-foreground text-background pointer-events-none flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs shadow-lg ${positionClasses}`, children: [label, shortcut && (_jsx(Kbd, { keys: shortcut, size: "sm", className: "bg-background/15 border-transparent" }))] }))] }));
|
|
59
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const getModKey = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? 'Cmd' : 'Ctrl';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { Badge, badgeVariants } from './Badge/Badge';
|
|
2
|
+
export { Button, buttonVariants } from './Button/Button';
|
|
3
|
+
export { Kbd, kbdVariants } from './Kbd/Kbd';
|
|
4
|
+
export { ProgressRing } from './ProgressRing/ProgressRing';
|
|
5
|
+
export { Slider } from './Slider/Slider';
|
|
6
|
+
export { ContextMenu } from './ContextMenu/ContextMenu';
|
|
7
|
+
export { Select } from './Select/Select';
|
|
8
|
+
export { Tooltip } from './Tooltip/Tooltip';
|
|
9
|
+
export { ThemeProvider } from './ThemeProvider/ThemeProvider';
|
|
10
|
+
export { ThemeCycler } from './ThemeCycler/ThemeCycler';
|
|
11
|
+
export { Toaster } from './Toaster/Toaster';
|
|
12
|
+
export { toast } from 'sonner';
|
|
13
|
+
export { useTheme } from 'next-themes';
|
|
14
|
+
export { cn } from './helpers/cn/cn';
|
|
15
|
+
export { getModKey } from './helpers/getModKey/getModKey';
|
|
16
|
+
export { cva } from 'class-variance-authority';
|