create-visualbuild-app 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 +123 -0
- package/package.json +85 -0
- package/scripts/create-builder-app.mjs +501 -0
- package/scripts/vb-dev.mjs +32 -0
- package/scripts/vb-generate.mjs +224 -0
- package/templates/default/app/README.md +21 -0
- package/templates/default/app/eslint.config.js +25 -0
- package/templates/default/app/index.html +15 -0
- package/templates/default/app/src/index.css +23 -0
- package/templates/default/app/src/main.tsx +10 -0
- package/templates/default/app/tsconfig.app.json +21 -0
- package/templates/default/app/tsconfig.json +7 -0
- package/templates/default/app/tsconfig.node.json +19 -0
- package/templates/default/app/visualbuild/generated-files.json +3 -0
- package/templates/default/app/visualbuild/pages.json +12 -0
- package/templates/default/app/vite.config.ts +7 -0
- package/templates/default/builder/README.md +21 -0
- package/templates/default/builder/eslint.config.js +25 -0
- package/templates/default/builder/tsconfig.app.json +21 -0
- package/templates/default/builder/tsconfig.json +7 -0
- package/templates/default/builder/tsconfig.node.json +22 -0
- package/visual-app-builder/index.html +15 -0
- package/visual-app-builder/server/parseReactPage.ts +571 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +37 -0
- package/visual-app-builder/shared/generateReactSource.mjs +443 -0
- package/visual-app-builder/src/App.tsx +874 -0
- package/visual-app-builder/src/components/Canvas.tsx +1059 -0
- package/visual-app-builder/src/components/CodePanel.tsx +812 -0
- package/visual-app-builder/src/components/ComponentSidebar.tsx +302 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +161 -0
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -0
- package/visual-app-builder/src/components/PropertiesPanel.tsx +692 -0
- package/visual-app-builder/src/components/Topbar.tsx +128 -0
- package/visual-app-builder/src/data/componentRegistry.tsx +292 -0
- package/visual-app-builder/src/data/primitiveRegistry.ts +368 -0
- package/visual-app-builder/src/index.css +111 -0
- package/visual-app-builder/src/main.tsx +10 -0
- package/visual-app-builder/src/stores/useAppStore.ts +1265 -0
- package/visual-app-builder/src/tailwindGeneratedSafelist.ts +36 -0
- package/visual-app-builder/src/types/index.ts +261 -0
- package/visual-app-builder/src/utils/codegen.ts +66 -0
- package/visual-app-builder/src/utils/projectFiles.ts +146 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +177 -0
- package/visual-app-builder/vite.config.ts +1479 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import { useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { primitiveRegistry } from '../data/primitiveRegistry'
|
|
3
|
+
import { useAppStore } from '../stores/useAppStore'
|
|
4
|
+
import type { PrimitiveType, PropSchema, VisualNode } from '../types'
|
|
5
|
+
|
|
6
|
+
export default function PropertiesPanel({
|
|
7
|
+
embedded = false,
|
|
8
|
+
}: {
|
|
9
|
+
embedded?: boolean
|
|
10
|
+
}) {
|
|
11
|
+
const {
|
|
12
|
+
pages,
|
|
13
|
+
appShell,
|
|
14
|
+
activePageId,
|
|
15
|
+
selectedComponentId,
|
|
16
|
+
updateNodeProps,
|
|
17
|
+
updatePageRoot,
|
|
18
|
+
} = useAppStore()
|
|
19
|
+
|
|
20
|
+
const activePage = pages.find((page) => page.id === activePageId)
|
|
21
|
+
const isPageRootSelected =
|
|
22
|
+
activePage?.root.id !== undefined && activePage.root.id === selectedComponentId
|
|
23
|
+
const selectedNode = isPageRootSelected
|
|
24
|
+
? activePage.root
|
|
25
|
+
: findNode(
|
|
26
|
+
[
|
|
27
|
+
...(appShell.header ? [appShell.header] : []),
|
|
28
|
+
...(activePage?.components ?? []),
|
|
29
|
+
...(appShell.footer ? [appShell.footer] : []),
|
|
30
|
+
],
|
|
31
|
+
selectedComponentId
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if (!selectedNode) {
|
|
35
|
+
return (
|
|
36
|
+
<aside
|
|
37
|
+
className={`${embedded ? 'h-full w-full' : 'w-60 border-l'} flex shrink-0 flex-col border-white/[0.06] bg-panel`}
|
|
38
|
+
>
|
|
39
|
+
<div className="px-3 py-2.5 border-b border-white/[0.06]">
|
|
40
|
+
<p className="text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
|
41
|
+
Properties
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="flex-1 flex flex-col items-center justify-center text-center px-4">
|
|
45
|
+
<p className="text-xs text-gray-600">
|
|
46
|
+
Select any element on the canvas to edit text, attributes, and Tailwind classes
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</aside>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const definition = primitiveRegistry[selectedNode.type]
|
|
54
|
+
|
|
55
|
+
const handleChange = (key: string, value: unknown) => {
|
|
56
|
+
if (isPageRootSelected && activePage) {
|
|
57
|
+
updatePageRoot(activePage.id, { props: { [key]: value } })
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
updateNodeProps(selectedNode.id, { [key]: value })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleRootTagChange = (type: PrimitiveType) => {
|
|
65
|
+
if (!activePage) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
updatePageRoot(activePage.id, { type })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<aside
|
|
74
|
+
className={`${embedded ? 'h-full w-full' : 'w-60 border-l'} flex shrink-0 flex-col overflow-hidden border-white/[0.06] bg-panel`}
|
|
75
|
+
>
|
|
76
|
+
<div className="px-3 py-2.5 border-b border-white/[0.06] flex items-center justify-between">
|
|
77
|
+
<p className="text-[10px] font-semibold uppercase tracking-widest text-gray-500">
|
|
78
|
+
Properties
|
|
79
|
+
</p>
|
|
80
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 bg-accent/20 text-accent rounded">
|
|
81
|
+
{isPageRootSelected ? 'Page root' : selectedNode.label ?? definition.label}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
|
86
|
+
{isPageRootSelected ? (
|
|
87
|
+
<RootTagField value={selectedNode.type} onChange={handleRootTagChange} />
|
|
88
|
+
) : (
|
|
89
|
+
<ReadOnlyField label="Tag" value={selectedNode.type} />
|
|
90
|
+
)}
|
|
91
|
+
{Object.entries(definition.propSchema).map(([key, schema]) => (
|
|
92
|
+
<PropField
|
|
93
|
+
key={`${selectedNode.id}:${key}`}
|
|
94
|
+
schema={schema}
|
|
95
|
+
value={selectedNode.props[key] ?? schema.default}
|
|
96
|
+
onChange={(value) => handleChange(key, value)}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</aside>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function RootTagField({
|
|
105
|
+
value,
|
|
106
|
+
onChange,
|
|
107
|
+
}: {
|
|
108
|
+
value: PrimitiveType
|
|
109
|
+
onChange: (type: PrimitiveType) => void
|
|
110
|
+
}) {
|
|
111
|
+
return (
|
|
112
|
+
<FieldShell label="Page root tag">
|
|
113
|
+
<select
|
|
114
|
+
value={value}
|
|
115
|
+
onChange={(event) => onChange(event.target.value as PrimitiveType)}
|
|
116
|
+
className="w-full px-2.5 py-1.5 text-xs bg-white/[0.05] border border-white/[0.08] rounded text-gray-200 focus:outline-none focus:border-accent/50 transition-colors"
|
|
117
|
+
>
|
|
118
|
+
{pageRootTags.map((tag) => (
|
|
119
|
+
<option key={tag} value={tag}>
|
|
120
|
+
{tag}
|
|
121
|
+
</option>
|
|
122
|
+
))}
|
|
123
|
+
</select>
|
|
124
|
+
</FieldShell>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pageRootTags: PrimitiveType[] = [
|
|
129
|
+
'div',
|
|
130
|
+
'main',
|
|
131
|
+
'section',
|
|
132
|
+
'article',
|
|
133
|
+
'header',
|
|
134
|
+
'footer',
|
|
135
|
+
'nav',
|
|
136
|
+
'aside',
|
|
137
|
+
'form',
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
function findNode(nodes: VisualNode[], nodeId: string | null): VisualNode | null {
|
|
141
|
+
if (!nodeId) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const node of nodes) {
|
|
146
|
+
if (node.id === nodeId) {
|
|
147
|
+
return node
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const child = findNode(node.children, nodeId)
|
|
151
|
+
|
|
152
|
+
if (child) {
|
|
153
|
+
return child
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ReadOnlyField({ label, value }: { label: string; value: string }) {
|
|
161
|
+
return (
|
|
162
|
+
<div className="space-y-1">
|
|
163
|
+
<label className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">
|
|
164
|
+
{label}
|
|
165
|
+
</label>
|
|
166
|
+
<div className="w-full px-2.5 py-1.5 text-xs bg-white/[0.03] border border-white/[0.06] rounded text-gray-500">
|
|
167
|
+
{value}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function PropField({
|
|
174
|
+
schema,
|
|
175
|
+
value,
|
|
176
|
+
onChange,
|
|
177
|
+
}: {
|
|
178
|
+
schema: PropSchema
|
|
179
|
+
value: unknown
|
|
180
|
+
onChange: (value: unknown) => void
|
|
181
|
+
}) {
|
|
182
|
+
if (schema.type === 'boolean') {
|
|
183
|
+
const checked = Boolean(value)
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex items-center justify-between">
|
|
187
|
+
<label className="text-xs text-gray-400">{schema.label}</label>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={() => onChange(!checked)}
|
|
191
|
+
className={`w-8 h-4 rounded-full transition-colors ${
|
|
192
|
+
checked ? 'bg-accent' : 'bg-white/10'
|
|
193
|
+
}`}
|
|
194
|
+
>
|
|
195
|
+
<span
|
|
196
|
+
className={`block w-3 h-3 rounded-full bg-white shadow transition-transform mx-0.5 ${
|
|
197
|
+
checked ? 'translate-x-4' : 'translate-x-0'
|
|
198
|
+
}`}
|
|
199
|
+
/>
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (schema.type === 'array') {
|
|
206
|
+
return <ArrayField schema={schema} value={value} onChange={onChange} />
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (schema.type === 'select') {
|
|
210
|
+
return (
|
|
211
|
+
<FieldShell label={schema.label}>
|
|
212
|
+
<select
|
|
213
|
+
value={String(value ?? schema.default)}
|
|
214
|
+
onChange={(event) => onChange(event.target.value)}
|
|
215
|
+
className="w-full px-2.5 py-1.5 text-xs bg-white/[0.05] border border-white/[0.08] rounded text-gray-200 focus:outline-none focus:border-accent/50 transition-colors"
|
|
216
|
+
>
|
|
217
|
+
{(schema.options ?? []).map((option) => (
|
|
218
|
+
<option key={option} value={option}>
|
|
219
|
+
{option}
|
|
220
|
+
</option>
|
|
221
|
+
))}
|
|
222
|
+
</select>
|
|
223
|
+
</FieldShell>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (schema.label === 'Tailwind classes') {
|
|
228
|
+
return (
|
|
229
|
+
<TailwindClassField
|
|
230
|
+
value={String(value ?? schema.default)}
|
|
231
|
+
onChange={onChange}
|
|
232
|
+
/>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (schema.type === 'text') {
|
|
237
|
+
return (
|
|
238
|
+
<FieldShell label={schema.label}>
|
|
239
|
+
<textarea
|
|
240
|
+
value={String(value ?? schema.default)}
|
|
241
|
+
onChange={(event) => onChange(event.target.value)}
|
|
242
|
+
rows={3}
|
|
243
|
+
className="w-full px-2.5 py-1.5 text-xs bg-white/[0.05] border border-white/[0.08] rounded text-gray-200 focus:outline-none focus:border-accent/50 transition-colors resize-none"
|
|
244
|
+
/>
|
|
245
|
+
</FieldShell>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<FieldShell label={schema.label}>
|
|
251
|
+
<input
|
|
252
|
+
type={schema.type === 'number' ? 'number' : 'text'}
|
|
253
|
+
value={String(value ?? schema.default)}
|
|
254
|
+
onChange={(event) =>
|
|
255
|
+
onChange(
|
|
256
|
+
schema.type === 'number'
|
|
257
|
+
? Number(event.target.value)
|
|
258
|
+
: event.target.value
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
className="w-full px-2.5 py-1.5 text-xs bg-white/[0.05] border border-white/[0.08] rounded text-gray-200 placeholder-gray-600 focus:outline-none focus:border-accent/50 transition-colors"
|
|
262
|
+
/>
|
|
263
|
+
</FieldShell>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function TailwindClassField({
|
|
268
|
+
value,
|
|
269
|
+
onChange,
|
|
270
|
+
}: {
|
|
271
|
+
value: string
|
|
272
|
+
onChange: (value: unknown) => void
|
|
273
|
+
}) {
|
|
274
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
|
275
|
+
const [caretIndex, setCaretIndex] = useState(value.length)
|
|
276
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
277
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
278
|
+
const token = getCurrentClassToken(value, caretIndex)
|
|
279
|
+
const suggestions = useMemo(() => {
|
|
280
|
+
if (!isFocused || token.value.length === 0) {
|
|
281
|
+
return []
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return tailwindClassSuggestions
|
|
285
|
+
.filter((className) => className.startsWith(token.value))
|
|
286
|
+
.slice(0, 8)
|
|
287
|
+
}, [isFocused, token.value])
|
|
288
|
+
|
|
289
|
+
const updateCaret = () => {
|
|
290
|
+
setCaretIndex(textareaRef.current?.selectionStart ?? value.length)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const applySuggestion = (className: string) => {
|
|
294
|
+
const before = value.slice(0, token.start)
|
|
295
|
+
const after = value.slice(token.end)
|
|
296
|
+
const needsLeadingSpace = before.length > 0 && !/\s$/.test(before)
|
|
297
|
+
const needsTrailingSpace = after.length === 0 || !/^\s/.test(after)
|
|
298
|
+
const nextValue = `${before}${needsLeadingSpace ? ' ' : ''}${className}${
|
|
299
|
+
needsTrailingSpace ? ' ' : ''
|
|
300
|
+
}${after}`
|
|
301
|
+
const nextCaret =
|
|
302
|
+
before.length + (needsLeadingSpace ? 1 : 0) + className.length + 1
|
|
303
|
+
|
|
304
|
+
onChange(nextValue)
|
|
305
|
+
setCaretIndex(nextCaret)
|
|
306
|
+
setActiveIndex(0)
|
|
307
|
+
window.setTimeout(() => {
|
|
308
|
+
textareaRef.current?.focus()
|
|
309
|
+
textareaRef.current?.setSelectionRange(nextCaret, nextCaret)
|
|
310
|
+
}, 0)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<FieldShell label="Tailwind classes">
|
|
315
|
+
<div className="relative">
|
|
316
|
+
<textarea
|
|
317
|
+
ref={textareaRef}
|
|
318
|
+
value={value}
|
|
319
|
+
onFocus={() => setIsFocused(true)}
|
|
320
|
+
onBlur={() => {
|
|
321
|
+
window.setTimeout(() => setIsFocused(false), 120)
|
|
322
|
+
}}
|
|
323
|
+
onChange={(event) => {
|
|
324
|
+
onChange(event.target.value)
|
|
325
|
+
setCaretIndex(event.target.selectionStart)
|
|
326
|
+
setActiveIndex(0)
|
|
327
|
+
}}
|
|
328
|
+
onSelect={updateCaret}
|
|
329
|
+
onKeyUp={updateCaret}
|
|
330
|
+
onKeyDown={(event) => {
|
|
331
|
+
if (suggestions.length === 0) {
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (event.key === 'ArrowDown') {
|
|
336
|
+
event.preventDefault()
|
|
337
|
+
setActiveIndex((index) => (index + 1) % suggestions.length)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (event.key === 'ArrowUp') {
|
|
342
|
+
event.preventDefault()
|
|
343
|
+
setActiveIndex(
|
|
344
|
+
(index) => (index - 1 + suggestions.length) % suggestions.length
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
350
|
+
event.preventDefault()
|
|
351
|
+
applySuggestion(suggestions[activeIndex])
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (event.key === 'Escape') {
|
|
356
|
+
setIsFocused(false)
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
rows={4}
|
|
360
|
+
className="w-full px-2.5 py-1.5 text-xs bg-white/[0.05] border border-white/[0.08] rounded text-gray-200 focus:outline-none focus:border-accent/50 transition-colors resize-none"
|
|
361
|
+
/>
|
|
362
|
+
{suggestions.length > 0 && (
|
|
363
|
+
<div className="absolute left-0 right-0 top-full z-30 mt-1 max-h-44 overflow-y-auto rounded border border-white/[0.12] bg-[#11151c] py-1 shadow-xl">
|
|
364
|
+
{suggestions.map((className, index) => (
|
|
365
|
+
<button
|
|
366
|
+
key={className}
|
|
367
|
+
type="button"
|
|
368
|
+
onMouseDown={(event) => {
|
|
369
|
+
event.preventDefault()
|
|
370
|
+
applySuggestion(className)
|
|
371
|
+
}}
|
|
372
|
+
className={`block w-full cursor-pointer px-2.5 py-1.5 text-left font-mono text-[11px] transition-colors ${
|
|
373
|
+
index === activeIndex
|
|
374
|
+
? 'bg-accent text-white'
|
|
375
|
+
: 'text-gray-300 hover:bg-white/[0.08] hover:text-white'
|
|
376
|
+
}`}
|
|
377
|
+
>
|
|
378
|
+
{className}
|
|
379
|
+
</button>
|
|
380
|
+
))}
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
<p className="text-[10px] text-gray-600">
|
|
385
|
+
Type a class prefix, then press Tab or Enter to accept.
|
|
386
|
+
</p>
|
|
387
|
+
</FieldShell>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getCurrentClassToken(value: string, caretIndex: number) {
|
|
392
|
+
const safeCaret = Math.max(0, Math.min(caretIndex, value.length))
|
|
393
|
+
const before = value.slice(0, safeCaret)
|
|
394
|
+
const tokenStart = Math.max(
|
|
395
|
+
before.lastIndexOf(' '),
|
|
396
|
+
before.lastIndexOf('\n'),
|
|
397
|
+
before.lastIndexOf('\t')
|
|
398
|
+
) + 1
|
|
399
|
+
const after = value.slice(safeCaret)
|
|
400
|
+
const nextBreak = after.search(/\s/)
|
|
401
|
+
const tokenEnd = nextBreak === -1 ? value.length : safeCaret + nextBreak
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
start: tokenStart,
|
|
405
|
+
end: tokenEnd,
|
|
406
|
+
value: value.slice(tokenStart, tokenEnd),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function ArrayField({
|
|
411
|
+
schema,
|
|
412
|
+
value,
|
|
413
|
+
onChange,
|
|
414
|
+
}: {
|
|
415
|
+
schema: PropSchema
|
|
416
|
+
value: unknown
|
|
417
|
+
onChange: (value: unknown) => void
|
|
418
|
+
}) {
|
|
419
|
+
const [draft, setDraft] = useState(formatArrayValue(value))
|
|
420
|
+
|
|
421
|
+
const handleChange = (rawValue: string) => {
|
|
422
|
+
setDraft(rawValue)
|
|
423
|
+
onChange(parseArrayValue(rawValue))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<FieldShell label={schema.label}>
|
|
428
|
+
<textarea
|
|
429
|
+
value={draft}
|
|
430
|
+
onChange={(event) => handleChange(event.target.value)}
|
|
431
|
+
rows={2}
|
|
432
|
+
className="w-full px-2.5 py-1.5 text-xs bg-white/[0.05] border border-white/[0.08] rounded text-gray-200 focus:outline-none focus:border-accent/50 transition-colors resize-none"
|
|
433
|
+
/>
|
|
434
|
+
<p className="text-[10px] text-gray-600">Comma or line separated</p>
|
|
435
|
+
</FieldShell>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function formatArrayValue(value: unknown) {
|
|
440
|
+
return Array.isArray(value) ? value.join(', ') : ''
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function parseArrayValue(value: string) {
|
|
444
|
+
return value
|
|
445
|
+
.split(/[,\n]/)
|
|
446
|
+
.map((item) => item.trim())
|
|
447
|
+
.filter(Boolean)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function FieldShell({
|
|
451
|
+
label,
|
|
452
|
+
children,
|
|
453
|
+
}: {
|
|
454
|
+
label: string
|
|
455
|
+
children: React.ReactNode
|
|
456
|
+
}) {
|
|
457
|
+
return (
|
|
458
|
+
<div className="space-y-1">
|
|
459
|
+
<label className="text-[10px] font-medium text-gray-500 uppercase tracking-wide">
|
|
460
|
+
{label}
|
|
461
|
+
</label>
|
|
462
|
+
{children}
|
|
463
|
+
</div>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const tailwindClassSuggestions = [
|
|
468
|
+
'block',
|
|
469
|
+
'inline',
|
|
470
|
+
'inline-block',
|
|
471
|
+
'flex',
|
|
472
|
+
'inline-flex',
|
|
473
|
+
'grid',
|
|
474
|
+
'hidden',
|
|
475
|
+
'contents',
|
|
476
|
+
'flex-row',
|
|
477
|
+
'flex-col',
|
|
478
|
+
'flex-wrap',
|
|
479
|
+
'items-start',
|
|
480
|
+
'items-center',
|
|
481
|
+
'items-end',
|
|
482
|
+
'items-stretch',
|
|
483
|
+
'justify-start',
|
|
484
|
+
'justify-center',
|
|
485
|
+
'justify-end',
|
|
486
|
+
'justify-between',
|
|
487
|
+
'justify-around',
|
|
488
|
+
'content-center',
|
|
489
|
+
'self-start',
|
|
490
|
+
'self-center',
|
|
491
|
+
'self-end',
|
|
492
|
+
'gap-1',
|
|
493
|
+
'gap-2',
|
|
494
|
+
'gap-3',
|
|
495
|
+
'gap-4',
|
|
496
|
+
'gap-6',
|
|
497
|
+
'gap-8',
|
|
498
|
+
'grid-cols-1',
|
|
499
|
+
'grid-cols-2',
|
|
500
|
+
'grid-cols-3',
|
|
501
|
+
'grid-cols-4',
|
|
502
|
+
'grid-cols-6',
|
|
503
|
+
'col-span-1',
|
|
504
|
+
'col-span-2',
|
|
505
|
+
'col-span-full',
|
|
506
|
+
'w-full',
|
|
507
|
+
'w-screen',
|
|
508
|
+
'w-auto',
|
|
509
|
+
'w-fit',
|
|
510
|
+
'w-1/2',
|
|
511
|
+
'w-1/3',
|
|
512
|
+
'w-2/3',
|
|
513
|
+
'w-1/4',
|
|
514
|
+
'w-3/4',
|
|
515
|
+
'min-w-0',
|
|
516
|
+
'max-w-sm',
|
|
517
|
+
'max-w-md',
|
|
518
|
+
'max-w-lg',
|
|
519
|
+
'max-w-xl',
|
|
520
|
+
'max-w-2xl',
|
|
521
|
+
'max-w-4xl',
|
|
522
|
+
'max-w-6xl',
|
|
523
|
+
'max-w-full',
|
|
524
|
+
'h-full',
|
|
525
|
+
'h-screen',
|
|
526
|
+
'h-auto',
|
|
527
|
+
'min-h-12',
|
|
528
|
+
'min-h-24',
|
|
529
|
+
'min-h-48',
|
|
530
|
+
'min-h-screen',
|
|
531
|
+
'p-0',
|
|
532
|
+
'p-1',
|
|
533
|
+
'p-2',
|
|
534
|
+
'p-3',
|
|
535
|
+
'p-4',
|
|
536
|
+
'p-6',
|
|
537
|
+
'p-8',
|
|
538
|
+
'px-2',
|
|
539
|
+
'px-3',
|
|
540
|
+
'px-4',
|
|
541
|
+
'px-6',
|
|
542
|
+
'px-8',
|
|
543
|
+
'py-1',
|
|
544
|
+
'py-2',
|
|
545
|
+
'py-3',
|
|
546
|
+
'py-4',
|
|
547
|
+
'py-6',
|
|
548
|
+
'py-8',
|
|
549
|
+
'm-0',
|
|
550
|
+
'mx-auto',
|
|
551
|
+
'mt-1',
|
|
552
|
+
'mt-2',
|
|
553
|
+
'mt-4',
|
|
554
|
+
'mb-1',
|
|
555
|
+
'mb-2',
|
|
556
|
+
'mb-4',
|
|
557
|
+
'space-y-1',
|
|
558
|
+
'space-y-2',
|
|
559
|
+
'space-y-3',
|
|
560
|
+
'space-y-4',
|
|
561
|
+
'text-left',
|
|
562
|
+
'text-center',
|
|
563
|
+
'text-right',
|
|
564
|
+
'text-xs',
|
|
565
|
+
'text-sm',
|
|
566
|
+
'text-base',
|
|
567
|
+
'text-lg',
|
|
568
|
+
'text-xl',
|
|
569
|
+
'text-2xl',
|
|
570
|
+
'text-3xl',
|
|
571
|
+
'text-4xl',
|
|
572
|
+
'text-5xl',
|
|
573
|
+
'font-thin',
|
|
574
|
+
'font-light',
|
|
575
|
+
'font-normal',
|
|
576
|
+
'font-medium',
|
|
577
|
+
'font-semibold',
|
|
578
|
+
'font-bold',
|
|
579
|
+
'font-extrabold',
|
|
580
|
+
'italic',
|
|
581
|
+
'not-italic',
|
|
582
|
+
'underline',
|
|
583
|
+
'line-through',
|
|
584
|
+
'leading-none',
|
|
585
|
+
'leading-tight',
|
|
586
|
+
'leading-normal',
|
|
587
|
+
'leading-relaxed',
|
|
588
|
+
'tracking-tight',
|
|
589
|
+
'tracking-normal',
|
|
590
|
+
'tracking-wide',
|
|
591
|
+
'text-white',
|
|
592
|
+
'text-black',
|
|
593
|
+
'text-gray-50',
|
|
594
|
+
'text-gray-100',
|
|
595
|
+
'text-gray-200',
|
|
596
|
+
'text-gray-300',
|
|
597
|
+
'text-gray-400',
|
|
598
|
+
'text-gray-500',
|
|
599
|
+
'text-gray-600',
|
|
600
|
+
'text-gray-700',
|
|
601
|
+
'text-gray-800',
|
|
602
|
+
'text-gray-900',
|
|
603
|
+
'text-red-500',
|
|
604
|
+
'text-orange-500',
|
|
605
|
+
'text-amber-500',
|
|
606
|
+
'text-yellow-500',
|
|
607
|
+
'text-green-500',
|
|
608
|
+
'text-emerald-500',
|
|
609
|
+
'text-teal-500',
|
|
610
|
+
'text-cyan-500',
|
|
611
|
+
'text-sky-500',
|
|
612
|
+
'text-blue-500',
|
|
613
|
+
'text-blue-600',
|
|
614
|
+
'text-indigo-500',
|
|
615
|
+
'text-violet-500',
|
|
616
|
+
'text-purple-500',
|
|
617
|
+
'text-pink-500',
|
|
618
|
+
'bg-white',
|
|
619
|
+
'bg-black',
|
|
620
|
+
'bg-transparent',
|
|
621
|
+
'bg-gray-50',
|
|
622
|
+
'bg-gray-100',
|
|
623
|
+
'bg-gray-200',
|
|
624
|
+
'bg-gray-300',
|
|
625
|
+
'bg-gray-800',
|
|
626
|
+
'bg-gray-900',
|
|
627
|
+
'bg-red-500',
|
|
628
|
+
'bg-orange-500',
|
|
629
|
+
'bg-yellow-500',
|
|
630
|
+
'bg-green-500',
|
|
631
|
+
'bg-blue-500',
|
|
632
|
+
'bg-blue-600',
|
|
633
|
+
'bg-indigo-600',
|
|
634
|
+
'bg-purple-600',
|
|
635
|
+
'border',
|
|
636
|
+
'border-0',
|
|
637
|
+
'border-2',
|
|
638
|
+
'border-t',
|
|
639
|
+
'border-b',
|
|
640
|
+
'border-l',
|
|
641
|
+
'border-r',
|
|
642
|
+
'border-transparent',
|
|
643
|
+
'border-white',
|
|
644
|
+
'border-black',
|
|
645
|
+
'border-gray-200',
|
|
646
|
+
'border-gray-300',
|
|
647
|
+
'border-gray-700',
|
|
648
|
+
'border-blue-500',
|
|
649
|
+
'rounded',
|
|
650
|
+
'rounded-sm',
|
|
651
|
+
'rounded-md',
|
|
652
|
+
'rounded-lg',
|
|
653
|
+
'rounded-xl',
|
|
654
|
+
'rounded-2xl',
|
|
655
|
+
'rounded-full',
|
|
656
|
+
'shadow',
|
|
657
|
+
'shadow-sm',
|
|
658
|
+
'shadow-md',
|
|
659
|
+
'shadow-lg',
|
|
660
|
+
'shadow-xl',
|
|
661
|
+
'overflow-hidden',
|
|
662
|
+
'overflow-auto',
|
|
663
|
+
'overflow-x-auto',
|
|
664
|
+
'relative',
|
|
665
|
+
'absolute',
|
|
666
|
+
'fixed',
|
|
667
|
+
'sticky',
|
|
668
|
+
'top-0',
|
|
669
|
+
'right-0',
|
|
670
|
+
'bottom-0',
|
|
671
|
+
'left-0',
|
|
672
|
+
'z-10',
|
|
673
|
+
'z-20',
|
|
674
|
+
'z-50',
|
|
675
|
+
'opacity-0',
|
|
676
|
+
'opacity-50',
|
|
677
|
+
'opacity-75',
|
|
678
|
+
'opacity-100',
|
|
679
|
+
'cursor-pointer',
|
|
680
|
+
'select-none',
|
|
681
|
+
'transition',
|
|
682
|
+
'transition-colors',
|
|
683
|
+
'duration-150',
|
|
684
|
+
'duration-200',
|
|
685
|
+
'hover:bg-gray-100',
|
|
686
|
+
'hover:bg-gray-900',
|
|
687
|
+
'hover:text-white',
|
|
688
|
+
'hover:text-black',
|
|
689
|
+
'focus:outline-none',
|
|
690
|
+
'focus:ring-2',
|
|
691
|
+
'focus:ring-blue-500',
|
|
692
|
+
]
|