radtools 0.1.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 +108 -0
- package/bin/radtools.js +5 -0
- package/dist/cli/index.js +427 -0
- package/package.json +55 -0
- package/templates/api-routes/assets/optimize/route.ts +94 -0
- package/templates/api-routes/assets/route.ts +159 -0
- package/templates/api-routes/components/create-folder/route.ts +55 -0
- package/templates/api-routes/components/route.ts +156 -0
- package/templates/api-routes/fonts/route.ts +96 -0
- package/templates/api-routes/fonts/upload/route.ts +79 -0
- package/templates/api-routes/read-css/route.ts +29 -0
- package/templates/api-routes/write-css/route.ts +423 -0
- package/templates/components/Rad_os/AppWindow.tsx +423 -0
- package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
- package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
- package/templates/components/icons/Icon.tsx +224 -0
- package/templates/components/icons/README.md +85 -0
- package/templates/components/icons/index.ts +20 -0
- package/templates/components/icons.tsx +164 -0
- package/templates/components/ui/Accordion.tsx +268 -0
- package/templates/components/ui/Alert.tsx +111 -0
- package/templates/components/ui/Badge.tsx +87 -0
- package/templates/components/ui/Breadcrumbs.tsx +88 -0
- package/templates/components/ui/Button.tsx +249 -0
- package/templates/components/ui/Card.tsx +137 -0
- package/templates/components/ui/Checkbox.tsx +137 -0
- package/templates/components/ui/ContextMenu.tsx +220 -0
- package/templates/components/ui/Dialog.tsx +264 -0
- package/templates/components/ui/Divider.tsx +70 -0
- package/templates/components/ui/DropdownMenu.tsx +301 -0
- package/templates/components/ui/HelpPanel.tsx +119 -0
- package/templates/components/ui/Input.tsx +176 -0
- package/templates/components/ui/Popover.tsx +211 -0
- package/templates/components/ui/Progress.tsx +158 -0
- package/templates/components/ui/Select.tsx +134 -0
- package/templates/components/ui/Sheet.tsx +316 -0
- package/templates/components/ui/Slider.tsx +223 -0
- package/templates/components/ui/Switch.tsx +155 -0
- package/templates/components/ui/Tabs.tsx +253 -0
- package/templates/components/ui/Toast.tsx +192 -0
- package/templates/components/ui/Tooltip.tsx +129 -0
- package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
- package/templates/components/ui/index.ts +84 -0
- package/templates/devtools/DevToolsPanel.tsx +261 -0
- package/templates/devtools/DevToolsProvider.tsx +43 -0
- package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
- package/templates/devtools/components/ColorPicker.tsx +33 -0
- package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
- package/templates/devtools/components/ContextualFooter.tsx +56 -0
- package/templates/devtools/components/DraggablePanel.tsx +43 -0
- package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
- package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
- package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
- package/templates/devtools/components/TokenDropdown.tsx +47 -0
- package/templates/devtools/components/TypographyFooter.tsx +145 -0
- package/templates/devtools/hooks/useMockState.ts +16 -0
- package/templates/devtools/index.ts +17 -0
- package/templates/devtools/lib/componentScanner.ts +78 -0
- package/templates/devtools/lib/cssParser.ts +465 -0
- package/templates/devtools/lib/searchIndexes.ts +45 -0
- package/templates/devtools/lib/selectorGenerator.ts +86 -0
- package/templates/devtools/store/index.ts +66 -0
- package/templates/devtools/store/slices/assetsSlice.ts +106 -0
- package/templates/devtools/store/slices/componentsSlice.ts +59 -0
- package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
- package/templates/devtools/store/slices/panelSlice.ts +17 -0
- package/templates/devtools/store/slices/typographySlice.ts +538 -0
- package/templates/devtools/store/slices/variablesSlice.ts +167 -0
- package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
- package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
- package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
- package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
- package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
- package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
- package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
- package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
- package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
- package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
- package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
- package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
- package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
- package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
- package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
- package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
- package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
- package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
- package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
- package/templates/devtools/types/index.ts +99 -0
- package/templates/globals.css +574 -0
- package/templates/hooks/index.ts +1 -0
- package/templates/hooks/useWindowManager.ts +212 -0
- package/templates/public/assets/icons/avatar.svg +18 -0
- package/templates/public/assets/icons/checkmark-filled.svg +14 -0
- package/templates/public/assets/icons/checkmark.svg +14 -0
- package/templates/public/assets/icons/chevron-down.svg +14 -0
- package/templates/public/assets/icons/close.svg +14 -0
- package/templates/public/assets/icons/copy.svg +14 -0
- package/templates/public/assets/icons/download.svg +14 -0
- package/templates/public/assets/icons/expand.svg +31 -0
- package/templates/public/assets/icons/file-blank.svg +17 -0
- package/templates/public/assets/icons/file-image.svg +19 -0
- package/templates/public/assets/icons/file-written.svg +17 -0
- package/templates/public/assets/icons/folder-closed.svg +17 -0
- package/templates/public/assets/icons/folder-open.svg +17 -0
- package/templates/public/assets/icons/hamburger.svg +18 -0
- package/templates/public/assets/icons/home-outline.svg +28 -0
- package/templates/public/assets/icons/home.svg +30 -0
- package/templates/public/assets/icons/hourglass.svg +25 -0
- package/templates/public/assets/icons/information-circle.svg +14 -0
- package/templates/public/assets/icons/information.svg +17 -0
- package/templates/public/assets/icons/lightning.svg +14 -0
- package/templates/public/assets/icons/locked.svg +17 -0
- package/templates/public/assets/icons/not-allowed.svg +14 -0
- package/templates/public/assets/icons/plus.svg +5 -0
- package/templates/public/assets/icons/power-thin.svg +17 -0
- package/templates/public/assets/icons/power.svg +17 -0
- package/templates/public/assets/icons/question-block.svg +14 -0
- package/templates/public/assets/icons/question.svg +17 -0
- package/templates/public/assets/icons/refresh-block.svg +14 -0
- package/templates/public/assets/icons/refresh.svg +17 -0
- package/templates/public/assets/icons/save.svg +14 -0
- package/templates/public/assets/icons/search.svg +25 -0
- package/templates/public/assets/icons/settings.svg +14 -0
- package/templates/public/assets/icons/trash-full.svg +21 -0
- package/templates/public/assets/icons/trash-open.svg +23 -0
- package/templates/public/assets/icons/trash.svg +18 -0
- package/templates/public/assets/icons/unlocked.svg +17 -0
- package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
- package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
- package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
- package/templates/public/assets/icons/wrench.svg +17 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect, createContext, useContext } from 'react';
|
|
4
|
+
import { Icon } from '@/components/icons';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
interface Position {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ContextMenuContextValue {
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
position: Position;
|
|
18
|
+
open: (position: Position) => void;
|
|
19
|
+
close: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ContextMenuProps {
|
|
23
|
+
/** Content that triggers context menu on right-click */
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
/** Additional classes for trigger container */
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ContextMenuContentProps {
|
|
30
|
+
/** Menu items */
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
/** Additional classes */
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ContextMenuItemProps {
|
|
37
|
+
/** Click handler */
|
|
38
|
+
onClick?: () => void;
|
|
39
|
+
/** Disabled state */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Destructive action (red text) */
|
|
42
|
+
destructive?: boolean;
|
|
43
|
+
/** Icon name (filename without .svg extension) */
|
|
44
|
+
iconName?: string;
|
|
45
|
+
/** Menu item content */
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
/** Additional classes */
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ContextMenuSeparatorProps {
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Context
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const ContextMenuContext = createContext<ContextMenuContextValue | null>(null);
|
|
60
|
+
|
|
61
|
+
function useContextMenu() {
|
|
62
|
+
const context = useContext(ContextMenuContext);
|
|
63
|
+
if (!context) {
|
|
64
|
+
throw new Error('ContextMenu components must be used within a ContextMenu provider');
|
|
65
|
+
}
|
|
66
|
+
return context;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Components
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Context menu container - wraps content that should have right-click menu
|
|
75
|
+
*/
|
|
76
|
+
export function ContextMenu({ children, className = '' }: ContextMenuProps) {
|
|
77
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
78
|
+
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
|
|
79
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
80
|
+
|
|
81
|
+
const open = (pos: Position) => {
|
|
82
|
+
setPosition(pos);
|
|
83
|
+
setIsOpen(true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const close = () => {
|
|
87
|
+
setIsOpen(false);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Handle right-click
|
|
91
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
open({ x: e.clientX, y: e.clientY });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Close on click outside
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
99
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
100
|
+
close();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
105
|
+
if (e.key === 'Escape') close();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (isOpen) {
|
|
109
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
110
|
+
document.addEventListener('keydown', handleEscape);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
115
|
+
document.removeEventListener('keydown', handleEscape);
|
|
116
|
+
};
|
|
117
|
+
}, [isOpen]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ContextMenuContext.Provider value={{ isOpen, position, open, close }}>
|
|
121
|
+
<div
|
|
122
|
+
ref={containerRef}
|
|
123
|
+
onContextMenu={handleContextMenu}
|
|
124
|
+
className={className}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</div>
|
|
128
|
+
</ContextMenuContext.Provider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Context menu dropdown content
|
|
134
|
+
*/
|
|
135
|
+
export function ContextMenuContent({ children, className = '' }: ContextMenuContentProps) {
|
|
136
|
+
const { isOpen, position, close } = useContextMenu();
|
|
137
|
+
|
|
138
|
+
if (!isOpen) return null;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
className={`
|
|
143
|
+
fixed z-[1000]
|
|
144
|
+
min-w-[160px]
|
|
145
|
+
bg-warm-cloud
|
|
146
|
+
border border-black
|
|
147
|
+
rounded-sm
|
|
148
|
+
shadow-[2px_2px_0_0_var(--color-black)]
|
|
149
|
+
py-1
|
|
150
|
+
${className}
|
|
151
|
+
`}
|
|
152
|
+
style={{
|
|
153
|
+
left: position.x,
|
|
154
|
+
top: position.y,
|
|
155
|
+
}}
|
|
156
|
+
onClick={close}
|
|
157
|
+
>
|
|
158
|
+
{children}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Context menu item
|
|
165
|
+
*/
|
|
166
|
+
export function ContextMenuItem({
|
|
167
|
+
onClick,
|
|
168
|
+
disabled = false,
|
|
169
|
+
destructive = false,
|
|
170
|
+
iconName,
|
|
171
|
+
children,
|
|
172
|
+
className = '',
|
|
173
|
+
}: ContextMenuItemProps) {
|
|
174
|
+
const { close } = useContextMenu();
|
|
175
|
+
|
|
176
|
+
const handleClick = () => {
|
|
177
|
+
if (!disabled && onClick) {
|
|
178
|
+
onClick();
|
|
179
|
+
close();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={handleClick}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
className={`
|
|
189
|
+
w-full flex items-center gap-2
|
|
190
|
+
px-3 py-1.5
|
|
191
|
+
font-mondwest text-base text-left
|
|
192
|
+
${destructive ? 'text-error-red' : 'text-black'}
|
|
193
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-sun-yellow cursor-pointer'}
|
|
194
|
+
${className}
|
|
195
|
+
`}
|
|
196
|
+
>
|
|
197
|
+
{iconName && (
|
|
198
|
+
<span className="w-4 h-4 flex items-center justify-center">
|
|
199
|
+
<Icon name={iconName} size={16} />
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
<span>{children}</span>
|
|
203
|
+
</button>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Context menu separator line
|
|
209
|
+
*/
|
|
210
|
+
export function ContextMenuSeparator({ className = '' }: ContextMenuSeparatorProps) {
|
|
211
|
+
return (
|
|
212
|
+
<div
|
|
213
|
+
className={`my-1 border-t ${className}`}
|
|
214
|
+
style={{ borderTopColor: 'var(--border-black-20)' }}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default ContextMenu;
|
|
220
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { useEscapeKey, useLockBodyScroll } from './hooks/useModalBehavior';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
interface DialogContextValue {
|
|
12
|
+
open: boolean;
|
|
13
|
+
setOpen: (open: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Context
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const DialogContext = createContext<DialogContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
function useDialogContext() {
|
|
23
|
+
const context = useContext(DialogContext);
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error('Dialog components must be used within a Dialog');
|
|
26
|
+
}
|
|
27
|
+
return context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Dialog Root
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
interface DialogProps {
|
|
35
|
+
/** Controlled open state */
|
|
36
|
+
open?: boolean;
|
|
37
|
+
/** Default open state for uncontrolled usage */
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
/** Callback when open state changes */
|
|
40
|
+
onOpenChange?: (open: boolean) => void;
|
|
41
|
+
/** Children */
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Dialog({
|
|
46
|
+
open: controlledOpen,
|
|
47
|
+
defaultOpen = false,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
children,
|
|
50
|
+
}: DialogProps) {
|
|
51
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
52
|
+
const isControlled = controlledOpen !== undefined;
|
|
53
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
54
|
+
|
|
55
|
+
const setOpen = useCallback((newOpen: boolean) => {
|
|
56
|
+
if (!isControlled) {
|
|
57
|
+
setInternalOpen(newOpen);
|
|
58
|
+
}
|
|
59
|
+
onOpenChange?.(newOpen);
|
|
60
|
+
}, [isControlled, onOpenChange]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<DialogContext.Provider value={{ open, setOpen }}>
|
|
64
|
+
{children}
|
|
65
|
+
</DialogContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Dialog Trigger
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
interface DialogTriggerProps {
|
|
74
|
+
/** Trigger element */
|
|
75
|
+
children: React.ReactElement;
|
|
76
|
+
/** Pass through as child instead of wrapping */
|
|
77
|
+
asChild?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function DialogTrigger({ children, asChild }: DialogTriggerProps) {
|
|
81
|
+
const { setOpen } = useDialogContext();
|
|
82
|
+
|
|
83
|
+
if (asChild && React.isValidElement(children)) {
|
|
84
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, {
|
|
85
|
+
onClick: () => setOpen(true),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<button type="button" onClick={() => setOpen(true)}>
|
|
91
|
+
{children}
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Dialog Portal & Overlay
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
interface DialogContentProps {
|
|
101
|
+
/** Additional className */
|
|
102
|
+
className?: string;
|
|
103
|
+
/** Children */
|
|
104
|
+
children: React.ReactNode;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function DialogContent({ className = '', children }: DialogContentProps) {
|
|
108
|
+
const { open, setOpen } = useDialogContext();
|
|
109
|
+
const [mounted, setMounted] = useState(false);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
setMounted(true);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
// Handle escape key
|
|
116
|
+
useEscapeKey(open, () => setOpen(false));
|
|
117
|
+
|
|
118
|
+
// Prevent body scroll when open
|
|
119
|
+
useLockBodyScroll(open);
|
|
120
|
+
|
|
121
|
+
if (!mounted || !open) return null;
|
|
122
|
+
|
|
123
|
+
return createPortal(
|
|
124
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
125
|
+
{/* Overlay */}
|
|
126
|
+
<div
|
|
127
|
+
className="absolute inset-0 bg-black/50 animate-fadeIn"
|
|
128
|
+
onClick={() => setOpen(false)}
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
{/* Content */}
|
|
133
|
+
<div
|
|
134
|
+
role="dialog"
|
|
135
|
+
aria-modal="true"
|
|
136
|
+
className={`
|
|
137
|
+
relative z-10
|
|
138
|
+
w-full max-w-lg mx-4
|
|
139
|
+
bg-warm-cloud
|
|
140
|
+
border-2 border-black
|
|
141
|
+
rounded-sm
|
|
142
|
+
shadow-[4px_4px_0_0_var(--color-black)]
|
|
143
|
+
animate-scaleIn
|
|
144
|
+
${className}
|
|
145
|
+
`.trim()}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</div>
|
|
149
|
+
</div>,
|
|
150
|
+
document.body
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Dialog Header, Title, Description
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
interface DialogHeaderProps {
|
|
159
|
+
/** Additional className */
|
|
160
|
+
className?: string;
|
|
161
|
+
/** Children */
|
|
162
|
+
children: React.ReactNode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function DialogHeader({ className = '', children }: DialogHeaderProps) {
|
|
166
|
+
return (
|
|
167
|
+
<div className={`px-6 pt-6 pb-4 border-b border-black/20 ${className}`.trim()}>
|
|
168
|
+
{children}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface DialogTitleProps {
|
|
174
|
+
/** Additional className */
|
|
175
|
+
className?: string;
|
|
176
|
+
/** Children */
|
|
177
|
+
children: React.ReactNode;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function DialogTitle({ className = '', children }: DialogTitleProps) {
|
|
181
|
+
return (
|
|
182
|
+
<h2 className={`font-joystix text-base uppercase text-black ${className}`.trim()}>
|
|
183
|
+
{children}
|
|
184
|
+
</h2>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface DialogDescriptionProps {
|
|
189
|
+
/** Additional className */
|
|
190
|
+
className?: string;
|
|
191
|
+
/** Children */
|
|
192
|
+
children: React.ReactNode;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function DialogDescription({ className = '', children }: DialogDescriptionProps) {
|
|
196
|
+
return (
|
|
197
|
+
<p className={`font-mondwest text-base text-black/70 mt-2 ${className}`.trim()}>
|
|
198
|
+
{children}
|
|
199
|
+
</p>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Dialog Body & Footer
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
interface DialogBodyProps {
|
|
208
|
+
/** Additional className */
|
|
209
|
+
className?: string;
|
|
210
|
+
/** Children */
|
|
211
|
+
children: React.ReactNode;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function DialogBody({ className = '', children }: DialogBodyProps) {
|
|
215
|
+
return (
|
|
216
|
+
<div className={`px-6 py-4 ${className}`.trim()}>
|
|
217
|
+
{children}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface DialogFooterProps {
|
|
223
|
+
/** Additional className */
|
|
224
|
+
className?: string;
|
|
225
|
+
/** Children */
|
|
226
|
+
children: React.ReactNode;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function DialogFooter({ className = '', children }: DialogFooterProps) {
|
|
230
|
+
return (
|
|
231
|
+
<div className={`px-6 pb-6 pt-4 border-t border-black/20 flex justify-end gap-2 ${className}`.trim()}>
|
|
232
|
+
{children}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// Dialog Close
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
interface DialogCloseProps {
|
|
242
|
+
/** Close button element */
|
|
243
|
+
children: React.ReactElement;
|
|
244
|
+
/** Pass through as child instead of wrapping */
|
|
245
|
+
asChild?: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function DialogClose({ children, asChild }: DialogCloseProps) {
|
|
249
|
+
const { setOpen } = useDialogContext();
|
|
250
|
+
|
|
251
|
+
if (asChild && React.isValidElement(children)) {
|
|
252
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, {
|
|
253
|
+
onClick: () => setOpen(false),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<button type="button" onClick={() => setOpen(false)}>
|
|
259
|
+
{children}
|
|
260
|
+
</button>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default Dialog;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
type DividerOrientation = 'horizontal' | 'vertical';
|
|
10
|
+
type DividerVariant = 'solid' | 'dashed' | 'decorated';
|
|
11
|
+
|
|
12
|
+
interface DividerProps {
|
|
13
|
+
/** Orientation */
|
|
14
|
+
orientation?: DividerOrientation;
|
|
15
|
+
/** Visual variant */
|
|
16
|
+
variant?: DividerVariant;
|
|
17
|
+
/** Additional classes */
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Component
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Divider component for separating content
|
|
27
|
+
*/
|
|
28
|
+
export function Divider({
|
|
29
|
+
orientation = 'horizontal',
|
|
30
|
+
variant = 'solid',
|
|
31
|
+
className = '',
|
|
32
|
+
}: DividerProps) {
|
|
33
|
+
// Decorated variant with diamond in center
|
|
34
|
+
if (variant === 'decorated') {
|
|
35
|
+
return (
|
|
36
|
+
<div className={`flex items-center gap-4 ${className}`}>
|
|
37
|
+
<div className="flex-1 h-[2px]" style={{ backgroundColor: 'var(--border-black-20)' }} />
|
|
38
|
+
<div className="w-2 h-2 bg-sun-yellow border border-black rotate-45" />
|
|
39
|
+
<div className="flex-1 h-[2px]" style={{ backgroundColor: 'var(--border-black-20)' }} />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Horizontal divider
|
|
45
|
+
if (orientation === 'horizontal') {
|
|
46
|
+
const borderStyle = variant === 'dashed' ? 'border-dashed' : 'border-solid';
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={`w-full border-t ${borderStyle} ${className}`}
|
|
50
|
+
style={{ borderTopColor: 'var(--border-black-20)' }}
|
|
51
|
+
role="separator"
|
|
52
|
+
aria-orientation="horizontal"
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Vertical divider
|
|
58
|
+
const borderStyle = variant === 'dashed' ? 'border-dashed' : 'border-solid';
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className={`h-full border-l ${borderStyle} ${className}`}
|
|
62
|
+
style={{ borderLeftColor: 'var(--border-black-20)' }}
|
|
63
|
+
role="separator"
|
|
64
|
+
aria-orientation="vertical"
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default Divider;
|
|
70
|
+
|