@djangocfg/ui-tools 2.1.382 → 2.1.383

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 (62) hide show
  1. package/dist/DictationField-U25MEYAL.mjs +4 -0
  2. package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
  3. package/dist/DictationField-XWR5VOID.cjs +13 -0
  4. package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
  5. package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
  6. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  7. package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
  8. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  9. package/dist/index.cjs +139 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +68 -1
  12. package/dist/index.d.ts +68 -1
  13. package/dist/index.mjs +141 -6
  14. package/dist/index.mjs.map +1 -1
  15. package/package.json +6 -13
  16. package/src/tools/Chat/index.ts +15 -0
  17. package/dist/DictationField-2ZLQWLYV.mjs +0 -4
  18. package/dist/DictationField-IPPJ54CU.cjs +0 -13
  19. package/dist/chunk-4LXG3NBV.mjs.map +0 -1
  20. package/dist/chunk-KMSBGNVC.cjs.map +0 -1
  21. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  22. package/src/stories/index.ts +0 -63
  23. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  24. package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
  25. package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
  26. package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
  27. package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
  28. package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
  29. package/src/tools/Chat/stories/06-header.story.tsx +0 -147
  30. package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
  31. package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
  32. package/src/tools/Chat/stories/shared/index.ts +0 -5
  33. package/src/tools/Chat/stories/shared/messages.ts +0 -39
  34. package/src/tools/Chat/stories/shared/personas.ts +0 -13
  35. package/src/tools/Chat/stories/shared/seeds.ts +0 -92
  36. package/src/tools/Chat/stories/shared/transports.ts +0 -36
  37. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  38. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  39. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  40. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  41. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  42. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  43. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  44. package/src/tools/Map/Map.story.tsx +0 -458
  45. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  46. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  47. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  48. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  49. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
  50. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
  51. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
  52. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
  53. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
  54. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
  55. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
  56. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
  57. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
  58. package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
  59. package/src/tools/Tour/Tour.story.tsx +0 -279
  60. package/src/tools/Tree/Tree.story.tsx +0 -620
  61. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  62. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -1,141 +0,0 @@
