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,211 @@
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 PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
12
+
13
+ interface PopoverContextValue {
14
+ open: boolean;
15
+ setOpen: (open: boolean) => void;
16
+ triggerRef: React.RefObject<HTMLElement | null>;
17
+ position: PopoverPosition;
18
+ }
19
+
20
+ // ============================================================================
21
+ // Context
22
+ // ============================================================================
23
+
24
+ const PopoverContext = createContext<PopoverContextValue | null>(null);
25
+
26
+ function usePopoverContext() {
27
+ const context = useContext(PopoverContext);
28
+ if (!context) {
29
+ throw new Error('Popover components must be used within a Popover');
30
+ }
31
+ return context;
32
+ }
33
+
34
+ // ============================================================================
35
+ // Popover Root
36
+ // ============================================================================
37
+
38
+ interface PopoverProps {
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?: PopoverPosition;
47
+ /** Children */
48
+ children: React.ReactNode;
49
+ }
50
+
51
+ export function Popover({
52
+ open: controlledOpen,
53
+ defaultOpen = false,
54
+ onOpenChange,
55
+ position = 'bottom',
56
+ children,
57
+ }: PopoverProps) {
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
+ <PopoverContext.Provider value={{ open, setOpen, triggerRef, position }}>
72
+ {children}
73
+ </PopoverContext.Provider>
74
+ );
75
+ }
76
+
77
+ // ============================================================================
78
+ // Popover Trigger
79
+ // ============================================================================
80
+
81
+ interface PopoverTriggerProps {
82
+ /** Trigger element */
83
+ children: React.ReactElement;
84
+ /** Pass through as child instead of wrapping */
85
+ asChild?: boolean;
86
+ }
87
+
88
+ export function PopoverTrigger({ children, asChild }: PopoverTriggerProps) {
89
+ const { open, setOpen, triggerRef } = usePopoverContext();
90
+
91
+ const handleClick = () => {
92
+ setOpen(!open);
93
+ };
94
+
95
+ if (asChild && React.isValidElement(children)) {
96
+ return React.cloneElement(children as React.ReactElement<{ onClick?: () => void; ref?: React.Ref<HTMLElement | null> }>, {
97
+ onClick: handleClick,
98
+ ref: triggerRef as React.Ref<HTMLElement | null>,
99
+ });
100
+ }
101
+
102
+ return (
103
+ <button
104
+ type="button"
105
+ ref={triggerRef as React.RefObject<HTMLButtonElement>}
106
+ onClick={handleClick}
107
+ >
108
+ {children}
109
+ </button>
110
+ );
111
+ }
112
+
113
+ // ============================================================================
114
+ // Popover Content
115
+ // ============================================================================
116
+
117
+ interface PopoverContentProps {
118
+ /** Additional className */
119
+ className?: string;
120
+ /** Children */
121
+ children: React.ReactNode;
122
+ /** Alignment relative to trigger */
123
+ align?: 'start' | 'center' | 'end';
124
+ }
125
+
126
+ export function PopoverContent({ className = '', children, align = 'center' }: PopoverContentProps) {
127
+ const { open, setOpen, triggerRef, position } = usePopoverContext();
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 = 8;
143
+
144
+ let top = 0;
145
+ let left = 0;
146
+
147
+ switch (position) {
148
+ case 'top':
149
+ top = trigger.top - content.height - gap;
150
+ left = trigger.left + (trigger.width - content.width) / 2;
151
+ break;
152
+ case 'bottom':
153
+ top = trigger.bottom + gap;
154
+ left = trigger.left + (trigger.width - content.width) / 2;
155
+ break;
156
+ case 'left':
157
+ top = trigger.top + (trigger.height - content.height) / 2;
158
+ left = trigger.left - content.width - gap;
159
+ break;
160
+ case 'right':
161
+ top = trigger.top + (trigger.height - content.height) / 2;
162
+ left = trigger.right + gap;
163
+ break;
164
+ }
165
+
166
+ // Adjust for alignment
167
+ if (position === 'top' || position === 'bottom') {
168
+ if (align === 'start') {
169
+ left = trigger.left;
170
+ } else if (align === 'end') {
171
+ left = trigger.right - content.width;
172
+ }
173
+ }
174
+
175
+ // Keep within viewport
176
+ top = Math.max(8, Math.min(top, window.innerHeight - content.height - 8));
177
+ left = Math.max(8, Math.min(left, window.innerWidth - content.width - 8));
178
+
179
+ setCoords({ top, left });
180
+ }, [open, position, align, triggerRef]);
181
+
182
+ // Handle click outside
183
+ useClickOutside(open, [contentRef, triggerRef], () => setOpen(false));
184
+
185
+ // Handle escape
186
+ useEscapeKey(open, () => setOpen(false));
187
+
188
+ if (!mounted || !open) return null;
189
+
190
+ return createPortal(
191
+ <div
192
+ ref={contentRef}
193
+ className={`
194
+ fixed z-50
195
+ bg-warm-cloud
196
+ border-2 border-black
197
+ rounded-sm
198
+ shadow-[2px_2px_0_0_var(--color-black)]
199
+ p-4
200
+ animate-fadeIn
201
+ ${className}
202
+ `.trim()}
203
+ style={{ top: coords.top, left: coords.left }}
204
+ >
205
+ {children}
206
+ </div>,
207
+ document.body
208
+ );
209
+ }
210
+
211
+ export default Popover;
@@ -0,0 +1,158 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ // ============================================================================
6
+ // Types
7
+ // ============================================================================
8
+
9
+ type ProgressVariant = 'default' | 'success' | 'warning' | 'error';
10
+ type ProgressSize = 'sm' | 'md' | 'lg';
11
+
12
+ interface ProgressProps {
13
+ /** Progress value (0-100) */
14
+ value: number;
15
+ /** Maximum value */
16
+ max?: number;
17
+ /** Visual variant */
18
+ variant?: ProgressVariant;
19
+ /** Size preset */
20
+ size?: ProgressSize;
21
+ /** Show percentage label */
22
+ showLabel?: boolean;
23
+ /** Additional classes */
24
+ className?: string;
25
+ }
26
+
27
+ // ============================================================================
28
+ // Styles
29
+ // ============================================================================
30
+
31
+ const sizeStyles: Record<ProgressSize, string> = {
32
+ sm: 'h-2',
33
+ md: 'h-4',
34
+ lg: 'h-6',
35
+ };
36
+
37
+ const variantStyles: Record<ProgressVariant, string> = {
38
+ default: 'bg-sun-yellow',
39
+ success: 'bg-success-green',
40
+ warning: 'bg-sunset-fuzz',
41
+ error: 'bg-error-red',
42
+ };
43
+
44
+ // ============================================================================
45
+ // Component
46
+ // ============================================================================
47
+
48
+ /**
49
+ * Progress bar with retro styling
50
+ */
51
+ export function Progress({
52
+ value,
53
+ max = 100,
54
+ variant = 'default',
55
+ size = 'md',
56
+ showLabel = false,
57
+ className = '',
58
+ }: ProgressProps) {
59
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
60
+
61
+ return (
62
+ <div className={`w-full ${className}`}>
63
+ {/* Track */}
64
+ <div
65
+ className={`
66
+ w-full
67
+ bg-warm-cloud
68
+ border border-black
69
+ rounded-sm
70
+ overflow-hidden
71
+ ${sizeStyles[size]}
72
+ `}
73
+ role="progressbar"
74
+ aria-valuenow={value}
75
+ aria-valuemin={0}
76
+ aria-valuemax={max}
77
+ >
78
+ {/* Fill */}
79
+ <div
80
+ className={`
81
+ h-full
82
+ ${variantStyles[variant]}
83
+ `}
84
+ style={{ width: `${percentage}%` }}
85
+ />
86
+ </div>
87
+
88
+ {/* Label */}
89
+ {showLabel && (
90
+ <div className="mt-1 font-joystix text-2xs text-black text-right">
91
+ {Math.round(percentage)}%
92
+ </div>
93
+ )}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ // ============================================================================
99
+ // Loading Spinner
100
+ // ============================================================================
101
+
102
+ interface SpinnerProps {
103
+ /** Size in pixels */
104
+ size?: number;
105
+ /** Additional classes */
106
+ className?: string;
107
+ /** Whether loading is completed - shows checkmark */
108
+ completed?: boolean;
109
+ }
110
+
111
+ // PixelCode loader frames - Private Use Area characters from PixelCode font
112
+ // These are the 6-frame loader animation characters (U+EE06-U+EE0B)
113
+ // Frame1: Frame2: Frame3: Frame4: Frame5: Frame6:
114
+ const LOADER_FRAMES = ['\uEE06', '\uEE07', '\uEE08', '\uEE09', '\uEE0A', '\uEE0B'];
115
+
116
+ /**
117
+ * PixelCode loader with animated frames that loop through 6 frames
118
+ * When completed, displays a checkmark (✓)
119
+ */
120
+ export function Spinner({ size = 24, className = '', completed = false }: SpinnerProps) {
121
+ const [frameIndex, setFrameIndex] = React.useState(0);
122
+
123
+ React.useEffect(() => {
124
+ if (completed) {
125
+ setFrameIndex(0); // Reset to first frame when completed
126
+ return;
127
+ }
128
+
129
+ const interval = setInterval(() => {
130
+ setFrameIndex((prev) => (prev + 1) % LOADER_FRAMES.length);
131
+ }, 150); // Change frame every 150ms for smooth animation
132
+
133
+ return () => clearInterval(interval);
134
+ }, [completed]);
135
+
136
+ const fontSize = size;
137
+ const displayChar = completed ? '✓' : LOADER_FRAMES[frameIndex];
138
+
139
+ return (
140
+ <div
141
+ className={`inline-block flex items-center justify-center ${className}`}
142
+ style={{
143
+ width: size,
144
+ height: size,
145
+ fontSize: fontSize,
146
+ fontFamily: 'PixelCode, monospace',
147
+ lineHeight: 1,
148
+ }}
149
+ aria-label={completed ? 'Completed' : 'Loading'}
150
+ role="status"
151
+ >
152
+ {displayChar}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ export default Progress;
158
+
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect } from 'react';
4
+
5
+ // ============================================================================
6
+ // Types
7
+ // ============================================================================
8
+
9
+ interface SelectOption {
10
+ value: string;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ interface SelectProps {
16
+ /** Available options */
17
+ options: SelectOption[];
18
+ /** Currently selected value */
19
+ value?: string;
20
+ /** Placeholder text when no value selected */
21
+ placeholder?: string;
22
+ /** Change handler */
23
+ onChange?: (value: string) => void;
24
+ /** Disabled state */
25
+ disabled?: boolean;
26
+ /** Error state */
27
+ error?: boolean;
28
+ /** Full width */
29
+ fullWidth?: boolean;
30
+ /** Additional classes */
31
+ className?: string;
32
+ }
33
+
34
+ // ============================================================================
35
+ // Component
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Custom select/dropdown with retro styling
40
+ */
41
+ export function Select({
42
+ options,
43
+ value,
44
+ placeholder = 'Select...',
45
+ onChange,
46
+ disabled = false,
47
+ error = false,
48
+ fullWidth = false,
49
+ className = '',
50
+ }: SelectProps) {
51
+ const [isOpen, setIsOpen] = useState(false);
52
+ const containerRef = useRef<HTMLDivElement>(null);
53
+
54
+ // Close dropdown when clicking outside
55
+ useEffect(() => {
56
+ function handleClickOutside(event: MouseEvent) {
57
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
58
+ setIsOpen(false);
59
+ }
60
+ }
61
+ document.addEventListener('mousedown', handleClickOutside);
62
+ return () => document.removeEventListener('mousedown', handleClickOutside);
63
+ }, []);
64
+
65
+ const selectedOption = options.find(opt => opt.value === value);
66
+
67
+ const handleSelect = (optionValue: string) => {
68
+ onChange?.(optionValue);
69
+ setIsOpen(false);
70
+ };
71
+
72
+ return (
73
+ <div
74
+ ref={containerRef}
75
+ className={`relative ${fullWidth ? 'w-full' : 'w-fit'} ${className}`}
76
+ >
77
+ {/* Trigger Button */}
78
+ <button
79
+ type="button"
80
+ onClick={() => !disabled && setIsOpen(!isOpen)}
81
+ disabled={disabled}
82
+ className={`
83
+ flex items-center justify-between gap-2
84
+ w-full h-10 px-3
85
+ font-mondwest text-base
86
+ bg-warm-cloud text-black
87
+ border rounded-sm
88
+ ${error ? 'border-error' : 'border-black'}
89
+ ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
90
+ ${isOpen ? 'shadow-[0_3px_0_0_var(--color-black)] -translate-y-0.5' : 'shadow-[0_1px_0_0_var(--color-black)]'}
91
+ `}
92
+ >
93
+ <span className={selectedOption ? 'text-black' : 'text-black/40'}>
94
+ {selectedOption?.label || placeholder}
95
+ </span>
96
+ <span className={`text-black ${isOpen ? 'rotate-180' : ''}`}>▼</span>
97
+ </button>
98
+
99
+ {/* Dropdown Menu */}
100
+ {isOpen && (
101
+ <div
102
+ className={`
103
+ absolute z-50 top-full left-0 right-0 mt-1
104
+ bg-warm-cloud
105
+ border border-black
106
+ rounded-sm
107
+ shadow-[2px_2px_0_0_var(--color-black)]
108
+ overflow-hidden
109
+ `}
110
+ >
111
+ {options.map((option) => (
112
+ <button
113
+ key={option.value}
114
+ type="button"
115
+ onClick={() => !option.disabled && handleSelect(option.value)}
116
+ disabled={option.disabled}
117
+ className={`
118
+ w-full px-3 py-2
119
+ font-mondwest text-base text-left
120
+ ${option.value === value ? 'bg-sun-yellow text-black' : 'text-black'}
121
+ ${option.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-sun-yellow cursor-pointer'}
122
+ `}
123
+ >
124
+ {option.label}
125
+ </button>
126
+ ))}
127
+ </div>
128
+ )}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ export default Select;
134
+