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.
Files changed (133) hide show
  1. package/README.md +108 -0
  2. package/bin/radtools.js +5 -0
  3. package/dist/cli/index.js +427 -0
  4. package/package.json +55 -0
  5. package/templates/api-routes/assets/optimize/route.ts +94 -0
  6. package/templates/api-routes/assets/route.ts +159 -0
  7. package/templates/api-routes/components/create-folder/route.ts +55 -0
  8. package/templates/api-routes/components/route.ts +156 -0
  9. package/templates/api-routes/fonts/route.ts +96 -0
  10. package/templates/api-routes/fonts/upload/route.ts +79 -0
  11. package/templates/api-routes/read-css/route.ts +29 -0
  12. package/templates/api-routes/write-css/route.ts +423 -0
  13. package/templates/components/Rad_os/AppWindow.tsx +423 -0
  14. package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
  15. package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
  16. package/templates/components/icons/Icon.tsx +224 -0
  17. package/templates/components/icons/README.md +85 -0
  18. package/templates/components/icons/index.ts +20 -0
  19. package/templates/components/icons.tsx +164 -0
  20. package/templates/components/ui/Accordion.tsx +268 -0
  21. package/templates/components/ui/Alert.tsx +111 -0
  22. package/templates/components/ui/Badge.tsx +87 -0
  23. package/templates/components/ui/Breadcrumbs.tsx +88 -0
  24. package/templates/components/ui/Button.tsx +249 -0
  25. package/templates/components/ui/Card.tsx +137 -0
  26. package/templates/components/ui/Checkbox.tsx +137 -0
  27. package/templates/components/ui/ContextMenu.tsx +220 -0
  28. package/templates/components/ui/Dialog.tsx +264 -0
  29. package/templates/components/ui/Divider.tsx +70 -0
  30. package/templates/components/ui/DropdownMenu.tsx +301 -0
  31. package/templates/components/ui/HelpPanel.tsx +119 -0
  32. package/templates/components/ui/Input.tsx +176 -0
  33. package/templates/components/ui/Popover.tsx +211 -0
  34. package/templates/components/ui/Progress.tsx +158 -0
  35. package/templates/components/ui/Select.tsx +134 -0
  36. package/templates/components/ui/Sheet.tsx +316 -0
  37. package/templates/components/ui/Slider.tsx +223 -0
  38. package/templates/components/ui/Switch.tsx +155 -0
  39. package/templates/components/ui/Tabs.tsx +253 -0
  40. package/templates/components/ui/Toast.tsx +192 -0
  41. package/templates/components/ui/Tooltip.tsx +129 -0
  42. package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
  43. package/templates/components/ui/index.ts +84 -0
  44. package/templates/devtools/DevToolsPanel.tsx +261 -0
  45. package/templates/devtools/DevToolsProvider.tsx +43 -0
  46. package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
  47. package/templates/devtools/components/ColorPicker.tsx +33 -0
  48. package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
  49. package/templates/devtools/components/ContextualFooter.tsx +56 -0
  50. package/templates/devtools/components/DraggablePanel.tsx +43 -0
  51. package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
  52. package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
  53. package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
  54. package/templates/devtools/components/TokenDropdown.tsx +47 -0
  55. package/templates/devtools/components/TypographyFooter.tsx +145 -0
  56. package/templates/devtools/hooks/useMockState.ts +16 -0
  57. package/templates/devtools/index.ts +17 -0
  58. package/templates/devtools/lib/componentScanner.ts +78 -0
  59. package/templates/devtools/lib/cssParser.ts +465 -0
  60. package/templates/devtools/lib/searchIndexes.ts +45 -0
  61. package/templates/devtools/lib/selectorGenerator.ts +86 -0
  62. package/templates/devtools/store/index.ts +66 -0
  63. package/templates/devtools/store/slices/assetsSlice.ts +106 -0
  64. package/templates/devtools/store/slices/componentsSlice.ts +59 -0
  65. package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
  66. package/templates/devtools/store/slices/panelSlice.ts +17 -0
  67. package/templates/devtools/store/slices/typographySlice.ts +538 -0
  68. package/templates/devtools/store/slices/variablesSlice.ts +167 -0
  69. package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
  70. package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
  71. package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
  72. package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
  73. package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
  74. package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
  75. package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
  76. package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
  77. package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
  78. package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
  79. package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
  80. package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
  81. package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
  82. package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
  83. package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
  84. package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
  85. package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
  86. package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
  87. package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
  88. package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
  89. package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
  90. package/templates/devtools/types/index.ts +99 -0
  91. package/templates/globals.css +574 -0
  92. package/templates/hooks/index.ts +1 -0
  93. package/templates/hooks/useWindowManager.ts +212 -0
  94. package/templates/public/assets/icons/avatar.svg +18 -0
  95. package/templates/public/assets/icons/checkmark-filled.svg +14 -0
  96. package/templates/public/assets/icons/checkmark.svg +14 -0
  97. package/templates/public/assets/icons/chevron-down.svg +14 -0
  98. package/templates/public/assets/icons/close.svg +14 -0
  99. package/templates/public/assets/icons/copy.svg +14 -0
  100. package/templates/public/assets/icons/download.svg +14 -0
  101. package/templates/public/assets/icons/expand.svg +31 -0
  102. package/templates/public/assets/icons/file-blank.svg +17 -0
  103. package/templates/public/assets/icons/file-image.svg +19 -0
  104. package/templates/public/assets/icons/file-written.svg +17 -0
  105. package/templates/public/assets/icons/folder-closed.svg +17 -0
  106. package/templates/public/assets/icons/folder-open.svg +17 -0
  107. package/templates/public/assets/icons/hamburger.svg +18 -0
  108. package/templates/public/assets/icons/home-outline.svg +28 -0
  109. package/templates/public/assets/icons/home.svg +30 -0
  110. package/templates/public/assets/icons/hourglass.svg +25 -0
  111. package/templates/public/assets/icons/information-circle.svg +14 -0
  112. package/templates/public/assets/icons/information.svg +17 -0
  113. package/templates/public/assets/icons/lightning.svg +14 -0
  114. package/templates/public/assets/icons/locked.svg +17 -0
  115. package/templates/public/assets/icons/not-allowed.svg +14 -0
  116. package/templates/public/assets/icons/plus.svg +5 -0
  117. package/templates/public/assets/icons/power-thin.svg +17 -0
  118. package/templates/public/assets/icons/power.svg +17 -0
  119. package/templates/public/assets/icons/question-block.svg +14 -0
  120. package/templates/public/assets/icons/question.svg +17 -0
  121. package/templates/public/assets/icons/refresh-block.svg +14 -0
  122. package/templates/public/assets/icons/refresh.svg +17 -0
  123. package/templates/public/assets/icons/save.svg +14 -0
  124. package/templates/public/assets/icons/search.svg +25 -0
  125. package/templates/public/assets/icons/settings.svg +14 -0
  126. package/templates/public/assets/icons/trash-full.svg +21 -0
  127. package/templates/public/assets/icons/trash-open.svg +23 -0
  128. package/templates/public/assets/icons/trash.svg +18 -0
  129. package/templates/public/assets/icons/unlocked.svg +17 -0
  130. package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
  131. package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
  132. package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
  133. 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
+