@djangocfg/ui-tools 2.1.119 → 2.1.120

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 (63) hide show
  1. package/dist/JsonTree-G2TPWQ4C.mjs +4 -0
  2. package/dist/{JsonTree-6RYAOPSS.mjs.map → JsonTree-G2TPWQ4C.mjs.map} +1 -1
  3. package/dist/JsonTree-TWXUBBIG.cjs +10 -0
  4. package/dist/{JsonTree-7OH6CIHT.cjs.map → JsonTree-TWXUBBIG.cjs.map} +1 -1
  5. package/dist/{Mermaid.client-PNXEC6YL.cjs → Mermaid.client-AF4WOQZR.cjs} +9 -11
  6. package/dist/Mermaid.client-AF4WOQZR.cjs.map +1 -0
  7. package/dist/{Mermaid.client-OKACITCW.mjs → Mermaid.client-W4QXJX7Q.mjs} +9 -11
  8. package/dist/Mermaid.client-W4QXJX7Q.mjs.map +1 -0
  9. package/dist/{PlaygroundLayout-SYMEAG3J.cjs → PlaygroundLayout-RZMJWH3Y.cjs} +25 -25
  10. package/dist/{PlaygroundLayout-SYMEAG3J.cjs.map → PlaygroundLayout-RZMJWH3Y.cjs.map} +1 -1
  11. package/dist/{PlaygroundLayout-UQRBU5RH.mjs → PlaygroundLayout-UQABCZ6K.mjs} +4 -4
  12. package/dist/{PlaygroundLayout-UQRBU5RH.mjs.map → PlaygroundLayout-UQABCZ6K.mjs.map} +1 -1
  13. package/dist/{chunk-UOMPPIED.mjs → chunk-4G4UGMOP.mjs} +3 -3
  14. package/dist/chunk-4G4UGMOP.mjs.map +1 -0
  15. package/dist/{chunk-47T5ECYV.cjs → chunk-CY3CQS26.cjs} +3 -3
  16. package/dist/chunk-CY3CQS26.cjs.map +1 -0
  17. package/dist/{chunk-5QT3QYFZ.cjs → chunk-EGYUND4E.cjs} +2 -2
  18. package/dist/chunk-EGYUND4E.cjs.map +1 -0
  19. package/dist/{chunk-DI3HUXHK.cjs → chunk-OYLQZT62.cjs} +2 -2
  20. package/dist/chunk-OYLQZT62.cjs.map +1 -0
  21. package/dist/{chunk-W6YHQI4F.mjs → chunk-OYYCGIBF.mjs} +2 -2
  22. package/dist/chunk-OYYCGIBF.mjs.map +1 -0
  23. package/dist/{chunk-G6PRZP5I.mjs → chunk-XZZ22EHP.mjs} +2 -2
  24. package/dist/chunk-XZZ22EHP.mjs.map +1 -0
  25. package/dist/{components-EASJYK45.mjs → components-4YSJ5ALL.mjs} +3 -3
  26. package/dist/{components-CJ2IB65O.cjs.map → components-4YSJ5ALL.mjs.map} +1 -1
  27. package/dist/{components-CJ2IB65O.cjs → components-BSTP3VLD.cjs} +7 -7
  28. package/dist/{components-EASJYK45.mjs.map → components-BSTP3VLD.cjs.map} +1 -1
  29. package/dist/index.cjs +27 -27
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.mjs +10 -10
  34. package/dist/index.mjs.map +1 -1
  35. package/package.json +9 -12
  36. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +102 -0
  37. package/src/tools/Gallery/Gallery.story.tsx +231 -0
  38. package/src/tools/Gallery/components/compact/GalleryCompact.tsx +72 -7
  39. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +83 -1
  40. package/src/tools/JsonForm/JsonForm.story.tsx +134 -0
  41. package/src/tools/JsonTree/JsonTree.story.tsx +140 -0
  42. package/src/tools/JsonTree/index.tsx +1 -1
  43. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +95 -0
  44. package/src/tools/Map/Map.story.tsx +300 -0
  45. package/src/tools/Mermaid/Mermaid.story.tsx +131 -0
  46. package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +2 -5
  47. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +7 -1
  48. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +4 -2
  49. package/src/tools/Mermaid/index.tsx +1 -1
  50. package/src/tools/PrettyCode/PrettyCode.story.tsx +116 -0
  51. package/src/tools/PrettyCode/index.tsx +1 -1
  52. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +87 -0
  53. package/src/tools/VideoPlayer/utils/resolvers.ts +2 -2
  54. package/dist/JsonTree-6RYAOPSS.mjs +0 -4
  55. package/dist/JsonTree-7OH6CIHT.cjs +0 -10
  56. package/dist/Mermaid.client-OKACITCW.mjs.map +0 -1
  57. package/dist/Mermaid.client-PNXEC6YL.cjs.map +0 -1
  58. package/dist/chunk-47T5ECYV.cjs.map +0 -1
  59. package/dist/chunk-5QT3QYFZ.cjs.map +0 -1
  60. package/dist/chunk-DI3HUXHK.cjs.map +0 -1
  61. package/dist/chunk-G6PRZP5I.mjs.map +0 -1
  62. package/dist/chunk-UOMPPIED.mjs.map +0 -1
  63. package/dist/chunk-W6YHQI4F.mjs.map +0 -1
