radtools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/bin/radtools.js +5 -0
- package/dist/cli/index.js +427 -0
- package/package.json +55 -0
- package/templates/api-routes/assets/optimize/route.ts +94 -0
- package/templates/api-routes/assets/route.ts +159 -0
- package/templates/api-routes/components/create-folder/route.ts +55 -0
- package/templates/api-routes/components/route.ts +156 -0
- package/templates/api-routes/fonts/route.ts +96 -0
- package/templates/api-routes/fonts/upload/route.ts +79 -0
- package/templates/api-routes/read-css/route.ts +29 -0
- package/templates/api-routes/write-css/route.ts +423 -0
- package/templates/components/Rad_os/AppWindow.tsx +423 -0
- package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
- package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
- package/templates/components/icons/Icon.tsx +224 -0
- package/templates/components/icons/README.md +85 -0
- package/templates/components/icons/index.ts +20 -0
- package/templates/components/icons.tsx +164 -0
- package/templates/components/ui/Accordion.tsx +268 -0
- package/templates/components/ui/Alert.tsx +111 -0
- package/templates/components/ui/Badge.tsx +87 -0
- package/templates/components/ui/Breadcrumbs.tsx +88 -0
- package/templates/components/ui/Button.tsx +249 -0
- package/templates/components/ui/Card.tsx +137 -0
- package/templates/components/ui/Checkbox.tsx +137 -0
- package/templates/components/ui/ContextMenu.tsx +220 -0
- package/templates/components/ui/Dialog.tsx +264 -0
- package/templates/components/ui/Divider.tsx +70 -0
- package/templates/components/ui/DropdownMenu.tsx +301 -0
- package/templates/components/ui/HelpPanel.tsx +119 -0
- package/templates/components/ui/Input.tsx +176 -0
- package/templates/components/ui/Popover.tsx +211 -0
- package/templates/components/ui/Progress.tsx +158 -0
- package/templates/components/ui/Select.tsx +134 -0
- package/templates/components/ui/Sheet.tsx +316 -0
- package/templates/components/ui/Slider.tsx +223 -0
- package/templates/components/ui/Switch.tsx +155 -0
- package/templates/components/ui/Tabs.tsx +253 -0
- package/templates/components/ui/Toast.tsx +192 -0
- package/templates/components/ui/Tooltip.tsx +129 -0
- package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
- package/templates/components/ui/index.ts +84 -0
- package/templates/devtools/DevToolsPanel.tsx +261 -0
- package/templates/devtools/DevToolsProvider.tsx +43 -0
- package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
- package/templates/devtools/components/ColorPicker.tsx +33 -0
- package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
- package/templates/devtools/components/ContextualFooter.tsx +56 -0
- package/templates/devtools/components/DraggablePanel.tsx +43 -0
- package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
- package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
- package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
- package/templates/devtools/components/TokenDropdown.tsx +47 -0
- package/templates/devtools/components/TypographyFooter.tsx +145 -0
- package/templates/devtools/hooks/useMockState.ts +16 -0
- package/templates/devtools/index.ts +17 -0
- package/templates/devtools/lib/componentScanner.ts +78 -0
- package/templates/devtools/lib/cssParser.ts +465 -0
- package/templates/devtools/lib/searchIndexes.ts +45 -0
- package/templates/devtools/lib/selectorGenerator.ts +86 -0
- package/templates/devtools/store/index.ts +66 -0
- package/templates/devtools/store/slices/assetsSlice.ts +106 -0
- package/templates/devtools/store/slices/componentsSlice.ts +59 -0
- package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
- package/templates/devtools/store/slices/panelSlice.ts +17 -0
- package/templates/devtools/store/slices/typographySlice.ts +538 -0
- package/templates/devtools/store/slices/variablesSlice.ts +167 -0
- package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
- package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
- package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
- package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
- package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
- package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
- package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
- package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
- package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
- package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
- package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
- package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
- package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
- package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
- package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
- package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
- package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
- package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
- package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
- package/templates/devtools/types/index.ts +99 -0
- package/templates/globals.css +574 -0
- package/templates/hooks/index.ts +1 -0
- package/templates/hooks/useWindowManager.ts +212 -0
- package/templates/public/assets/icons/avatar.svg +18 -0
- package/templates/public/assets/icons/checkmark-filled.svg +14 -0
- package/templates/public/assets/icons/checkmark.svg +14 -0
- package/templates/public/assets/icons/chevron-down.svg +14 -0
- package/templates/public/assets/icons/close.svg +14 -0
- package/templates/public/assets/icons/copy.svg +14 -0
- package/templates/public/assets/icons/download.svg +14 -0
- package/templates/public/assets/icons/expand.svg +31 -0
- package/templates/public/assets/icons/file-blank.svg +17 -0
- package/templates/public/assets/icons/file-image.svg +19 -0
- package/templates/public/assets/icons/file-written.svg +17 -0
- package/templates/public/assets/icons/folder-closed.svg +17 -0
- package/templates/public/assets/icons/folder-open.svg +17 -0
- package/templates/public/assets/icons/hamburger.svg +18 -0
- package/templates/public/assets/icons/home-outline.svg +28 -0
- package/templates/public/assets/icons/home.svg +30 -0
- package/templates/public/assets/icons/hourglass.svg +25 -0
- package/templates/public/assets/icons/information-circle.svg +14 -0
- package/templates/public/assets/icons/information.svg +17 -0
- package/templates/public/assets/icons/lightning.svg +14 -0
- package/templates/public/assets/icons/locked.svg +17 -0
- package/templates/public/assets/icons/not-allowed.svg +14 -0
- package/templates/public/assets/icons/plus.svg +5 -0
- package/templates/public/assets/icons/power-thin.svg +17 -0
- package/templates/public/assets/icons/power.svg +17 -0
- package/templates/public/assets/icons/question-block.svg +14 -0
- package/templates/public/assets/icons/question.svg +17 -0
- package/templates/public/assets/icons/refresh-block.svg +14 -0
- package/templates/public/assets/icons/refresh.svg +17 -0
- package/templates/public/assets/icons/save.svg +14 -0
- package/templates/public/assets/icons/search.svg +25 -0
- package/templates/public/assets/icons/settings.svg +14 -0
- package/templates/public/assets/icons/trash-full.svg +21 -0
- package/templates/public/assets/icons/trash-open.svg +23 -0
- package/templates/public/assets/icons/trash.svg +18 -0
- package/templates/public/assets/icons/unlocked.svg +17 -0
- package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
- package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
- package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
- package/templates/public/assets/icons/wrench.svg +17 -0
|
@@ -0,0 +1,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
|
+
|