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,254 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import type { Tab } from '../types';
5
+ import { Input } from '@/components/ui/Input';
6
+ import { Icon } from '@/components/icons';
7
+ import { SEARCH_INDEX, type SearchableItem } from '../tabs/ComponentsTab/DesignSystemTab';
8
+ import { TYPOGRAPHY_SEARCH_INDEX, type TypographySearchableItem } from '../lib/searchIndexes';
9
+
10
+ interface PrimaryNavigationFooterProps {
11
+ activeTab: Tab;
12
+ onTabChange: (tab: Tab) => void;
13
+ // Search props
14
+ componentSearchQuery?: string;
15
+ onComponentSearchChange?: (query: string) => void;
16
+ typographySearchQuery?: string;
17
+ onTypographySearchChange?: (query: string) => void;
18
+ }
19
+
20
+ const TABS_CONFIG: { id: Tab; label: string; iconName?: string }[] = [
21
+ { id: 'variables', label: 'Variables', iconName: 'settings' },
22
+ { id: 'typography', label: 'Typography', iconName: 'file-written' },
23
+ { id: 'components', label: 'Components', iconName: 'wrench' },
24
+ { id: 'assets', label: 'Assets', iconName: 'folder-open' },
25
+ { id: 'mockStates', label: 'Mock States', iconName: 'wrench' },
26
+ ];
27
+
28
+ export function PrimaryNavigationFooter({
29
+ activeTab,
30
+ onTabChange,
31
+ componentSearchQuery = '',
32
+ onComponentSearchChange,
33
+ typographySearchQuery = '',
34
+ onTypographySearchChange,
35
+ }: PrimaryNavigationFooterProps) {
36
+ const [showAutocomplete, setShowAutocomplete] = useState(false);
37
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0);
38
+ const inputRef = useRef<HTMLInputElement>(null);
39
+ const containerRef = useRef<HTMLDivElement>(null);
40
+
41
+ const showSearch = activeTab === 'components' || activeTab === 'typography';
42
+ const searchQuery = activeTab === 'components' ? componentSearchQuery : typographySearchQuery;
43
+ const onSearchChange = activeTab === 'components' ? onComponentSearchChange : onTypographySearchChange;
44
+
45
+ // Get matching suggestions
46
+ const suggestions = searchQuery
47
+ ? (activeTab === 'components'
48
+ ? SEARCH_INDEX.filter((item) =>
49
+ item.text.toLowerCase().includes(searchQuery.toLowerCase())
50
+ ).slice(0, 10)
51
+ : TYPOGRAPHY_SEARCH_INDEX.filter((item) => {
52
+ const queryLower = searchQuery.toLowerCase();
53
+ return (
54
+ item.text.toLowerCase().includes(queryLower) ||
55
+ item.aliases.some((alias) => alias.toLowerCase().includes(queryLower)) ||
56
+ item.element.toLowerCase().includes(queryLower)
57
+ );
58
+ }).slice(0, 10))
59
+ : [];
60
+
61
+ // Close autocomplete when clicking outside
62
+ useEffect(() => {
63
+ const handleClickOutside = (event: MouseEvent) => {
64
+ if (
65
+ containerRef.current &&
66
+ !containerRef.current.contains(event.target as Node)
67
+ ) {
68
+ setShowAutocomplete(false);
69
+ }
70
+ };
71
+
72
+ if (showSearch) {
73
+ document.addEventListener('mousedown', handleClickOutside);
74
+ return () => document.removeEventListener('mousedown', handleClickOutside);
75
+ }
76
+ }, [showSearch]);
77
+
78
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
79
+ if (onSearchChange) {
80
+ onSearchChange(e.target.value);
81
+ }
82
+ setShowAutocomplete(true);
83
+ setSelectedSuggestionIndex(0);
84
+ };
85
+
86
+ const handleSelectSuggestion = (item: SearchableItem | TypographySearchableItem) => {
87
+ if (onSearchChange) {
88
+ onSearchChange(item.text);
89
+ }
90
+ setShowAutocomplete(false);
91
+
92
+ // Scroll to section for typography
93
+ if (activeTab === 'typography' && 'sectionId' in item) {
94
+ const sectionElement = document.getElementById(`typography-${item.sectionId}`);
95
+ if (sectionElement) {
96
+ sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
97
+ }
98
+ }
99
+ };
100
+
101
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
102
+ if (e.key === 'Tab' && suggestions.length > 0 && showAutocomplete) {
103
+ e.preventDefault();
104
+ if (suggestions[selectedSuggestionIndex]) {
105
+ handleSelectSuggestion(suggestions[selectedSuggestionIndex] as SearchableItem | TypographySearchableItem);
106
+ }
107
+ } else if (e.key === 'ArrowDown') {
108
+ e.preventDefault();
109
+ setShowAutocomplete(true);
110
+ setSelectedSuggestionIndex((prev) =>
111
+ prev < suggestions.length - 1 ? prev + 1 : prev
112
+ );
113
+ } else if (e.key === 'ArrowUp') {
114
+ e.preventDefault();
115
+ setSelectedSuggestionIndex((prev) => (prev > 0 ? prev - 1 : 0));
116
+ } else if (e.key === 'Enter' && suggestions.length > 0 && showAutocomplete) {
117
+ e.preventDefault();
118
+ if (suggestions[selectedSuggestionIndex]) {
119
+ handleSelectSuggestion(suggestions[selectedSuggestionIndex] as SearchableItem | TypographySearchableItem);
120
+ }
121
+ } else if (e.key === 'Escape') {
122
+ setShowAutocomplete(false);
123
+ }
124
+ };
125
+
126
+ const highlightText = (text: string, query: string) => {
127
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
128
+ if (index === -1) return text;
129
+ return (
130
+ <>
131
+ {text.substring(0, index)}
132
+ <span className="bg-sun-yellow">{text.substring(index, index + query.length)}</span>
133
+ {text.substring(index + query.length)}
134
+ </>
135
+ );
136
+ };
137
+
138
+ return (
139
+ <div className="flex items-center justify-between gap-4 px-2 py-2 bg-warm-cloud border-t border-black">
140
+ {/* Left: Primary Tabs */}
141
+ <div className="flex gap-1 items-center overflow-x-auto">
142
+ {TABS_CONFIG.map((tab) => (
143
+ <button
144
+ key={tab.id}
145
+ type="button"
146
+ onClick={() => onTabChange(tab.id)}
147
+ className={`flex items-center justify-center gap-2 px-4 py-2 font-joystix text-xs uppercase cursor-pointer select-none text-black transition-all duration-200 ease-out relative border border-black rounded-sm flex-none ${
148
+ activeTab === tab.id ? 'bg-sun-yellow' : 'bg-transparent hover:bg-black/5'
149
+ }`}
150
+ >
151
+ {tab.iconName && <Icon name={tab.iconName} size={14} />}
152
+ {tab.label}
153
+ </button>
154
+ ))}
155
+ </div>
156
+
157
+ {/* Right: Search (only for Components and Typography) */}
158
+ {showSearch && (
159
+ <div className="relative flex-shrink-0 w-fit" ref={containerRef}>
160
+ <Input
161
+ ref={inputRef}
162
+ type="text"
163
+ iconName="search"
164
+ placeholder={
165
+ activeTab === 'components'
166
+ ? 'Search components...'
167
+ : 'Search typography (H1, Heading 1, P, Paragraph...)'
168
+ }
169
+ value={searchQuery}
170
+ onChange={handleInputChange}
171
+ onKeyDown={handleKeyDown}
172
+ onFocus={() => setShowAutocomplete(true)}
173
+ />
174
+ {/* Autocomplete */}
175
+ {showAutocomplete && suggestions.length > 0 && (
176
+ <div className="absolute z-50 w-full mt-1 bg-warm-cloud border border-black rounded-sm shadow-[4px_4px_0_0_var(--color-black)] max-h-64 overflow-y-auto bottom-full mb-1">
177
+ {suggestions.map((item, index) => {
178
+ if (activeTab === 'components') {
179
+ const componentItem = item as SearchableItem;
180
+ const SECTION_TITLES: Record<string, string> = {
181
+ buttons: 'Buttons',
182
+ cards: 'Cards',
183
+ forms: 'Forms',
184
+ feedback: 'Feedback',
185
+ navigation: 'Navigation',
186
+ overlays: 'Overlays',
187
+ };
188
+ const sectionTitle = SECTION_TITLES[componentItem.sectionId];
189
+ const isSubsection = componentItem.subsectionTitle !== undefined;
190
+ const displayTitle = isSubsection ? componentItem.subsectionTitle : sectionTitle;
191
+
192
+ return (
193
+ <button
194
+ key={`${componentItem.sectionId}-${componentItem.text}-${index}`}
195
+ type="button"
196
+ onClick={() => {
197
+ if (onSearchChange) onSearchChange(componentItem.text);
198
+ setShowAutocomplete(false);
199
+ }}
200
+ className={`w-full text-left px-3 py-2 font-mondwest text-sm transition-colors ${
201
+ index === selectedSuggestionIndex
202
+ ? 'bg-sun-yellow text-black'
203
+ : 'bg-warm-cloud text-black hover:bg-black/5'
204
+ } ${isSubsection ? 'pl-6' : ''}`}
205
+ >
206
+ <div className="flex items-center justify-between">
207
+ <div className="flex flex-col gap-0.5">
208
+ {displayTitle && (
209
+ <span className="font-joystix text-xs font-bold text-black/60 uppercase">
210
+ {displayTitle}
211
+ </span>
212
+ )}
213
+ <span>{highlightText(componentItem.text, searchQuery)}</span>
214
+ </div>
215
+ <span className="text-xs text-black/40 uppercase">{componentItem.type}</span>
216
+ </div>
217
+ </button>
218
+ );
219
+ } else {
220
+ const typographyItem = item as TypographySearchableItem;
221
+ return (
222
+ <button
223
+ key={`${typographyItem.sectionId}-${typographyItem.text}-${index}`}
224
+ type="button"
225
+ onClick={() => handleSelectSuggestion(typographyItem)}
226
+ className={`w-full text-left px-3 py-2 font-mondwest text-sm transition-colors ${
227
+ index === selectedSuggestionIndex
228
+ ? 'bg-sun-yellow text-black'
229
+ : 'bg-warm-cloud text-black hover:bg-black/5'
230
+ }`}
231
+ >
232
+ <div className="flex items-center justify-between">
233
+ <div className="flex flex-col gap-0.5">
234
+ <span className="font-joystix text-xs font-bold text-black/60 uppercase">
235
+ {typographyItem.sectionId}
236
+ </span>
237
+ <span>{highlightText(typographyItem.text, searchQuery)}</span>
238
+ </div>
239
+ <span className="text-xs text-black/40 uppercase font-mono">
240
+ {`<${typographyItem.element}>`}
241
+ </span>
242
+ </div>
243
+ </button>
244
+ );
245
+ }
246
+ })}
247
+ </div>
248
+ )}
249
+ </div>
250
+ )}
251
+ </div>
252
+ );
253
+ }
254
+
@@ -0,0 +1,253 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect, useMemo } from 'react';
4
+ import type { BaseColor } from '../types';
5
+
6
+ interface SearchableColorDropdownProps {
7
+ /** Available base colors to select from */
8
+ colors: BaseColor[];
9
+ /** Currently selected base color ID */
10
+ value?: string;
11
+ /** Placeholder text when no value selected */
12
+ placeholder?: string;
13
+ /** Change handler - receives the selected BaseColor ID */
14
+ onChange?: (colorId: string) => void;
15
+ /** Disabled state */
16
+ disabled?: boolean;
17
+ /** Additional classes */
18
+ className?: string;
19
+ }
20
+
21
+ /**
22
+ * Searchable dropdown for selecting base colors
23
+ * - Search by display name or hex value
24
+ * - Shows color swatch preview for each option
25
+ */
26
+ export function SearchableColorDropdown({
27
+ colors,
28
+ value,
29
+ placeholder = 'Select color...',
30
+ onChange,
31
+ disabled = false,
32
+ className = '',
33
+ }: SearchableColorDropdownProps) {
34
+ const [isOpen, setIsOpen] = useState(false);
35
+ const [searchQuery, setSearchQuery] = useState('');
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+ const inputRef = useRef<HTMLInputElement>(null);
38
+
39
+ // Close dropdown when clicking outside
40
+ useEffect(() => {
41
+ function handleClickOutside(event: MouseEvent) {
42
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
43
+ setIsOpen(false);
44
+ setSearchQuery('');
45
+ }
46
+ }
47
+ document.addEventListener('mousedown', handleClickOutside);
48
+ return () => document.removeEventListener('mousedown', handleClickOutside);
49
+ }, []);
50
+
51
+ // Focus input when dropdown opens
52
+ useEffect(() => {
53
+ if (isOpen && inputRef.current) {
54
+ inputRef.current.focus();
55
+ }
56
+ }, [isOpen]);
57
+
58
+ // Find selected color
59
+ const selectedColor = colors.find(c => c.id === value);
60
+
61
+ // Filter colors by search query (name or hex)
62
+ const filteredColors = useMemo(() => {
63
+ if (!searchQuery.trim()) return colors;
64
+
65
+ const query = searchQuery.toLowerCase();
66
+ return colors.filter(color =>
67
+ color.displayName.toLowerCase().includes(query) ||
68
+ color.name.toLowerCase().includes(query) ||
69
+ color.value.toLowerCase().includes(query)
70
+ );
71
+ }, [colors, searchQuery]);
72
+
73
+ // Group colors by category
74
+ const groupedColors = useMemo(() => {
75
+ const brand = filteredColors.filter(c => c.category === 'brand');
76
+ const neutral = filteredColors.filter(c => c.category === 'neutral');
77
+ return { brand, neutral };
78
+ }, [filteredColors]);
79
+
80
+ const handleSelect = (colorId: string) => {
81
+ onChange?.(colorId);
82
+ setIsOpen(false);
83
+ setSearchQuery('');
84
+ };
85
+
86
+ const handleKeyDown = (e: React.KeyboardEvent) => {
87
+ if (e.key === 'Escape') {
88
+ setIsOpen(false);
89
+ setSearchQuery('');
90
+ }
91
+ };
92
+
93
+ return (
94
+ <div
95
+ ref={containerRef}
96
+ className={`relative w-full ${className}`}
97
+ >
98
+ {/* Trigger Button */}
99
+ <button
100
+ type="button"
101
+ onClick={() => !disabled && setIsOpen(!isOpen)}
102
+ disabled={disabled}
103
+ className={`
104
+ flex items-center justify-between gap-2
105
+ w-full h-10 px-3
106
+ font-mondwest text-base
107
+ bg-warm-cloud text-black
108
+ border rounded-sm
109
+ border-black
110
+ ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
111
+ ${isOpen ? 'shadow-[0_3px_0_0_var(--color-black)] -translate-y-0.5' : 'shadow-[0_1px_0_0_var(--color-black)]'}
112
+ `}
113
+ >
114
+ <div className="flex items-center gap-2 min-w-0">
115
+ {selectedColor && (
116
+ <div
117
+ className="w-4 h-4 rounded-xs border border-black flex-shrink-0"
118
+ style={{ backgroundColor: selectedColor.value }}
119
+ />
120
+ )}
121
+ <span className={`truncate ${selectedColor ? 'text-black' : 'text-black/40'}`}>
122
+ {selectedColor?.displayName || placeholder}
123
+ </span>
124
+ </div>
125
+ <span className={`text-black flex-shrink-0 ${isOpen ? 'rotate-180' : ''}`}>▼</span>
126
+ </button>
127
+
128
+ {/* Dropdown Menu */}
129
+ {isOpen && (
130
+ <div
131
+ className={`
132
+ absolute z-50 top-full left-0 right-0 mt-1
133
+ bg-warm-cloud
134
+ border border-black
135
+ rounded-sm
136
+ shadow-[2px_2px_0_0_var(--color-black)]
137
+ overflow-hidden
138
+ max-h-[300px] flex flex-col
139
+ `}
140
+ onKeyDown={handleKeyDown}
141
+ >
142
+ {/* Search Input */}
143
+ <div className="p-2 border-b" style={{ borderColor: 'var(--border-black-20)' }}>
144
+ <input
145
+ ref={inputRef}
146
+ type="text"
147
+ value={searchQuery}
148
+ onChange={(e) => setSearchQuery(e.target.value)}
149
+ placeholder="Search by name or hex..."
150
+ className="
151
+ w-full h-8 px-2
152
+ font-mondwest text-sm
153
+ bg-white text-black
154
+ border border-black rounded-sm
155
+ outline-none
156
+ placeholder:text-black/40
157
+ "
158
+ />
159
+ </div>
160
+
161
+ {/* Options List */}
162
+ <div className="overflow-y-auto flex-1">
163
+ {filteredColors.length === 0 ? (
164
+ <div className="px-3 py-2 text-black/50 font-mondwest text-base">
165
+ No colors found
166
+ </div>
167
+ ) : (
168
+ <>
169
+ {/* Brand Colors */}
170
+ {groupedColors.brand.length > 0 && (
171
+ <div>
172
+ <div
173
+ className="px-3 py-1 text-black/50 font-joystix text-xs uppercase tracking-wider"
174
+ style={{ backgroundColor: 'var(--border-black-10)' }}
175
+ >
176
+ Brand
177
+ </div>
178
+ {groupedColors.brand.map((color) => (
179
+ <ColorOption
180
+ key={color.id}
181
+ color={color}
182
+ isSelected={color.id === value}
183
+ onSelect={handleSelect}
184
+ />
185
+ ))}
186
+ </div>
187
+ )}
188
+
189
+ {/* Neutral Colors */}
190
+ {groupedColors.neutral.length > 0 && (
191
+ <div>
192
+ <div
193
+ className="px-3 py-1 text-black/50 font-joystix text-xs uppercase tracking-wider"
194
+ style={{ backgroundColor: 'var(--border-black-10)' }}
195
+ >
196
+ Neutrals
197
+ </div>
198
+ {groupedColors.neutral.map((color) => (
199
+ <ColorOption
200
+ key={color.id}
201
+ color={color}
202
+ isSelected={color.id === value}
203
+ onSelect={handleSelect}
204
+ />
205
+ ))}
206
+ </div>
207
+ )}
208
+ </>
209
+ )}
210
+ </div>
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ // Individual color option
218
+ function ColorOption({
219
+ color,
220
+ isSelected,
221
+ onSelect,
222
+ }: {
223
+ color: BaseColor;
224
+ isSelected: boolean;
225
+ onSelect: (id: string) => void;
226
+ }) {
227
+ return (
228
+ <button
229
+ type="button"
230
+ onClick={() => onSelect(color.id)}
231
+ className={`
232
+ w-full px-3 py-2
233
+ flex items-center gap-2
234
+ font-mondwest text-base text-left
235
+ ${isSelected ? 'bg-sun-yellow text-black' : 'text-black hover:bg-sun-yellow'}
236
+ cursor-pointer
237
+ `}
238
+ >
239
+ {/* Color Swatch */}
240
+ <div
241
+ className="w-4 h-4 rounded-xs border border-black flex-shrink-0"
242
+ style={{ backgroundColor: color.value }}
243
+ />
244
+ {/* Name */}
245
+ <span className="flex-1 truncate">{color.displayName}</span>
246
+ {/* Hex Value */}
247
+ <span className="text-sm text-black/50 font-mono uppercase">{color.value}</span>
248
+ </button>
249
+ );
250
+ }
251
+
252
+ export default SearchableColorDropdown;
253
+
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import type { Tab } from '../types';
4
+ import { ComponentsSecondaryNav } from './ComponentsSecondaryNav';
5
+
6
+ interface SecondaryNavigationProps {
7
+ activeTab: Tab;
8
+ // Components tab props
9
+ componentSubTab?: string;
10
+ onComponentSubTabChange?: (tab: string) => void;
11
+ componentTabs?: Array<{ id: string; label: string }>;
12
+ onAddComponentFolder?: (folderName: string) => void;
13
+ }
14
+
15
+ export function SecondaryNavigation({
16
+ activeTab,
17
+ componentSubTab,
18
+ onComponentSubTabChange,
19
+ componentTabs,
20
+ onAddComponentFolder,
21
+ }: SecondaryNavigationProps) {
22
+ // Only show secondary nav for Components tab
23
+ if (activeTab === 'components') {
24
+ return (
25
+ <ComponentsSecondaryNav
26
+ activeSubTab={componentSubTab || 'design-system'}
27
+ onSubTabChange={onComponentSubTabChange || (() => {})}
28
+ tabs={componentTabs || []}
29
+ onAddFolder={onAddComponentFolder || (() => {})}
30
+ />
31
+ );
32
+ }
33
+
34
+ return null;
35
+ }
36
+
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { useDevToolsStore } from '../store';
4
+
5
+ interface TokenDropdownProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ label?: string;
9
+ }
10
+
11
+ export function TokenDropdown({ value, onChange, label }: TokenDropdownProps) {
12
+ const baseColors = useDevToolsStore((state) => state.baseColors);
13
+
14
+ return (
15
+ <div className="flex items-center gap-2">
16
+ {label && (
17
+ <label className="font-joystix text-xs uppercase text-black/60 min-w-[60px]">{label}</label>
18
+ )}
19
+ <select
20
+ value={value}
21
+ onChange={(e) => onChange(e.target.value)}
22
+ className="flex-1 px-2 py-1.5 font-mondwest text-base bg-warm-cloud border border-black rounded-sm text-black focus:outline-none focus:ring-2 focus:ring-tertiary cursor-pointer"
23
+ >
24
+ <option value="">Select a color...</option>
25
+ <optgroup label="Brand Colors">
26
+ {baseColors
27
+ .filter((c) => c.category === 'brand')
28
+ .map((color) => (
29
+ <option key={color.id} value={`--brand-${color.name}`}>
30
+ {color.name} ({color.value})
31
+ </option>
32
+ ))}
33
+ </optgroup>
34
+ <optgroup label="Neutral Colors">
35
+ {baseColors
36
+ .filter((c) => c.category === 'neutral')
37
+ .map((color) => (
38
+ <option key={color.id} value={`--neutral-${color.name}`}>
39
+ {color.name} ({color.value})
40
+ </option>
41
+ ))}
42
+ </optgroup>
43
+ </select>
44
+ </div>
45
+ );
46
+ }
47
+