@djangocfg/ui-tools 2.1.382 → 2.1.384
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/dist/ChatRoot-JVR3M3H2.mjs +5 -0
- package/dist/{ChatRoot-6IZFM5HM.mjs.map → ChatRoot-JVR3M3H2.mjs.map} +1 -1
- package/dist/ChatRoot-LXIUBOXF.cjs +14 -0
- package/dist/{ChatRoot-LW4XNIKP.cjs.map → ChatRoot-LXIUBOXF.cjs.map} +1 -1
- package/dist/DictationField-U25MEYAL.mjs +4 -0
- package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
- package/dist/DictationField-XWR5VOID.cjs +13 -0
- package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
- package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
- package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
- package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
- package/dist/chunk-C2YN6WEO.mjs.map +1 -0
- package/dist/{chunk-OZAU3QWD.cjs → chunk-HPK3EWBF.cjs} +8 -8
- package/dist/chunk-HPK3EWBF.cjs.map +1 -0
- package/dist/{chunk-UWVP6LCW.mjs → chunk-PEKBT75W.mjs} +8 -8
- package/dist/chunk-PEKBT75W.mjs.map +1 -0
- package/dist/index.cjs +192 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -6
- package/dist/index.d.ts +78 -6
- package/dist/index.mjs +143 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -13
- package/src/tools/Chat/core/audio/defaults.ts +16 -11
- package/src/tools/Chat/core/audio/sounds/error.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/mention.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/notification.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/received.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/sent.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/start.ts +3 -0
- package/src/tools/Chat/index.ts +15 -0
- package/src/tools/SpeechRecognition/core/audio/defaults.ts +4 -4
- package/dist/ChatRoot-6IZFM5HM.mjs +0 -5
- package/dist/ChatRoot-LW4XNIKP.cjs +0 -14
- package/dist/DictationField-2ZLQWLYV.mjs +0 -4
- package/dist/DictationField-IPPJ54CU.cjs +0 -13
- package/dist/chunk-4LXG3NBV.mjs.map +0 -1
- package/dist/chunk-KMSBGNVC.cjs.map +0 -1
- package/dist/chunk-OZAU3QWD.cjs.map +0 -1
- package/dist/chunk-UWVP6LCW.mjs.map +0 -1
- package/src/audio-assets.d.ts +0 -8
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
- package/src/stories/index.ts +0 -63
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
- package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
- package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
- package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
- package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
- package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
- package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
- package/src/tools/Chat/stories/06-header.story.tsx +0 -147
- package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
- package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
- package/src/tools/Chat/stories/shared/index.ts +0 -5
- package/src/tools/Chat/stories/shared/messages.ts +0 -39
- package/src/tools/Chat/stories/shared/personas.ts +0 -13
- package/src/tools/Chat/stories/shared/seeds.ts +0 -92
- package/src/tools/Chat/stories/shared/transports.ts +0 -36
- package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
- package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
- package/src/tools/Gallery/Gallery.story.tsx +0 -237
- package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
- package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
- package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
- package/src/tools/Map/Map.story.tsx +0 -458
- package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
- package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
- package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
- package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
- package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
- package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
- package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
- package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
- package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
- package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
- package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
- package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
- package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
- package/src/tools/Tour/Tour.story.tsx +0 -279
- package/src/tools/Tree/Tree.story.tsx +0 -620
- package/src/tools/Uploader/Uploader.story.tsx +0 -415
- package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
|
@@ -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
|
-
};
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { defineStory } from '@djangocfg/playground';
|
|
2
|
-
import { MarkdownEditor } from './MarkdownEditor';
|
|
3
|
-
import { mentionPresets } from './mentionPresets';
|
|
4
|
-
import { useState, type ReactNode } from 'react';
|
|
5
|
-
import type { MentionConfig, MentionMarkdownRenderer } from './types';
|
|
6
|
-
|
|
7
|
-
export default defineStory({
|
|
8
|
-
title: 'Tools/Markdown Editor',
|
|
9
|
-
component: MarkdownEditor,
|
|
10
|
-
description: 'WYSIWYG markdown editor with Tiptap. Supports headings, lists, mentions, and more.',
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const SAMPLE_MARKDOWN = `# Character Bio
|
|
14
|
-
|
|
15
|
-
**Type:** Maltipoo (apricot coat)
|
|
16
|
-
**Age:** 3 years
|
|
17
|
-
|
|
18
|
-
## Visual Design
|
|
19
|
-
|
|
20
|
-
Medium-small Maltipoo, compact and agile.
|
|
21
|
-
|
|
22
|
-
- Sharp, intelligent eyes (dark brown)
|
|
23
|
-
- Simple collar (hides micro-gear)
|
|
24
|
-
- Expressions shift quickly
|
|
25
|
-
|
|
26
|
-
## Personality
|
|
27
|
-
|
|
28
|
-
> Pixar Note: Her face must carry duality.
|
|
29
|
-
|
|
30
|
-
1. Emotionally guarded
|
|
31
|
-
2. Independent and decisive
|
|
32
|
-
3. Dry sense of humor
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
const MENTION_ITEMS: MentionConfig = {
|
|
36
|
-
items: [
|
|
37
|
-
{ id: '1', label: 'Alice', description: 'Protagonist', thumbnail: 'https://i.pravatar.cc/48?u=alice' },
|
|
38
|
-
{ id: '2', label: 'Bob', description: 'Antagonist', thumbnail: 'https://i.pravatar.cc/48?u=bob' },
|
|
39
|
-
{ id: '3', label: 'Charlie', description: 'Side character', thumbnail: 'https://i.pravatar.cc/48?u=charlie' },
|
|
40
|
-
{ id: '4', label: 'Diana', description: 'Narrator' },
|
|
41
|
-
{ id: '5', label: 'Eve', description: 'Mystery character' },
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export function Default() {
|
|
46
|
-
const [value, setValue] = useState(SAMPLE_MARKDOWN);
|
|
47
|
-
return (
|
|
48
|
-
<div style={{ maxWidth: 700 }}>
|
|
49
|
-
<MarkdownEditor value={value} onChange={setValue} />
|
|
50
|
-
<RawPreview value={value} />
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function WithMentions() {
|
|
56
|
-
const [value, setValue] = useState('Hello @Alice! This scene features @Bob too.\n\nType @ to mention characters.');
|
|
57
|
-
const [ids, setIds] = useState<string[]>([]);
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div style={{ maxWidth: 700 }}>
|
|
61
|
-
<MarkdownEditor
|
|
62
|
-
value={value}
|
|
63
|
-
onChange={setValue}
|
|
64
|
-
mentions={MENTION_ITEMS}
|
|
65
|
-
onMentionIdsChange={setIds}
|
|
66
|
-
placeholder="Describe the scene... Use @ to mention characters"
|
|
67
|
-
/>
|
|
68
|
-
{ids.length > 0 && (
|
|
69
|
-
<div style={{ marginTop: 8, fontSize: 12, opacity: 0.6 }}>
|
|
70
|
-
Mentioned IDs: {ids.join(', ')}
|
|
71
|
-
</div>
|
|
72
|
-
)}
|
|
73
|
-
<RawPreview value={value} />
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function Empty() {
|
|
79
|
-
const [value, setValue] = useState('');
|
|
80
|
-
return (
|
|
81
|
-
<div style={{ maxWidth: 600 }}>
|
|
82
|
-
<MarkdownEditor value={value} onChange={setValue} placeholder="Start writing..." />
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function Disabled() {
|
|
88
|
-
return (
|
|
89
|
-
<div style={{ maxWidth: 600 }}>
|
|
90
|
-
<MarkdownEditor value="This editor is **read-only**." onChange={() => {}} disabled />
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function NoToolbar() {
|
|
96
|
-
const [value, setValue] = useState('Plain text without toolbar.\n\nStill supports **markdown** shortcuts.');
|
|
97
|
-
return (
|
|
98
|
-
<div style={{ maxWidth: 600 }}>
|
|
99
|
-
<MarkdownEditor value={value} onChange={setValue} showToolbar={false} />
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function Compact() {
|
|
105
|
-
const [value, setValue] = useState('Short note');
|
|
106
|
-
return (
|
|
107
|
-
<div style={{ maxWidth: 400 }}>
|
|
108
|
-
<MarkdownEditor value={value} onChange={setValue} minHeight={60} placeholder="Quick note..." />
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function WithCustomUriPreset() {
|
|
114
|
-
const [value, setValue] = useState(
|
|
115
|
-
'Reference @Alice and @Bob — markdown will carry machine-readable URIs.\n\nType @ to add more.',
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<SerializationDemo
|
|
120
|
-
description={
|
|
121
|
-
<>
|
|
122
|
-
<code>mentionPresets.customUri('myapp', 'user')</code> — emits
|
|
123
|
-
<code>{' @[Label](myapp://user/id)'}</code>. Useful when downstream
|
|
124
|
-
parses the markdown back into deep-links while keeping the visible
|
|
125
|
-
<code>@</code> handle.
|
|
126
|
-
</>
|
|
127
|
-
}
|
|
128
|
-
value={value}
|
|
129
|
-
onChange={setValue}
|
|
130
|
-
renderMarkdown={mentionPresets.customUri('myapp', 'user')}
|
|
131
|
-
/>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function WithMarkdownLinkPreset() {
|
|
136
|
-
const [value, setValue] = useState('Ping @Alice when ready, cc @Charlie.');
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<SerializationDemo
|
|
140
|
-
description={
|
|
141
|
-
<>
|
|
142
|
-
<code>mentionPresets.markdownLink('https://example.com/u/')</code> —
|
|
143
|
-
serializes mentions as ordinary clickable links: <code>[@Label](https://example.com/u/id)</code>.
|
|
144
|
-
</>
|
|
145
|
-
}
|
|
146
|
-
value={value}
|
|
147
|
-
onChange={setValue}
|
|
148
|
-
renderMarkdown={mentionPresets.markdownLink('https://example.com/u/')}
|
|
149
|
-
/>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function WithCustomRenderer() {
|
|
154
|
-
const [value, setValue] = useState('Tag @Alice and @Bob to assign the task.');
|
|
155
|
-
|
|
156
|
-
// Hand-rolled serializer — any (attrs) => string works.
|
|
157
|
-
const renderMarkdown: MentionMarkdownRenderer = ({ id, label }) =>
|
|
158
|
-
`{{user:${id}|${label || 'unknown'}}}`;
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<SerializationDemo
|
|
162
|
-
description={
|
|
163
|
-
<>
|
|
164
|
-
Inline custom renderer: <code>{`({ id, label }) => \`{{user:\${id}|\${label}}}\``}</code>.
|
|
165
|
-
Shows that the serializer is just a function — you control the wire format.
|
|
166
|
-
</>
|
|
167
|
-
}
|
|
168
|
-
value={value}
|
|
169
|
-
onChange={setValue}
|
|
170
|
-
renderMarkdown={renderMarkdown}
|
|
171
|
-
/>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
interface SerializationDemoProps {
|
|
176
|
-
description: ReactNode;
|
|
177
|
-
value: string;
|
|
178
|
-
onChange: (v: string) => void;
|
|
179
|
-
renderMarkdown: MentionMarkdownRenderer;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function SerializationDemo({ description, value, onChange, renderMarkdown }: SerializationDemoProps) {
|
|
183
|
-
const mentions: MentionConfig = { ...MENTION_ITEMS, renderMarkdown };
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, maxWidth: 900 }}>
|
|
187
|
-
<div>
|
|
188
|
-
<p style={{ fontSize: 12, opacity: 0.7, marginBottom: 8 }}>{description}</p>
|
|
189
|
-
<MarkdownEditor
|
|
190
|
-
value={value}
|
|
191
|
-
onChange={onChange}
|
|
192
|
-
mentions={mentions}
|
|
193
|
-
placeholder="Type @ to insert a mention..."
|
|
194
|
-
/>
|
|
195
|
-
</div>
|
|
196
|
-
<div>
|
|
197
|
-
<div style={{ fontSize: 11, opacity: 0.5, marginBottom: 4, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
|
198
|
-
Serialized markdown
|
|
199
|
-
</div>
|
|
200
|
-
<pre
|
|
201
|
-
style={{
|
|
202
|
-
fontSize: 12,
|
|
203
|
-
padding: 12,
|
|
204
|
-
background: 'rgba(127,127,127,0.08)',
|
|
205
|
-
borderRadius: 6,
|
|
206
|
-
whiteSpace: 'pre-wrap',
|
|
207
|
-
wordBreak: 'break-word',
|
|
208
|
-
margin: 0,
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
{value || '(empty)'}
|
|
212
|
-
</pre>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function RawPreview({ value }: { value: string }) {
|
|
219
|
-
return (
|
|
220
|
-
<details style={{ marginTop: 16 }}>
|
|
221
|
-
<summary style={{ cursor: 'pointer', opacity: 0.5, fontSize: 12 }}>Raw markdown</summary>
|
|
222
|
-
<pre style={{ fontSize: 11, opacity: 0.6, whiteSpace: 'pre-wrap', marginTop: 8 }}>{value}</pre>
|
|
223
|
-
</details>
|
|
224
|
-
);
|
|
225
|
-
}
|