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,301 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useState, useRef, useEffect, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { useEscapeKey, useClickOutside } from './hooks/useModalBehavior';
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ type DropdownPosition = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
12
+
13
+ interface DropdownContextValue {
14
+ open: boolean;
15
+ setOpen: (open: boolean) => void;
16
+ triggerRef: React.RefObject<HTMLElement | null>;
17
+ position: DropdownPosition;
18
+ }
19
+
20
+ // ============================================================================
21
+ // Context
22
+ // ============================================================================
23
+
24
+ const DropdownContext = createContext<DropdownContextValue | null>(null);
25
+
26
+ function useDropdownContext() {
27
+ const context = useContext(DropdownContext);
28
+ if (!context) {
29
+ throw new Error('DropdownMenu components must be used within a DropdownMenu');
30
+ }
31
+ return context;
32
+ }
33
+
34
+ // ============================================================================
35
+ // Dropdown Menu Root
36
+ // ============================================================================
37
+
38
+ interface DropdownMenuProps {
39
+ /** Controlled open state */
40
+ open?: boolean;
41
+ /** Default open state */
42
+ defaultOpen?: boolean;
43
+ /** Callback when open state changes */
44
+ onOpenChange?: (open: boolean) => void;
45
+ /** Position relative to trigger */
46
+ position?: DropdownPosition;
47
+ /** Children */
48
+ children: React.ReactNode;
49
+ }
50
+
51
+ export function DropdownMenu({
52
+ open: controlledOpen,
53
+ defaultOpen = false,
54
+ onOpenChange,
55
+ position = 'bottom-start',
56
+ children,
57
+ }: DropdownMenuProps) {
58
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
59
+ const isControlled = controlledOpen !== undefined;
60
+ const open = isControlled ? controlledOpen : internalOpen;
61
+ const triggerRef = useRef<HTMLElement>(null);
62
+
63
+ const setOpen = useCallback((newOpen: boolean) => {
64
+ if (!isControlled) {
65
+ setInternalOpen(newOpen);
66
+ }
67
+ onOpenChange?.(newOpen);
68
+ }, [isControlled, onOpenChange]);
69
+
70
+ return (
71
+ <DropdownContext.Provider value={{ open, setOpen, triggerRef, position }}>
72
+ <div className="relative inline-block">
73
+ {children}
74
+ </div>
75
+ </DropdownContext.Provider>
76
+ );
77
+ }
78
+
79
+ // ============================================================================
80
+ // Dropdown Menu Trigger
81
+ // ============================================================================
82
+
83
+ interface DropdownMenuTriggerProps {
84
+ /** Trigger element */
85
+ children: React.ReactElement;
86
+ /** Pass through as child instead of wrapping */
87
+ asChild?: boolean;
88
+ }
89
+
90
+ export function DropdownMenuTrigger({ children, asChild }: DropdownMenuTriggerProps) {
91
+ const { open, setOpen, triggerRef } = useDropdownContext();
92
+
93
+ const handleClick = () => {
94
+ setOpen(!open);
95
+ };
96
+
97
+ if (asChild && React.isValidElement(children)) {
98
+ return React.cloneElement(children as React.ReactElement<{ onClick?: () => void; ref?: React.Ref<HTMLElement | null> }>, {
99
+ onClick: handleClick,
100
+ ref: triggerRef as React.Ref<HTMLElement | null>,
101
+ });
102
+ }
103
+
104
+ return (
105
+ <button
106
+ type="button"
107
+ ref={triggerRef as React.RefObject<HTMLButtonElement>}
108
+ onClick={handleClick}
109
+ >
110
+ {children}
111
+ </button>
112
+ );
113
+ }
114
+
115
+ // ============================================================================
116
+ // Dropdown Menu Content
117
+ // ============================================================================
118
+
119
+ interface DropdownMenuContentProps {
120
+ /** Additional className */
121
+ className?: string;
122
+ /** Children */
123
+ children: React.ReactNode;
124
+ }
125
+
126
+ export function DropdownMenuContent({ className = '', children }: DropdownMenuContentProps) {
127
+ const { open, setOpen, triggerRef, position } = useDropdownContext();
128
+ const contentRef = useRef<HTMLDivElement>(null);
129
+ const [mounted, setMounted] = useState(false);
130
+ const [coords, setCoords] = useState({ top: 0, left: 0 });
131
+
132
+ useEffect(() => {
133
+ setMounted(true);
134
+ }, []);
135
+
136
+ // Calculate position
137
+ useEffect(() => {
138
+ if (!open || !triggerRef.current || !contentRef.current) return;
139
+
140
+ const trigger = triggerRef.current.getBoundingClientRect();
141
+ const content = contentRef.current.getBoundingClientRect();
142
+ const gap = 4;
143
+
144
+ let top = 0;
145
+ let left = 0;
146
+
147
+ switch (position) {
148
+ case 'bottom-start':
149
+ top = trigger.bottom + gap;
150
+ left = trigger.left;
151
+ break;
152
+ case 'bottom-end':
153
+ top = trigger.bottom + gap;
154
+ left = trigger.right - content.width;
155
+ break;
156
+ case 'top-start':
157
+ top = trigger.top - content.height - gap;
158
+ left = trigger.left;
159
+ break;
160
+ case 'top-end':
161
+ top = trigger.top - content.height - gap;
162
+ left = trigger.right - content.width;
163
+ break;
164
+ }
165
+
166
+ // Keep within viewport
167
+ top = Math.max(8, Math.min(top, window.innerHeight - content.height - 8));
168
+ left = Math.max(8, Math.min(left, window.innerWidth - content.width - 8));
169
+
170
+ setCoords({ top, left });
171
+ }, [open, position, triggerRef]);
172
+
173
+ // Handle click outside
174
+ useClickOutside(open, [contentRef, triggerRef], () => setOpen(false));
175
+
176
+ // Handle escape
177
+ useEscapeKey(open, () => setOpen(false));
178
+
179
+ if (!mounted || !open) return null;
180
+
181
+ return createPortal(
182
+ <div
183
+ ref={contentRef}
184
+ role="menu"
185
+ className={`
186
+ fixed z-50
187
+ min-w-[8rem]
188
+ bg-warm-cloud
189
+ border-2 border-black
190
+ rounded-sm
191
+ shadow-[2px_2px_0_0_var(--color-black)]
192
+ py-1
193
+ animate-fadeIn
194
+ ${className}
195
+ `.trim()}
196
+ style={{ top: coords.top, left: coords.left }}
197
+ >
198
+ {children}
199
+ </div>,
200
+ document.body
201
+ );
202
+ }
203
+
204
+ // ============================================================================
205
+ // Dropdown Menu Item
206
+ // ============================================================================
207
+
208
+ interface DropdownMenuItemProps {
209
+ /** Item content */
210
+ children: React.ReactNode;
211
+ /** Click handler */
212
+ onClick?: () => void;
213
+ /** Disabled state */
214
+ disabled?: boolean;
215
+ /** Destructive styling */
216
+ destructive?: boolean;
217
+ /** Additional className */
218
+ className?: string;
219
+ }
220
+
221
+ export function DropdownMenuItem({
222
+ children,
223
+ onClick,
224
+ disabled = false,
225
+ destructive = false,
226
+ className = '',
227
+ }: DropdownMenuItemProps) {
228
+ const { setOpen } = useDropdownContext();
229
+
230
+ const handleClick = () => {
231
+ if (disabled) return;
232
+ onClick?.();
233
+ setOpen(false);
234
+ };
235
+
236
+ return (
237
+ <button
238
+ type="button"
239
+ role="menuitem"
240
+ onClick={handleClick}
241
+ disabled={disabled}
242
+ className={`
243
+ w-full px-4 py-2
244
+ text-left
245
+ font-mondwest text-base
246
+ ${destructive ? 'text-error-red' : 'text-black'}
247
+ ${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-black/5 cursor-pointer'}
248
+ transition-colors
249
+ ${className}
250
+ `.trim()}
251
+ >
252
+ {children}
253
+ </button>
254
+ );
255
+ }
256
+
257
+ // ============================================================================
258
+ // Dropdown Menu Separator
259
+ // ============================================================================
260
+
261
+ interface DropdownMenuSeparatorProps {
262
+ /** Additional className */
263
+ className?: string;
264
+ }
265
+
266
+ export function DropdownMenuSeparator({ className = '' }: DropdownMenuSeparatorProps) {
267
+ return (
268
+ <div
269
+ role="separator"
270
+ className={`h-px bg-warm-cloud/20 my-1 ${className}`.trim()}
271
+ />
272
+ );
273
+ }
274
+
275
+ // ============================================================================
276
+ // Dropdown Menu Label
277
+ // ============================================================================
278
+
279
+ interface DropdownMenuLabelProps {
280
+ /** Label content */
281
+ children: React.ReactNode;
282
+ /** Additional className */
283
+ className?: string;
284
+ }
285
+
286
+ export function DropdownMenuLabel({ children, className = '' }: DropdownMenuLabelProps) {
287
+ return (
288
+ <div
289
+ className={`
290
+ px-4 py-1
291
+ font-joystix text-2xs uppercase
292
+ text-black/50
293
+ ${className}
294
+ `.trim()}
295
+ >
296
+ {children}
297
+ </div>
298
+ );
299
+ }
300
+
301
+ export default DropdownMenu;
@@ -0,0 +1,119 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef } from 'react';
4
+ import { Button } from './Button';
5
+
6
+ // ============================================================================
7
+ // Types
8
+ // ============================================================================
9
+
10
+ interface HelpPanelProps {
11
+ /** Whether the panel is open */
12
+ isOpen: boolean;
13
+ /** Callback when panel should close */
14
+ onClose: () => void;
15
+ /** Help content to display */
16
+ children: React.ReactNode;
17
+ /** Optional title for the help panel */
18
+ title?: string;
19
+ /** Additional className */
20
+ className?: string;
21
+ }
22
+
23
+ // ============================================================================
24
+ // Component
25
+ // ============================================================================
26
+
27
+ /**
28
+ * Slide-in help panel that appears from the right side of the window.
29
+ * Used to display contextual help content within app windows.
30
+ */
31
+ export function HelpPanel({
32
+ isOpen,
33
+ onClose,
34
+ children,
35
+ title = 'Help',
36
+ className = '',
37
+ }: HelpPanelProps) {
38
+ const panelRef = useRef<HTMLDivElement>(null);
39
+
40
+ // Handle escape key to close
41
+ useEffect(() => {
42
+ const handleEscape = (e: KeyboardEvent) => {
43
+ if (e.key === 'Escape' && isOpen) {
44
+ onClose();
45
+ }
46
+ };
47
+
48
+ document.addEventListener('keydown', handleEscape);
49
+ return () => document.removeEventListener('keydown', handleEscape);
50
+ }, [isOpen, onClose]);
51
+
52
+ // Handle click outside to close
53
+ useEffect(() => {
54
+ const handleClickOutside = (e: MouseEvent) => {
55
+ if (panelRef.current && !panelRef.current.contains(e.target as Node) && isOpen) {
56
+ onClose();
57
+ }
58
+ };
59
+
60
+ // Add slight delay to prevent immediate close on open click
61
+ const timeoutId = setTimeout(() => {
62
+ document.addEventListener('mousedown', handleClickOutside);
63
+ }, 100);
64
+
65
+ return () => {
66
+ clearTimeout(timeoutId);
67
+ document.removeEventListener('mousedown', handleClickOutside);
68
+ };
69
+ }, [isOpen, onClose]);
70
+
71
+ if (!isOpen) return null;
72
+
73
+ return (
74
+ <div
75
+ className={`
76
+ absolute inset-0 z-50
77
+ bg-black/20
78
+ flex justify-end
79
+ `}
80
+ >
81
+ <div
82
+ ref={panelRef}
83
+ className={`
84
+ h-full w-72 max-w-[80%]
85
+ bg-warm-cloud
86
+ border-l border-black
87
+ shadow-[-4px_0_0_0_var(--color-black)]
88
+ flex flex-col
89
+ animate-slide-in-right
90
+ ${className}
91
+ `}
92
+ >
93
+ {/* Header */}
94
+ <div className="flex items-center justify-between px-4 py-3 border-b border-black">
95
+ <span className="font-joystix text-xs text-black uppercase">
96
+ {title}
97
+ </span>
98
+ <Button
99
+ variant="ghost"
100
+ size="md"
101
+ iconOnly={true}
102
+ iconName="close"
103
+ onClick={onClose}
104
+ />
105
+ </div>
106
+
107
+ {/* Content */}
108
+ <div className="flex-1 overflow-auto p-4">
109
+ <div className="font-mondwest text-base text-black space-y-4">
110
+ {children}
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export default HelpPanel;
119
+
@@ -0,0 +1,176 @@
1
+ 'use client';
2
+
3
+ import React, { forwardRef } from 'react';
4
+ import { Icon } from '@/components/icons';
5
+
6
+ // ============================================================================
7
+ // Types
8
+ // ============================================================================
9
+
10
+ type InputSize = 'sm' | 'md' | 'lg';
11
+
12
+ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
13
+ /** Size preset */
14
+ size?: InputSize;
15
+ /** Error state */
16
+ error?: boolean;
17
+ /** Full width */
18
+ fullWidth?: boolean;
19
+ /** Icon name (filename without .svg extension) - displays on the left */
20
+ iconName?: string;
21
+ /** Additional classes */
22
+ className?: string;
23
+ }
24
+
25
+ interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
26
+ /** Error state */
27
+ error?: boolean;
28
+ /** Full width */
29
+ fullWidth?: boolean;
30
+ /** Additional classes */
31
+ className?: string;
32
+ }
33
+
34
+ // ============================================================================
35
+ // Styles
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Base input styles matching the retro aesthetic
40
+ */
41
+ const baseStyles = `
42
+ font-mondwest
43
+ bg-warm-cloud text-black
44
+ border border-black
45
+ rounded-sm
46
+ placeholder:text-black/40
47
+ focus:outline-none
48
+ focus:ring-2 focus:ring-sun-yellow focus:ring-offset-0
49
+ disabled:opacity-50 disabled:cursor-not-allowed
50
+ `;
51
+
52
+ /**
53
+ * Size presets
54
+ */
55
+ const sizeStyles: Record<InputSize, string> = {
56
+ sm: 'h-8 px-2 text-sm',
57
+ md: 'h-10 px-3 text-base',
58
+ lg: 'h-12 px-4 text-base',
59
+ };
60
+
61
+ /**
62
+ * Error state styles
63
+ */
64
+ const errorStyles = `
65
+ border-sun-red
66
+ focus:ring-sun-red
67
+ `;
68
+
69
+ // ============================================================================
70
+ // Components
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Text input with retro styling
75
+ */
76
+ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
77
+ {
78
+ size = 'md',
79
+ error = false,
80
+ fullWidth = false,
81
+ iconName,
82
+ className = '',
83
+ ...props
84
+ },
85
+ ref
86
+ ) {
87
+ const iconSize = size === 'sm' ? 14 : size === 'lg' ? 18 : 16;
88
+ const paddingLeft = iconName ? (size === 'sm' ? 'pl-8' : size === 'lg' ? 'pl-12' : 'pl-10') : '';
89
+
90
+ const classes = [
91
+ baseStyles,
92
+ sizeStyles[size],
93
+ error ? errorStyles : '',
94
+ fullWidth ? 'w-full' : '',
95
+ paddingLeft,
96
+ className,
97
+ ]
98
+ .join(' ')
99
+ .replace(/\s+/g, ' ')
100
+ .trim();
101
+
102
+ const input = (
103
+ <input ref={ref} className={classes} {...props} />
104
+ );
105
+
106
+ if (iconName) {
107
+ return (
108
+ <div className="relative">
109
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
110
+ <Icon name={iconName} size={iconSize} className="text-black/40" />
111
+ </div>
112
+ {input}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ return input;
118
+ });
119
+
120
+ /**
121
+ * Textarea with retro styling
122
+ */
123
+ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextArea(
124
+ {
125
+ error = false,
126
+ fullWidth = false,
127
+ className = '',
128
+ ...props
129
+ },
130
+ ref
131
+ ) {
132
+ const classes = [
133
+ baseStyles,
134
+ 'px-3 py-2 text-base',
135
+ 'resize-y min-h-24',
136
+ error ? errorStyles : '',
137
+ fullWidth ? 'w-full' : '',
138
+ className,
139
+ ]
140
+ .join(' ')
141
+ .replace(/\s+/g, ' ')
142
+ .trim();
143
+
144
+ return (
145
+ <textarea ref={ref} className={classes} {...props} />
146
+ );
147
+ });
148
+
149
+ // ============================================================================
150
+ // Label Component
151
+ // ============================================================================
152
+
153
+ interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
154
+ children: React.ReactNode;
155
+ required?: boolean;
156
+ className?: string;
157
+ }
158
+
159
+ /**
160
+ * Form label
161
+ */
162
+ export function Label({ children, required, className = '', ...props }: LabelProps) {
163
+ return (
164
+ <label
165
+ className={className}
166
+ {...props}
167
+ >
168
+ {children}
169
+ {required && <span className="text-error-red ml-1">*</span>}
170
+ </label>
171
+ );
172
+ }
173
+
174
+ export default Input;
175
+
176
+