dune-react 0.0.15 → 0.0.18
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/components/puck-base/button.js +1 -1
- package/dist/components/puck-base/core/fields.d.ts +33 -10
- package/dist/components/puck-base/core/fields.js +27 -3
- package/dist/components/puck-base/core/styles.d.ts +1 -5
- package/dist/components/puck-base/core/styles.js +1 -5
- package/dist/components/puck-base/core/with-editable.js +33 -21
- package/dist/components/puck-base/editor-context.d.ts +2 -0
- package/dist/components/puck-base/fields/action-field.js +19 -43
- package/dist/components/puck-base/fields/auto-field.js +27 -214
- package/dist/components/puck-base/fields/color-field.d.ts +6 -0
- package/dist/components/puck-base/fields/color-field.js +37 -0
- package/dist/components/puck-base/fields/index.d.ts +8 -0
- package/dist/components/puck-base/fields/location-field.d.ts +44 -0
- package/dist/components/puck-base/fields/location-field.js +207 -0
- package/dist/components/puck-base/fields/object-field.d.ts +8 -0
- package/dist/components/puck-base/fields/object-field.js +30 -0
- package/dist/components/puck-base/fields/radio-toggle-field.d.ts +10 -0
- package/dist/components/puck-base/fields/radio-toggle-field.js +53 -0
- package/dist/components/puck-base/fields/virtualized-select-field.d.ts +13 -0
- package/dist/components/puck-base/fields/virtualized-select-field.js +146 -0
- package/dist/components/puck-base/image.js +175 -104
- package/dist/components/puck-base/index.d.ts +2 -3
- package/dist/components/puck-block/location-sections/location-1/index.js +17 -22
- package/dist/components/puck-block/location-sections/location-1/location.d.ts +5 -7
- package/dist/components/puck-block/location-sections/location-1/location.js +15 -12
- package/dist/components/puck-block/location-sections/location-2/index.js +28 -24
- package/dist/components/puck-block/location-sections/location-2/location.d.ts +6 -8
- package/dist/components/puck-block/location-sections/location-2/location.js +18 -15
- package/dist/components/puck-block/location-sections/location-3/index.js +43 -20
- package/dist/components/puck-block/location-sections/location-3/location.d.ts +5 -6
- package/dist/components/puck-block/location-sections/location-3/location.js +96 -86
- package/dist/components/puck-block/location-sections/props.d.ts +9 -10
- package/dist/components/shadcn/slider.js +4 -1
- package/package.json +4 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Input } from "../../shadcn/input.js";
|
|
4
|
+
import { Label } from "../../shadcn/label.js";
|
|
5
|
+
const ColorField = memo(function ColorField2({
|
|
6
|
+
label,
|
|
7
|
+
value,
|
|
8
|
+
onChange
|
|
9
|
+
}) {
|
|
10
|
+
const colorValue = String(value ?? "#000000");
|
|
11
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
|
|
12
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
13
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
14
|
+
/* @__PURE__ */ jsx(
|
|
15
|
+
"input",
|
|
16
|
+
{
|
|
17
|
+
type: "color",
|
|
18
|
+
value: colorValue.startsWith("#") ? colorValue : "#000000",
|
|
19
|
+
onChange: (e) => onChange(e.target.value),
|
|
20
|
+
className: "h-9 w-9 shrink-0 cursor-pointer rounded-md border p-0.5"
|
|
21
|
+
}
|
|
22
|
+
),
|
|
23
|
+
/* @__PURE__ */ jsx(
|
|
24
|
+
Input,
|
|
25
|
+
{
|
|
26
|
+
value: colorValue,
|
|
27
|
+
onChange: (e) => onChange(e.target.value),
|
|
28
|
+
placeholder: "#000000",
|
|
29
|
+
className: "flex-1"
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
] })
|
|
33
|
+
] });
|
|
34
|
+
});
|
|
35
|
+
export {
|
|
36
|
+
ColorField
|
|
37
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { FieldDef, AutoFieldProps, FieldsPanelProps } from "./types";
|
|
2
|
+
export { AutoField } from "./auto-field";
|
|
3
|
+
export { ColorField } from "./color-field";
|
|
4
|
+
export { RadioToggleField } from "./radio-toggle-field";
|
|
5
|
+
export { ObjectField } from "./object-field";
|
|
6
|
+
export { VirtualizedSelectField, LARGE_SELECT_THRESHOLD, } from "./virtualized-select-field";
|
|
7
|
+
export { LocationField } from "./location-field";
|
|
8
|
+
export { ActionField, ACTION_TYPE_FIELD_MAP, ACTION_TYPE_OPTIONS, PageActionFields, ExternalActionFields, EmailActionFields, PhoneActionFields, SectionActionFields, DownloadActionFields, type ActionTypeFieldProps, } from "./action-field";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MapLocation } from "../core/fields";
|
|
2
|
+
interface GLatLng {
|
|
3
|
+
lat(): number;
|
|
4
|
+
lng(): number;
|
|
5
|
+
}
|
|
6
|
+
interface GGeometry {
|
|
7
|
+
location?: GLatLng;
|
|
8
|
+
}
|
|
9
|
+
interface GPlaceResult {
|
|
10
|
+
formatted_address?: string;
|
|
11
|
+
geometry?: GGeometry;
|
|
12
|
+
place_id?: string;
|
|
13
|
+
address_components?: unknown[];
|
|
14
|
+
}
|
|
15
|
+
interface GAutocomplete {
|
|
16
|
+
getPlace(): GPlaceResult;
|
|
17
|
+
addListener(event: string, handler: () => void): void;
|
|
18
|
+
}
|
|
19
|
+
interface GAutocompleteConstructor {
|
|
20
|
+
new (input: HTMLInputElement, opts?: Record<string, unknown>): GAutocomplete;
|
|
21
|
+
}
|
|
22
|
+
interface GGoogleMaps {
|
|
23
|
+
places: {
|
|
24
|
+
Autocomplete: GAutocompleteConstructor;
|
|
25
|
+
};
|
|
26
|
+
event: {
|
|
27
|
+
clearInstanceListeners(instance: unknown): void;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface GGoogle {
|
|
31
|
+
maps: GGoogleMaps;
|
|
32
|
+
}
|
|
33
|
+
declare global {
|
|
34
|
+
interface Window {
|
|
35
|
+
google?: GGoogle;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export interface LocationFieldProps {
|
|
39
|
+
name: string;
|
|
40
|
+
value: MapLocation | undefined;
|
|
41
|
+
onChange: (value: MapLocation) => void;
|
|
42
|
+
}
|
|
43
|
+
export declare const LocationField: import("react").NamedExoticComponent<LocationFieldProps>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useState, useRef, useEffect, useCallback } from "react";
|
|
4
|
+
import { Input } from "../../shadcn/input.js";
|
|
5
|
+
import { Label } from "../../shadcn/label.js";
|
|
6
|
+
import { Loader2, Search, X, MapPin } from "lucide-react";
|
|
7
|
+
import { useEditorContext } from "../editor-context.js";
|
|
8
|
+
let googleLoadPromise = null;
|
|
9
|
+
function loadGooglePlaces(apiKey) {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
if (googleLoadPromise) return googleLoadPromise;
|
|
12
|
+
if (typeof window !== "undefined" && ((_b = (_a = window.google) == null ? void 0 : _a.maps) == null ? void 0 : _b.places)) {
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
googleLoadPromise = new Promise((resolve, reject) => {
|
|
16
|
+
const script = document.createElement("script");
|
|
17
|
+
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
|
|
18
|
+
script.async = true;
|
|
19
|
+
script.onload = () => resolve();
|
|
20
|
+
script.onerror = () => {
|
|
21
|
+
googleLoadPromise = null;
|
|
22
|
+
reject(new Error("Failed to load Google Maps API"));
|
|
23
|
+
};
|
|
24
|
+
document.head.appendChild(script);
|
|
25
|
+
});
|
|
26
|
+
return googleLoadPromise;
|
|
27
|
+
}
|
|
28
|
+
const LocationField = memo(function LocationField2({
|
|
29
|
+
value,
|
|
30
|
+
onChange
|
|
31
|
+
}) {
|
|
32
|
+
const { googleMapsApiKey } = useEditorContext();
|
|
33
|
+
const apiKey = googleMapsApiKey || "";
|
|
34
|
+
const [inputValue, setInputValue] = useState((value == null ? void 0 : value.address) || "");
|
|
35
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
36
|
+
const [loadError, setLoadError] = useState(false);
|
|
37
|
+
const inputRef = useRef(null);
|
|
38
|
+
const autocompleteRef = useRef(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if ((value == null ? void 0 : value.address) && value.address !== inputValue) {
|
|
41
|
+
setInputValue(value.address);
|
|
42
|
+
}
|
|
43
|
+
}, [value == null ? void 0 : value.address]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
setLoadError(true);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
loadGooglePlaces(apiKey).then(() => setIsLoaded(true)).catch(() => setLoadError(true));
|
|
50
|
+
}, [apiKey]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!isLoaded || !inputRef.current || autocompleteRef.current) return;
|
|
53
|
+
const G = window.google;
|
|
54
|
+
const autocomplete = new G.maps.places.Autocomplete(inputRef.current, {
|
|
55
|
+
fields: [
|
|
56
|
+
"formatted_address",
|
|
57
|
+
"geometry",
|
|
58
|
+
"place_id",
|
|
59
|
+
"address_components"
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
autocomplete.addListener("place_changed", () => {
|
|
63
|
+
var _a;
|
|
64
|
+
const place = autocomplete.getPlace();
|
|
65
|
+
if (!((_a = place.geometry) == null ? void 0 : _a.location)) return;
|
|
66
|
+
const lat = place.geometry.location.lat();
|
|
67
|
+
const lng = place.geometry.location.lng();
|
|
68
|
+
const address = place.formatted_address || "";
|
|
69
|
+
const newLocation = {
|
|
70
|
+
address,
|
|
71
|
+
lat,
|
|
72
|
+
lng,
|
|
73
|
+
zoom: (value == null ? void 0 : value.zoom) ?? 14,
|
|
74
|
+
placeId: place.place_id || ""
|
|
75
|
+
};
|
|
76
|
+
setInputValue(address);
|
|
77
|
+
onChange(newLocation);
|
|
78
|
+
});
|
|
79
|
+
autocompleteRef.current = autocomplete;
|
|
80
|
+
return () => {
|
|
81
|
+
var _a;
|
|
82
|
+
(_a = window.google) == null ? void 0 : _a.maps.event.clearInstanceListeners(autocomplete);
|
|
83
|
+
autocompleteRef.current = null;
|
|
84
|
+
};
|
|
85
|
+
}, [isLoaded]);
|
|
86
|
+
const handleClear = useCallback(() => {
|
|
87
|
+
var _a;
|
|
88
|
+
setInputValue("");
|
|
89
|
+
onChange({
|
|
90
|
+
address: "",
|
|
91
|
+
lat: 0,
|
|
92
|
+
lng: 0,
|
|
93
|
+
zoom: 14,
|
|
94
|
+
placeId: ""
|
|
95
|
+
});
|
|
96
|
+
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
97
|
+
}, [onChange]);
|
|
98
|
+
const handleZoomChange = useCallback(
|
|
99
|
+
(e) => {
|
|
100
|
+
if (!value) return;
|
|
101
|
+
onChange({ ...value, zoom: Number(e.target.value) });
|
|
102
|
+
},
|
|
103
|
+
[value, onChange]
|
|
104
|
+
);
|
|
105
|
+
const embedUrl = value && value.lat !== 0 ? `https://maps.google.com/maps?q=${value.lat},${value.lng}&z=${value.zoom ?? 14}&output=embed` : null;
|
|
106
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-3", children: [
|
|
107
|
+
/* @__PURE__ */ jsx(Label, { children: "Location" }),
|
|
108
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
109
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3", children: !isLoaded && !loadError ? /* @__PURE__ */ jsx(Loader2, { className: "text-muted-foreground size-4 animate-spin" }) : /* @__PURE__ */ jsx(Search, { className: "text-muted-foreground size-4" }) }),
|
|
110
|
+
/* @__PURE__ */ jsx(
|
|
111
|
+
Input,
|
|
112
|
+
{
|
|
113
|
+
ref: inputRef,
|
|
114
|
+
value: inputValue,
|
|
115
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
116
|
+
placeholder: loadError ? "Enter address manually..." : "Search address or place...",
|
|
117
|
+
className: "pl-9 pr-8"
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
inputValue && /* @__PURE__ */ jsx(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
onClick: handleClear,
|
|
125
|
+
className: "text-muted-foreground hover:text-foreground absolute inset-y-0 right-0 flex items-center pr-3",
|
|
126
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
] }),
|
|
130
|
+
loadError && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
132
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Latitude" }),
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
Input,
|
|
135
|
+
{
|
|
136
|
+
type: "number",
|
|
137
|
+
step: "any",
|
|
138
|
+
value: (value == null ? void 0 : value.lat) ?? "",
|
|
139
|
+
onChange: (e) => onChange({
|
|
140
|
+
...value || { address: "", lng: 0, zoom: 14 },
|
|
141
|
+
lat: Number(e.target.value)
|
|
142
|
+
}),
|
|
143
|
+
placeholder: "37.7749",
|
|
144
|
+
className: "h-8 text-xs"
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
149
|
+
/* @__PURE__ */ jsx(Label, { className: "text-xs", children: "Longitude" }),
|
|
150
|
+
/* @__PURE__ */ jsx(
|
|
151
|
+
Input,
|
|
152
|
+
{
|
|
153
|
+
type: "number",
|
|
154
|
+
step: "any",
|
|
155
|
+
value: (value == null ? void 0 : value.lng) ?? "",
|
|
156
|
+
onChange: (e) => onChange({
|
|
157
|
+
...value || { address: "", lat: 0, zoom: 14 },
|
|
158
|
+
lng: Number(e.target.value)
|
|
159
|
+
}),
|
|
160
|
+
placeholder: "-122.4194",
|
|
161
|
+
className: "h-8 text-xs"
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
] })
|
|
165
|
+
] }),
|
|
166
|
+
value && value.lat !== 0 && /* @__PURE__ */ jsxs("div", { className: "border-border bg-muted/50 space-y-2 rounded-lg border p-3", children: [
|
|
167
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
168
|
+
/* @__PURE__ */ jsx(MapPin, { className: "text-primary mt-0.5 size-3.5 shrink-0" }),
|
|
169
|
+
/* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium leading-tight", children: value.address })
|
|
170
|
+
] }),
|
|
171
|
+
/* @__PURE__ */ jsxs("div", { className: "text-muted-foreground pl-5.5 text-xs", children: [
|
|
172
|
+
value.lat.toFixed(4),
|
|
173
|
+
", ",
|
|
174
|
+
value.lng.toFixed(4)
|
|
175
|
+
] })
|
|
176
|
+
] }),
|
|
177
|
+
embedUrl && /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border", children: /* @__PURE__ */ jsx(
|
|
178
|
+
"iframe",
|
|
179
|
+
{
|
|
180
|
+
src: embedUrl,
|
|
181
|
+
title: "Location preview",
|
|
182
|
+
className: "h-[160px] w-full border-0",
|
|
183
|
+
loading: "lazy",
|
|
184
|
+
referrerPolicy: "no-referrer-when-downgrade"
|
|
185
|
+
}
|
|
186
|
+
) }),
|
|
187
|
+
value && value.lat !== 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
188
|
+
/* @__PURE__ */ jsx(Label, { className: "shrink-0 text-xs", children: "Zoom" }),
|
|
189
|
+
/* @__PURE__ */ jsx(
|
|
190
|
+
"input",
|
|
191
|
+
{
|
|
192
|
+
type: "range",
|
|
193
|
+
min: 1,
|
|
194
|
+
max: 20,
|
|
195
|
+
step: 1,
|
|
196
|
+
value: value.zoom ?? 14,
|
|
197
|
+
onChange: handleZoomChange,
|
|
198
|
+
className: "h-1.5 flex-1 cursor-pointer appearance-none rounded-full bg-gray-200 accent-current"
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-6 text-right text-xs tabular-nums", children: value.zoom ?? 14 })
|
|
202
|
+
] })
|
|
203
|
+
] });
|
|
204
|
+
});
|
|
205
|
+
export {
|
|
206
|
+
LocationField
|
|
207
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FieldDef } from "./types";
|
|
2
|
+
/** 嵌套对象字段:按 objectFields 递归渲染子 AutoField,onChange 合并回对象 */
|
|
3
|
+
export declare const ObjectField: import("react").NamedExoticComponent<{
|
|
4
|
+
field: FieldDef;
|
|
5
|
+
name: string;
|
|
6
|
+
value: any;
|
|
7
|
+
onChange: (value: any) => void;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Label } from "../../shadcn/label.js";
|
|
4
|
+
import { AutoField } from "./auto-field.js";
|
|
5
|
+
const ObjectField = memo(function ObjectField2({
|
|
6
|
+
field,
|
|
7
|
+
name,
|
|
8
|
+
value,
|
|
9
|
+
onChange
|
|
10
|
+
}) {
|
|
11
|
+
const objectFields = field.objectFields || {};
|
|
12
|
+
const label = field.label || name;
|
|
13
|
+
const objValue = value && typeof value === "object" ? value : {};
|
|
14
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
|
|
15
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
16
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-lg border p-3", children: Object.entries(objectFields).map(([key, subField]) => /* @__PURE__ */ jsx(
|
|
17
|
+
AutoField,
|
|
18
|
+
{
|
|
19
|
+
field: subField,
|
|
20
|
+
name: key,
|
|
21
|
+
value: objValue[key],
|
|
22
|
+
onChange: (val) => onChange({ ...objValue, [key]: val })
|
|
23
|
+
},
|
|
24
|
+
key
|
|
25
|
+
)) })
|
|
26
|
+
] });
|
|
27
|
+
});
|
|
28
|
+
export {
|
|
29
|
+
ObjectField
|
|
30
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Label } from "../../shadcn/label.js";
|
|
4
|
+
import { Switch } from "../../shadcn/switch.js";
|
|
5
|
+
import { ToggleGroup, ToggleGroupItem } from "../../shadcn/toggle-group.js";
|
|
6
|
+
const RadioToggleField = memo(function RadioToggleField2({
|
|
7
|
+
label,
|
|
8
|
+
options,
|
|
9
|
+
value,
|
|
10
|
+
onChange
|
|
11
|
+
}) {
|
|
12
|
+
const isBooleanToggle = options.length === 2 && options.some((o) => o.value === true) && options.some((o) => o.value === false);
|
|
13
|
+
if (isBooleanToggle) {
|
|
14
|
+
return /* @__PURE__ */ jsxs("div", { className: "bg-muted/50 mb-3 flex items-center justify-between rounded-xl p-3", children: [
|
|
15
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
16
|
+
/* @__PURE__ */ jsx(
|
|
17
|
+
Switch,
|
|
18
|
+
{
|
|
19
|
+
checked: !!value,
|
|
20
|
+
onCheckedChange: (checked) => onChange(checked)
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
] });
|
|
24
|
+
}
|
|
25
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
|
|
26
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
ToggleGroup,
|
|
29
|
+
{
|
|
30
|
+
type: "single",
|
|
31
|
+
value: String(value ?? ""),
|
|
32
|
+
onValueChange: (val) => {
|
|
33
|
+
if (!val) return;
|
|
34
|
+
const opt = options.find((o) => String(o.value) === val);
|
|
35
|
+
if (opt) onChange(opt.value);
|
|
36
|
+
},
|
|
37
|
+
className: "justify-start",
|
|
38
|
+
children: options.map((opt) => /* @__PURE__ */ jsx(
|
|
39
|
+
ToggleGroupItem,
|
|
40
|
+
{
|
|
41
|
+
value: String(opt.value),
|
|
42
|
+
size: "sm",
|
|
43
|
+
children: opt.label
|
|
44
|
+
},
|
|
45
|
+
String(opt.value)
|
|
46
|
+
))
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
] });
|
|
50
|
+
});
|
|
51
|
+
export {
|
|
52
|
+
RadioToggleField
|
|
53
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const LARGE_SELECT_THRESHOLD = 200;
|
|
2
|
+
type SelectOption = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: any;
|
|
5
|
+
};
|
|
6
|
+
/** 超大下拉:选项超过阈值时使用虚拟列表 + 搜索,减轻 DOM 压力 */
|
|
7
|
+
export declare const VirtualizedSelectField: import("react").NamedExoticComponent<{
|
|
8
|
+
label: string;
|
|
9
|
+
options: SelectOption[];
|
|
10
|
+
value: any;
|
|
11
|
+
onChange: (value: any) => void;
|
|
12
|
+
}>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useState, useRef, useMemo, useCallback, useEffect } from "react";
|
|
3
|
+
import { Input } from "../../shadcn/input.js";
|
|
4
|
+
import { Label } from "../../shadcn/label.js";
|
|
5
|
+
import { Button } from "../../shadcn/button.js";
|
|
6
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../../shadcn/popover.js";
|
|
7
|
+
import { ChevronDownIcon, CheckIcon } from "lucide-react";
|
|
8
|
+
const LARGE_SELECT_THRESHOLD = 200;
|
|
9
|
+
const VIRTUAL_ITEM_HEIGHT = 32;
|
|
10
|
+
const VIRTUAL_VIEWPORT_HEIGHT = 256;
|
|
11
|
+
const VIRTUAL_OVERSCAN = 6;
|
|
12
|
+
const VirtualizedSelectField = memo(function VirtualizedSelectField2({
|
|
13
|
+
label,
|
|
14
|
+
options,
|
|
15
|
+
value,
|
|
16
|
+
onChange
|
|
17
|
+
}) {
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
19
|
+
const [search, setSearch] = useState("");
|
|
20
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
21
|
+
const viewportRef = useRef(null);
|
|
22
|
+
const normalizedOptions = useMemo(
|
|
23
|
+
() => options.map((opt) => ({
|
|
24
|
+
...opt,
|
|
25
|
+
stringValue: String(opt.value)
|
|
26
|
+
})),
|
|
27
|
+
[options]
|
|
28
|
+
);
|
|
29
|
+
const selectedString = String(value ?? "");
|
|
30
|
+
const filteredOptions = useMemo(() => {
|
|
31
|
+
const query = search.trim().toLowerCase();
|
|
32
|
+
if (!query) return normalizedOptions;
|
|
33
|
+
return normalizedOptions.filter(
|
|
34
|
+
(opt) => opt.label.toLowerCase().includes(query) || opt.stringValue.toLowerCase().includes(query)
|
|
35
|
+
);
|
|
36
|
+
}, [normalizedOptions, search]);
|
|
37
|
+
const selectedLabel = useMemo(() => {
|
|
38
|
+
const selected = normalizedOptions.find(
|
|
39
|
+
(opt) => opt.stringValue === selectedString
|
|
40
|
+
);
|
|
41
|
+
return (selected == null ? void 0 : selected.label) ?? selectedString;
|
|
42
|
+
}, [normalizedOptions, selectedString]);
|
|
43
|
+
const totalHeight = filteredOptions.length * VIRTUAL_ITEM_HEIGHT;
|
|
44
|
+
const startIndex = Math.max(
|
|
45
|
+
0,
|
|
46
|
+
Math.floor(scrollTop / VIRTUAL_ITEM_HEIGHT) - VIRTUAL_OVERSCAN
|
|
47
|
+
);
|
|
48
|
+
const visibleCount = Math.ceil(VIRTUAL_VIEWPORT_HEIGHT / VIRTUAL_ITEM_HEIGHT) + VIRTUAL_OVERSCAN * 2;
|
|
49
|
+
const endIndex = Math.min(filteredOptions.length, startIndex + visibleCount);
|
|
50
|
+
const visibleOptions = filteredOptions.slice(startIndex, endIndex);
|
|
51
|
+
const handleSelect = useCallback(
|
|
52
|
+
(stringValue) => {
|
|
53
|
+
const selected = normalizedOptions.find(
|
|
54
|
+
(opt) => opt.stringValue === stringValue
|
|
55
|
+
);
|
|
56
|
+
onChange(selected ? selected.value : stringValue);
|
|
57
|
+
setOpen(false);
|
|
58
|
+
},
|
|
59
|
+
[normalizedOptions, onChange]
|
|
60
|
+
);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!open || !viewportRef.current) return;
|
|
63
|
+
const selectedIndex = filteredOptions.findIndex(
|
|
64
|
+
(opt) => opt.stringValue === selectedString
|
|
65
|
+
);
|
|
66
|
+
if (selectedIndex < 0) return;
|
|
67
|
+
const targetTop = selectedIndex * VIRTUAL_ITEM_HEIGHT - VIRTUAL_VIEWPORT_HEIGHT / 2;
|
|
68
|
+
const clampedTop = Math.max(
|
|
69
|
+
0,
|
|
70
|
+
Math.min(targetTop, totalHeight - VIRTUAL_VIEWPORT_HEIGHT)
|
|
71
|
+
);
|
|
72
|
+
viewportRef.current.scrollTop = clampedTop;
|
|
73
|
+
setScrollTop(clampedTop);
|
|
74
|
+
}, [open, filteredOptions, selectedString, totalHeight]);
|
|
75
|
+
return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
|
|
76
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
77
|
+
/* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
78
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
79
|
+
Button,
|
|
80
|
+
{
|
|
81
|
+
type: "button",
|
|
82
|
+
variant: "outline",
|
|
83
|
+
className: "w-full justify-between font-normal",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: selectedLabel || "Select..." }),
|
|
86
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 opacity-60" })
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
) }),
|
|
90
|
+
/* @__PURE__ */ jsxs(PopoverContent, { className: "w-(--radix-popover-trigger-width) p-2", children: [
|
|
91
|
+
/* @__PURE__ */ jsx(
|
|
92
|
+
Input,
|
|
93
|
+
{
|
|
94
|
+
value: search,
|
|
95
|
+
onChange: (e) => setSearch(e.target.value),
|
|
96
|
+
placeholder: "Search options...",
|
|
97
|
+
className: "mb-2"
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
/* @__PURE__ */ jsx(
|
|
101
|
+
"div",
|
|
102
|
+
{
|
|
103
|
+
ref: viewportRef,
|
|
104
|
+
className: "relative touch-pan-y overflow-y-auto overscroll-contain rounded-md border",
|
|
105
|
+
style: {
|
|
106
|
+
height: VIRTUAL_VIEWPORT_HEIGHT,
|
|
107
|
+
WebkitOverflowScrolling: "touch"
|
|
108
|
+
},
|
|
109
|
+
onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
|
|
110
|
+
children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground p-3 text-sm", children: "No options found." }) : /* @__PURE__ */ jsx("div", { style: { height: totalHeight, position: "relative" }, children: /* @__PURE__ */ jsx(
|
|
111
|
+
"div",
|
|
112
|
+
{
|
|
113
|
+
style: {
|
|
114
|
+
position: "absolute",
|
|
115
|
+
top: startIndex * VIRTUAL_ITEM_HEIGHT,
|
|
116
|
+
left: 0,
|
|
117
|
+
right: 0
|
|
118
|
+
},
|
|
119
|
+
children: visibleOptions.map((opt) => {
|
|
120
|
+
const isSelected = opt.stringValue === selectedString;
|
|
121
|
+
return /* @__PURE__ */ jsxs(
|
|
122
|
+
"button",
|
|
123
|
+
{
|
|
124
|
+
type: "button",
|
|
125
|
+
className: "hover:bg-accent hover:text-accent-foreground flex h-8 w-full items-center justify-between px-2 text-left text-sm",
|
|
126
|
+
onClick: () => handleSelect(opt.stringValue),
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: opt.label }),
|
|
129
|
+
isSelected ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-primary ml-2 size-4 shrink-0" }) : null
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
opt.stringValue
|
|
133
|
+
);
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
) })
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
] })
|
|
140
|
+
] })
|
|
141
|
+
] });
|
|
142
|
+
});
|
|
143
|
+
export {
|
|
144
|
+
LARGE_SELECT_THRESHOLD,
|
|
145
|
+
VirtualizedSelectField
|
|
146
|
+
};
|