@tpzdsp/next-toolkit 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -2
- package/src/assets/styles/globals.css +2 -0
- package/src/assets/styles/ol.css +122 -0
- package/src/components/Modal/Modal.stories.tsx +252 -0
- package/src/components/Modal/Modal.test.tsx +248 -0
- package/src/components/Modal/Modal.tsx +61 -0
- package/src/components/accordion/Accordion.stories.tsx +235 -0
- package/src/components/accordion/Accordion.test.tsx +199 -0
- package/src/components/accordion/Accordion.tsx +47 -0
- package/src/components/divider/RuleDivider.stories.tsx +255 -0
- package/src/components/divider/RuleDivider.test.tsx +164 -0
- package/src/components/divider/RuleDivider.tsx +18 -0
- package/src/components/index.ts +9 -2
- package/src/components/layout/header/HeaderAuthClient.tsx +16 -8
- package/src/components/layout/header/HeaderNavClient.tsx +2 -2
- package/src/components/map/LayerSwitcherControl.ts +147 -0
- package/src/components/map/Map.tsx +230 -0
- package/src/components/map/MapContext.tsx +211 -0
- package/src/components/map/Popup.tsx +74 -0
- package/src/components/map/basemaps.ts +79 -0
- package/src/components/map/geocoder.ts +61 -0
- package/src/components/map/geometries.ts +60 -0
- package/src/components/map/images/basemaps/OS.png +0 -0
- package/src/components/map/images/basemaps/dark.png +0 -0
- package/src/components/map/images/basemaps/sat-map-tiler.png +0 -0
- package/src/components/map/images/basemaps/satellite-map-tiler.png +0 -0
- package/src/components/map/images/basemaps/satellite.png +0 -0
- package/src/components/map/images/basemaps/streets.png +0 -0
- package/src/components/map/images/openlayers-logo.png +0 -0
- package/src/components/map/index.ts +10 -0
- package/src/components/map/map.ts +40 -0
- package/src/components/map/osOpenNamesSearch.ts +54 -0
- package/src/components/map/projections.ts +14 -0
- package/src/components/select/Select.stories.tsx +336 -0
- package/src/components/select/Select.test.tsx +473 -0
- package/src/components/select/Select.tsx +132 -0
- package/src/components/select/SelectSkeleton.stories.tsx +195 -0
- package/src/components/select/SelectSkeleton.test.tsx +105 -0
- package/src/components/select/SelectSkeleton.tsx +16 -0
- package/src/components/select/common.ts +4 -0
- package/src/contexts/index.ts +0 -5
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useClickOutside.test.ts +290 -0
- package/src/hooks/useClickOutside.ts +26 -0
- package/src/types.ts +51 -1
- package/src/utils/http.ts +143 -0
- package/src/utils/index.ts +1 -0
- package/src/components/link/NextLinkWrapper.tsx +0 -66
- package/src/contexts/ThemeContext.tsx +0 -72
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { transform } from 'ol/proj';
|
|
2
|
+
import proj4 from 'proj4';
|
|
3
|
+
|
|
4
|
+
import './projections';
|
|
5
|
+
|
|
6
|
+
export const EPSG_27700 = 'EPSG:27700';
|
|
7
|
+
export const EPSG_4326 = 'EPSG:4326';
|
|
8
|
+
export const EPSG_3857 = 'EPSG:3857';
|
|
9
|
+
const POINT_LENGTH = 2;
|
|
10
|
+
const BBOX_LENGTH = 4;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts coordinates from EPSG:27700 (OSGB36) to EPSG:4326 (WGS84).
|
|
14
|
+
* Supports single points (x, y) or bounding boxes (xmin, ymin, xmax, ymax).
|
|
15
|
+
*
|
|
16
|
+
* @param coords - Either an [x, y] array or a [xmin, ymin, xmax, ymax] array.
|
|
17
|
+
*
|
|
18
|
+
* @returns Converted coordinates in EPSG:4326.
|
|
19
|
+
*/
|
|
20
|
+
export const transform27700to4326 = (
|
|
21
|
+
coords: [number, number] | [number, number, number, number],
|
|
22
|
+
) => {
|
|
23
|
+
if (coords.length === POINT_LENGTH) {
|
|
24
|
+
// Single point transformation
|
|
25
|
+
return proj4(EPSG_27700, EPSG_4326, coords);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (coords.length === BBOX_LENGTH) {
|
|
29
|
+
// Bounding box transformation (SW and NE corners)
|
|
30
|
+
const [xmin, ymin, xmax, ymax] = coords;
|
|
31
|
+
|
|
32
|
+
const [swLon, swLat] = proj4(EPSG_27700, EPSG_4326, [xmin, ymin]);
|
|
33
|
+
const [neLon, neLat] = proj4(EPSG_27700, EPSG_4326, [xmax, ymax]);
|
|
34
|
+
|
|
35
|
+
const minLon = Math.min(swLon, neLon);
|
|
36
|
+
const maxLon = Math.max(swLon, neLon);
|
|
37
|
+
const minLat = Math.min(swLat, neLat);
|
|
38
|
+
const maxLat = Math.max(swLat, neLat);
|
|
39
|
+
|
|
40
|
+
return [minLon, minLat, maxLon, maxLat]; // Correct bbox
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error('Invalid coordinate input. Must be [x, y] or [xmin, ymin, xmax, ymax].');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Transforms polygon coordinates from one projection to another.
|
|
48
|
+
*
|
|
49
|
+
* @param coords - A polygon in the format [[[x, y], ...]].
|
|
50
|
+
* @param from - Source projection (e.g., 'EPSG:27700').
|
|
51
|
+
* @param to - Target projection (e.g., 'EPSG:3857').
|
|
52
|
+
* @returns Transformed polygon coordinates.
|
|
53
|
+
*/
|
|
54
|
+
export const transformPolygonCoords = (
|
|
55
|
+
coords: number[][][],
|
|
56
|
+
from: string,
|
|
57
|
+
to: string,
|
|
58
|
+
): number[][][] => {
|
|
59
|
+
return coords.map((ring) => ring.map(([x, y]) => transform([x, y], from, to)));
|
|
60
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './basemaps';
|
|
2
|
+
export * from './geocoder';
|
|
3
|
+
export * from './geometries';
|
|
4
|
+
export * from './LayerSwitcherControl';
|
|
5
|
+
export * from './map';
|
|
6
|
+
export * from './Map';
|
|
7
|
+
export * from './MapContext';
|
|
8
|
+
export * from './osOpenNamesSearch';
|
|
9
|
+
export * from './Popup';
|
|
10
|
+
export * from './projections';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Map } from 'ol';
|
|
2
|
+
|
|
3
|
+
import type { PopupDirection } from '../../types';
|
|
4
|
+
|
|
5
|
+
export const LAYER_NAMES = {
|
|
6
|
+
Aoi: 'aoi-layer',
|
|
7
|
+
Boundary: 'boundary-layer',
|
|
8
|
+
SamplingPoints: 'sampling-points-layer',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const POPUP_WIDTH = 300;
|
|
12
|
+
const POPUP_HEIGHT = 300;
|
|
13
|
+
|
|
14
|
+
export const getPopupPositionClass = (coordinate: number[], map: Map): PopupDirection => {
|
|
15
|
+
const pixel = map.getPixelFromCoordinate(coordinate);
|
|
16
|
+
const [x, y] = pixel;
|
|
17
|
+
|
|
18
|
+
const isTop = y > POPUP_HEIGHT;
|
|
19
|
+
const isBottom = y < POPUP_HEIGHT;
|
|
20
|
+
const isLeft = x > POPUP_WIDTH;
|
|
21
|
+
const isRight = x < POPUP_WIDTH;
|
|
22
|
+
|
|
23
|
+
if (isTop && isLeft) {
|
|
24
|
+
return 'top-left';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isBottom && isLeft) {
|
|
28
|
+
return 'bottom-left';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isBottom && isRight) {
|
|
32
|
+
return 'bottom-right';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isTop && isRight) {
|
|
36
|
+
return 'top-right';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'bottom-right';
|
|
40
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FeatureCollection } from 'geojson';
|
|
2
|
+
|
|
3
|
+
export type SearchOption = {
|
|
4
|
+
url?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom provider for OS OpenNames search.
|
|
9
|
+
*/
|
|
10
|
+
export const osOpenNamesSearch = (options?: SearchOption) => {
|
|
11
|
+
const { url } = options ?? {};
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
/**
|
|
15
|
+
* Get the URL and parameters needed for the request.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} opt - Options object containing the query.
|
|
18
|
+
*/
|
|
19
|
+
getParameters: (opt: { query: string }) => {
|
|
20
|
+
return {
|
|
21
|
+
url,
|
|
22
|
+
params: {
|
|
23
|
+
query: opt.query,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Process the API response and format it for ol-geocoder.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} results - OS Names API response.
|
|
32
|
+
*/
|
|
33
|
+
handleResponse: (response: FeatureCollection) => {
|
|
34
|
+
const { features } = response;
|
|
35
|
+
|
|
36
|
+
if (!features?.length) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return features.map((feature) => {
|
|
41
|
+
if (feature.geometry.type === 'Point') {
|
|
42
|
+
return {
|
|
43
|
+
lon: feature.geometry.coordinates[0],
|
|
44
|
+
lat: feature.geometry.coordinates[1],
|
|
45
|
+
address: feature?.properties?.address,
|
|
46
|
+
bbox: feature.bbox,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.error('Geometry type is not Point');
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { register } from 'ol/proj/proj4';
|
|
2
|
+
import proj4 from 'proj4';
|
|
3
|
+
|
|
4
|
+
// Define EPSG:27700 (British National Grid)
|
|
5
|
+
proj4.defs(
|
|
6
|
+
'EPSG:27700',
|
|
7
|
+
'+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' +
|
|
8
|
+
'+x_0=400000 +y_0=-100000 +ellps=airy ' +
|
|
9
|
+
'+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' +
|
|
10
|
+
'+units=m +no_defs',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
// Register the projection with OpenLayers
|
|
14
|
+
register(proj4);
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
import { fn } from '@storybook/test';
|
|
6
|
+
|
|
7
|
+
import { Select } from './Select';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Components/Select',
|
|
11
|
+
component: Select,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
args: {
|
|
17
|
+
onChange: fn(),
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof Select>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
const OPTIONS = [
|
|
25
|
+
{ value: 'chocolate', label: 'Chocolate' },
|
|
26
|
+
{ value: 'strawberry', label: 'Strawberry' },
|
|
27
|
+
{ value: 'vanilla', label: 'Vanilla' },
|
|
28
|
+
{ value: 'mint', label: 'Mint' },
|
|
29
|
+
{ value: 'cookies', label: 'Cookies & Cream' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const GROUPED_OPTIONS = [
|
|
33
|
+
{
|
|
34
|
+
label: 'Fruits',
|
|
35
|
+
options: [
|
|
36
|
+
{ value: 'apple', label: 'Apple' },
|
|
37
|
+
{ value: 'banana', label: 'Banana' },
|
|
38
|
+
{ value: 'orange', label: 'Orange' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'Vegetables',
|
|
43
|
+
options: [
|
|
44
|
+
{ value: 'carrot', label: 'Carrot' },
|
|
45
|
+
{ value: 'broccoli', label: 'Broccoli' },
|
|
46
|
+
{ value: 'spinach', label: 'Spinach' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const USERS = [
|
|
52
|
+
{ value: 'john', label: 'John Doe', email: 'john@example.com' },
|
|
53
|
+
{ value: 'jane', label: 'Jane Smith', email: 'jane@example.com' },
|
|
54
|
+
{ value: 'bob', label: 'Bob Johnson', email: 'bob@example.com' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const FLAVOUR_PLACEHOLDER_TEXT = 'Select a flavour...';
|
|
58
|
+
const FLAVOUR_MULTI_PLACEHOLDER_TEXT = 'Select multiple flavours...';
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
options: OPTIONS,
|
|
63
|
+
placeholder: FLAVOUR_PLACEHOLDER_TEXT,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithDefaultValue: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
options: OPTIONS,
|
|
70
|
+
defaultValue: OPTIONS[0],
|
|
71
|
+
placeholder: FLAVOUR_PLACEHOLDER_TEXT,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const Clearable: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
options: OPTIONS,
|
|
78
|
+
defaultValue: OPTIONS[1],
|
|
79
|
+
isClearable: true,
|
|
80
|
+
placeholder: FLAVOUR_PLACEHOLDER_TEXT,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Searchable: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
options: OPTIONS,
|
|
87
|
+
isSearchable: true,
|
|
88
|
+
placeholder: 'Search and select...',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const MultiSelect: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
options: OPTIONS,
|
|
95
|
+
isMulti: true,
|
|
96
|
+
placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const MultiSelectWithValues: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
options: OPTIONS,
|
|
103
|
+
isMulti: true,
|
|
104
|
+
defaultValue: [OPTIONS[0], OPTIONS[2]],
|
|
105
|
+
placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const MultiSelectClearable: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
options: OPTIONS,
|
|
112
|
+
isMulti: true,
|
|
113
|
+
isClearable: true,
|
|
114
|
+
defaultValue: [OPTIONS[0], OPTIONS[1]],
|
|
115
|
+
placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const Disabled: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
options: OPTIONS,
|
|
122
|
+
isDisabled: true,
|
|
123
|
+
defaultValue: OPTIONS[0],
|
|
124
|
+
placeholder: 'This select is disabled',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const Loading: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
options: OPTIONS,
|
|
131
|
+
isLoading: true,
|
|
132
|
+
placeholder: 'Loading...',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const GroupedOptions: Story = {
|
|
137
|
+
args: {
|
|
138
|
+
options: GROUPED_OPTIONS,
|
|
139
|
+
placeholder: 'Select a food item...',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const NoOptionsMessage: Story = {
|
|
144
|
+
args: {
|
|
145
|
+
options: [],
|
|
146
|
+
placeholder: 'No options available',
|
|
147
|
+
noOptionsMessage: () => 'No options found',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const CustomFormatOptionLabel: Story = {
|
|
152
|
+
args: {
|
|
153
|
+
options: USERS,
|
|
154
|
+
placeholder: 'Select a user...',
|
|
155
|
+
formatOptionLabel: (data: unknown) => {
|
|
156
|
+
const option = data as { value: string; label: string; email: string };
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div>
|
|
160
|
+
<div className="font-medium">{option.label}</div>
|
|
161
|
+
|
|
162
|
+
<div className="text-sm text-gray-500">{option.email}</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const CustomClassNames: Story = {
|
|
170
|
+
args: {
|
|
171
|
+
options: OPTIONS,
|
|
172
|
+
placeholder: 'Custom styled select...',
|
|
173
|
+
classNames: {
|
|
174
|
+
control: () => 'border-2 border-purple-500 rounded-lg',
|
|
175
|
+
option: (state) =>
|
|
176
|
+
// eslint-disable-next-line sonarjs/no-nested-conditional
|
|
177
|
+
state.isSelected ? 'bg-purple-500 text-white' : state.isFocused ? 'bg-purple-100' : '',
|
|
178
|
+
menu: () => 'border-purple-500',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const SmallSize: Story = {
|
|
184
|
+
args: {
|
|
185
|
+
options: OPTIONS,
|
|
186
|
+
placeholder: 'Small select...',
|
|
187
|
+
classNames: {
|
|
188
|
+
control: () => 'min-h-8 text-sm',
|
|
189
|
+
option: () => 'px-2 py-1 text-sm',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const LargeSize: Story = {
|
|
195
|
+
args: {
|
|
196
|
+
options: OPTIONS,
|
|
197
|
+
placeholder: 'Large select...',
|
|
198
|
+
classNames: {
|
|
199
|
+
control: () => 'min-h-12 text-lg',
|
|
200
|
+
option: () => 'px-6 py-3 text-lg',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const ControlledComponent: Story = {
|
|
206
|
+
render: () => {
|
|
207
|
+
const [value, setValue] = useState<{ value: string; label: string } | null>(OPTIONS[0]);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="space-y-4">
|
|
211
|
+
<Select
|
|
212
|
+
options={OPTIONS}
|
|
213
|
+
value={value}
|
|
214
|
+
onChange={(newValue) => setValue(newValue)}
|
|
215
|
+
placeholder="Controlled select..."
|
|
216
|
+
/>
|
|
217
|
+
|
|
218
|
+
<div className="text-sm text-gray-600">Selected: {value ? value.label : 'None'}</div>
|
|
219
|
+
|
|
220
|
+
<button
|
|
221
|
+
onClick={() => setValue(OPTIONS[2])}
|
|
222
|
+
className="px-3 py-1 bg-blue-500 text-white rounded text-sm"
|
|
223
|
+
>
|
|
224
|
+
Set to Vanilla
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
export const WithLongOptions: Story = {
|
|
231
|
+
args: {
|
|
232
|
+
options: [
|
|
233
|
+
{ value: 'short', label: 'Short' },
|
|
234
|
+
{
|
|
235
|
+
value: 'long',
|
|
236
|
+
label: 'This is a very long option that might overflow and needs to be handled properly',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
value: 'medium',
|
|
240
|
+
label: 'Medium length option text',
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
placeholder: 'Select an option...',
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const Async: Story = {
|
|
248
|
+
render: () => {
|
|
249
|
+
const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
|
|
250
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
251
|
+
|
|
252
|
+
const loadOptions = () => {
|
|
253
|
+
setIsLoading(true);
|
|
254
|
+
// Simulate API call
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
setOptions([
|
|
257
|
+
{ value: 'async1', label: 'Async Option 1' },
|
|
258
|
+
{ value: 'async2', label: 'Async Option 2' },
|
|
259
|
+
{ value: 'async3', label: 'Async Option 3' },
|
|
260
|
+
]);
|
|
261
|
+
setIsLoading(false);
|
|
262
|
+
}, 1000);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
loadOptions();
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className="space-y-4">
|
|
271
|
+
<Select options={options} isLoading={isLoading} placeholder="Loading async options..." />
|
|
272
|
+
|
|
273
|
+
<button onClick={loadOptions} className="px-3 py-1 bg-green-500 text-white rounded text-sm">
|
|
274
|
+
Reload Options
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const FormIntegration: Story = {
|
|
282
|
+
render: () => {
|
|
283
|
+
const [formData, setFormData] = useState<{
|
|
284
|
+
flavour: { value: string; label: string } | null;
|
|
285
|
+
toppings: { value: string; label: string }[];
|
|
286
|
+
}>({
|
|
287
|
+
flavour: null,
|
|
288
|
+
toppings: [],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<form className="space-y-4 p-4 border rounded">
|
|
293
|
+
<div>
|
|
294
|
+
<label htmlFor="flavour" className="block text-sm font-medium mb-1">
|
|
295
|
+
Flavour
|
|
296
|
+
</label>
|
|
297
|
+
|
|
298
|
+
<Select
|
|
299
|
+
id="flavour"
|
|
300
|
+
options={OPTIONS}
|
|
301
|
+
value={formData.flavour}
|
|
302
|
+
onChange={(value) => setFormData({ ...formData, flavour: value })}
|
|
303
|
+
placeholder="Select a flavour..."
|
|
304
|
+
isClearable
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div>
|
|
309
|
+
<label htmlFor="toppings" className="block text-sm font-medium mb-1">
|
|
310
|
+
Toppings
|
|
311
|
+
</label>
|
|
312
|
+
|
|
313
|
+
<Select
|
|
314
|
+
id="toppings"
|
|
315
|
+
options={[
|
|
316
|
+
{ value: 'sprinkles', label: 'Sprinkles' },
|
|
317
|
+
{ value: 'nuts', label: 'Nuts' },
|
|
318
|
+
{ value: 'chocolate-chips', label: 'Chocolate Chips' },
|
|
319
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
320
|
+
]}
|
|
321
|
+
value={formData.toppings}
|
|
322
|
+
onChange={(value) =>
|
|
323
|
+
setFormData({ ...formData, toppings: Array.isArray(value) ? [...value] : [] })
|
|
324
|
+
}
|
|
325
|
+
placeholder="Select toppings..."
|
|
326
|
+
isMulti
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div className="text-sm text-gray-600">
|
|
331
|
+
<pre>{JSON.stringify(formData, null, 2)}</pre>
|
|
332
|
+
</div>
|
|
333
|
+
</form>
|
|
334
|
+
);
|
|
335
|
+
},
|
|
336
|
+
};
|