1
- import { defineStory, useBoolean, useSelect, useNumber } from '@djangocfg/playground';
2
- import JsonTree from './index';
3
-
4
- export default defineStory({
5
- title: 'Tools/Json Tree',
6
- component: JsonTree,
7
- description: 'Interactive JSON tree viewer with expand/collapse.',
8
- });
9
-
10
- const sampleData = {
11
- user: {
12
- id: 'usr_123',
13
- name: 'John Doe',
14
- email: 'john@example.com',
15
- roles: ['admin', 'user'],
16
- settings: {
17
- theme: 'dark',
18
- notifications: true,
19
- language: 'en',
20
- },
21
- },
22
- metadata: {
23
- createdAt: '2024-01-15T10:30:00Z',
24
- updatedAt: '2024-01-20T14:45:00Z',
25
- version: 2,
26
- },
27
- };
28
-
29
- const apiResponse = {
30
- status: 200,
31
- data: {
32
- vehicles: [
33
- { id: 1, make: 'BMW', model: 'X5', year: 2023, price: 65000 },
34
- { id: 2, make: 'Mercedes', model: 'GLE', year: 2022, price: 72000 },
35
- { id: 3, make: 'Audi', model: 'Q7', year: 2023, price: 68000 },
36
- ],
37
- pagination: {
38
- page: 1,
39
- perPage: 10,
40
- total: 156,
41
- hasMore: true,
42
- },
43
- },
44
- meta: {
45
- requestId: 'req_abc123',
46
- duration: 45,
47
- },
48
- };
49
-
50
- const nestedData = {
51
- level1: {
52
- level2: {
53
- level3: {
54
- level4: {
55
- level5: {
56
- value: 'deeply nested',
57
- },
58
- },
59
- },
60
- },
61
- },
62
- };
63
-
64
- const DATA_SAMPLES = {
65
- user: sampleData,
66
- api: apiResponse,
67
- nested: nestedData,
68
- };
69
-
70
- export const Interactive = () => {
71
- const [dataSource] = useSelect('dataSource', {
72
- options: ['user', 'api', 'nested'] as const,
73
- defaultValue: 'api',
74
- label: 'Data Source',
75
- description: 'Select JSON data to display',
76
- });
77
-
78
- const [showExpandControls] = useBoolean('showExpandControls', {
79
- defaultValue: true,
80
- label: 'Expand Controls',
81
- description: 'Show expand/collapse all buttons',
82
- });
83
-
84
- const [showActionButtons] = useBoolean('showActionButtons', {
85
- defaultValue: true,
86
- label: 'Action Buttons',
87
- description: 'Show copy/download buttons',
88
- });
89
-
90
- const [maxAutoExpandDepth] = useNumber('maxAutoExpandDepth', {
91
- defaultValue: 2,
92
- min: 0,
93
- max: 10,
94
- label: 'Auto Expand Depth',
95
- description: 'Maximum depth to expand automatically',
96
- });
97
-
98
- return (
99
- <div className="max-w-2xl h-96">
100
- <JsonTree
101
- data={DATA_SAMPLES[dataSource]}
102
- config={{
103
- maxAutoExpandDepth,
104
- showExpandControls,
105
- showActionButtons,
106
- className: 'h-full',
107
- }}
108
- />
109
- </div>
110
- );
111
- };
112
-
113
- export const Default = () => (
114
- <div className="max-w-2xl">
115
- <JsonTree data={sampleData} />
116
- </div>
117
- );
118
-
119
- export const APIResponse = () => (
120
- <div className="max-w-2xl">
121
- <JsonTree data={apiResponse} />
122
- </div>
123
- );
124
-
125
- export const DeepNesting = () => (
126
- <div className="max-w-2xl">
127
- <JsonTree data={nestedData} />
128
- </div>
129
- );
130
-
131
- export const CollapsedByDefault = () => (
132
- <div className="max-w-2xl h-96">
133
- <JsonTree data={apiResponse} config={{ maxAutoExpandDepth: 0, className: 'h-full' }} />
134
- </div>
135
- );
136
-
137
- export const WithMaxDepth = () => (
138
- <div className="max-w-2xl h-96">
139
- <JsonTree data={apiResponse} config={{ maxAutoExpandDepth: 2, className: 'h-full' }} />
140
- </div>
141
- );
@@ -1,95 +0,0 @@
1
- import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
2
- import { LottiePlayer } from './index';
3
-
4
- export default defineStory({
5
- title: 'Tools/Lottie Player',
6
- component: LottiePlayer,
7
- description: 'Lottie animation player for JSON animations.',
8
- });
9
-
10
- // Public Lottie animation URLs
11
- const ANIMATIONS = {
12
- loading: 'https://assets2.lottiefiles.com/packages/lf20_usmfx6bp.json',
13
- success: 'https://assets4.lottiefiles.com/packages/lf20_jbrw3hcz.json',
14
- rocket: 'https://assets3.lottiefiles.com/packages/lf20_l3qxn9jy.json',
15
- heart: 'https://assets7.lottiefiles.com/packages/lf20_3vbOcw.json',
16
- };
17
-
18
- export const Interactive = () => {
19
- const [animation] = useSelect('animation', {
20
- options: ['loading', 'success', 'rocket', 'heart'] as const,
21
- defaultValue: 'rocket',
22
- label: 'Animation',
23
- description: 'Select Lottie animation',
24
- });
25
-
26
- const [loop] = useBoolean('loop', {
27
- defaultValue: true,
28
- label: 'Loop',
29
- description: 'Loop animation playback',
30
- });
31
-
32
- const [autoplay] = useBoolean('autoplay', {
33
- defaultValue: true,
34
- label: 'Autoplay',
35
- description: 'Auto-start animation',
36
- });
37
-
38
- const [controls] = useBoolean('controls', {
39
- defaultValue: false,
40
- label: 'Show Controls',
41
- description: 'Display playback controls',
42
- });
43
-
44
- return (
45
- <div className="w-64 h-64">
46
- <LottiePlayer
47
- src={ANIMATIONS[animation]}
48
- loop={loop}
49
- autoplay={autoplay}
50
- controls={controls}
51
- />
52
- </div>
53
- );
54
- };
55
-
56
- export const Loading = () => (
57
- <div className="w-48 h-48">
58
- <LottiePlayer src={ANIMATIONS.loading} loop autoplay />
59
- </div>
60
- );
61
-
62
- export const Success = () => (
63
- <div className="w-48 h-48">
64
- <LottiePlayer src={ANIMATIONS.success} autoplay />
65
- </div>
66
- );
67
-
68
- export const Rocket = () => (
69
- <div className="w-64 h-64">
70
- <LottiePlayer src={ANIMATIONS.rocket} loop autoplay />
71
- </div>
72
- );
73
-
74
- export const Heart = () => (
75
- <div className="w-32 h-32">
76
- <LottiePlayer src={ANIMATIONS.heart} loop autoplay />
77
- </div>
78
- );
79
-
80
- export const WithControls = () => (
81
- <div className="w-64 h-64">
82
- <LottiePlayer
83
- src={ANIMATIONS.rocket}
84
- loop
85
- autoplay
86
- controls
87
- />
88
- </div>
89
- );
90
-
91
- export const Paused = () => (
92
- <div className="w-48 h-48">
93
- <LottiePlayer src={ANIMATIONS.loading} loop />
94
- </div>
95
- );
@@ -1,458 +0,0 @@
1
- import { useState, useMemo } from 'react';
2
- import { defineStory, useSelect, useNumber, useBoolean } from '@djangocfg/playground';
3
- import { MapContainer, MapMarker, MapPopup, MapProvider, MapView, MapCluster, useMapControl } from './index';
4
- import { offsetOverlappingMarkers } from './utils/spiderfy';
5
-
6
- export default defineStory({
7
- title: 'Tools/Map',
8
- component: MapContainer,
9
- description: 'Interactive map component using MapLibre GL.',
10
- });
11
-
12
- // Sample property locations
13
- const PROPERTIES = {
14
- dubai: {
15
- id: 'dubai-marina',
16
- latitude: 25.0805,
17
- longitude: 55.1403,
18
- name: 'Dubai Marina Tower',
19
- address: 'Dubai Marina, Dubai, UAE',
20
- price: '$2,500,000',
21
- },
22
- london: {
23
- id: 'london-chelsea',
24
- latitude: 51.4875,
25
- longitude: -0.1687,
26
- name: 'Chelsea Townhouse',
27
- address: 'Chelsea, London, UK',
28
- price: '£3,200,000',
29
- },
30
- nyc: {
31
- id: 'nyc-manhattan',
32
- latitude: 40.7580,
33
- longitude: -73.9855,
34
- name: 'Manhattan Penthouse',
35
- address: 'Midtown, New York, USA',
36
- price: '$5,800,000',
37
- },
38
- paris: {
39
- id: 'paris-marais',
40
- latitude: 48.8566,
41
- longitude: 2.3522,
42
- name: 'Le Marais Apartment',
43
- address: 'Le Marais, Paris, France',
44
- price: '€1,900,000',
45
- },
46
- tokyo: {
47
- id: 'tokyo-shibuya',
48
- latitude: 35.6595,
49
- longitude: 139.7004,
50
- name: 'Shibuya Residence',
51
- address: 'Shibuya, Tokyo, Japan',
52
- price: '¥180,000,000',
53
- },
54
- };
55
-
56
- export const Interactive = () => {
57
- const [property] = useSelect('property', {
58
- options: ['dubai', 'london', 'nyc', 'paris', 'tokyo'] as const,
59
- defaultValue: 'dubai',
60
- label: 'Property',
61
- description: 'Select property location',
62
- });
63
-
64
- const [mapStyle] = useSelect('mapStyle', {
65
- options: ['light', 'dark', 'streets', 'satellite'] as const,
66
- defaultValue: 'light',
67
- label: 'Map Style',
68
- description: 'Map visual style',
69
- });
70
-
71
- const [zoom] = useNumber('zoom', {
72
- defaultValue: 14,
73
- min: 1,
74
- max: 18,
75
- label: 'Zoom Level',
76
- description: 'Map zoom level',
77
- });
78
-
79
- const [showMarker] = useBoolean('showMarker', {
80
- defaultValue: true,
81
- label: 'Show Marker',
82
- description: 'Display property marker',
83
- });
84
-
85
- const [showResetButton] = useBoolean('showResetButton', {
86
- defaultValue: true,
87
- label: 'Reset Button',
88
- description: 'Show reset view button',
89
- });
90
-
91
- const loc = PROPERTIES[property];
92
- const googleMapsUrl = `https://www.google.com/maps?q=${loc.latitude},${loc.longitude}`;
93
-
94
- return (
95
- <div className="space-y-4">
96
- {/* Property info */}
97
- <div className="p-4 rounded-lg border border-border bg-card">
98
- <h3 className="font-semibold text-foreground">{loc.name}</h3>
99
- <p className="text-sm text-muted-foreground">{loc.address}</p>
100
- <p className="text-lg font-bold text-primary mt-2">{loc.price}</p>
101
- </div>
102
-
103
- {/* Map */}
104
- <div className="h-80 rounded-xl overflow-hidden border border-border" key={`${property}-${zoom}-${mapStyle}`}>
105
- <MapContainer
106
- initialViewport={{
107
- latitude: loc.latitude,
108
- longitude: loc.longitude,
109
- zoom,
110
- }}
111
- mapStyle={mapStyle}
112
- openInMapsUrl={googleMapsUrl}
113
- showResetButton={showResetButton}
114
- autoResetDelay={5000}
115
- >
116
- {showMarker && (
117
- <MapMarker marker={loc} color="#10b981" size={32} />
118
- )}
119
- </MapContainer>
120
- </div>
121
- </div>
122
- );
123
- };
124
-
125
- export const PropertyCard = () => {
126
- const loc = PROPERTIES.dubai;
127
- const googleMapsUrl = `https://www.google.com/maps?q=${loc.latitude},${loc.longitude}`;
128
-
129
- return (
130
- <div className="max-w-md rounded-xl border border-border bg-card overflow-hidden">
131
- {/* Map */}
132
- <div className="aspect-[2/1]">
133
- <MapContainer
134
- initialViewport={{
135
- latitude: loc.latitude,
136
- longitude: loc.longitude,
137
- zoom: 14,
138
- }}
139
- mapStyle="light"
140
- attributionControl={false}
141
- openInMapsUrl={googleMapsUrl}
142
- autoResetDelay={5000}
143
- >
144
- <MapMarker marker={loc} color="#10b981" size={32} />
145
- </MapContainer>
146
- </div>
147
-
148
- {/* Property info */}
149
- <div className="p-4">
150
- <h3 className="font-semibold text-foreground">{loc.name}</h3>
151
- <p className="text-sm text-muted-foreground">{loc.address}</p>
152
- <p className="text-lg font-bold text-primary mt-2">{loc.price}</p>
153
- </div>
154
- </div>
155
- );
156
- };
157
-
158
- export const DarkStyle = () => {
159
- const loc = PROPERTIES.tokyo;
160
-
161
- return (
162
- <div className="h-96 rounded-xl overflow-hidden border border-border">
163
- <MapContainer
164
- initialViewport={{
165
- latitude: loc.latitude,
166
- longitude: loc.longitude,
167
- zoom: 13,
168
- }}
169
- mapStyle="dark"
170
- >
171
- <MapMarker marker={loc} color="#f97316" size={28} />
172
- </MapContainer>
173
- </div>
174
- );
175
- };
176
-
177
- export const MultipleMarkers = () => (
178
- <div className="h-96 rounded-xl overflow-hidden border border-border">
179
- <MapContainer
180
- initialViewport={{
181
- latitude: 40,
182
- longitude: 0,
183
- zoom: 2,
184
- }}
185
- mapStyle="light"
186
- showResetButton
187
- >
188
- {Object.values(PROPERTIES).map((prop) => (
189
- <MapMarker key={prop.id} marker={prop} color="#3b82f6" size={24} />
190
- ))}
191
- </MapContainer>
192
- </div>
193
- );
194
-
195
- export const WithAutoReset = () => {
196
- const loc = PROPERTIES.london;
197
-
198
- return (
199
- <div className="space-y-2">
200
- <p className="text-sm text-muted-foreground">
201
- Pan/zoom the map — it will reset after 3 seconds of inactivity
202
- </p>
203
- <div className="h-80 rounded-xl overflow-hidden border border-border">
204
- <MapContainer
205
- initialViewport={{
206
- latitude: loc.latitude,
207
- longitude: loc.longitude,
208
- zoom: 15,
209
- }}
210
- mapStyle="streets"
211
- autoResetDelay={3000}
212
- showResetButton
213
- >
214
- <MapMarker marker={loc} color="#ec4899" size={32} />
215
- </MapContainer>
216
- </div>
217
- </div>
218
- );
219
- };
220
-
221
- type PropertyKey = keyof typeof PROPERTIES;
222
-
223
- // Inner component that uses useMapControl (must be inside MapProvider)
224
- function MapWithPopupInner({
225
- selectedId,
226
- setSelectedId,
227
- }: {
228
- selectedId: PropertyKey | null;
229
- setSelectedId: (id: PropertyKey | null) => void;
230
- }) {
231
- const { flyTo } = useMapControl();
232
- const selectedProperty = selectedId ? PROPERTIES[selectedId] : null;
233
-
234
- const handleMarkerClick = (key: PropertyKey) => {
235
- const prop = PROPERTIES[key];
236
- setSelectedId(key);
237
- // Fly to the selected marker with animation
238
- flyTo([prop.longitude, prop.latitude], 12, { duration: 1500 });
239
- };
240
-
241
- return (
242
- <MapView mapStyle="light" showResetButton>
243
- {(Object.entries(PROPERTIES) as [PropertyKey, typeof PROPERTIES[PropertyKey]][]).map(([key, prop]) => (
244
- <MapMarker
245
- key={prop.id}
246
- marker={prop}
247
- color={selectedId === key ? '#10b981' : '#3b82f6'}
248
- size={selectedId === key ? 36 : 28}
249
- onClick={() => handleMarkerClick(key)}
250
- />
251
- ))}
252
-
253
- {selectedProperty && (
254
- <MapPopup
255
- longitude={selectedProperty.longitude}
256
- latitude={selectedProperty.latitude}
257
- onClose={() => setSelectedId(null)}
258
- anchor="bottom"
259
- offset={20}
260
- >
261
- <div className="p-3 min-w-48">
262
- <h3 className="font-semibold text-foreground text-sm">{selectedProperty.name}</h3>
263
- <p className="text-xs text-muted-foreground mt-1">{selectedProperty.address}</p>
264
- <p className="text-base font-bold text-primary mt-2">{selectedProperty.price}</p>
265
- <a
266
- href={`https://www.google.com/maps?q=${selectedProperty.latitude},${selectedProperty.longitude}`}
267
- target="_blank"
268
- rel="noopener noreferrer"
269
- className="mt-3 block w-full text-center text-xs px-3 py-1.5 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
270
- >
271
- View on Google Maps
272
- </a>
273
- </div>
274
- </MapPopup>
275
- )}
276
- </MapView>
277
- );
278
- }
279
-
280
- export const WithPopup = () => {
281
- const [selectedId, setSelectedId] = useState<PropertyKey | null>(null);
282
-
283
- return (
284
- <div className="space-y-2">
285
- <p className="text-sm text-muted-foreground">
286
- Click on markers to fly to property and see details
287
- </p>
288
- <div className="h-96 rounded-xl overflow-hidden border border-border">
289
- <MapProvider
290
- initialViewport={{
291
- latitude: 40,
292
- longitude: 0,
293
- zoom: 2,
294
- }}
295
- >
296
- <MapWithPopupInner selectedId={selectedId} setSelectedId={setSelectedId} />
297
- </MapProvider>
298
- </div>
299
- </div>
300
- );
301
- };
302
-
303
- // =============================================================================
304
- // Spiderfy Demo - Overlapping Markers
305
- // =============================================================================
306
-
307
- // Generate multiple points at the same location (simulating overlapping markers)
308
- const SPIDERFY_POINTS: GeoJSON.FeatureCollection = {
309
- type: 'FeatureCollection',
310
- features: [
311
- // Group 1: 5 apartments at same building in Paris
312
- ...Array.from({ length: 5 }, (_, i) => ({
313
- type: 'Feature' as const,
314
- properties: {
315
- id: `paris-apt-${i + 1}`,
316
- name: `Paris Apartment ${i + 1}`,
317
- floor: i + 1,
318
- price: `€${(800 + i * 50).toLocaleString()},000`,
319
- },
320
- geometry: {
321
- type: 'Point' as const,
322
- coordinates: [2.3522, 48.8566], // Exact same location
323
- },
324
- })),
325
- // Group 2: 3 offices at same building in London
326
- ...Array.from({ length: 3 }, (_, i) => ({
327
- type: 'Feature' as const,
328
- properties: {
329
- id: `london-office-${i + 1}`,
330
- name: `London Office ${i + 1}`,
331
- floor: i * 2 + 1,
332
- price: `£${(1200 + i * 100).toLocaleString()},000`,
333
- },
334
- geometry: {
335
- type: 'Point' as const,
336
- coordinates: [-0.1276, 51.5074], // Same location
337
- },
338
- })),
339
- // Group 3: 8 units in NYC building
340
- ...Array.from({ length: 8 }, (_, i) => ({
341
- type: 'Feature' as const,
342
- properties: {
343
- id: `nyc-unit-${i + 1}`,
344
- name: `NYC Unit ${i + 1}`,
345
- floor: i + 1,
346
- price: `$${(500 + i * 75).toLocaleString()},000`,
347
- },
348
- geometry: {
349
- type: 'Point' as const,
350
- coordinates: [-73.9857, 40.7484], // Same location (Empire State)
351
- },
352
- })),
353
- // Single point in Tokyo (no overlap)
354
- {
355
- type: 'Feature' as const,
356
- properties: {
357
- id: 'tokyo-single',
358
- name: 'Tokyo Tower View',
359
- floor: 1,
360
- price: '¥95,000,000',
361
- },
362
- geometry: {
363
- type: 'Point' as const,
364
- coordinates: [139.7454, 35.6586],
365
- },
366
- },
367
- ],
368
- };
369
-
370
- export const SpiderfyCluster = () => {
371
- const [selectedFeature, setSelectedFeature] = useState<GeoJSON.Feature | null>(null);
372
-
373
- // Pre-process data: offset overlapping points so they're visible when unclustered
374
- const processedData = useMemo(() => {
375
- // Convert GeoJSON features to MarkerData format for offsetting
376
- const markers = SPIDERFY_POINTS.features.map((f, i) => ({
377
- id: f.properties?.id || `point-${i}`,
378
- longitude: (f.geometry as GeoJSON.Point).coordinates[0],
379
- latitude: (f.geometry as GeoJSON.Point).coordinates[1],
380
- data: f.properties,
381
- }));
382
-
383
- // Apply offset to overlapping markers
384
- const offsetMarkers = offsetOverlappingMarkers(markers, {
385
- spiralRadius: 0.0003, // ~30m spread
386
- });
387
-
388
- // Convert back to GeoJSON
389
- return {
390
- type: 'FeatureCollection' as const,
391
- features: offsetMarkers.map((m) => ({
392
- type: 'Feature' as const,
393
- properties: m.data,
394
- geometry: {
395
- type: 'Point' as const,
396
- coordinates: [m.longitude, m.latitude],
397
- },
398
- })),
399
- };
400
- }, []);
401
-
402
- return (
403
- <div className="space-y-2">
404
- <p className="text-sm text-muted-foreground">
405
- Overlapping points are automatically offset so they're visible at high zoom.
406
- <br />
407
- <span className="text-xs">Paris: 5 apartments • London: 3 offices • NYC: 8 units • Tokyo: 1 property</span>
408
- </p>
409
- <div className="h-[500px] rounded-xl overflow-hidden border border-border">
410
- <MapContainer
411
- initialViewport={{
412
- latitude: 45,
413
- longitude: -20,
414
- zoom: 2,
415
- }}
416
- mapStyle="light"
417
- showResetButton
418
- >
419
- <MapCluster
420
- sourceId="spiderfy-demo"
421
- data={processedData}
422
- clusterRadius={60}
423
- clusterMaxZoom={14}
424
- colors={['#3b82f6', '#8b5cf6', '#ec4899']}
425
- onPointClick={(feature) => setSelectedFeature(feature)}
426
- renderPopup={(feature, onClose) => (
427
- <div className="p-3 min-w-48 bg-card rounded-lg shadow-lg border">
428
- <h3 className="font-semibold text-foreground text-sm">
429
- {feature.properties?.name}
430
- </h3>
431
- <p className="text-xs text-muted-foreground mt-1">
432
- Floor {feature.properties?.floor}
433
- </p>
434
- <p className="text-base font-bold text-primary mt-2">
435
- {feature.properties?.price}
436
- </p>
437
- <button
438
- onClick={onClose}
439
- className="mt-3 w-full text-xs px-3 py-1.5 bg-muted text-muted-foreground rounded-md hover:bg-muted/80 transition-colors"
440
- >
441
- Close
442
- </button>
443
- </div>
444
- )}
445
- popupAnchor="bottom"
446
- popupOffset={15}
447
- />
448
- </MapContainer>
449
- </div>
450
- {selectedFeature && (
451
- <div className="p-3 rounded-lg border border-border bg-card text-sm">
452
- <span className="text-muted-foreground">Last clicked: </span>
453
- <span className="font-medium">{selectedFeature.properties?.name}</span>
454
- </div>
455
- )}
456
- </div>
457
- );
458
- };