create-visualbuild-app 0.1.0 → 1.0.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 +306 -123
- package/docs/user-guide.md +759 -0
- package/package.json +92 -85
- package/scripts/create-builder-app.mjs +513 -501
- package/scripts/vb-dev.mjs +41 -32
- package/scripts/vb-generate.mjs +332 -224
- package/templates/default/app/.vercelignore +6 -0
- package/templates/default/app/README.md +56 -21
- package/templates/default/app/visualbuild/components.json +3 -0
- package/templates/default/app/visualbuild/config.d.ts +48 -0
- package/templates/default/app/visualbuild.config.ts +38 -0
- package/templates/default/builder/README.md +37 -21
- package/visual-app-builder/server/parseReactPage.ts +776 -571
- package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
- package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
- package/visual-app-builder/shared/generateReactSource.mjs +608 -443
- package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
- package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
- package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
- package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
- package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
- package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
- package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
- package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
- package/visual-app-builder/src/App.tsx +1090 -874
- package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
- package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
- package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
- package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
- package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
- package/visual-app-builder/src/components/Topbar.tsx +257 -128
- package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
- package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
- package/visual-app-builder/src/index.css +383 -111
- package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
- package/visual-app-builder/src/theme.ts +71 -0
- package/visual-app-builder/src/types/index.ts +350 -261
- package/visual-app-builder/src/utils/codegen.ts +90 -66
- package/visual-app-builder/src/utils/deployment.ts +54 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
- package/visual-app-builder/vite.config.ts +1946 -1479
|
@@ -1,302 +1,365 @@
|
|
|
1
|
-
import { useDraggable } from '@dnd-kit/core'
|
|
2
|
-
import { CSS } from '@dnd-kit/utilities'
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { components } from '../data/componentRegistry'
|
|
5
|
-
import { primitives } from '../data/primitiveRegistry'
|
|
6
|
-
import { useAppStore } from '../stores/useAppStore'
|
|
7
|
-
import type { ComponentDefinition, PrimitiveDefinition } from '../types'
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
</
|
|
243
|
-
</
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
{
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
1
|
+
import { useDraggable } from '@dnd-kit/core'
|
|
2
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { components } from '../data/componentRegistry'
|
|
5
|
+
import { primitives } from '../data/primitiveRegistry'
|
|
6
|
+
import { useAppStore } from '../stores/useAppStore'
|
|
7
|
+
import type { ComponentDefinition, PrimitiveDefinition } from '../types'
|
|
8
|
+
import type { CustomComponentDefinition } from '../types'
|
|
9
|
+
import ProjectExplorer from './ProjectExplorer'
|
|
10
|
+
|
|
11
|
+
export default function ComponentSidebar({
|
|
12
|
+
view,
|
|
13
|
+
selectedProjectFile,
|
|
14
|
+
onSelectProjectFile,
|
|
15
|
+
}: {
|
|
16
|
+
view: 'files' | 'elements'
|
|
17
|
+
selectedProjectFile: string | null
|
|
18
|
+
onSelectProjectFile: (path: string | null) => void
|
|
19
|
+
}) {
|
|
20
|
+
const {
|
|
21
|
+
activePageId,
|
|
22
|
+
selectedComponentId,
|
|
23
|
+
addComponent,
|
|
24
|
+
addPrimitive,
|
|
25
|
+
addPrimitiveToNode,
|
|
26
|
+
setAppShellComponent,
|
|
27
|
+
customComponents,
|
|
28
|
+
addCustomComponent,
|
|
29
|
+
} = useAppStore()
|
|
30
|
+
const globalComponents = components.filter((component) =>
|
|
31
|
+
['Navbar', 'Footer'].includes(component.type)
|
|
32
|
+
)
|
|
33
|
+
const pageComponents = components.filter(
|
|
34
|
+
(component) => !['Navbar', 'Footer'].includes(component.type)
|
|
35
|
+
)
|
|
36
|
+
const primitiveGroups = groupPrimitivesByCategory(primitives)
|
|
37
|
+
const [openSections, setOpenSections] = useState<Set<string>>(
|
|
38
|
+
() => new Set(['Global', 'Page Blocks', 'Custom', 'Semantic'])
|
|
39
|
+
)
|
|
40
|
+
const toggleSection = (section: string) => {
|
|
41
|
+
setOpenSections((current) => {
|
|
42
|
+
const next = new Set(current)
|
|
43
|
+
|
|
44
|
+
if (next.has(section)) {
|
|
45
|
+
next.delete(section)
|
|
46
|
+
} else {
|
|
47
|
+
next.add(section)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return next
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<aside className="flex h-full w-full flex-col overflow-hidden bg-sidebar">
|
|
55
|
+
<div className="px-3 py-2.5 border-b border-white/[0.06]">
|
|
56
|
+
<p className="text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
|
57
|
+
{view === 'files' ? 'Project files' : 'Elements'}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
className={`min-h-0 flex-1 ${
|
|
63
|
+
view === 'files' ? 'overflow-hidden' : 'overflow-y-auto p-2 space-y-1'
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
{view === 'files' ? (
|
|
67
|
+
<ProjectExplorer
|
|
68
|
+
selectedPath={selectedProjectFile}
|
|
69
|
+
onSelectFile={onSelectProjectFile}
|
|
70
|
+
/>
|
|
71
|
+
) : (
|
|
72
|
+
<>
|
|
73
|
+
<AccordionSection
|
|
74
|
+
title="Global"
|
|
75
|
+
count={globalComponents.length}
|
|
76
|
+
isOpen={openSections.has('Global')}
|
|
77
|
+
onToggle={() => toggleSection('Global')}
|
|
78
|
+
>
|
|
79
|
+
{globalComponents.map((component) => {
|
|
80
|
+
const slot = component.type === 'Footer' ? 'footer' : 'header'
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ComponentSidebarItem
|
|
84
|
+
key={component.type}
|
|
85
|
+
component={component}
|
|
86
|
+
isDisabled={false}
|
|
87
|
+
onAdd={() => setAppShellComponent(slot, component.type)}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
})}
|
|
91
|
+
</AccordionSection>
|
|
92
|
+
|
|
93
|
+
{customComponents.length > 0 && (
|
|
94
|
+
<AccordionSection
|
|
95
|
+
title="Custom"
|
|
96
|
+
count={customComponents.length}
|
|
97
|
+
isOpen={openSections.has('Custom')}
|
|
98
|
+
onToggle={() => toggleSection('Custom')}
|
|
99
|
+
>
|
|
100
|
+
{customComponents.map((component) => (
|
|
101
|
+
<CustomComponentSidebarItem
|
|
102
|
+
key={component.name}
|
|
103
|
+
component={component}
|
|
104
|
+
onAdd={() => addCustomComponent(activePageId, component.name)}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</AccordionSection>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<AccordionSection
|
|
111
|
+
title="Page Blocks"
|
|
112
|
+
count={pageComponents.length}
|
|
113
|
+
isOpen={openSections.has('Page Blocks')}
|
|
114
|
+
onToggle={() => toggleSection('Page Blocks')}
|
|
115
|
+
>
|
|
116
|
+
{pageComponents.map((component) => {
|
|
117
|
+
return (
|
|
118
|
+
<ComponentSidebarItem
|
|
119
|
+
key={component.type}
|
|
120
|
+
component={component}
|
|
121
|
+
isDisabled={false}
|
|
122
|
+
onAdd={() => addComponent(activePageId, component.type)}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
})}
|
|
126
|
+
</AccordionSection>
|
|
127
|
+
|
|
128
|
+
{primitiveGroups.map(([category, items]) => (
|
|
129
|
+
<AccordionSection
|
|
130
|
+
key={category}
|
|
131
|
+
title={category}
|
|
132
|
+
count={items.length}
|
|
133
|
+
isOpen={openSections.has(category)}
|
|
134
|
+
onToggle={() => toggleSection(category)}
|
|
135
|
+
>
|
|
136
|
+
{items.map((primitive) => (
|
|
137
|
+
<PrimitiveSidebarItem
|
|
138
|
+
key={primitive.type}
|
|
139
|
+
primitive={primitive}
|
|
140
|
+
isClickDisabled={selectedComponentId === null}
|
|
141
|
+
onAdd={() => {
|
|
142
|
+
if (selectedComponentId) {
|
|
143
|
+
addPrimitiveToNode(selectedComponentId, primitive.type)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
addPrimitive(activePageId, primitive.type)
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
))}
|
|
151
|
+
</AccordionSection>
|
|
152
|
+
))}
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{view === 'elements' && (
|
|
158
|
+
<div className="px-3 py-2 border-t border-white/[0.06]">
|
|
159
|
+
<p className="text-[10px] text-gray-600">Drag or click to add</p>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</aside>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function CustomComponentSidebarItem({
|
|
167
|
+
component,
|
|
168
|
+
onAdd,
|
|
169
|
+
}: {
|
|
170
|
+
component: CustomComponentDefinition
|
|
171
|
+
onAdd: () => void
|
|
172
|
+
}) {
|
|
173
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
|
174
|
+
useDraggable({
|
|
175
|
+
id: `custom:${component.name}`,
|
|
176
|
+
data: {
|
|
177
|
+
kind: 'custom-component',
|
|
178
|
+
name: component.name,
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<button
|
|
184
|
+
ref={setNodeRef}
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={onAdd}
|
|
187
|
+
style={{ transform: CSS.Translate.toString(transform) }}
|
|
188
|
+
className={`w-full flex cursor-grab select-none items-center gap-2.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.06] active:cursor-grabbing ${
|
|
189
|
+
isDragging ? 'bg-white/[0.08] opacity-70' : ''
|
|
190
|
+
}`}
|
|
191
|
+
{...listeners}
|
|
192
|
+
{...attributes}
|
|
193
|
+
>
|
|
194
|
+
<span className="w-5 text-center text-base leading-none text-gray-500">
|
|
195
|
+
{component.icon}
|
|
196
|
+
</span>
|
|
197
|
+
<div className="min-w-0">
|
|
198
|
+
<p className="text-xs font-medium leading-tight text-gray-300">
|
|
199
|
+
{component.label}
|
|
200
|
+
</p>
|
|
201
|
+
<p className="mt-0.5 truncate text-[10px] leading-tight text-gray-600">
|
|
202
|
+
{component.description}
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
</button>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function AccordionSection({
|
|
210
|
+
title,
|
|
211
|
+
count,
|
|
212
|
+
isOpen,
|
|
213
|
+
onToggle,
|
|
214
|
+
children,
|
|
215
|
+
}: {
|
|
216
|
+
title: string
|
|
217
|
+
count: number
|
|
218
|
+
isOpen: boolean
|
|
219
|
+
onToggle: () => void
|
|
220
|
+
children: React.ReactNode
|
|
221
|
+
}) {
|
|
222
|
+
return (
|
|
223
|
+
<section className="border-b border-white/[0.04] pb-1 last:border-b-0">
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={onToggle}
|
|
227
|
+
className="flex w-full cursor-pointer select-none items-center justify-between rounded px-2 py-1.5 text-left transition-colors hover:bg-white/[0.04]"
|
|
228
|
+
>
|
|
229
|
+
<span className="text-[10px] font-medium uppercase tracking-widest text-gray-500">
|
|
230
|
+
{title}
|
|
231
|
+
</span>
|
|
232
|
+
<span className="flex items-center gap-1 text-[10px] text-gray-600">
|
|
233
|
+
{count}
|
|
234
|
+
<span
|
|
235
|
+
className={`inline-block transition-transform ${
|
|
236
|
+
isOpen ? 'rotate-90' : ''
|
|
237
|
+
}`}
|
|
238
|
+
aria-hidden="true"
|
|
239
|
+
>
|
|
240
|
+
>
|
|
241
|
+
</span>
|
|
242
|
+
</span>
|
|
243
|
+
</button>
|
|
244
|
+
{isOpen && <div className="space-y-0.5 pb-1">{children}</div>}
|
|
245
|
+
</section>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function groupPrimitivesByCategory(items: PrimitiveDefinition[]) {
|
|
250
|
+
const groups = new Map<string, PrimitiveDefinition[]>()
|
|
251
|
+
|
|
252
|
+
for (const item of items) {
|
|
253
|
+
groups.set(item.category, [...(groups.get(item.category) ?? []), item])
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return Array.from(groups.entries())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function ComponentSidebarItem({
|
|
260
|
+
component,
|
|
261
|
+
isDisabled,
|
|
262
|
+
onAdd,
|
|
263
|
+
}: {
|
|
264
|
+
component: ComponentDefinition
|
|
265
|
+
isDisabled: boolean
|
|
266
|
+
onAdd: () => void
|
|
267
|
+
}) {
|
|
268
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
|
269
|
+
useDraggable({
|
|
270
|
+
id: `sidebar:${component.type}`,
|
|
271
|
+
disabled: isDisabled,
|
|
272
|
+
data: {
|
|
273
|
+
kind: 'sidebar-component',
|
|
274
|
+
type: component.type,
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
const style = {
|
|
278
|
+
transform: CSS.Translate.toString(transform),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<button
|
|
283
|
+
ref={setNodeRef}
|
|
284
|
+
type="button"
|
|
285
|
+
disabled={isDisabled}
|
|
286
|
+
onClick={onAdd}
|
|
287
|
+
style={style}
|
|
288
|
+
className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md text-left group transition-colors ${
|
|
289
|
+
isDisabled
|
|
290
|
+
? 'opacity-45 cursor-not-allowed'
|
|
291
|
+
: 'cursor-grab select-none hover:bg-white/[0.06] active:cursor-grabbing'
|
|
292
|
+
} ${isDragging ? 'opacity-70 bg-white/[0.08]' : ''}`}
|
|
293
|
+
{...listeners}
|
|
294
|
+
{...attributes}
|
|
295
|
+
>
|
|
296
|
+
<span className="text-base text-gray-500 group-hover:text-accent transition-colors w-5 text-center leading-none">
|
|
297
|
+
{component.icon}
|
|
298
|
+
</span>
|
|
299
|
+
<div className="min-w-0">
|
|
300
|
+
<p className="text-xs font-medium text-gray-300 group-hover:text-white transition-colors leading-tight">
|
|
301
|
+
{component.label}
|
|
302
|
+
</p>
|
|
303
|
+
<p className="text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors leading-tight mt-0.5 truncate">
|
|
304
|
+
{getComponentDescription(component.type, component.description)}
|
|
305
|
+
</p>
|
|
306
|
+
</div>
|
|
307
|
+
</button>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function PrimitiveSidebarItem({
|
|
312
|
+
primitive,
|
|
313
|
+
isClickDisabled,
|
|
314
|
+
onAdd,
|
|
315
|
+
}: {
|
|
316
|
+
primitive: PrimitiveDefinition
|
|
317
|
+
isClickDisabled: boolean
|
|
318
|
+
onAdd: () => void
|
|
319
|
+
}) {
|
|
320
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
|
321
|
+
useDraggable({
|
|
322
|
+
id: `primitive:${primitive.type}`,
|
|
323
|
+
data: {
|
|
324
|
+
kind: 'primitive',
|
|
325
|
+
type: primitive.type,
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
const style = {
|
|
329
|
+
transform: CSS.Translate.toString(transform),
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<button
|
|
334
|
+
ref={setNodeRef}
|
|
335
|
+
type="button"
|
|
336
|
+
onClick={onAdd}
|
|
337
|
+
style={style}
|
|
338
|
+
title={
|
|
339
|
+
isClickDisabled
|
|
340
|
+
? `Drag ${primitive.label} onto a component or select a component first`
|
|
341
|
+
: `Add ${primitive.label} to selected component`
|
|
342
|
+
}
|
|
343
|
+
className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md text-left group transition-colors cursor-grab select-none hover:bg-white/[0.06] active:cursor-grabbing ${
|
|
344
|
+
isDragging ? 'opacity-70 bg-white/[0.08]' : ''
|
|
345
|
+
} ${isClickDisabled ? 'opacity-80' : ''}`}
|
|
346
|
+
{...listeners}
|
|
347
|
+
{...attributes}
|
|
348
|
+
>
|
|
349
|
+
<div className="min-w-0">
|
|
350
|
+
<p className="text-xs font-medium text-gray-300 group-hover:text-white transition-colors leading-tight">
|
|
351
|
+
{primitive.label}
|
|
352
|
+
</p>
|
|
353
|
+
<p className="text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors leading-tight mt-0.5 truncate">
|
|
354
|
+
{primitive.description}
|
|
355
|
+
</p>
|
|
356
|
+
</div>
|
|
357
|
+
</button>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getComponentDescription(type: string, fallback: string) {
|
|
362
|
+
if (type === 'Navbar') return 'Global header across all pages'
|
|
363
|
+
if (type === 'Footer') return 'Global footer across all pages'
|
|
364
|
+
return fallback
|
|
365
|
+
}
|