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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Components for RadTools
|
|
3
|
+
* SVG icons copied from radOS reference
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Icon } from './icons/Icon';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Standard Icon Sizes
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const ICON_SIZE = {
|
|
14
|
+
xs: 14,
|
|
15
|
+
sm: 18,
|
|
16
|
+
md: 20,
|
|
17
|
+
lg: 24,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type IconSize = keyof typeof ICON_SIZE;
|
|
21
|
+
|
|
22
|
+
interface IconProps {
|
|
23
|
+
className?: string;
|
|
24
|
+
size?: number | string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// UI Icons
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export function CloseIcon({ className = '', size = 10 }: IconProps) {
|
|
32
|
+
return (
|
|
33
|
+
<svg
|
|
34
|
+
width={size}
|
|
35
|
+
height={size}
|
|
36
|
+
viewBox="0 0 10 11"
|
|
37
|
+
fill="none"
|
|
38
|
+
className={className}
|
|
39
|
+
>
|
|
40
|
+
<path
|
|
41
|
+
fillRule="evenodd"
|
|
42
|
+
clipRule="evenodd"
|
|
43
|
+
d="M1.11111 0.5H0V1.61111H1.11111V2.72222H2.22222V3.83333H3.33333V4.94444H4.44444V6.05556H3.33333V7.16667H2.22222V8.27778H1.11111V9.38889H0V10.5H1.11111H2.22222V9.38889H3.33333V8.27778H4.44444V7.16667H5.55556V8.27778H6.66667V9.38889H7.77778V10.5H8.88889H10V9.38889H8.88889V8.27778H7.77778V7.16667H6.66667V6.05556H5.55556V4.94444H6.66667V3.83333H7.77778V2.72222H8.88889V1.61111H10V0.5H8.88889H7.77778V1.61111H6.66667V2.72222H5.55556V3.83333H4.44444V2.72222H3.33333V1.61111H2.22222V0.5H1.11111Z"
|
|
44
|
+
fill="currentColor"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CopyIcon({ className = '', size = 18 }: IconProps) {
|
|
51
|
+
return (
|
|
52
|
+
<svg
|
|
53
|
+
width={size}
|
|
54
|
+
height={size}
|
|
55
|
+
viewBox="0 0 28 28"
|
|
56
|
+
fill="none"
|
|
57
|
+
className={className}
|
|
58
|
+
>
|
|
59
|
+
<path
|
|
60
|
+
fillRule="evenodd"
|
|
61
|
+
clipRule="evenodd"
|
|
62
|
+
d="M8.71111 0H28V19.2889H19.2889V28H0V8.71111H8.71111V0ZM11 8.71111H19.2889V17H26V2.5H11V8.71111ZM17 11.5H2.5V25.5H17V11.5Z"
|
|
63
|
+
fill="currentColor"
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function CopiedIcon({ className = '', size = 18 }: IconProps) {
|
|
70
|
+
return (
|
|
71
|
+
<svg
|
|
72
|
+
width={size}
|
|
73
|
+
height={typeof size === 'number' ? size * (20 / 26) : size}
|
|
74
|
+
viewBox="0 0 26 20"
|
|
75
|
+
fill="none"
|
|
76
|
+
className={className}
|
|
77
|
+
>
|
|
78
|
+
<path
|
|
79
|
+
d="M11.4187 11.4375H8.56875V8.5875H5.7V5.71875H2.85V8.5875H0V11.4375H2.85V14.2875H5.7V17.1563H8.56875V20.0062H11.4187V17.1563H14.2875V14.2875H17.1375V11.4375H19.9875V8.5875H22.8562V5.71875H25.7062V2.86875H22.8562V0H19.9875V2.86875H17.1375V5.71875H14.2875V8.5875H11.4187V11.4375Z"
|
|
80
|
+
fill="currentColor"
|
|
81
|
+
/>
|
|
82
|
+
</svg>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function HelpIcon({ className = '', size = 16 }: IconProps) {
|
|
87
|
+
return (
|
|
88
|
+
<svg
|
|
89
|
+
width={size}
|
|
90
|
+
height={size}
|
|
91
|
+
viewBox="0 0 16 16"
|
|
92
|
+
fill="none"
|
|
93
|
+
className={className}
|
|
94
|
+
>
|
|
95
|
+
{/* Outer circle */}
|
|
96
|
+
<path d="M5 0H11V2H5V0Z" fill="currentColor" />
|
|
97
|
+
<path d="M3 2H5V4H3V2Z" fill="currentColor" />
|
|
98
|
+
<path d="M11 2H13V4H11V2Z" fill="currentColor" />
|
|
99
|
+
<path d="M2 4H4V6H2V4Z" fill="currentColor" />
|
|
100
|
+
<path d="M12 4H14V6H12V4Z" fill="currentColor" />
|
|
101
|
+
<path d="M0 5H2V11H0V5Z" fill="currentColor" />
|
|
102
|
+
<path d="M14 5H16V11H14V5Z" fill="currentColor" />
|
|
103
|
+
<path d="M2 10H4V12H2V10Z" fill="currentColor" />
|
|
104
|
+
<path d="M12 10H14V12H12V10Z" fill="currentColor" />
|
|
105
|
+
<path d="M3 12H5V14H3V12Z" fill="currentColor" />
|
|
106
|
+
<path d="M11 12H13V14H11V12Z" fill="currentColor" />
|
|
107
|
+
<path d="M5 14H11V16H5V14Z" fill="currentColor" />
|
|
108
|
+
{/* Question mark */}
|
|
109
|
+
<path d="M6 4H10V5H6V4Z" fill="currentColor" />
|
|
110
|
+
<path d="M10 5H11V7H10V5Z" fill="currentColor" />
|
|
111
|
+
<path d="M8 7H10V8H8V7Z" fill="currentColor" />
|
|
112
|
+
<path d="M7 8H9V10H7V8Z" fill="currentColor" />
|
|
113
|
+
<path d="M7 11H9V13H7V11Z" fill="currentColor" />
|
|
114
|
+
</svg>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ComponentsIcon({ className = '', size = 16 }: IconProps) {
|
|
119
|
+
return (
|
|
120
|
+
<svg
|
|
121
|
+
width={size}
|
|
122
|
+
height={size}
|
|
123
|
+
viewBox="0 0 24 24"
|
|
124
|
+
fill="currentColor"
|
|
125
|
+
className={className}
|
|
126
|
+
>
|
|
127
|
+
{/* 2x2 grid of squares representing components */}
|
|
128
|
+
<rect x="2" y="2" width="9" height="9" />
|
|
129
|
+
<rect x="13" y="2" width="9" height="9" />
|
|
130
|
+
<rect x="2" y="13" width="9" height="9" />
|
|
131
|
+
<rect x="13" y="13" width="9" height="9" />
|
|
132
|
+
</svg>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ChevronIcon({ className = '', size = 8 }: IconProps) {
|
|
137
|
+
return (
|
|
138
|
+
<svg
|
|
139
|
+
width={size}
|
|
140
|
+
height={size}
|
|
141
|
+
viewBox="0 0 8 10"
|
|
142
|
+
fill="none"
|
|
143
|
+
className={className}
|
|
144
|
+
>
|
|
145
|
+
<path
|
|
146
|
+
d="M7.21826 5.64286L5.91799 5.64286L5.91799 6.92857L4.64346 6.92857L4.64346 8.21429L3.35605 8.21429L3.35605 9.5L0.781248 9.5L0.781249 8.21429L2.06865 8.21429L2.06865 6.92857L3.35605 6.92857L3.35605 5.64286L4.64346 5.64286L4.64346 4.35714L3.35605 4.35714L3.35605 3.07143L2.06865 3.07143L2.06865 1.78572L0.78125 1.78571L0.78125 0.5L3.35606 0.5L3.35606 1.78572L4.64346 1.78572L4.64346 3.07143L5.91799 3.07143L5.91799 4.35714L7.21826 4.35714L7.21826 5.64286Z"
|
|
147
|
+
fill="currentColor"
|
|
148
|
+
/>
|
|
149
|
+
</svg>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default {
|
|
154
|
+
CloseIcon,
|
|
155
|
+
CopyIcon,
|
|
156
|
+
CopiedIcon,
|
|
157
|
+
HelpIcon,
|
|
158
|
+
ComponentsIcon,
|
|
159
|
+
ChevronIcon,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Re-export Icon component from icons directory
|
|
163
|
+
export { Icon };
|
|
164
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
type AccordionType = 'single' | 'multiple';
|
|
10
|
+
|
|
11
|
+
interface AccordionContextValue {
|
|
12
|
+
type: AccordionType;
|
|
13
|
+
expandedItems: Set<string>;
|
|
14
|
+
toggleItem: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AccordionItemContextValue {
|
|
18
|
+
value: string;
|
|
19
|
+
isExpanded: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Context
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null);
|
|
27
|
+
const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
|
|
28
|
+
|
|
29
|
+
function useAccordionContext() {
|
|
30
|
+
const context = useContext(AccordionContext);
|
|
31
|
+
if (!context) {
|
|
32
|
+
throw new Error('Accordion components must be used within an Accordion');
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function useAccordionItemContext() {
|
|
38
|
+
const context = useContext(AccordionItemContext);
|
|
39
|
+
if (!context) {
|
|
40
|
+
throw new Error('AccordionTrigger/AccordionContent must be used within an AccordionItem');
|
|
41
|
+
}
|
|
42
|
+
return context;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Accordion Root
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
interface AccordionProps {
|
|
50
|
+
/** Allow single or multiple items to be expanded at once */
|
|
51
|
+
type?: AccordionType;
|
|
52
|
+
/** Default expanded item(s) - string for single, array for multiple */
|
|
53
|
+
defaultValue?: string | string[];
|
|
54
|
+
/** Controlled expanded value(s) */
|
|
55
|
+
value?: string | string[];
|
|
56
|
+
/** Callback when expanded items change */
|
|
57
|
+
onValueChange?: (value: string | string[]) => void;
|
|
58
|
+
/** Additional className */
|
|
59
|
+
className?: string;
|
|
60
|
+
/** Children */
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function Accordion({
|
|
65
|
+
type = 'single',
|
|
66
|
+
defaultValue,
|
|
67
|
+
value: controlledValue,
|
|
68
|
+
onValueChange,
|
|
69
|
+
className = '',
|
|
70
|
+
children,
|
|
71
|
+
}: AccordionProps) {
|
|
72
|
+
// Initialize expanded items
|
|
73
|
+
const getInitialExpanded = (): Set<string> => {
|
|
74
|
+
const initial = controlledValue ?? defaultValue;
|
|
75
|
+
if (!initial) return new Set();
|
|
76
|
+
return new Set(Array.isArray(initial) ? initial : [initial]);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const [expandedItems, setExpandedItems] = useState<Set<string>>(getInitialExpanded);
|
|
80
|
+
|
|
81
|
+
// Sync with controlled value
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (controlledValue !== undefined) {
|
|
84
|
+
setExpandedItems(new Set(Array.isArray(controlledValue) ? controlledValue : [controlledValue]));
|
|
85
|
+
}
|
|
86
|
+
}, [controlledValue]);
|
|
87
|
+
|
|
88
|
+
const toggleItem = useCallback((itemValue: string) => {
|
|
89
|
+
setExpandedItems((prev) => {
|
|
90
|
+
const next = new Set(prev);
|
|
91
|
+
|
|
92
|
+
if (next.has(itemValue)) {
|
|
93
|
+
next.delete(itemValue);
|
|
94
|
+
} else {
|
|
95
|
+
if (type === 'single') {
|
|
96
|
+
next.clear();
|
|
97
|
+
}
|
|
98
|
+
next.add(itemValue);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Notify parent of change
|
|
102
|
+
if (onValueChange) {
|
|
103
|
+
const newValue = Array.from(next);
|
|
104
|
+
onValueChange(type === 'single' ? (newValue[0] ?? '') : newValue);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
}, [type, onValueChange]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<AccordionContext.Provider value={{ type, expandedItems, toggleItem }}>
|
|
113
|
+
<div className={`space-y-0 ${className}`}>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
</AccordionContext.Provider>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Accordion Item
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
interface AccordionItemProps {
|
|
125
|
+
/** Unique value for this item */
|
|
126
|
+
value: string;
|
|
127
|
+
/** Additional className */
|
|
128
|
+
className?: string;
|
|
129
|
+
/** Children (AccordionTrigger and AccordionContent) */
|
|
130
|
+
children: React.ReactNode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function AccordionItem({ value, className = '', children }: AccordionItemProps) {
|
|
134
|
+
const { expandedItems } = useAccordionContext();
|
|
135
|
+
const isExpanded = expandedItems.has(value);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<AccordionItemContext.Provider value={{ value, isExpanded }}>
|
|
139
|
+
<div
|
|
140
|
+
className={`
|
|
141
|
+
border border-black
|
|
142
|
+
bg-warm-cloud
|
|
143
|
+
-mt-px first:mt-0
|
|
144
|
+
${className}
|
|
145
|
+
`.trim()}
|
|
146
|
+
data-state={isExpanded ? 'open' : 'closed'}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</div>
|
|
150
|
+
</AccordionItemContext.Provider>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Accordion Trigger
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
interface AccordionTriggerProps {
|
|
159
|
+
/** Additional className */
|
|
160
|
+
className?: string;
|
|
161
|
+
/** Children (header content) */
|
|
162
|
+
children: React.ReactNode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function AccordionTrigger({ className = '', children }: AccordionTriggerProps) {
|
|
166
|
+
const { toggleItem } = useAccordionContext();
|
|
167
|
+
const { value, isExpanded } = useAccordionItemContext();
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={() => toggleItem(value)}
|
|
173
|
+
className={`
|
|
174
|
+
w-full flex items-center justify-between
|
|
175
|
+
px-4 py-3
|
|
176
|
+
font-joystix text-sm uppercase text-black
|
|
177
|
+
bg-transparent
|
|
178
|
+
hover:bg-black/5
|
|
179
|
+
transition-colors
|
|
180
|
+
cursor-pointer
|
|
181
|
+
${className}
|
|
182
|
+
`.trim()}
|
|
183
|
+
aria-expanded={isExpanded}
|
|
184
|
+
>
|
|
185
|
+
<span>{children}</span>
|
|
186
|
+
<span
|
|
187
|
+
className="text-[1rem] font-mondwest select-none"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
>
|
|
190
|
+
{isExpanded ? '−' : '+'}
|
|
191
|
+
</span>
|
|
192
|
+
</button>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Accordion Content
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
interface AccordionContentProps {
|
|
201
|
+
/** Additional className */
|
|
202
|
+
className?: string;
|
|
203
|
+
/** Children (content) */
|
|
204
|
+
children: React.ReactNode;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function AccordionContent({ className = '', children }: AccordionContentProps) {
|
|
208
|
+
const { isExpanded } = useAccordionItemContext();
|
|
209
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
210
|
+
const [height, setHeight] = useState<number | undefined>(undefined);
|
|
211
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const content = contentRef.current;
|
|
215
|
+
if (!content) return;
|
|
216
|
+
|
|
217
|
+
if (isExpanded) {
|
|
218
|
+
// Expanding: measure content height and animate
|
|
219
|
+
const scrollHeight = content.scrollHeight;
|
|
220
|
+
setHeight(scrollHeight);
|
|
221
|
+
setIsAnimating(true);
|
|
222
|
+
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
setIsAnimating(false);
|
|
225
|
+
setHeight(undefined); // Remove fixed height after animation
|
|
226
|
+
}, 200);
|
|
227
|
+
|
|
228
|
+
return () => clearTimeout(timer);
|
|
229
|
+
} else {
|
|
230
|
+
// Collapsing: set current height first, then animate to 0
|
|
231
|
+
const scrollHeight = content.scrollHeight;
|
|
232
|
+
setHeight(scrollHeight);
|
|
233
|
+
setIsAnimating(true);
|
|
234
|
+
|
|
235
|
+
// Force reflow before setting height to 0
|
|
236
|
+
requestAnimationFrame(() => {
|
|
237
|
+
setHeight(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
setIsAnimating(false);
|
|
242
|
+
}, 200);
|
|
243
|
+
|
|
244
|
+
return () => clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
}, [isExpanded]);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div
|
|
250
|
+
ref={contentRef}
|
|
251
|
+
className={`
|
|
252
|
+
overflow-hidden
|
|
253
|
+
transition-[height] duration-200 ease-out
|
|
254
|
+
${className}
|
|
255
|
+
`.trim()}
|
|
256
|
+
style={{
|
|
257
|
+
height: isAnimating ? height : (isExpanded ? 'auto' : 0),
|
|
258
|
+
}}
|
|
259
|
+
aria-hidden={!isExpanded}
|
|
260
|
+
>
|
|
261
|
+
<div className="px-4 pb-4">
|
|
262
|
+
{children}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default Accordion;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Icon } from '@/components/icons';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
type AlertVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
11
|
+
|
|
12
|
+
interface AlertProps {
|
|
13
|
+
/** Alert variant */
|
|
14
|
+
variant?: AlertVariant;
|
|
15
|
+
/** Alert title */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Alert content */
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
/** Show close button */
|
|
20
|
+
closable?: boolean;
|
|
21
|
+
/** Close handler */
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
/** Icon name (filename without .svg extension) - overrides variant default */
|
|
24
|
+
iconName?: string;
|
|
25
|
+
/** Additional className */
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Styles
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const variantStyles: Record<AlertVariant, string> = {
|
|
34
|
+
default: 'bg-warm-cloud border-black text-black',
|
|
35
|
+
success: 'bg-green/10 border-green text-black',
|
|
36
|
+
warning: 'bg-sun-yellow/10 border-sun-yellow text-black',
|
|
37
|
+
error: 'bg-error-red/10 border-error-red text-black',
|
|
38
|
+
info: 'bg-sky-blue/10 border-sky-blue text-black',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const variantIconMap: Record<AlertVariant, string> = {
|
|
42
|
+
default: 'information-circle',
|
|
43
|
+
success: 'checkmark-filled',
|
|
44
|
+
warning: 'warning-triangle-filled-2',
|
|
45
|
+
error: 'close',
|
|
46
|
+
info: 'information-circle',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Component
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Alert component - Static alert banners
|
|
55
|
+
*/
|
|
56
|
+
export function Alert({
|
|
57
|
+
variant = 'default',
|
|
58
|
+
title,
|
|
59
|
+
children,
|
|
60
|
+
closable = false,
|
|
61
|
+
onClose,
|
|
62
|
+
iconName,
|
|
63
|
+
className = '',
|
|
64
|
+
}: AlertProps) {
|
|
65
|
+
const displayIconName = iconName || variantIconMap[variant];
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
role="alert"
|
|
70
|
+
className={`
|
|
71
|
+
p-4
|
|
72
|
+
border-2
|
|
73
|
+
rounded-sm
|
|
74
|
+
${variantStyles[variant]}
|
|
75
|
+
${className}
|
|
76
|
+
`.trim()}
|
|
77
|
+
>
|
|
78
|
+
<div className="flex items-start gap-3">
|
|
79
|
+
{/* Icon */}
|
|
80
|
+
<span className="flex-shrink-0 mt-0.5">
|
|
81
|
+
<Icon name={displayIconName} size={16} />
|
|
82
|
+
</span>
|
|
83
|
+
|
|
84
|
+
{/* Content */}
|
|
85
|
+
<div className="flex-1 min-w-0">
|
|
86
|
+
{title && (
|
|
87
|
+
<p className="font-joystix text-xs uppercase mb-1">
|
|
88
|
+
{title}
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
<div className="font-mondwest text-base text-black/80">
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Close Button */}
|
|
97
|
+
{closable && (
|
|
98
|
+
<button
|
|
99
|
+
onClick={onClose}
|
|
100
|
+
className="text-black/50 hover:text-black flex-shrink-0 -mt-1"
|
|
101
|
+
aria-label="Close"
|
|
102
|
+
>
|
|
103
|
+
<Icon name="close" size={16} />
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default Alert;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
10
|
+
type BadgeSize = 'sm' | 'md';
|
|
11
|
+
|
|
12
|
+
interface BadgeProps {
|
|
13
|
+
/** Visual variant */
|
|
14
|
+
variant?: BadgeVariant;
|
|
15
|
+
/** Size preset */
|
|
16
|
+
size?: BadgeSize;
|
|
17
|
+
/** Badge content */
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
/** Additional classes */
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Styles
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base badge styles
|
|
29
|
+
*/
|
|
30
|
+
const baseStyles = `
|
|
31
|
+
inline-flex items-center justify-center
|
|
32
|
+
font-joystix uppercase
|
|
33
|
+
rounded-sm
|
|
34
|
+
whitespace-nowrap
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Size presets
|
|
39
|
+
*/
|
|
40
|
+
const sizeStyles: Record<BadgeSize, string> = {
|
|
41
|
+
sm: 'px-2 py-0.5 text-2xs',
|
|
42
|
+
md: 'px-2.5 py-1 text-xs',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Variant color schemes
|
|
47
|
+
*/
|
|
48
|
+
const variantStyles: Record<BadgeVariant, string> = {
|
|
49
|
+
default: 'bg-warm-cloud text-black border border-black',
|
|
50
|
+
success: 'bg-green text-black border border-black',
|
|
51
|
+
warning: 'bg-sun-yellow text-black border border-black',
|
|
52
|
+
error: 'bg-error-red text-black border border-black',
|
|
53
|
+
info: 'bg-sky-blue text-black border border-black',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Component
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Badge component for status indicators and labels
|
|
62
|
+
*/
|
|
63
|
+
export function Badge({
|
|
64
|
+
variant = 'default',
|
|
65
|
+
size = 'md',
|
|
66
|
+
children,
|
|
67
|
+
className = '',
|
|
68
|
+
}: BadgeProps) {
|
|
69
|
+
const classes = [
|
|
70
|
+
baseStyles,
|
|
71
|
+
sizeStyles[size],
|
|
72
|
+
variantStyles[variant],
|
|
73
|
+
className,
|
|
74
|
+
]
|
|
75
|
+
.join(' ')
|
|
76
|
+
.replace(/\s+/g, ' ')
|
|
77
|
+
.trim();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<span className={classes}>
|
|
81
|
+
{children}
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default Badge;
|
|
87
|
+
|