@djangocfg/ui-tools 2.1.119 → 2.1.121
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/JsonTree-G2TPWQ4C.mjs +4 -0
- package/dist/{JsonTree-6RYAOPSS.mjs.map → JsonTree-G2TPWQ4C.mjs.map} +1 -1
- package/dist/JsonTree-TWXUBBIG.cjs +10 -0
- package/dist/{JsonTree-7OH6CIHT.cjs.map → JsonTree-TWXUBBIG.cjs.map} +1 -1
- package/dist/{Mermaid.client-PNXEC6YL.cjs → Mermaid.client-AF4WOQZR.cjs} +9 -11
- package/dist/Mermaid.client-AF4WOQZR.cjs.map +1 -0
- package/dist/{Mermaid.client-OKACITCW.mjs → Mermaid.client-W4QXJX7Q.mjs} +9 -11
- package/dist/Mermaid.client-W4QXJX7Q.mjs.map +1 -0
- package/dist/{PlaygroundLayout-SYMEAG3J.cjs → PlaygroundLayout-RZMJWH3Y.cjs} +25 -25
- package/dist/{PlaygroundLayout-SYMEAG3J.cjs.map → PlaygroundLayout-RZMJWH3Y.cjs.map} +1 -1
- package/dist/{PlaygroundLayout-UQRBU5RH.mjs → PlaygroundLayout-UQABCZ6K.mjs} +4 -4
- package/dist/{PlaygroundLayout-UQRBU5RH.mjs.map → PlaygroundLayout-UQABCZ6K.mjs.map} +1 -1
- package/dist/{chunk-UOMPPIED.mjs → chunk-4G4UGMOP.mjs} +3 -3
- package/dist/chunk-4G4UGMOP.mjs.map +1 -0
- package/dist/{chunk-47T5ECYV.cjs → chunk-CY3CQS26.cjs} +3 -3
- package/dist/chunk-CY3CQS26.cjs.map +1 -0
- package/dist/{chunk-5QT3QYFZ.cjs → chunk-EGYUND4E.cjs} +2 -2
- package/dist/chunk-EGYUND4E.cjs.map +1 -0
- package/dist/{chunk-DI3HUXHK.cjs → chunk-OYLQZT62.cjs} +2 -2
- package/dist/chunk-OYLQZT62.cjs.map +1 -0
- package/dist/{chunk-W6YHQI4F.mjs → chunk-OYYCGIBF.mjs} +2 -2
- package/dist/chunk-OYYCGIBF.mjs.map +1 -0
- package/dist/{chunk-G6PRZP5I.mjs → chunk-XZZ22EHP.mjs} +2 -2
- package/dist/chunk-XZZ22EHP.mjs.map +1 -0
- package/dist/{components-EASJYK45.mjs → components-4YSJ5ALL.mjs} +3 -3
- package/dist/{components-CJ2IB65O.cjs.map → components-4YSJ5ALL.mjs.map} +1 -1
- package/dist/{components-CJ2IB65O.cjs → components-BSTP3VLD.cjs} +7 -7
- package/dist/{components-EASJYK45.mjs.map → components-BSTP3VLD.cjs.map} +1 -1
- package/dist/index.cjs +27 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +10 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -12
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +102 -0
- package/src/tools/Gallery/Gallery.story.tsx +237 -0
- package/src/tools/Gallery/components/compact/GalleryCompact.tsx +105 -10
- package/src/tools/Gallery/components/compact/index.ts +1 -1
- package/src/tools/Gallery/components/index.ts +1 -1
- package/src/tools/Gallery/components/media/GalleryImage.tsx +2 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +86 -1
- package/src/tools/Gallery/index.ts +1 -1
- package/src/tools/JsonForm/JsonForm.story.tsx +134 -0
- package/src/tools/JsonTree/JsonTree.story.tsx +140 -0
- package/src/tools/JsonTree/index.tsx +1 -1
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +95 -0
- package/src/tools/Map/Map.story.tsx +300 -0
- package/src/tools/Mermaid/Mermaid.story.tsx +131 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +2 -5
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +7 -1
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +4 -2
- package/src/tools/Mermaid/index.tsx +1 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +116 -0
- package/src/tools/PrettyCode/index.tsx +1 -1
- package/src/tools/VideoPlayer/VideoPlayer.story.tsx +87 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +2 -2
- package/dist/JsonTree-6RYAOPSS.mjs +0 -4
- package/dist/JsonTree-7OH6CIHT.cjs +0 -10
- package/dist/Mermaid.client-OKACITCW.mjs.map +0 -1
- package/dist/Mermaid.client-PNXEC6YL.cjs.map +0 -1
- package/dist/chunk-47T5ECYV.cjs.map +0 -1
- package/dist/chunk-5QT3QYFZ.cjs.map +0 -1
- package/dist/chunk-DI3HUXHK.cjs.map +0 -1
- package/dist/chunk-G6PRZP5I.mjs.map +0 -1
- package/dist/chunk-UOMPPIED.mjs.map +0 -1
- package/dist/chunk-W6YHQI4F.mjs.map +0 -1
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const Default = () => (
|
|
113
|
+
<div className="max-w-2xl">
|
|
114
|
+
<JsonTree data={sampleData} />
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const APIResponse = () => (
|
|
119
|
+
<div className="max-w-2xl">
|
|
120
|
+
<JsonTree data={apiResponse} />
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export const DeepNesting = () => (
|
|
125
|
+
<div className="max-w-2xl">
|
|
126
|
+
<JsonTree data={nestedData} />
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
export const CollapsedByDefault = () => (
|
|
131
|
+
<div className="max-w-2xl h-96">
|
|
132
|
+
<JsonTree data={apiResponse} config={{ maxAutoExpandDepth: 0 }} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export const WithMaxDepth = () => (
|
|
137
|
+
<div className="max-w-2xl h-96">
|
|
138
|
+
<JsonTree data={apiResponse} config={{ maxAutoExpandDepth: 2 }} />
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
);
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { defineStory, useSelect, useNumber, useBoolean } from '@djangocfg/playground';
|
|
3
|
+
import { MapContainer, MapMarker, MapPopup, MapProvider, MapView, useMapControl } from './index';
|
|
4
|
+
|
|
5
|
+
export default defineStory({
|
|
6
|
+
title: 'Tools/Map',
|
|
7
|
+
component: MapContainer,
|
|
8
|
+
description: 'Interactive map component using MapLibre GL.',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Sample property locations
|
|
12
|
+
const PROPERTIES = {
|
|
13
|
+
dubai: {
|
|
14
|
+
id: 'dubai-marina',
|
|
15
|
+
latitude: 25.0805,
|
|
16
|
+
longitude: 55.1403,
|
|
17
|
+
name: 'Dubai Marina Tower',
|
|
18
|
+
address: 'Dubai Marina, Dubai, UAE',
|
|
19
|
+
price: '$2,500,000',
|
|
20
|
+
},
|
|
21
|
+
london: {
|
|
22
|
+
id: 'london-chelsea',
|
|
23
|
+
latitude: 51.4875,
|
|
24
|
+
longitude: -0.1687,
|
|
25
|
+
name: 'Chelsea Townhouse',
|
|
26
|
+
address: 'Chelsea, London, UK',
|
|
27
|
+
price: '£3,200,000',
|
|
28
|
+
},
|
|
29
|
+
nyc: {
|
|
30
|
+
id: 'nyc-manhattan',
|
|
31
|
+
latitude: 40.7580,
|
|
32
|
+
longitude: -73.9855,
|
|
33
|
+
name: 'Manhattan Penthouse',
|
|
34
|
+
address: 'Midtown, New York, USA',
|
|
35
|
+
price: '$5,800,000',
|
|
36
|
+
},
|
|
37
|
+
paris: {
|
|
38
|
+
id: 'paris-marais',
|
|
39
|
+
latitude: 48.8566,
|
|
40
|
+
longitude: 2.3522,
|
|
41
|
+
name: 'Le Marais Apartment',
|
|
42
|
+
address: 'Le Marais, Paris, France',
|
|
43
|
+
price: '€1,900,000',
|
|
44
|
+
},
|
|
45
|
+
tokyo: {
|
|
46
|
+
id: 'tokyo-shibuya',
|
|
47
|
+
latitude: 35.6595,
|
|
48
|
+
longitude: 139.7004,
|
|
49
|
+
name: 'Shibuya Residence',
|
|
50
|
+
address: 'Shibuya, Tokyo, Japan',
|
|
51
|
+
price: '¥180,000,000',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Interactive = () => {
|
|
56
|
+
const [property] = useSelect('property', {
|
|
57
|
+
options: ['dubai', 'london', 'nyc', 'paris', 'tokyo'] as const,
|
|
58
|
+
defaultValue: 'dubai',
|
|
59
|
+
label: 'Property',
|
|
60
|
+
description: 'Select property location',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const [mapStyle] = useSelect('mapStyle', {
|
|
64
|
+
options: ['light', 'dark', 'streets', 'satellite'] as const,
|
|
65
|
+
defaultValue: 'light',
|
|
66
|
+
label: 'Map Style',
|
|
67
|
+
description: 'Map visual style',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const [zoom] = useNumber('zoom', {
|
|
71
|
+
defaultValue: 14,
|
|
72
|
+
min: 1,
|
|
73
|
+
max: 18,
|
|
74
|
+
label: 'Zoom Level',
|
|
75
|
+
description: 'Map zoom level',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const [showMarker] = useBoolean('showMarker', {
|
|
79
|
+
defaultValue: true,
|
|
80
|
+
label: 'Show Marker',
|
|
81
|
+
description: 'Display property marker',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [showResetButton] = useBoolean('showResetButton', {
|
|
85
|
+
defaultValue: true,
|
|
86
|
+
label: 'Reset Button',
|
|
87
|
+
description: 'Show reset view button',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const loc = PROPERTIES[property];
|
|
91
|
+
const googleMapsUrl = `https://www.google.com/maps?q=${loc.latitude},${loc.longitude}`;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-4">
|
|
95
|
+
{/* Property info */}
|
|
96
|
+
<div className="p-4 rounded-lg border border-border bg-card">
|
|
97
|
+
<h3 className="font-semibold text-foreground">{loc.name}</h3>
|
|
98
|
+
<p className="text-sm text-muted-foreground">{loc.address}</p>
|
|
99
|
+
<p className="text-lg font-bold text-primary mt-2">{loc.price}</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Map */}
|
|
103
|
+
<div className="h-80 rounded-xl overflow-hidden border border-border" key={`${property}-${zoom}-${mapStyle}`}>
|
|
104
|
+
<MapContainer
|
|
105
|
+
initialViewport={{
|
|
106
|
+
latitude: loc.latitude,
|
|
107
|
+
longitude: loc.longitude,
|
|
108
|
+
zoom,
|
|
109
|
+
}}
|
|
110
|
+
mapStyle={mapStyle}
|
|
111
|
+
openInMapsUrl={googleMapsUrl}
|
|
112
|
+
showResetButton={showResetButton}
|
|
113
|
+
autoResetDelay={5000}
|
|
114
|
+
>
|
|
115
|
+
{showMarker && (
|
|
116
|
+
<MapMarker marker={loc} color="#10b981" size={32} />
|
|
117
|
+
)}
|
|
118
|
+
</MapContainer>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const PropertyCard = () => {
|
|
125
|
+
const loc = PROPERTIES.dubai;
|
|
126
|
+
const googleMapsUrl = `https://www.google.com/maps?q=${loc.latitude},${loc.longitude}`;
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="max-w-md rounded-xl border border-border bg-card overflow-hidden">
|
|
130
|
+
{/* Map */}
|
|
131
|
+
<div className="aspect-[2/1]">
|
|
132
|
+
<MapContainer
|
|
133
|
+
initialViewport={{
|
|
134
|
+
latitude: loc.latitude,
|
|
135
|
+
longitude: loc.longitude,
|
|
136
|
+
zoom: 14,
|
|
137
|
+
}}
|
|
138
|
+
mapStyle="light"
|
|
139
|
+
attributionControl={false}
|
|
140
|
+
openInMapsUrl={googleMapsUrl}
|
|
141
|
+
autoResetDelay={5000}
|
|
142
|
+
>
|
|
143
|
+
<MapMarker marker={loc} color="#10b981" size={32} />
|
|
144
|
+
</MapContainer>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Property info */}
|
|
148
|
+
<div className="p-4">
|
|
149
|
+
<h3 className="font-semibold text-foreground">{loc.name}</h3>
|
|
150
|
+
<p className="text-sm text-muted-foreground">{loc.address}</p>
|
|
151
|
+
<p className="text-lg font-bold text-primary mt-2">{loc.price}</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const DarkStyle = () => {
|
|
158
|
+
const loc = PROPERTIES.tokyo;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="h-96 rounded-xl overflow-hidden border border-border">
|
|
162
|
+
<MapContainer
|
|
163
|
+
initialViewport={{
|
|
164
|
+
latitude: loc.latitude,
|
|
165
|
+
longitude: loc.longitude,
|
|
166
|
+
zoom: 13,
|
|
167
|
+
}}
|
|
168
|
+
mapStyle="dark"
|
|
169
|
+
>
|
|
170
|
+
<MapMarker marker={loc} color="#f97316" size={28} />
|
|
171
|
+
</MapContainer>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const MultipleMarkers = () => (
|
|
177
|
+
<div className="h-96 rounded-xl overflow-hidden border border-border">
|
|
178
|
+
<MapContainer
|
|
179
|
+
initialViewport={{
|
|
180
|
+
latitude: 40,
|
|
181
|
+
longitude: 0,
|
|
182
|
+
zoom: 2,
|
|
183
|
+
}}
|
|
184
|
+
mapStyle="light"
|
|
185
|
+
showResetButton
|
|
186
|
+
>
|
|
187
|
+
{Object.values(PROPERTIES).map((prop) => (
|
|
188
|
+
<MapMarker key={prop.id} marker={prop} color="#3b82f6" size={24} />
|
|
189
|
+
))}
|
|
190
|
+
</MapContainer>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
export const WithAutoReset = () => {
|
|
195
|
+
const loc = PROPERTIES.london;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="space-y-2">
|
|
199
|
+
<p className="text-sm text-muted-foreground">
|
|
200
|
+
Pan/zoom the map — it will reset after 3 seconds of inactivity
|
|
201
|
+
</p>
|
|
202
|
+
<div className="h-80 rounded-xl overflow-hidden border border-border">
|
|
203
|
+
<MapContainer
|
|
204
|
+
initialViewport={{
|
|
205
|
+
latitude: loc.latitude,
|
|
206
|
+
longitude: loc.longitude,
|
|
207
|
+
zoom: 15,
|
|
208
|
+
}}
|
|
209
|
+
mapStyle="streets"
|
|
210
|
+
autoResetDelay={3000}
|
|
211
|
+
showResetButton
|
|
212
|
+
>
|
|
213
|
+
<MapMarker marker={loc} color="#ec4899" size={32} />
|
|
214
|
+
</MapContainer>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type PropertyKey = keyof typeof PROPERTIES;
|
|
221
|
+
|
|
222
|
+
// Inner component that uses useMapControl (must be inside MapProvider)
|
|
223
|
+
function MapWithPopupInner({
|
|
224
|
+
selectedId,
|
|
225
|
+
setSelectedId,
|
|
226
|
+
}: {
|
|
227
|
+
selectedId: PropertyKey | null;
|
|
228
|
+
setSelectedId: (id: PropertyKey | null) => void;
|
|
229
|
+
}) {
|
|
230
|
+
const { flyTo } = useMapControl();
|
|
231
|
+
const selectedProperty = selectedId ? PROPERTIES[selectedId] : null;
|
|
232
|
+
|
|
233
|
+
const handleMarkerClick = (key: PropertyKey) => {
|
|
234
|
+
const prop = PROPERTIES[key];
|
|
235
|
+
setSelectedId(key);
|
|
236
|
+
// Fly to the selected marker with animation
|
|
237
|
+
flyTo([prop.longitude, prop.latitude], 12, { duration: 1500 });
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<MapView mapStyle="light" showResetButton>
|
|
242
|
+
{(Object.entries(PROPERTIES) as [PropertyKey, typeof PROPERTIES[PropertyKey]][]).map(([key, prop]) => (
|
|
243
|
+
<MapMarker
|
|
244
|
+
key={prop.id}
|
|
245
|
+
marker={prop}
|
|
246
|
+
color={selectedId === key ? '#10b981' : '#3b82f6'}
|
|
247
|
+
size={selectedId === key ? 36 : 28}
|
|
248
|
+
onClick={() => handleMarkerClick(key)}
|
|
249
|
+
/>
|
|
250
|
+
))}
|
|
251
|
+
|
|
252
|
+
{selectedProperty && (
|
|
253
|
+
<MapPopup
|
|
254
|
+
longitude={selectedProperty.longitude}
|
|
255
|
+
latitude={selectedProperty.latitude}
|
|
256
|
+
onClose={() => setSelectedId(null)}
|
|
257
|
+
anchor="bottom"
|
|
258
|
+
offset={20}
|
|
259
|
+
>
|
|
260
|
+
<div className="p-3 min-w-48">
|
|
261
|
+
<h3 className="font-semibold text-foreground text-sm">{selectedProperty.name}</h3>
|
|
262
|
+
<p className="text-xs text-muted-foreground mt-1">{selectedProperty.address}</p>
|
|
263
|
+
<p className="text-base font-bold text-primary mt-2">{selectedProperty.price}</p>
|
|
264
|
+
<a
|
|
265
|
+
href={`https://www.google.com/maps?q=${selectedProperty.latitude},${selectedProperty.longitude}`}
|
|
266
|
+
target="_blank"
|
|
267
|
+
rel="noopener noreferrer"
|
|
268
|
+
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"
|
|
269
|
+
>
|
|
270
|
+
View on Google Maps
|
|
271
|
+
</a>
|
|
272
|
+
</div>
|
|
273
|
+
</MapPopup>
|
|
274
|
+
)}
|
|
275
|
+
</MapView>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const WithPopup = () => {
|
|
280
|
+
const [selectedId, setSelectedId] = useState<PropertyKey | null>(null);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div className="space-y-2">
|
|
284
|
+
<p className="text-sm text-muted-foreground">
|
|
285
|
+
Click on markers to fly to property and see details
|
|
286
|
+
</p>
|
|
287
|
+
<div className="h-96 rounded-xl overflow-hidden border border-border">
|
|
288
|
+
<MapProvider
|
|
289
|
+
initialViewport={{
|
|
290
|
+
latitude: 40,
|
|
291
|
+
longitude: 0,
|
|
292
|
+
zoom: 2,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<MapWithPopupInner selectedId={selectedId} setSelectedId={setSelectedId} />
|
|
296
|
+
</MapProvider>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
};
|