@@ -0,0 +1,231 @@
1
+ import { defineStory, useBoolean, useSelect, useNumber } from '@djangocfg/playground';
2
+ import { Gallery, GalleryCompact, type GalleryMediaItem } from './index';
3
+
4
+ // Sample images for testing
5
+ const SAMPLE_IMAGES: GalleryMediaItem[] = [
6
+ {
7
+ id: '1',
8
+ src: 'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?w=800',
9
+ thumbnail: 'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?w=200',
10
+ alt: 'Classic car',
11
+ },
12
+ {
13
+ id: '2',
14
+ src: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=800',
15
+ thumbnail: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=200',
16
+ alt: 'Porsche',
17
+ },
18
+ {
19
+ id: '3',
20
+ src: 'https://images.unsplash.com/photo-1542362567-b07e54358753?w=800',
21
+ thumbnail: 'https://images.unsplash.com/photo-1542362567-b07e54358753?w=200',
22
+ alt: 'BMW',
23
+ },
24
+ {
25
+ id: '4',
26
+ src: 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=800',
27
+ thumbnail: 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=200',
28
+ alt: 'Mercedes',
29
+ },
30
+ {
31
+ id: '5',
32
+ src: 'https://images.unsplash.com/photo-1617531653332-bd46c24f2068?w=800',
33
+ thumbnail: 'https://images.unsplash.com/photo-1617531653332-bd46c24f2068?w=200',
34
+ alt: 'Tesla',
35
+ },
36
+ ];
37
+
38
+ export default defineStory({
39
+ title: 'Tools/Gallery',
40
+ component: Gallery,
41
+ description: 'Full-featured gallery with lightbox, thumbnails, and keyboard navigation.',
42
+ });
43
+
44
+ export const CarouselMode = () => (
45
+ <div className="max-w-4xl">
46
+ <Gallery
47
+ images={SAMPLE_IMAGES}
48
+ previewMode="carousel"
49
+ showThumbnails
50
+ showControls
51
+ showCounter
52
+ enableLightbox
53
+ enableKeyboard
54
+ aspectRatio={16 / 9}
55
+ />
56
+ </div>
57
+ );
58
+
59
+ export const GridMode = () => (
60
+ <div className="max-w-4xl">
61
+ <Gallery
62
+ images={SAMPLE_IMAGES}
63
+ previewMode="grid"
64
+ previewCount={5}
65
+ enableLightbox
66
+ aspectRatio={16 / 9}
67
+ />
68
+ </div>
69
+ );
70
+
71
+ export const WithoutThumbnails = () => (
72
+ <div className="max-w-4xl">
73
+ <Gallery
74
+ images={SAMPLE_IMAGES}
75
+ previewMode="carousel"
76
+ showThumbnails={false}
77
+ showControls
78
+ enableLightbox
79
+ aspectRatio={16 / 9}
80
+ />
81
+ </div>
82
+ );
83
+
84
+ export const Compact = {
85
+ render: (args: {
86
+ showDots: boolean;
87
+ showArrows: boolean;
88
+ enableZoom: boolean;
89
+ showCounter: boolean;
90
+ }) => (
91
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
92
+ <div className="rounded-xl border border-border overflow-hidden">
93
+ <div className="aspect-[4/3]">
94
+ <GalleryCompact
95
+ images={SAMPLE_IMAGES}
96
+ showDots={args.showDots}
97
+ showArrows={args.showArrows}
98
+ enableZoom={args.enableZoom}
99
+ showCounter={args.showCounter}
100
+ />
101
+ </div>
102
+ <div className="p-4">
103
+ <h3 className="font-medium">Vehicle Title</h3>
104
+ <p className="text-sm text-muted-foreground">$25,000</p>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ ),
109
+ args: {
110
+ showDots: true,
111
+ showArrows: true,
112
+ enableZoom: true,
113
+ showCounter: false,
114
+ },
115
+ argTypes: {
116
+ showDots: { control: 'boolean', description: 'Show navigation dots' },
117
+ showArrows: { control: 'boolean', description: 'Show arrow buttons' },
118
+ enableZoom: { control: 'boolean', description: 'Enable zoom on hover' },
119
+ showCounter: { control: 'boolean', description: 'Show image counter' },
120
+ },
121
+ };
122
+
123
+ export const SingleImage = () => (
124
+ <div className="w-64">
125
+ <div className="rounded-xl border border-border overflow-hidden">
126
+ <div className="aspect-[4/3]">
127
+ <GalleryCompact images={SAMPLE_IMAGES.slice(0, 1)} enableZoom />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ );
132
+
133
+ export const EmptyState = () => (
134
+ <div className="w-64">
135
+ <div className="rounded-xl border border-border overflow-hidden">
136
+ <div className="aspect-[4/3]">
137
+ <GalleryCompact images={[]} />
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+
143
+ /**
144
+ * Interactive story using fixture hooks
145
+ * Controls appear automatically in the right sidebar
146
+ */
147
+ export const Interactive = () => {
148
+ // These hooks auto-register controls in the Properties panel
149
+ const [showDots] = useBoolean('showDots', {
150
+ defaultValue: true,
151
+ label: 'Show Dots',
152
+ description: 'Display navigation dots at the bottom',
153
+ });
154
+
155
+ const [showArrows] = useBoolean('showArrows', {
156
+ defaultValue: true,
157
+ label: 'Show Arrows',
158
+ description: 'Display navigation arrows on hover',
159
+ });
160
+
161
+ const [enableZoom] = useBoolean('enableZoom', {
162
+ defaultValue: true,
163
+ label: 'Enable Zoom',
164
+ description: 'Zoom effect on image hover',
165
+ });
166
+
167
+ const [showCounter] = useBoolean('showCounter', {
168
+ defaultValue: false,
169
+ label: 'Show Counter',
170
+ description: 'Display image counter badge',
171
+ });
172
+
173
+ const [maxDots] = useNumber('maxDots', {
174
+ defaultValue: 5,
175
+ min: 1,
176
+ max: 10,
177
+ label: 'Max Dots',
178
+ description: 'Maximum number of dots to display',
179
+ });
180
+
181
+ const [previewMode] = useSelect('previewMode', {
182
+ options: ['carousel', 'grid'] as const,
183
+ defaultValue: 'carousel',
184
+ label: 'Preview Mode',
185
+ description: 'Gallery preview display mode',
186
+ });
187
+
188
+ return (
189
+ <div className="space-y-8">
190
+ {/* GalleryCompact */}
191
+ <div>
192
+ <h3 className="text-lg font-semibold mb-4">GalleryCompact</h3>
193
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
194
+ <div className="rounded-xl border border-border overflow-hidden">
195
+ <div className="aspect-[4/3]">
196
+ <GalleryCompact
197
+ images={SAMPLE_IMAGES}
198
+ showDots={showDots}
199
+ showArrows={showArrows}
200
+ enableZoom={enableZoom}
201
+ showCounter={showCounter}
202
+ maxDots={maxDots}
203
+ />
204
+ </div>
205
+ <div className="p-4">
206
+ <h3 className="font-medium">Interactive Card</h3>
207
+ <p className="text-sm text-muted-foreground">Use controls on the right →</p>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ {/* Full Gallery */}
214
+ <div>
215
+ <h3 className="text-lg font-semibold mb-4">Full Gallery ({previewMode})</h3>
216
+ <div className="max-w-4xl">
217
+ <Gallery
218
+ images={SAMPLE_IMAGES}
219
+ previewMode={previewMode}
220
+ showThumbnails
221
+ showControls
222
+ showCounter={showCounter}
223
+ enableLightbox
224
+ enableKeyboard
225
+ aspectRatio={16 / 9}
226
+ />
227
+ </div>
228
+ </div>
229
+ </div>
230
+ );
231
+ };
@@ -56,6 +56,12 @@ export const GalleryCompact = memo(function GalleryCompact({
56
56
  const [api, setApi] = useState<CarouselApi>()
57
57
  const [currentIndex, setCurrentIndex] = useState(0)
58
58
  const [isHovered, setIsHovered] = useState(false)
59
+ // Track if component is mounted (client-side) to avoid hydration mismatches
60
+ const [isMounted, setIsMounted] = useState(false)
61
+
62
+ useEffect(() => {
63
+ setIsMounted(true)
64
+ }, [])
59
65
 
60
66
  const total = images.length
61
67
  const hasMultiple = total > 1
@@ -90,23 +96,27 @@ export const GalleryCompact = memo(function GalleryCompact({
90
96
  }
91
97
  }, [api])
92
98
 
93
- // Navigation handlers
99
+ // Navigation handlers - stop all propagation to parent Link
100
+ const stopEvent = useCallback((e: React.MouseEvent | React.PointerEvent) => {
101
+ e.preventDefault()
102
+ e.stopPropagation()
103
+ e.nativeEvent.stopImmediatePropagation()
104
+ }, [])
105
+
94
106
  const handlePrev = useCallback(
95
107
  (e: React.MouseEvent) => {
96
- e.preventDefault()
97
- e.stopPropagation()
108
+ stopEvent(e)
98
109
  api?.scrollPrev()
99
110
  },
100
- [api]
111
+ [api, stopEvent]
101
112
  )
102
113
 
103
114
  const handleNext = useCallback(
104
115
  (e: React.MouseEvent) => {
105
- e.preventDefault()
106
- e.stopPropagation()
116
+ stopEvent(e)
107
117
  api?.scrollNext()
108
118
  },
109
- [api]
119
+ [api, stopEvent]
110
120
  )
111
121
 
112
122
  // Dot click handler
@@ -165,6 +175,57 @@ export const GalleryCompact = memo(function GalleryCompact({
165
175
  const visibleDots = images.slice(0, maxDots).map((_, i) => i)
166
176
  const remainingCount = total > maxDots ? total - maxDots : 0
167
177
 
178
+ // SSR fallback - render first image only to avoid hydration mismatch
179
+ // The carousel requires DOM access and will cause issues during SSR
180
+ if (!isMounted) {
181
+ return (
182
+ <div
183
+ className={cn('relative w-full h-full overflow-hidden', className)}
184
+ onClick={onClick}
185
+ >
186
+ <GalleryMedia
187
+ media={images[0]}
188
+ className="w-full h-full"
189
+ priority
190
+ />
191
+ {/* Show dots indicator placeholder for consistent layout */}
192
+ {showDots && (
193
+ <div
194
+ className={cn(
195
+ 'absolute bottom-3 left-1/2 -translate-x-1/2 z-10',
196
+ 'flex items-center gap-1.5',
197
+ 'px-2 py-1 rounded-full',
198
+ 'bg-black/30 backdrop-blur-sm'
199
+ )}
200
+ >
201
+ {visibleDots.map((index) => (
202
+ <div
203
+ key={index}
204
+ className={cn(
205
+ 'rounded-full',
206
+ index === 0
207
+ ? 'w-2 h-2 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
208
+ : 'w-1.5 h-1.5 bg-white/50'
209
+ )}
210
+ />
211
+ ))}
212
+ {remainingCount > 0 && (
213
+ <span className="text-white/80 text-[10px] font-medium ml-0.5">
214
+ +{remainingCount}
215
+ </span>
216
+ )}
217
+ </div>
218
+ )}
219
+ {/* Counter badge placeholder */}
220
+ {showCounter && (
221
+ <div className="absolute bottom-3 right-3 z-10 bg-black/50 backdrop-blur-sm text-white text-xs px-2 py-1 rounded-full font-medium">
222
+ 1 / {total}
223
+ </div>
224
+ )}
225
+ </div>
226
+ )
227
+ }
228
+
168
229
  return (
169
230
  <div
170
231
  className={cn('relative w-full h-full group/gallery', className)}
@@ -226,6 +287,8 @@ export const GalleryCompact = memo(function GalleryCompact({
226
287
  'focus:outline-none focus:ring-2 focus:ring-white/50'
227
288
  )}
228
289
  onClick={handlePrev}
290
+ onMouseDown={stopEvent}
291
+ onPointerDown={stopEvent}
229
292
  aria-label="Previous image"
230
293
  >
231
294
  <ChevronLeft className="w-4 h-4" />
@@ -246,6 +309,8 @@ export const GalleryCompact = memo(function GalleryCompact({
246
309
  'focus:outline-none focus:ring-2 focus:ring-white/50'
247
310
  )}
248
311
  onClick={handleNext}
312
+ onMouseDown={stopEvent}
313
+ onPointerDown={stopEvent}
249
314
  aria-label="Next image"
250
315
  >
251
316
  <ChevronRight className="w-4 h-4" />
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
4
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
5
  import { cn } from '@djangocfg/ui-core/lib'
6
6
  import {
7
7
  Carousel,
@@ -65,6 +65,12 @@ export const GalleryCarousel = memo(function GalleryCarousel({
65
65
  className,
66
66
  }: GalleryCarouselProps) {
67
67
  const [api, setApi] = React.useState<CarouselApi>()
68
+ // Track if component is mounted (client-side) to avoid hydration mismatches
69
+ const [isMounted, setIsMounted] = useState(false)
70
+
71
+ useEffect(() => {
72
+ setIsMounted(true)
73
+ }, [])
68
74
 
69
75
  // Notify parent when API is ready
70
76
  useEffect(() => {
@@ -161,6 +167,82 @@ export const GalleryCarousel = memo(function GalleryCarousel({
161
167
  pointerStartRef.current = null
162
168
  }, [enableLightbox, onLightboxOpen])
163
169
 
170
+ // SSR fallback - render first image only to avoid hydration mismatch
171
+ // The carousel requires DOM access and will cause issues during SSR
172
+ if (!isMounted) {
173
+ return (
174
+ <div className={cn('space-y-3', className)}>
175
+ <div className="relative rounded-xl overflow-hidden">
176
+ <div
177
+ className="relative bg-muted"
178
+ style={{ aspectRatio }}
179
+ >
180
+ <GalleryMedia
181
+ media={images[0]}
182
+ className="w-full h-full"
183
+ priority
184
+ />
185
+ {/* Dark overlay */}
186
+ <div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-black/20 pointer-events-none" />
187
+ </div>
188
+
189
+ {/* Counter */}
190
+ {showCounter && hasMultiple && (
191
+ <div
192
+ className={cn(
193
+ 'absolute bottom-3 right-3 z-10',
194
+ 'bg-black/60 backdrop-blur-sm text-white px-3 py-1.5 rounded-full',
195
+ 'text-sm font-medium'
196
+ )}
197
+ >
198
+ 1 / {total}
199
+ </div>
200
+ )}
201
+
202
+ {/* Mobile dots */}
203
+ {hasMultiple && (
204
+ <div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 md:hidden z-10">
205
+ {mobileDots.map((index) => (
206
+ <div
207
+ key={index}
208
+ className={cn(
209
+ 'rounded-full',
210
+ index === 0
211
+ ? 'w-2.5 h-2.5 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
212
+ : 'w-2 h-2 bg-white/40'
213
+ )}
214
+ />
215
+ ))}
216
+ {remainingCount > 0 && (
217
+ <span className="text-white/70 text-xs ml-1">+{remainingCount}</span>
218
+ )}
219
+ </div>
220
+ )}
221
+ </div>
222
+
223
+ {/* Thumbnails placeholder */}
224
+ {showThumbnails && hasMultiple && (
225
+ <div className="hidden md:flex gap-2">
226
+ {images.slice(0, 8).map((image, index) => (
227
+ <div
228
+ key={image.id}
229
+ className={cn(
230
+ 'w-16 h-16 rounded-lg overflow-hidden bg-muted flex-shrink-0',
231
+ index === 0 && 'ring-2 ring-primary'
232
+ )}
233
+ >
234
+ <GalleryMedia
235
+ media={image}
236
+ className="w-full h-full"
237
+ />
238
+ </div>
239
+ ))}
240
+ </div>
241
+ )}
242
+ </div>
243
+ )
244
+ }
245
+
164
246
  return (
165
247
  <div className={cn('space-y-3', className)}>
166
248
  <Carousel
@@ -0,0 +1,134 @@
1
+ import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
2
+ import { JsonSchemaForm } from './index';
3
+
4
+ export default defineStory({
5
+ title: 'Tools/Json Schema Form',
6
+ component: JsonSchemaForm,
7
+ description: 'Dynamic form generator from JSON Schema using react-jsonschema-form.',
8
+ });
9
+
10
+ const SCHEMAS = {
11
+ simple: {
12
+ schema: {
13
+ type: 'object' as const,
14
+ required: ['name', 'email'],
15
+ properties: {
16
+ name: { type: 'string' as const, title: 'Name' },
17
+ email: { type: 'string' as const, title: 'Email', format: 'email' },
18
+ age: { type: 'integer' as const, title: 'Age', minimum: 0, maximum: 120 },
19
+ },
20
+ },
21
+ uiSchema: {},
22
+ },
23
+ vehicle: {
24
+ schema: {
25
+ type: 'object' as const,
26
+ required: ['make', 'model', 'year'],
27
+ properties: {
28
+ make: {
29
+ type: 'string' as const,
30
+ title: 'Make',
31
+ enum: ['BMW', 'Mercedes', 'Audi', 'Porsche', 'Tesla'],
32
+ },
33
+ model: { type: 'string' as const, title: 'Model' },
34
+ year: { type: 'integer' as const, title: 'Year', minimum: 1990, maximum: 2025 },
35
+ price: { type: 'number' as const, title: 'Price ($)' },
36
+ features: {
37
+ type: 'array' as const,
38
+ title: 'Features',
39
+ items: {
40
+ type: 'string' as const,
41
+ enum: ['Leather', 'Navigation', 'Sunroof', 'Heated Seats', 'Parking Sensors'],
42
+ },
43
+ uniqueItems: true,
44
+ },
45
+ description: { type: 'string' as const, title: 'Description' },
46
+ },
47
+ },
48
+ uiSchema: {
49
+ description: { 'ui:widget': 'textarea' },
50
+ features: { 'ui:widget': 'checkboxes' },
51
+ },
52
+ },
53
+ contact: {
54
+ schema: {
55
+ type: 'object' as const,
56
+ required: ['firstName', 'lastName', 'email'],
57
+ properties: {
58
+ firstName: { type: 'string' as const, title: 'First Name' },
59
+ lastName: { type: 'string' as const, title: 'Last Name' },
60
+ email: { type: 'string' as const, title: 'Email', format: 'email' },
61
+ phone: { type: 'string' as const, title: 'Phone' },
62
+ message: { type: 'string' as const, title: 'Message' },
63
+ subscribe: { type: 'boolean' as const, title: 'Subscribe to newsletter' },
64
+ },
65
+ },
66
+ uiSchema: {
67
+ message: { 'ui:widget': 'textarea' },
68
+ },
69
+ },
70
+ };
71
+
72
+ export const Interactive = () => {
73
+ const [schemaType] = useSelect('schemaType', {
74
+ options: ['simple', 'vehicle', 'contact'] as const,
75
+ defaultValue: 'vehicle',
76
+ label: 'Schema Type',
77
+ description: 'Select form schema',
78
+ });
79
+
80
+ const [liveValidate] = useBoolean('liveValidate', {
81
+ defaultValue: false,
82
+ label: 'Live Validate',
83
+ description: 'Validate on every change',
84
+ });
85
+
86
+ const config = SCHEMAS[schemaType];
87
+
88
+ return (
89
+ <div className="max-w-lg">
90
+ <JsonSchemaForm
91
+ schema={config.schema}
92
+ uiSchema={config.uiSchema}
93
+ liveValidate={liveValidate}
94
+ onSubmit={(data) => console.log('Submitted:', data.formData)}
95
+ />
96
+ </div>
97
+ );
98
+ };
99
+
100
+ export const SimpleForm = () => (
101
+ <div className="max-w-md">
102
+ <JsonSchemaForm
103
+ schema={SCHEMAS.simple.schema}
104
+ onSubmit={(data) => console.log('Submitted:', data.formData)}
105
+ />
106
+ </div>
107
+ );
108
+
109
+ export const VehicleForm = () => (
110
+ <div className="max-w-lg">
111
+ <JsonSchemaForm
112
+ schema={SCHEMAS.vehicle.schema}
113
+ uiSchema={SCHEMAS.vehicle.uiSchema}
114
+ onSubmit={(data) => console.log('Submitted:', data.formData)}
115
+ />
116
+ </div>
117
+ );
118
+
119
+ export const WithDefaultValues = () => (
120
+ <div className="max-w-lg">
121
+ <JsonSchemaForm
122
+ schema={SCHEMAS.vehicle.schema}
123
+ uiSchema={SCHEMAS.vehicle.uiSchema}
124
+ formData={{
125
+ make: 'BMW',
126
+ model: 'X5',
127
+ year: 2023,
128
+ price: 65000,
129
+ features: ['Leather', 'Navigation'],
130
+ }}
131
+ onSubmit={(data) => console.log('Submitted:', data.formData)}
132
+ />
133
+ </div>
134
+ );