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,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useDevToolsStore } from '../../store';
|
|
5
|
+
import { ComponentList } from './ComponentList';
|
|
6
|
+
import { Button } from '@/components/ui/Button';
|
|
7
|
+
import type { DiscoveredComponent } from '../../types';
|
|
8
|
+
|
|
9
|
+
interface DynamicFolderTabProps {
|
|
10
|
+
folderName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DynamicFolderTab({ folderName }: DynamicFolderTabProps) {
|
|
14
|
+
const { components, isLoading, scanComponents } = useDevToolsStore();
|
|
15
|
+
const [folderComponents, setFolderComponents] = useState<DiscoveredComponent[]>([]);
|
|
16
|
+
|
|
17
|
+
// Scan components from this specific folder
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const scanFolder = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`/api/devtools/components?folder=${encodeURIComponent(folderName)}`);
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
setFolderComponents(data.components || []);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
setFolderComponents([]);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
scanFolder();
|
|
30
|
+
}, [folderName]);
|
|
31
|
+
|
|
32
|
+
// Try to import preview file if it exists
|
|
33
|
+
const [PreviewContent, setPreviewContent] = useState<React.ComponentType | null>(null);
|
|
34
|
+
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const loadPreview = async () => {
|
|
38
|
+
try {
|
|
39
|
+
// Dynamic import of preview file
|
|
40
|
+
const previewModule = await import(`./previews/${folderName}.tsx`);
|
|
41
|
+
setPreviewContent(() => previewModule.default || previewModule.Preview);
|
|
42
|
+
setPreviewError(null);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Preview file doesn't exist yet - that's fine
|
|
45
|
+
setPreviewContent(null);
|
|
46
|
+
setPreviewError(null);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
loadPreview();
|
|
51
|
+
}, [folderName]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col h-full overflow-auto pt-4 pb-4 pl-4 pr-2 bg-[var(--color-white)] border border-black rounded space-y-4">
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<div className="flex items-center justify-between">
|
|
57
|
+
<h2 className="font-joystix text-sm uppercase text-black">{folderName}</h2>
|
|
58
|
+
<Button
|
|
59
|
+
variant="outline"
|
|
60
|
+
size="sm"
|
|
61
|
+
iconName="refresh"
|
|
62
|
+
onClick={async () => {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(`/api/devtools/components?folder=${encodeURIComponent(folderName)}`);
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
setFolderComponents(data.components || []);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Failed to refresh
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
disabled={isLoading}
|
|
72
|
+
>
|
|
73
|
+
{isLoading ? 'Scanning...' : 'Refresh'}
|
|
74
|
+
</Button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Stats */}
|
|
78
|
+
<div className="flex gap-4 font-mondwest text-base text-black/60">
|
|
79
|
+
<span>{folderComponents.length} components found</span>
|
|
80
|
+
<span>in /components/{folderName}/</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Loading State */}
|
|
84
|
+
{isLoading && folderComponents.length === 0 && (
|
|
85
|
+
<div className="text-center py-8 text-black/60 font-mondwest text-base">
|
|
86
|
+
Scanning components...
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Preview Content (if exists) */}
|
|
91
|
+
{PreviewContent && (
|
|
92
|
+
<div className="mb-6">
|
|
93
|
+
<PreviewContent />
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Component List */}
|
|
98
|
+
{!isLoading && folderComponents.length === 0 && (
|
|
99
|
+
<div className="text-center py-8">
|
|
100
|
+
<p className="text-black/60 font-mondwest text-base mb-2">No components found</p>
|
|
101
|
+
<p className="font-mondwest text-sm text-black/60">
|
|
102
|
+
Create components with default exports in /components/{folderName}/
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{folderComponents.length > 0 && (
|
|
108
|
+
<ComponentList components={folderComponents} folderName={folderName} />
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@/components/icons';
|
|
4
|
+
import type { DiscoveredComponent } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface PropDisplayProps {
|
|
7
|
+
component: DiscoveredComponent;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PropDisplay({ component }: PropDisplayProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-3">
|
|
13
|
+
<div className="flex items-center justify-between">
|
|
14
|
+
<h3 className="font-joystix text-sm uppercase text-black">{component.name}</h3>
|
|
15
|
+
<span className="font-mondwest text-base text-black/60 font-mono">{component.path}</span>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{component.props.length > 0 ? (
|
|
19
|
+
<div className="border border-black rounded-md overflow-hidden">
|
|
20
|
+
<table className="w-full font-mondwest text-base">
|
|
21
|
+
<thead className="bg-black/10">
|
|
22
|
+
<tr>
|
|
23
|
+
<th className="text-left px-3 py-2 text-black/60 font-joystix text-xs uppercase">Prop</th>
|
|
24
|
+
<th className="text-left px-3 py-2 text-black/60 font-joystix text-xs uppercase">Type</th>
|
|
25
|
+
<th className="text-left px-3 py-2 text-black/60 font-joystix text-xs uppercase">Required</th>
|
|
26
|
+
<th className="text-left px-3 py-2 text-black/60 font-joystix text-xs uppercase">Default</th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
{component.props.map((prop) => (
|
|
31
|
+
<tr key={prop.name} className="border-t border-black" style={{ borderTopColor: 'var(--border-black-20)' }}>
|
|
32
|
+
<td className="px-3 py-2 font-mono text-black">{prop.name}</td>
|
|
33
|
+
<td className="px-3 py-2 font-mono text-black/60">{prop.type}</td>
|
|
34
|
+
<td className="px-3 py-2">
|
|
35
|
+
{prop.required ? (
|
|
36
|
+
<Icon name="checkmark-filled" size={14} className="text-error-red" />
|
|
37
|
+
) : (
|
|
38
|
+
<span className="text-black/60">—</span>
|
|
39
|
+
)}
|
|
40
|
+
</td>
|
|
41
|
+
<td className="px-3 py-2 font-mono text-black/60">
|
|
42
|
+
{prop.defaultValue || '—'}
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
))}
|
|
46
|
+
</tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
50
|
+
<p className="font-mondwest text-base text-black/60 italic">No props defined for this component</p>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { Tabs, TabList, TabTrigger, TabContent, useToast } from '@/components/ui';
|
|
5
|
+
import { DesignSystemTab } from './DesignSystemTab';
|
|
6
|
+
import { DynamicFolderTab } from './DynamicFolderTab';
|
|
7
|
+
import { AddTabButton } from './AddTabButton';
|
|
8
|
+
import { COMPONENT_TABS, type ComponentTabConfig } from './tabConfig';
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'devtools-dynamic-component-tabs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load dynamic tabs from localStorage
|
|
14
|
+
*/
|
|
15
|
+
function loadDynamicTabs(): ComponentTabConfig[] {
|
|
16
|
+
if (typeof window === 'undefined') return [];
|
|
17
|
+
try {
|
|
18
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
19
|
+
return stored ? JSON.parse(stored) : [];
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Save dynamic tabs to localStorage
|
|
27
|
+
*/
|
|
28
|
+
function saveDynamicTabs(tabs: ComponentTabConfig[]) {
|
|
29
|
+
if (typeof window === 'undefined') return;
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(tabs));
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignore storage errors
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ComponentsTabProps {
|
|
38
|
+
activeSubTab?: string;
|
|
39
|
+
searchQuery?: string;
|
|
40
|
+
onTabsChange?: (tabs: Array<{ id: string; label: string }>) => void;
|
|
41
|
+
onAddFolder?: (folderName: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ComponentsTab({
|
|
45
|
+
activeSubTab = 'design-system',
|
|
46
|
+
searchQuery = '',
|
|
47
|
+
onTabsChange,
|
|
48
|
+
onAddFolder,
|
|
49
|
+
}: ComponentsTabProps) {
|
|
50
|
+
const [dynamicTabs, setDynamicTabs] = useState<ComponentTabConfig[]>([]);
|
|
51
|
+
const { addToast } = useToast();
|
|
52
|
+
|
|
53
|
+
// Load dynamic tabs on mount
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setDynamicTabs(loadDynamicTabs());
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
// Memoize allTabs to prevent unnecessary re-renders
|
|
59
|
+
const allTabs = useMemo(() => [...COMPONENT_TABS, ...dynamicTabs], [dynamicTabs]);
|
|
60
|
+
|
|
61
|
+
// Memoize the mapped tabs array to prevent unnecessary effect runs
|
|
62
|
+
const tabsForParent = useMemo(
|
|
63
|
+
() => allTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
64
|
+
[allTabs]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Notify parent of tabs change
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (onTabsChange) {
|
|
70
|
+
onTabsChange(tabsForParent);
|
|
71
|
+
}
|
|
72
|
+
}, [tabsForParent, onTabsChange]);
|
|
73
|
+
|
|
74
|
+
const handleAddFolder = async (folderName: string) => {
|
|
75
|
+
// Validate folder name
|
|
76
|
+
if (!folderName || !/^[a-zA-Z0-9_-]+$/.test(folderName)) {
|
|
77
|
+
addToast({
|
|
78
|
+
title: 'Invalid folder name',
|
|
79
|
+
description: 'Folder name must contain only letters, numbers, underscores, or hyphens',
|
|
80
|
+
variant: 'warning',
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if folder already exists
|
|
86
|
+
if (dynamicTabs.some((tab) => tab.id === `folder-${folderName}`)) {
|
|
87
|
+
addToast({
|
|
88
|
+
title: 'Folder exists',
|
|
89
|
+
description: 'A tab for this folder already exists',
|
|
90
|
+
variant: 'warning',
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Create folder via API
|
|
97
|
+
const response = await fetch('/api/devtools/components/create-folder', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ folderName }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const error = await response.json();
|
|
105
|
+
throw new Error(error.error || 'Failed to create folder');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add new tab
|
|
109
|
+
const newTab: ComponentTabConfig = {
|
|
110
|
+
id: `folder-${folderName}`,
|
|
111
|
+
label: folderName,
|
|
112
|
+
description: `Components from /components/${folderName}/`,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const updatedTabs = [...dynamicTabs, newTab];
|
|
116
|
+
setDynamicTabs(updatedTabs);
|
|
117
|
+
saveDynamicTabs(updatedTabs);
|
|
118
|
+
|
|
119
|
+
// Tabs will be updated via useEffect that calls onTabsChange
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to create folder:', error);
|
|
122
|
+
addToast({
|
|
123
|
+
title: 'Failed to create folder',
|
|
124
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
125
|
+
variant: 'error',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Expose handleAddFolder for footer access
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
(window as any).__componentsTabAddFolder = handleAddFolder;
|
|
133
|
+
return () => {
|
|
134
|
+
delete (window as any).__componentsTabAddFolder;
|
|
135
|
+
};
|
|
136
|
+
}, [dynamicTabs]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="flex flex-col h-full">
|
|
140
|
+
{/* Tab Content - no internal navigation */}
|
|
141
|
+
<div className="flex-1 overflow-hidden">
|
|
142
|
+
{allTabs.map((tab: ComponentTabConfig) => {
|
|
143
|
+
// Design system tab
|
|
144
|
+
if (tab.id === 'design-system' && activeSubTab === 'design-system') {
|
|
145
|
+
return (
|
|
146
|
+
<div key={tab.id} className="h-full pr-2 pl-2 pb-2 rounded">
|
|
147
|
+
<DesignSystemTab searchQuery={searchQuery} />
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Dynamic folder tabs
|
|
153
|
+
if (tab.id.startsWith('folder-') && activeSubTab === tab.id) {
|
|
154
|
+
const folderName = tab.id.replace('folder-', '');
|
|
155
|
+
return (
|
|
156
|
+
<div key={tab.id} className="h-full pr-2 pl-2 pb-2 rounded">
|
|
157
|
+
<DynamicFolderTab folderName={folderName} />
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { WindowTitleBar } from '@/components/Rad_os/WindowTitleBar';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Section Component
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="mb-6">
|
|
13
|
+
<h3
|
|
14
|
+
className="mb-3 border-b border-black/20 pb-2"
|
|
15
|
+
>
|
|
16
|
+
{title}
|
|
17
|
+
</h3>
|
|
18
|
+
<div className="space-y-3">
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function PropsDisplay({ props }: { props: string }) {
|
|
26
|
+
return (
|
|
27
|
+
<code className="bg-black/5 px-2 py-1 rounded-sm block mt-2">
|
|
28
|
+
{props}
|
|
29
|
+
</code>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Row({ children, props }: {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
props?: string;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
40
|
+
{children}
|
|
41
|
+
</div>
|
|
42
|
+
{props && <PropsDisplay props={props} />}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Preview Content
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
function WindowTitleBarContent() {
|
|
53
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-6">
|
|
57
|
+
<div className="p-4 border border-black bg-[var(--color-cream)] rounded flex flex-col gap-4 mb-4">
|
|
58
|
+
<h3>
|
|
59
|
+
Basic Usage
|
|
60
|
+
</h3>
|
|
61
|
+
<div className="flex flex-col gap-4">
|
|
62
|
+
<WindowTitleBar
|
|
63
|
+
title="My Application"
|
|
64
|
+
windowId="my-app"
|
|
65
|
+
onClose={() => setIsOpen(false)}
|
|
66
|
+
data-edit-scope="component-definition"
|
|
67
|
+
data-component="WindowTitleBar"
|
|
68
|
+
/>
|
|
69
|
+
<PropsDisplay props='title: string, windowId: string, onClose: () => void' />
|
|
70
|
+
<WindowTitleBar
|
|
71
|
+
title="With Icon"
|
|
72
|
+
windowId="my-app-icon"
|
|
73
|
+
onClose={() => setIsOpen(false)}
|
|
74
|
+
iconName="home"
|
|
75
|
+
/>
|
|
76
|
+
<PropsDisplay props='iconName?: string' />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="p-4 border border-black bg-[var(--color-cream)] rounded flex flex-col gap-4 mb-4">
|
|
81
|
+
<h3>
|
|
82
|
+
Visibility Controls
|
|
83
|
+
</h3>
|
|
84
|
+
<div className="flex flex-col gap-4">
|
|
85
|
+
<WindowTitleBar
|
|
86
|
+
title="Hidden Title"
|
|
87
|
+
windowId="no-title"
|
|
88
|
+
onClose={() => {}}
|
|
89
|
+
showTitle={false}
|
|
90
|
+
/>
|
|
91
|
+
<PropsDisplay props='showTitle={false}' />
|
|
92
|
+
<WindowTitleBar
|
|
93
|
+
title="No Copy Button"
|
|
94
|
+
windowId="no-copy"
|
|
95
|
+
onClose={() => {}}
|
|
96
|
+
showCopyButton={false}
|
|
97
|
+
/>
|
|
98
|
+
<PropsDisplay props='showCopyButton={false}' />
|
|
99
|
+
<WindowTitleBar
|
|
100
|
+
title="No Close Button"
|
|
101
|
+
windowId="no-close"
|
|
102
|
+
onClose={() => {}}
|
|
103
|
+
showCloseButton={false}
|
|
104
|
+
/>
|
|
105
|
+
<PropsDisplay props='showCloseButton={false}' />
|
|
106
|
+
<WindowTitleBar
|
|
107
|
+
title="Minimal"
|
|
108
|
+
windowId="minimal"
|
|
109
|
+
onClose={() => {}}
|
|
110
|
+
showTitle={false}
|
|
111
|
+
showCopyButton={false}
|
|
112
|
+
showCloseButton={false}
|
|
113
|
+
/>
|
|
114
|
+
<PropsDisplay props='showTitle={false}, showCopyButton={false}, showCloseButton={false}' />
|
|
115
|
+
<WindowTitleBar
|
|
116
|
+
title="With Fullscreen"
|
|
117
|
+
windowId="with-fullscreen"
|
|
118
|
+
onClose={() => {}}
|
|
119
|
+
showFullscreenButton={true}
|
|
120
|
+
onFullscreen={() => alert('Fullscreen clicked!')}
|
|
121
|
+
/>
|
|
122
|
+
<PropsDisplay props='showFullscreenButton={true}, onFullscreen: () => void' />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="p-4 border border-black bg-[var(--color-cream)] rounded flex flex-col gap-4 mb-4">
|
|
127
|
+
<h3>
|
|
128
|
+
Help Button
|
|
129
|
+
</h3>
|
|
130
|
+
<div className="flex flex-col gap-4">
|
|
131
|
+
<WindowTitleBar
|
|
132
|
+
title="With Help"
|
|
133
|
+
windowId="with-help"
|
|
134
|
+
onClose={() => {}}
|
|
135
|
+
showHelpButton={true}
|
|
136
|
+
helpContent={
|
|
137
|
+
<div>
|
|
138
|
+
<p className="mb-2">This is the help content.</p>
|
|
139
|
+
<p>
|
|
140
|
+
You can add any React content here to help users understand how to use this window.
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
}
|
|
144
|
+
helpTitle="Window Help"
|
|
145
|
+
/>
|
|
146
|
+
<PropsDisplay props='showHelpButton={true}, helpContent: React.ReactNode, helpTitle?: string' />
|
|
147
|
+
<WindowTitleBar
|
|
148
|
+
title="Default Help"
|
|
149
|
+
windowId="default-help"
|
|
150
|
+
onClose={() => {}}
|
|
151
|
+
showHelpButton={true}
|
|
152
|
+
/>
|
|
153
|
+
<PropsDisplay props='showHelpButton={true} (default help content)' />
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="p-4 border border-black bg-[var(--color-cream)] rounded flex flex-col gap-4 mb-4">
|
|
158
|
+
<h3>
|
|
159
|
+
Action Button
|
|
160
|
+
</h3>
|
|
161
|
+
<div className="flex flex-col gap-4">
|
|
162
|
+
<WindowTitleBar
|
|
163
|
+
title="With Action Button"
|
|
164
|
+
windowId="with-action"
|
|
165
|
+
onClose={() => {}}
|
|
166
|
+
showActionButton={true}
|
|
167
|
+
actionButton={{
|
|
168
|
+
text: "Connect Wallet",
|
|
169
|
+
onClick: () => alert('Wallet connect clicked!'),
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
<PropsDisplay props='showActionButton={true}, actionButton: ActionButtonConfig' />
|
|
173
|
+
<WindowTitleBar
|
|
174
|
+
title="External Link"
|
|
175
|
+
windowId="external-link"
|
|
176
|
+
onClose={() => {}}
|
|
177
|
+
showActionButton={true}
|
|
178
|
+
actionButton={{
|
|
179
|
+
text: "Visit Site",
|
|
180
|
+
href: "https://example.com",
|
|
181
|
+
target: "_blank",
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
<PropsDisplay props='actionButton with href' />
|
|
185
|
+
<WindowTitleBar
|
|
186
|
+
title="With Icon"
|
|
187
|
+
windowId="with-icon"
|
|
188
|
+
onClose={() => {}}
|
|
189
|
+
showActionButton={true}
|
|
190
|
+
actionButton={{
|
|
191
|
+
text: "Download",
|
|
192
|
+
iconName: "download",
|
|
193
|
+
onClick: () => alert('Download clicked!'),
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
<PropsDisplay props='actionButton with icon' />
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div className="p-4 border border-black bg-[var(--color-cream)] rounded flex flex-col gap-4 mb-4">
|
|
201
|
+
<h3>
|
|
202
|
+
All Features Combined
|
|
203
|
+
</h3>
|
|
204
|
+
<div className="flex flex-col gap-4">
|
|
205
|
+
<WindowTitleBar
|
|
206
|
+
title="Full Featured"
|
|
207
|
+
windowId="full-featured"
|
|
208
|
+
onClose={() => {}}
|
|
209
|
+
showHelpButton={true}
|
|
210
|
+
showActionButton={true}
|
|
211
|
+
helpContent={
|
|
212
|
+
<div>
|
|
213
|
+
<p className="mb-2">Complete Help Guide</p>
|
|
214
|
+
<ul className="list-disc list-inside space-y-1">
|
|
215
|
+
<li>This window has all features enabled</li>
|
|
216
|
+
<li>Help button opens contextual help</li>
|
|
217
|
+
<li>Action button performs custom actions</li>
|
|
218
|
+
<li>Copy button shares the window link</li>
|
|
219
|
+
</ul>
|
|
220
|
+
</div>
|
|
221
|
+
}
|
|
222
|
+
helpTitle="Complete Guide"
|
|
223
|
+
actionButton={{
|
|
224
|
+
text: "Get Started",
|
|
225
|
+
iconName: "lightning",
|
|
226
|
+
onClick: () => alert('Get started clicked!'),
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
<PropsDisplay props='All optional props enabled' />
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="p-4 border border-black bg-[var(--color-cream)] rounded flex flex-col gap-4 mb-4">
|
|
234
|
+
<h3>
|
|
235
|
+
Custom Styling
|
|
236
|
+
</h3>
|
|
237
|
+
<div className="flex flex-col gap-4">
|
|
238
|
+
<WindowTitleBar
|
|
239
|
+
title="Custom Styled"
|
|
240
|
+
windowId="custom-styled"
|
|
241
|
+
onClose={() => {}}
|
|
242
|
+
className="bg-sun-yellow/20"
|
|
243
|
+
/>
|
|
244
|
+
<PropsDisplay props='className: string' />
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Main Export
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
export default function Preview() {
|
|
256
|
+
return (
|
|
257
|
+
<div className="space-y-6">
|
|
258
|
+
<WindowTitleBarContent />
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab Configuration for Components Tab
|
|
3
|
+
*
|
|
4
|
+
* This config allows adding new component tabs dynamically.
|
|
5
|
+
* Each tab can have its own content component.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ComponentTabConfig {
|
|
9
|
+
/** Unique identifier for the tab */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Display label for the tab */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Optional description */
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default component tabs configuration
|
|
19
|
+
*
|
|
20
|
+
* To add a new tab:
|
|
21
|
+
* 1. Add an entry here with a unique id and label
|
|
22
|
+
* 2. Create a corresponding component in the ComponentsTab folder
|
|
23
|
+
* 3. Update the ComponentsTab index.tsx to render the new component
|
|
24
|
+
*/
|
|
25
|
+
export const COMPONENT_TABS: ComponentTabConfig[] = [
|
|
26
|
+
{
|
|
27
|
+
id: 'folder-Rad_os',
|
|
28
|
+
label: 'RadOS',
|
|
29
|
+
description: 'RadOS window components (AppWindow, WindowTitleBar, etc.)',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'design-system',
|
|
33
|
+
label: 'Design System',
|
|
34
|
+
description: 'Core UI components from the design system',
|
|
35
|
+
},
|
|
36
|
+
// Dynamic folder tabs are added via localStorage and the Add button
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a tab config by ID
|
|
41
|
+
*/
|
|
42
|
+
export function getTabById(id: string): ComponentTabConfig | undefined {
|
|
43
|
+
return COMPONENT_TABS.find((tab) => tab.id === id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a tab ID is valid
|
|
48
|
+
*/
|
|
49
|
+
export function isValidTabId(id: string): boolean {
|
|
50
|
+
return COMPONENT_TABS.some((tab) => tab.id === id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default COMPONENT_TABS;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useDevToolsStore } from '../../store';
|
|
4
|
+
import { Button } from '@/components/ui/Button';
|
|
5
|
+
|
|
6
|
+
export function MockStatesTab() {
|
|
7
|
+
const { mockStates, toggleMockState } = useDevToolsStore();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex flex-col h-full overflow-auto pt-4 pb-4 pl-4 pr-2 bg-[var(--color-white)] border border-black rounded space-y-4">
|
|
11
|
+
<h2 className="font-joystix text-sm uppercase text-black">Mock States</h2>
|
|
12
|
+
<p className="font-mondwest text-base text-black/60">
|
|
13
|
+
Use <code className="px-1 bg-black/10 rounded-xs font-mono">useMockState('wallet')</code> in your components.
|
|
14
|
+
</p>
|
|
15
|
+
<div className="grid grid-cols-1 gap-2">
|
|
16
|
+
{mockStates.map((state) => (
|
|
17
|
+
<Button
|
|
18
|
+
key={state.id}
|
|
19
|
+
variant={state.active ? 'primary' : 'outline'}
|
|
20
|
+
fullWidth
|
|
21
|
+
onClick={() => toggleMockState(state.id)}
|
|
22
|
+
>
|
|
23
|
+
{state.name}
|
|
24
|
+
</Button>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|