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.
Files changed (44) hide show
  1. package/README.md +123 -0
  2. package/package.json +85 -0
  3. package/scripts/create-builder-app.mjs +501 -0
  4. package/scripts/vb-dev.mjs +32 -0
  5. package/scripts/vb-generate.mjs +224 -0
  6. package/templates/default/app/README.md +21 -0
  7. package/templates/default/app/eslint.config.js +25 -0
  8. package/templates/default/app/index.html +15 -0
  9. package/templates/default/app/src/index.css +23 -0
  10. package/templates/default/app/src/main.tsx +10 -0
  11. package/templates/default/app/tsconfig.app.json +21 -0
  12. package/templates/default/app/tsconfig.json +7 -0
  13. package/templates/default/app/tsconfig.node.json +19 -0
  14. package/templates/default/app/visualbuild/generated-files.json +3 -0
  15. package/templates/default/app/visualbuild/pages.json +12 -0
  16. package/templates/default/app/vite.config.ts +7 -0
  17. package/templates/default/builder/README.md +21 -0
  18. package/templates/default/builder/eslint.config.js +25 -0
  19. package/templates/default/builder/tsconfig.app.json +21 -0
  20. package/templates/default/builder/tsconfig.json +7 -0
  21. package/templates/default/builder/tsconfig.node.json +22 -0
  22. package/visual-app-builder/index.html +15 -0
  23. package/visual-app-builder/server/parseReactPage.ts +571 -0
  24. package/visual-app-builder/shared/generateReactSource.d.mts +37 -0
  25. package/visual-app-builder/shared/generateReactSource.mjs +443 -0
  26. package/visual-app-builder/src/App.tsx +874 -0
  27. package/visual-app-builder/src/components/Canvas.tsx +1059 -0
  28. package/visual-app-builder/src/components/CodePanel.tsx +812 -0
  29. package/visual-app-builder/src/components/ComponentSidebar.tsx +302 -0
  30. package/visual-app-builder/src/components/PageSwitcher.tsx +161 -0
  31. package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -0
  32. package/visual-app-builder/src/components/PropertiesPanel.tsx +692 -0
  33. package/visual-app-builder/src/components/Topbar.tsx +128 -0
  34. package/visual-app-builder/src/data/componentRegistry.tsx +292 -0
  35. package/visual-app-builder/src/data/primitiveRegistry.ts +368 -0
  36. package/visual-app-builder/src/index.css +111 -0
  37. package/visual-app-builder/src/main.tsx +10 -0
  38. package/visual-app-builder/src/stores/useAppStore.ts +1265 -0
  39. package/visual-app-builder/src/tailwindGeneratedSafelist.ts +36 -0
  40. package/visual-app-builder/src/types/index.ts +261 -0
  41. package/visual-app-builder/src/utils/codegen.ts +66 -0
  42. package/visual-app-builder/src/utils/projectFiles.ts +146 -0
  43. package/visual-app-builder/src/utils/projectPersistence.ts +177 -0
  44. 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
+ ]