apex-design-cli 1.0.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/dist/index.d.ts +1 -0
- package/dist/index.js +747 -0
- package/package.json +57 -0
- package/registry/components/accordion.json +26 -0
- package/registry/components/alert.json +25 -0
- package/registry/components/avatar.json +26 -0
- package/registry/components/badge.json +24 -0
- package/registry/components/breadcrumb.json +28 -0
- package/registry/components/button.json +25 -0
- package/registry/components/card.json +28 -0
- package/registry/components/checkbox.json +23 -0
- package/registry/components/command.json +31 -0
- package/registry/components/dialog.json +32 -0
- package/registry/components/divider.json +22 -0
- package/registry/components/dropdown-menu.json +36 -0
- package/registry/components/empty-state.json +21 -0
- package/registry/components/error-message.json +20 -0
- package/registry/components/field-group.json +20 -0
- package/registry/components/helper-text.json +20 -0
- package/registry/components/input.json +21 -0
- package/registry/components/label.json +20 -0
- package/registry/components/progress.json +22 -0
- package/registry/components/radio.json +23 -0
- package/registry/components/select.json +32 -0
- package/registry/components/spinner.json +20 -0
- package/registry/components/switch.json +22 -0
- package/registry/components/table.json +27 -0
- package/registry/components/tabs.json +25 -0
- package/registry/components/textarea.json +21 -0
- package/registry/components/theme-toggler.json +24 -0
- package/registry/components/toast.json +31 -0
- package/registry/components/tooltip.json +26 -0
- package/registry/components/use-theme.json +19 -0
- package/registry/components/utils.json +21 -0
- package/registry/registry.json +35 -0
- package/registry/source/accordion.tsx +55 -0
- package/registry/source/alert.tsx +102 -0
- package/registry/source/avatar.tsx +137 -0
- package/registry/source/badge.tsx +38 -0
- package/registry/source/breadcrumb.tsx +109 -0
- package/registry/source/button.tsx +58 -0
- package/registry/source/card.tsx +108 -0
- package/registry/source/checkbox.tsx +170 -0
- package/registry/source/command.tsx +195 -0
- package/registry/source/dialog.tsx +133 -0
- package/registry/source/divider.tsx +84 -0
- package/registry/source/dropdown-menu.tsx +209 -0
- package/registry/source/empty-state.tsx +88 -0
- package/registry/source/error-message.tsx +49 -0
- package/registry/source/field-group.tsx +53 -0
- package/registry/source/helper-text.tsx +40 -0
- package/registry/source/input.tsx +219 -0
- package/registry/source/label.tsx +60 -0
- package/registry/source/progress.tsx +84 -0
- package/registry/source/radio.tsx +161 -0
- package/registry/source/select.tsx +278 -0
- package/registry/source/spinner.tsx +84 -0
- package/registry/source/switch.tsx +104 -0
- package/registry/source/table.tsx +116 -0
- package/registry/source/tabs.tsx +55 -0
- package/registry/source/textarea.tsx +129 -0
- package/registry/source/theme-toggler.tsx +94 -0
- package/registry/source/toast.tsx +166 -0
- package/registry/source/tooltip.tsx +55 -0
- package/registry/source/use-theme.tsx +102 -0
- package/registry/source/utils.ts +13 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
|
|
4
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
5
|
+
/** Additional CSS classes */
|
|
6
|
+
className?: string;
|
|
7
|
+
/** Textarea size */
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
/** Error state */
|
|
10
|
+
error?: boolean;
|
|
11
|
+
/** Label text */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Helper text shown below textarea */
|
|
14
|
+
helperText?: string;
|
|
15
|
+
/** Error message shown when error={true} */
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
/** Mark as required */
|
|
18
|
+
required?: boolean;
|
|
19
|
+
/** Show character count */
|
|
20
|
+
showCount?: boolean;
|
|
21
|
+
/** Resize behavior */
|
|
22
|
+
resize?: 'none' | 'vertical' | 'both';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
className,
|
|
29
|
+
size = 'md',
|
|
30
|
+
error = false,
|
|
31
|
+
label,
|
|
32
|
+
helperText,
|
|
33
|
+
errorMessage,
|
|
34
|
+
required,
|
|
35
|
+
showCount = false,
|
|
36
|
+
resize = 'vertical',
|
|
37
|
+
maxLength,
|
|
38
|
+
value,
|
|
39
|
+
id,
|
|
40
|
+
...props
|
|
41
|
+
},
|
|
42
|
+
ref
|
|
43
|
+
) => {
|
|
44
|
+
const textareaId = id || React.useId();
|
|
45
|
+
const helperTextId = `${textareaId}-helper`;
|
|
46
|
+
const errorMessageId = `${textareaId}-error`;
|
|
47
|
+
|
|
48
|
+
const currentLength = typeof value === 'string' ? value.length : 0;
|
|
49
|
+
|
|
50
|
+
const sizeClasses = {
|
|
51
|
+
sm: 'min-h-[80px] px-3 py-2 text-sm',
|
|
52
|
+
md: 'min-h-[120px] px-3 py-2 text-base',
|
|
53
|
+
lg: 'min-h-[160px] px-4 py-3 text-lg',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const resizeClasses = {
|
|
57
|
+
none: 'resize-none',
|
|
58
|
+
vertical: 'resize-y',
|
|
59
|
+
both: 'resize',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="w-full">
|
|
64
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
65
|
+
{label && (
|
|
66
|
+
<label
|
|
67
|
+
htmlFor={textareaId}
|
|
68
|
+
className="block text-sm font-medium text-semantic-fg-secondary"
|
|
69
|
+
>
|
|
70
|
+
{label}
|
|
71
|
+
{required && <span className="ml-1 text-semantic-fg-error">*</span>}
|
|
72
|
+
</label>
|
|
73
|
+
)}
|
|
74
|
+
{showCount && maxLength && (
|
|
75
|
+
<span className="text-xs text-semantic-fg-secondary">
|
|
76
|
+
{currentLength}/{maxLength}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<textarea
|
|
82
|
+
ref={ref}
|
|
83
|
+
id={textareaId}
|
|
84
|
+
value={value}
|
|
85
|
+
maxLength={maxLength}
|
|
86
|
+
className={cn(
|
|
87
|
+
'w-full rounded-md border bg-semantic-control-bg transition-colors',
|
|
88
|
+
'placeholder:text-semantic-control-placeholder',
|
|
89
|
+
'focus:outline-none focus:ring-2 focus:ring-offset-0',
|
|
90
|
+
'disabled:cursor-not-allowed disabled:bg-semantic-bg-disabled disabled:text-semantic-fg-disabled',
|
|
91
|
+
sizeClasses[size],
|
|
92
|
+
resizeClasses[resize],
|
|
93
|
+
error
|
|
94
|
+
? 'border-semantic-control-border-error text-semantic-control-fg focus:ring-semantic-control-border-error focus:border-semantic-control-border-error'
|
|
95
|
+
: 'border-semantic-control-border text-semantic-control-fg focus:ring-semantic-focus focus:border-semantic-border-focus',
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
98
|
+
aria-invalid={error ? 'true' : 'false'}
|
|
99
|
+
aria-describedby={
|
|
100
|
+
error && errorMessage
|
|
101
|
+
? errorMessageId
|
|
102
|
+
: helperText
|
|
103
|
+
? helperTextId
|
|
104
|
+
: undefined
|
|
105
|
+
}
|
|
106
|
+
aria-required={required}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
{!error && helperText && (
|
|
111
|
+
<p id={helperTextId} className="mt-1.5 text-sm text-semantic-fg-secondary">
|
|
112
|
+
{helperText}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{error && errorMessage && (
|
|
117
|
+
<p id={errorMessageId} className="mt-1.5 text-sm text-semantic-fg-error">
|
|
118
|
+
{errorMessage}
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
Textarea.displayName = 'Textarea';
|
|
127
|
+
|
|
128
|
+
export { Textarea };
|
|
129
|
+
export default Textarea;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Sun, Moon, Monitor } from 'lucide-react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
import { useTheme } from '../../hooks/useTheme';
|
|
4
|
+
|
|
5
|
+
export interface ThemeTogglerProps {
|
|
6
|
+
/** Show system option alongside light/dark */
|
|
7
|
+
showSystem?: boolean;
|
|
8
|
+
/** Size of the toggler */
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
/** Additional CSS classes */
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sizeMap = {
|
|
15
|
+
sm: { button: 'h-8 w-8', icon: 'h-4 w-4' },
|
|
16
|
+
md: { button: 'h-9 w-9', icon: 'h-5 w-5' },
|
|
17
|
+
lg: { button: 'h-10 w-10', icon: 'h-6 w-6' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function ThemeToggler({ showSystem = false, size = 'md', className }: ThemeTogglerProps) {
|
|
21
|
+
const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();
|
|
22
|
+
const { button: buttonSize, icon: iconSize } = sizeMap[size];
|
|
23
|
+
|
|
24
|
+
if (showSystem) {
|
|
25
|
+
return (
|
|
26
|
+
<div role="radiogroup" aria-label="Theme selector" className={cn('inline-flex items-center rounded-lg border border-semantic-border-default bg-semantic-bg-sunken p-1', className)}>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
role="radio"
|
|
30
|
+
aria-checked={theme === 'light'}
|
|
31
|
+
onClick={() => setTheme('light')}
|
|
32
|
+
className={cn(
|
|
33
|
+
'inline-flex items-center justify-center rounded-md p-1.5 transition-colors',
|
|
34
|
+
theme === 'light'
|
|
35
|
+
? 'bg-semantic-bg-segment-active text-semantic-fg-segment-active shadow-sm'
|
|
36
|
+
: 'text-semantic-fg-muted hover:text-semantic-fg-secondary'
|
|
37
|
+
)}
|
|
38
|
+
aria-label="Light mode"
|
|
39
|
+
>
|
|
40
|
+
<Sun className={iconSize} aria-hidden="true" />
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
role="radio"
|
|
45
|
+
aria-checked={theme === 'dark'}
|
|
46
|
+
onClick={() => setTheme('dark')}
|
|
47
|
+
className={cn(
|
|
48
|
+
'inline-flex items-center justify-center rounded-md p-1.5 transition-colors',
|
|
49
|
+
theme === 'dark'
|
|
50
|
+
? 'bg-semantic-bg-segment-active text-semantic-fg-segment-active shadow-sm'
|
|
51
|
+
: 'text-semantic-fg-muted hover:text-semantic-fg-secondary'
|
|
52
|
+
)}
|
|
53
|
+
aria-label="Dark mode"
|
|
54
|
+
>
|
|
55
|
+
<Moon className={iconSize} aria-hidden="true" />
|
|
56
|
+
</button>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
role="radio"
|
|
60
|
+
aria-checked={theme === 'system'}
|
|
61
|
+
onClick={() => setTheme('system')}
|
|
62
|
+
className={cn(
|
|
63
|
+
'inline-flex items-center justify-center rounded-md p-1.5 transition-colors',
|
|
64
|
+
theme === 'system'
|
|
65
|
+
? 'bg-semantic-bg-segment-active text-semantic-fg-segment-active shadow-sm'
|
|
66
|
+
: 'text-semantic-fg-muted hover:text-semantic-fg-secondary'
|
|
67
|
+
)}
|
|
68
|
+
aria-label="System theme"
|
|
69
|
+
>
|
|
70
|
+
<Monitor className={iconSize} aria-hidden="true" />
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={toggleTheme}
|
|
80
|
+
className={cn(
|
|
81
|
+
buttonSize,
|
|
82
|
+
'inline-flex items-center justify-center rounded-md border border-semantic-border-default bg-semantic-bg-elevated text-semantic-fg-secondary transition-colors hover:bg-semantic-bg-hover hover:text-semantic-fg-primary',
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
aria-label={resolvedTheme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
|
86
|
+
>
|
|
87
|
+
{resolvedTheme === 'light' ? (
|
|
88
|
+
<Moon className={iconSize} aria-hidden="true" />
|
|
89
|
+
) : (
|
|
90
|
+
<Sun className={iconSize} aria-hidden="true" />
|
|
91
|
+
)}
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as ToastPrimitives from '@radix-ui/react-toast';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
import { X, CheckCircle2, XCircle, AlertCircle, Info } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
|
|
7
|
+
const ToastProvider = ToastPrimitives.Provider;
|
|
8
|
+
|
|
9
|
+
const ToastViewport = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<ToastPrimitives.Viewport
|
|
14
|
+
ref={ref}
|
|
15
|
+
aria-live="polite"
|
|
16
|
+
aria-atomic="true"
|
|
17
|
+
className={cn(
|
|
18
|
+
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
));
|
|
24
|
+
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
25
|
+
|
|
26
|
+
const toastVariants = cva(
|
|
27
|
+
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
default: 'border-semantic-border-default bg-semantic-bg-elevated text-semantic-fg-primary',
|
|
32
|
+
success: 'success border-semantic-status-success-border bg-semantic-status-success-bg text-semantic-status-success-fg',
|
|
33
|
+
error: 'error border-semantic-status-error-border bg-semantic-status-error-bg text-semantic-status-error-fg',
|
|
34
|
+
warning: 'warning border-semantic-status-warning-border bg-semantic-status-warning-bg text-semantic-status-warning-fg',
|
|
35
|
+
info: 'info border-semantic-status-info-border bg-semantic-status-info-bg text-semantic-status-info-fg',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
variant: 'default',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const Toast = React.forwardRef<
|
|
45
|
+
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
46
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
47
|
+
VariantProps<typeof toastVariants>
|
|
48
|
+
>(({ className, variant, ...props }, ref) => {
|
|
49
|
+
return (
|
|
50
|
+
<ToastPrimitives.Root
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn(toastVariants({ variant }), className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
Toast.displayName = ToastPrimitives.Root.displayName;
|
|
58
|
+
|
|
59
|
+
const ToastAction = React.forwardRef<
|
|
60
|
+
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
61
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
62
|
+
>(({ className, ...props }, ref) => (
|
|
63
|
+
<ToastPrimitives.Action
|
|
64
|
+
ref={ref}
|
|
65
|
+
className={cn(
|
|
66
|
+
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-semantic-border-default bg-transparent px-3 text-sm font-medium ring-offset-semantic-offset transition-colors hover:bg-semantic-bg-hover focus:outline-none focus:ring-2 focus:ring-semantic-focus focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.success]:border-semantic-status-success-border group-[.success]:hover:bg-semantic-status-success-bg group-[.error]:border-semantic-status-error-border group-[.error]:hover:bg-semantic-status-error-bg',
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
));
|
|
72
|
+
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
|
73
|
+
|
|
74
|
+
const ToastClose = React.forwardRef<
|
|
75
|
+
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
76
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
77
|
+
>(({ className, ...props }, ref) => (
|
|
78
|
+
<ToastPrimitives.Close
|
|
79
|
+
ref={ref}
|
|
80
|
+
className={cn(
|
|
81
|
+
'absolute right-2 top-2 rounded-md p-1 opacity-0 transition-opacity hover:opacity-100 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100',
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
toast-close=""
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
<X className="h-4 w-4" aria-hidden="true" />
|
|
88
|
+
<span className="sr-only">Close</span>
|
|
89
|
+
</ToastPrimitives.Close>
|
|
90
|
+
));
|
|
91
|
+
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
|
92
|
+
|
|
93
|
+
const ToastTitle = React.forwardRef<
|
|
94
|
+
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
95
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
96
|
+
>(({ className, ...props }, ref) => (
|
|
97
|
+
<ToastPrimitives.Title
|
|
98
|
+
ref={ref}
|
|
99
|
+
className={cn('text-sm font-semibold', className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
));
|
|
103
|
+
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
|
104
|
+
|
|
105
|
+
const ToastDescription = React.forwardRef<
|
|
106
|
+
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
107
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
108
|
+
>(({ className, ...props }, ref) => (
|
|
109
|
+
<ToastPrimitives.Description
|
|
110
|
+
ref={ref}
|
|
111
|
+
className={cn('text-sm opacity-90', className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
));
|
|
115
|
+
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
|
116
|
+
|
|
117
|
+
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
|
118
|
+
|
|
119
|
+
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
|
120
|
+
|
|
121
|
+
export interface ToastContentProps {
|
|
122
|
+
title?: string;
|
|
123
|
+
description?: string;
|
|
124
|
+
action?: ToastActionElement;
|
|
125
|
+
variant?: 'default' | 'success' | 'error' | 'warning' | 'info';
|
|
126
|
+
icon?: React.ReactNode;
|
|
127
|
+
showIcon?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ToastContent = React.forwardRef<HTMLDivElement, ToastContentProps>(
|
|
131
|
+
({ title, description, icon, showIcon = true, variant = 'default' }, ref) => {
|
|
132
|
+
const defaultIcons = {
|
|
133
|
+
default: <Info className="h-5 w-5" />,
|
|
134
|
+
info: <Info className="h-5 w-5 text-semantic-status-info-icon" />,
|
|
135
|
+
success: <CheckCircle2 className="h-5 w-5 text-semantic-status-success-icon" />,
|
|
136
|
+
error: <XCircle className="h-5 w-5 text-semantic-status-error-icon" />,
|
|
137
|
+
warning: <AlertCircle className="h-5 w-5 text-semantic-status-warning-icon" />,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const iconToRender = icon || defaultIcons[variant];
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div ref={ref} className="flex gap-3">
|
|
144
|
+
{showIcon && <div className="flex-shrink-0" aria-hidden="true">{iconToRender}</div>}
|
|
145
|
+
<div className="grid gap-1">
|
|
146
|
+
{title && <ToastTitle>{title}</ToastTitle>}
|
|
147
|
+
{description && <ToastDescription>{description}</ToastDescription>}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
ToastContent.displayName = 'ToastContent';
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
type ToastProps,
|
|
157
|
+
type ToastActionElement,
|
|
158
|
+
ToastProvider,
|
|
159
|
+
ToastViewport,
|
|
160
|
+
Toast,
|
|
161
|
+
ToastTitle,
|
|
162
|
+
ToastDescription,
|
|
163
|
+
ToastClose,
|
|
164
|
+
ToastAction,
|
|
165
|
+
ToastContent,
|
|
166
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
6
|
+
|
|
7
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
8
|
+
|
|
9
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
10
|
+
|
|
11
|
+
const TooltipContent = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
14
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
15
|
+
<TooltipPrimitive.Content
|
|
16
|
+
ref={ref}
|
|
17
|
+
sideOffset={sideOffset}
|
|
18
|
+
className={cn(
|
|
19
|
+
'z-50 overflow-hidden rounded-md border border-semantic-border-default bg-semantic-bg-elevated px-3 py-1.5 text-sm text-semantic-fg-primary shadow-md',
|
|
20
|
+
'animate-in fade-in-0 zoom-in-95',
|
|
21
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
22
|
+
'data-[side=bottom]:slide-in-from-top-2',
|
|
23
|
+
'data-[side=left]:slide-in-from-right-2',
|
|
24
|
+
'data-[side=right]:slide-in-from-left-2',
|
|
25
|
+
'data-[side=top]:slide-in-from-bottom-2',
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
));
|
|
31
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
32
|
+
|
|
33
|
+
export interface SimpleTooltipProps {
|
|
34
|
+
content: React.ReactNode;
|
|
35
|
+
children: React.ReactElement;
|
|
36
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
37
|
+
delayDuration?: number;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SimpleTooltip = React.forwardRef<HTMLDivElement, SimpleTooltipProps>(
|
|
42
|
+
({ content, children, side = 'top', delayDuration = 200, className }, ref) => {
|
|
43
|
+
return (
|
|
44
|
+
<Tooltip delayDuration={delayDuration}>
|
|
45
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
46
|
+
<TooltipContent side={side} className={className} ref={ref}>
|
|
47
|
+
{content}
|
|
48
|
+
</TooltipContent>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
SimpleTooltip.displayName = 'SimpleTooltip';
|
|
54
|
+
|
|
55
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, SimpleTooltip };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type Theme = 'light' | 'dark' | 'system';
|
|
5
|
+
type ResolvedTheme = 'light' | 'dark';
|
|
6
|
+
|
|
7
|
+
interface ThemeContextValue {
|
|
8
|
+
theme: Theme;
|
|
9
|
+
resolvedTheme: ResolvedTheme;
|
|
10
|
+
setTheme: (theme: Theme) => void;
|
|
11
|
+
toggleTheme: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
15
|
+
|
|
16
|
+
const STORAGE_KEY = 'ds-theme';
|
|
17
|
+
|
|
18
|
+
function getSystemTheme(): ResolvedTheme {
|
|
19
|
+
if (typeof window === 'undefined') return 'light';
|
|
20
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveTheme(theme: Theme): ResolvedTheme {
|
|
24
|
+
return theme === 'system' ? getSystemTheme() : theme;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applyTheme(resolved: ResolvedTheme) {
|
|
28
|
+
const root = document.documentElement;
|
|
29
|
+
root.classList.remove('light', 'dark');
|
|
30
|
+
root.classList.add(resolved);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ThemeProviderProps {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
defaultTheme?: Theme;
|
|
36
|
+
storageKey?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ThemeProvider({
|
|
40
|
+
children,
|
|
41
|
+
defaultTheme = 'system',
|
|
42
|
+
storageKey = STORAGE_KEY,
|
|
43
|
+
}: ThemeProviderProps) {
|
|
44
|
+
const [theme, setThemeState] = useState<Theme>(() => {
|
|
45
|
+
if (typeof window === 'undefined') return defaultTheme;
|
|
46
|
+
const stored = localStorage.getItem(storageKey) as Theme | null;
|
|
47
|
+
return stored || defaultTheme;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
|
|
51
|
+
resolveTheme(theme)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const setTheme = useCallback(
|
|
55
|
+
(newTheme: Theme) => {
|
|
56
|
+
setThemeState(newTheme);
|
|
57
|
+
localStorage.setItem(storageKey, newTheme);
|
|
58
|
+
const resolved = resolveTheme(newTheme);
|
|
59
|
+
setResolvedTheme(resolved);
|
|
60
|
+
applyTheme(resolved);
|
|
61
|
+
},
|
|
62
|
+
[storageKey]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const toggleTheme = useCallback(() => {
|
|
66
|
+
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
|
|
67
|
+
}, [resolvedTheme, setTheme]);
|
|
68
|
+
|
|
69
|
+
// Apply theme on mount
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
applyTheme(resolveTheme(theme));
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
// Listen for system theme changes
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (theme !== 'system') return;
|
|
77
|
+
|
|
78
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
79
|
+
const handler = () => {
|
|
80
|
+
const resolved = getSystemTheme();
|
|
81
|
+
setResolvedTheme(resolved);
|
|
82
|
+
applyTheme(resolved);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
mediaQuery.addEventListener('change', handler);
|
|
86
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
87
|
+
}, [theme]);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, toggleTheme }}>
|
|
91
|
+
{children}
|
|
92
|
+
</ThemeContext.Provider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useTheme(): ThemeContextValue {
|
|
97
|
+
const context = useContext(ThemeContext);
|
|
98
|
+
if (!context) {
|
|
99
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
100
|
+
}
|
|
101
|
+
return context;
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Utility function to merge Tailwind CSS classes with proper precedence
|
|
6
|
+
* Uses clsx to handle conditional classes and tailwind-merge to resolve conflicts
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* cn('px-4 py-2', isLarge && 'px-6 py-3', className)
|
|
10
|
+
*/
|
|
11
|
+
export function cn(...inputs: ClassValue[]) {
|
|
12
|
+
return twMerge(clsx(inputs));
|
|
13
|
+
}
|