dune-react 0.0.15 → 0.0.16

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.
@@ -4,11 +4,11 @@ import { useState, useCallback } from "react";
4
4
  import { SafeDynamicIcon } from "./safe-dynamic-icon.js";
5
5
  import { Button, buttonVariants } from "../shadcn/button.js";
6
6
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "../shadcn/dialog.js";
7
- import { AutoField } from "./fields/auto-field.js";
8
7
  import { button } from "./core/fields.js";
9
8
  import { cn } from "../../utils/css-utils.js";
10
9
  import { resolveActionUrl } from "./core/types.js";
11
10
  import { withEditable } from "./core/with-editable.js";
11
+ import { AutoField } from "./fields/auto-field.js";
12
12
  const BUTTON_FIELDS = button.objectFields;
13
13
  function ButtonEditDialog({
14
14
  open,
@@ -13,9 +13,9 @@ export declare const action: {
13
13
  render: typeof ActionField;
14
14
  };
15
15
  export declare const actionDefaults: {
16
- readonly type: "external";
16
+ readonly type: "page";
17
17
  readonly pageUrl: "";
18
- readonly externalUrl: "#";
18
+ readonly externalUrl: "";
19
19
  readonly openInNewTab: "false";
20
20
  readonly email: "";
21
21
  readonly subject: "";
@@ -139,9 +139,9 @@ export declare const buttons: {
139
139
  readonly defaultItemProps: {
140
140
  readonly label: "Button";
141
141
  readonly action: {
142
- readonly type: "external";
142
+ readonly type: "page";
143
143
  readonly pageUrl: "";
144
- readonly externalUrl: "#";
144
+ readonly externalUrl: "";
145
145
  readonly openInNewTab: "false";
146
146
  readonly email: "";
147
147
  readonly subject: "";
@@ -354,9 +354,9 @@ export declare const contentFields: {
354
354
  readonly defaultItemProps: {
355
355
  readonly label: "Button";
356
356
  readonly action: {
357
- readonly type: "external";
357
+ readonly type: "page";
358
358
  readonly pageUrl: "";
359
- readonly externalUrl: "#";
359
+ readonly externalUrl: "";
360
360
  readonly openInNewTab: "false";
361
361
  readonly email: "";
362
362
  readonly subject: "";
@@ -496,9 +496,9 @@ export declare const contentFieldsWithFeatures: {
496
496
  readonly defaultItemProps: {
497
497
  readonly label: "Button";
498
498
  readonly action: {
499
- readonly type: "external";
499
+ readonly type: "page";
500
500
  readonly pageUrl: "";
501
- readonly externalUrl: "#";
501
+ readonly externalUrl: "";
502
502
  readonly openInNewTab: "false";
503
503
  readonly email: "";
504
504
  readonly subject: "";
@@ -668,9 +668,9 @@ export declare const cards: {
668
668
  readonly button: {
669
669
  readonly label: "Button";
670
670
  readonly action: {
671
- readonly type: "external";
671
+ readonly type: "page";
672
672
  readonly pageUrl: "";
673
- readonly externalUrl: "#";
673
+ readonly externalUrl: "";
674
674
  readonly openInNewTab: "false";
675
675
  readonly email: "";
676
676
  readonly subject: "";
@@ -1,6 +1,7 @@
1
1
  import { fieldTypes } from "../field.js";
2
2
  import { ActionField } from "../fields/action-field.js";
3
3
  import { IconPickerField } from "../icon-picker-field.js";
4
+ import { backgroundColor, backgroundImage, bannerStylesDefaults, bannerStylesField, createStylesDefaults, createStylesField, heroStylesDefaults, padding, paddingDefaults, paddingLevel, sectionBaseStyleDefaults, sectionBaseStyleFields, sectionOverlay, sectionStyle } from "./styles.js";
4
5
  const formMethods = ["get", "post", "put", "patch", "delete"];
5
6
  const icon = {
6
7
  type: "custom",
@@ -12,9 +13,9 @@ const action = {
12
13
  render: ActionField
13
14
  };
14
15
  const actionDefaults = {
15
- type: "external",
16
+ type: "page",
16
17
  pageUrl: "",
17
- externalUrl: "#",
18
+ externalUrl: "",
18
19
  openInNewTab: "false",
19
20
  email: "",
20
21
  subject: "",
@@ -243,13 +244,19 @@ const formDefaults = {
243
244
  export {
244
245
  action,
245
246
  actionDefaults,
247
+ backgroundColor,
248
+ backgroundImage,
246
249
  badge,
250
+ bannerStylesDefaults,
251
+ bannerStylesField,
247
252
  button,
248
253
  buttons,
249
254
  card,
250
255
  cards,
251
256
  contentFields,
252
257
  contentFieldsWithFeatures,
258
+ createStylesDefaults,
259
+ createStylesField,
253
260
  description,
254
261
  features,
255
262
  field,
@@ -258,10 +265,18 @@ export {
258
265
  formDefaults,
259
266
  getPlaceholderImageUrl,
260
267
  heading,
268
+ heroStylesDefaults,
261
269
  icon,
262
270
  image,
263
271
  image16x9Placeholder,
264
272
  image1x1Placeholder,
265
273
  image9x16Placeholder,
266
- images
274
+ images,
275
+ padding,
276
+ paddingDefaults,
277
+ paddingLevel,
278
+ sectionBaseStyleDefaults,
279
+ sectionBaseStyleFields,
280
+ sectionOverlay,
281
+ sectionStyle
267
282
  };
@@ -17,11 +17,13 @@ function withEditable(Component, config) {
17
17
  const dispatch = usePuckDispatch();
18
18
  const appState = usePuckAppState();
19
19
  const wrapperRef = useRef(null);
20
+ const portalRef = useRef(null);
20
21
  const isBlock = BLOCK_LEVEL_TYPES.has(config.type);
21
22
  useEffect(() => {
22
- if (!wrapperRef.current) return;
23
- return registerOverlayPortal(wrapperRef.current, { disableDrag: true });
24
- }, []);
23
+ const target = isBlock ? portalRef.current : wrapperRef.current;
24
+ if (!target) return;
25
+ return registerOverlayPortal(target, { disableDrag: true });
26
+ }, [isBlock]);
25
27
  const getLocation = useCallback(() => {
26
28
  const componentId = getComponentIdFromDOM(wrapperRef.current);
27
29
  if (!componentId || !(appState == null ? void 0 : appState.data)) return null;
@@ -53,6 +55,8 @@ function withEditable(Component, config) {
53
55
  },
54
56
  [getLocation, dispatch]
55
57
  );
58
+ const { previewScale = 1 } = useEditorContext();
59
+ const inverseScale = 1 / previewScale;
56
60
  const handleEdit = useCallback(
57
61
  (e) => {
58
62
  e.preventDefault();
@@ -63,24 +67,32 @@ function withEditable(Component, config) {
63
67
  },
64
68
  [props, handleSave]
65
69
  );
66
- return /* @__PURE__ */ jsxs(
67
- "span",
68
- {
69
- ref: wrapperRef,
70
- className: isBlock ? "group relative block h-full w-full" : "group relative inline-flex",
71
- children: [
72
- /* @__PURE__ */ jsx(Component, { ...props }),
73
- /* @__PURE__ */ jsx(
74
- "span",
75
- {
76
- onClick: handleEdit,
77
- className: "absolute inset-0 z-40 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100",
78
- children: /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-black/50 shadow-md backdrop-blur-sm", children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-white" }) })
79
- }
80
- )
81
- ]
82
- }
83
- );
70
+ if (isBlock) {
71
+ return /* @__PURE__ */ jsxs("span", { ref: wrapperRef, className: "group relative block h-full w-full", children: [
72
+ /* @__PURE__ */ jsx(Component, { ...props }),
73
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 z-40 flex items-center justify-center", children: /* @__PURE__ */ jsx(
74
+ "span",
75
+ {
76
+ ref: portalRef,
77
+ onClick: handleEdit,
78
+ style: { transform: `scale(${inverseScale})`, transformOrigin: "center center" },
79
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-black/50 shadow-md backdrop-blur-sm",
80
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-white" })
81
+ }
82
+ ) })
83
+ ] });
84
+ }
85
+ return /* @__PURE__ */ jsxs("span", { ref: wrapperRef, className: "group relative inline-flex", children: [
86
+ /* @__PURE__ */ jsx(Component, { ...props }),
87
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 z-40 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100", children: /* @__PURE__ */ jsx(
88
+ "span",
89
+ {
90
+ onClick: handleEdit,
91
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-black/50 shadow-md backdrop-blur-sm",
92
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-white" })
93
+ }
94
+ ) })
95
+ ] });
84
96
  }
85
97
  return function EditableComponent(props) {
86
98
  const { isEditor: isEditorMode } = useEditorContext();
@@ -2,10 +2,9 @@
2
2
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
3
  import { memo, useCallback } from "react";
4
4
  import { Input } from "../../shadcn/input.js";
5
- import { Button } from "../../shadcn/button.js";
6
5
  import { Label } from "../../shadcn/label.js";
7
6
  import { Tabs, TabsList, TabsTrigger } from "../../shadcn/tabs.js";
8
- import { LinkIcon, XIcon, ClipboardIcon, GlobeIcon } from "lucide-react";
7
+ import { LinkIcon, XIcon, GlobeIcon } from "lucide-react";
9
8
  import { useEditorContext } from "../editor-context.js";
10
9
  const ACTION_TYPE_OPTIONS = [
11
10
  { label: "Page Link", value: "page" },
@@ -25,53 +24,30 @@ const PageActionFields = memo(function PageActionFields2({
25
24
  const pages = sitePages ?? [];
26
25
  const pageUrl = value.pageUrl ?? "";
27
26
  const selectedSlug = pageUrl.startsWith("/") ? pageUrl.slice(1) : null;
28
- const handlePaste = useCallback(async () => {
29
- try {
30
- const text = await navigator.clipboard.readText();
31
- if (text) onChange({ pageUrl: text.trim() });
32
- } catch {
33
- }
34
- }, [onChange]);
35
27
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
36
28
  /* @__PURE__ */ jsx(Label, { children: "Page URL" }),
37
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-2 rounded-lg border px-3 py-2", children: [
39
- /* @__PURE__ */ jsx(LinkIcon, { className: "text-muted-foreground size-4 shrink-0" }),
40
- /* @__PURE__ */ jsx(
41
- "input",
42
- {
43
- name: `${name}.pageUrl`,
44
- className: "w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground",
45
- placeholder: "/about",
46
- value: pageUrl,
47
- onChange: (e) => onChange({ pageUrl: e.target.value })
48
- }
49
- ),
50
- pageUrl && /* @__PURE__ */ jsx(
51
- "button",
52
- {
53
- type: "button",
54
- className: "text-muted-foreground hover:text-foreground shrink-0",
55
- onClick: () => onChange({ pageUrl: "" }),
56
- children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5" })
57
- }
58
- )
59
- ] }),
60
- /* @__PURE__ */ jsxs(
61
- Button,
29
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-2 rounded-lg border px-3 py-2", children: [
30
+ /* @__PURE__ */ jsx(LinkIcon, { className: "text-muted-foreground size-4 shrink-0" }),
31
+ /* @__PURE__ */ jsx(
32
+ "input",
33
+ {
34
+ name: `${name}.pageUrl`,
35
+ className: "w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground",
36
+ placeholder: "/about",
37
+ value: pageUrl,
38
+ onChange: (e) => onChange({ pageUrl: e.target.value })
39
+ }
40
+ ),
41
+ pageUrl && /* @__PURE__ */ jsx(
42
+ "button",
62
43
  {
63
44
  type: "button",
64
- variant: "outline",
65
- size: "sm",
66
- className: "shrink-0 gap-1.5",
67
- onClick: handlePaste,
68
- children: [
69
- /* @__PURE__ */ jsx(ClipboardIcon, { className: "size-3.5" }),
70
- "Paste"
71
- ]
45
+ className: "text-muted-foreground hover:text-foreground shrink-0",
46
+ onClick: () => onChange({ pageUrl: "" }),
47
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5" })
72
48
  }
73
49
  )
74
- ] }),
50
+ ] }) }),
75
51
  pages.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
76
52
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs font-medium", children: "Site pages" }),
77
53
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: pages.map((page) => {
@@ -1,225 +1,33 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { memo, useState, useRef, useMemo, useCallback, useEffect } from "react";
2
+ import { memo } from "react";
3
3
  import { Input } from "../../shadcn/input.js";
4
4
  import { Textarea } from "../../shadcn/textarea.js";
5
5
  import { Label } from "../../shadcn/label.js";
6
- import { Switch } from "../../shadcn/switch.js";
7
- import { Button } from "../../shadcn/button.js";
8
6
  import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../../shadcn/select.js";
9
- import { Popover, PopoverTrigger, PopoverContent } from "../../shadcn/popover.js";
10
- import { ToggleGroup, ToggleGroupItem } from "../../shadcn/toggle-group.js";
11
- import { ChevronDownIcon, CheckIcon } from "lucide-react";
12
- const LARGE_SELECT_THRESHOLD = 200;
13
- const VIRTUAL_ITEM_HEIGHT = 32;
14
- const VIRTUAL_VIEWPORT_HEIGHT = 256;
15
- const VIRTUAL_OVERSCAN = 6;
16
- const RadioToggleField = memo(function RadioToggleField2({
17
- label,
18
- options,
19
- value,
20
- onChange
21
- }) {
22
- const isBooleanToggle = options.length === 2 && options.some((o) => o.value === true) && options.some((o) => o.value === false);
23
- if (isBooleanToggle) {
24
- return /* @__PURE__ */ jsxs("div", { className: "bg-muted/50 mb-3 flex items-center justify-between rounded-xl p-3", children: [
25
- /* @__PURE__ */ jsx(Label, { children: label }),
26
- /* @__PURE__ */ jsx(
27
- Switch,
28
- {
29
- checked: !!value,
30
- onCheckedChange: (checked) => onChange(checked)
31
- }
32
- )
33
- ] });
34
- }
35
- return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
36
- /* @__PURE__ */ jsx(Label, { children: label }),
37
- /* @__PURE__ */ jsx(
38
- ToggleGroup,
39
- {
40
- type: "single",
41
- value: String(value ?? ""),
42
- onValueChange: (val) => {
43
- if (!val) return;
44
- const opt = options.find((o) => String(o.value) === val);
45
- if (opt) onChange(opt.value);
46
- },
47
- className: "justify-start",
48
- children: options.map((opt) => /* @__PURE__ */ jsx(
49
- ToggleGroupItem,
50
- {
51
- value: String(opt.value),
52
- size: "sm",
53
- children: opt.label
54
- },
55
- String(opt.value)
56
- ))
57
- }
58
- )
59
- ] });
60
- });
61
- const ObjectField = memo(function ObjectField2({
62
- field,
63
- name,
64
- value,
65
- onChange
66
- }) {
67
- const objectFields = field.objectFields || {};
68
- const label = field.label || name;
69
- const objValue = value && typeof value === "object" ? value : {};
70
- return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
71
- /* @__PURE__ */ jsx(Label, { children: label }),
72
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border p-3", children: Object.entries(objectFields).map(([key, subField]) => /* @__PURE__ */ jsx(
73
- AutoField,
74
- {
75
- field: subField,
76
- name: key,
77
- value: objValue[key],
78
- onChange: (val) => onChange({ ...objValue, [key]: val })
79
- },
80
- key
81
- )) })
82
- ] });
83
- });
84
- const VirtualizedSelectField = memo(function VirtualizedSelectField2({
85
- label,
86
- options,
87
- value,
88
- onChange
89
- }) {
90
- const [open, setOpen] = useState(false);
91
- const [search, setSearch] = useState("");
92
- const [scrollTop, setScrollTop] = useState(0);
93
- const viewportRef = useRef(null);
94
- const normalizedOptions = useMemo(
95
- () => options.map((opt) => ({
96
- ...opt,
97
- stringValue: String(opt.value)
98
- })),
99
- [options]
100
- );
101
- const selectedString = String(value ?? "");
102
- const filteredOptions = useMemo(() => {
103
- const query = search.trim().toLowerCase();
104
- if (!query) return normalizedOptions;
105
- return normalizedOptions.filter(
106
- (opt) => opt.label.toLowerCase().includes(query) || opt.stringValue.toLowerCase().includes(query)
107
- );
108
- }, [normalizedOptions, search]);
109
- const selectedLabel = useMemo(() => {
110
- const selected = normalizedOptions.find(
111
- (opt) => opt.stringValue === selectedString
112
- );
113
- return (selected == null ? void 0 : selected.label) ?? selectedString;
114
- }, [normalizedOptions, selectedString]);
115
- const totalHeight = filteredOptions.length * VIRTUAL_ITEM_HEIGHT;
116
- const startIndex = Math.max(
117
- 0,
118
- Math.floor(scrollTop / VIRTUAL_ITEM_HEIGHT) - VIRTUAL_OVERSCAN
119
- );
120
- const visibleCount = Math.ceil(VIRTUAL_VIEWPORT_HEIGHT / VIRTUAL_ITEM_HEIGHT) + VIRTUAL_OVERSCAN * 2;
121
- const endIndex = Math.min(filteredOptions.length, startIndex + visibleCount);
122
- const visibleOptions = filteredOptions.slice(startIndex, endIndex);
123
- const handleSelect = useCallback(
124
- (stringValue) => {
125
- const selected = normalizedOptions.find(
126
- (opt) => opt.stringValue === stringValue
127
- );
128
- onChange(selected ? selected.value : stringValue);
129
- setOpen(false);
130
- },
131
- [normalizedOptions, onChange]
132
- );
133
- useEffect(() => {
134
- if (!open || !viewportRef.current) return;
135
- const selectedIndex = filteredOptions.findIndex(
136
- (opt) => opt.stringValue === selectedString
137
- );
138
- if (selectedIndex < 0) return;
139
- const targetTop = selectedIndex * VIRTUAL_ITEM_HEIGHT - VIRTUAL_VIEWPORT_HEIGHT / 2;
140
- const clampedTop = Math.max(
141
- 0,
142
- Math.min(targetTop, totalHeight - VIRTUAL_VIEWPORT_HEIGHT)
143
- );
144
- viewportRef.current.scrollTop = clampedTop;
145
- setScrollTop(clampedTop);
146
- }, [open, filteredOptions, selectedString, totalHeight]);
147
- return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
148
- /* @__PURE__ */ jsx(Label, { children: label }),
149
- /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
150
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
151
- Button,
152
- {
153
- type: "button",
154
- variant: "outline",
155
- className: "w-full justify-between font-normal",
156
- children: [
157
- /* @__PURE__ */ jsx("span", { className: "truncate", children: selectedLabel || "Select..." }),
158
- /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 opacity-60" })
159
- ]
160
- }
161
- ) }),
162
- /* @__PURE__ */ jsxs(PopoverContent, { className: "w-(--radix-popover-trigger-width) p-2", children: [
163
- /* @__PURE__ */ jsx(
164
- Input,
165
- {
166
- value: search,
167
- onChange: (e) => setSearch(e.target.value),
168
- placeholder: "Search options...",
169
- className: "mb-2"
170
- }
171
- ),
172
- /* @__PURE__ */ jsx(
173
- "div",
174
- {
175
- ref: viewportRef,
176
- className: "relative touch-pan-y overflow-y-auto overscroll-contain rounded-md border",
177
- style: {
178
- height: VIRTUAL_VIEWPORT_HEIGHT,
179
- WebkitOverflowScrolling: "touch"
180
- },
181
- onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
182
- 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(
183
- "div",
184
- {
185
- style: {
186
- position: "absolute",
187
- top: startIndex * VIRTUAL_ITEM_HEIGHT,
188
- left: 0,
189
- right: 0
190
- },
191
- children: visibleOptions.map((opt) => {
192
- const isSelected = opt.stringValue === selectedString;
193
- return /* @__PURE__ */ jsxs(
194
- "button",
195
- {
196
- type: "button",
197
- className: "hover:bg-accent hover:text-accent-foreground flex h-8 w-full items-center justify-between px-2 text-left text-sm",
198
- onClick: () => handleSelect(opt.stringValue),
199
- children: [
200
- /* @__PURE__ */ jsx("span", { className: "truncate", children: opt.label }),
201
- isSelected ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-primary ml-2 size-4 shrink-0" }) : null
202
- ]
203
- },
204
- opt.stringValue
205
- );
206
- })
207
- }
208
- ) })
209
- }
210
- )
211
- ] })
212
- ] })
213
- ] });
214
- });
7
+ import { ColorField } from "./color-field.js";
8
+ import { RadioToggleField } from "./radio-toggle-field.js";
9
+ import { ObjectField } from "./object-field.js";
10
+ import { LARGE_SELECT_THRESHOLD, VirtualizedSelectField } from "./virtualized-select-field.js";
11
+ const KEY_FIELD_MAP = {
12
+ color: ColorField,
13
+ backgroundColor: ColorField,
14
+ "background-color": ColorField,
15
+ bgColor: ColorField,
16
+ borderColor: ColorField,
17
+ "border-color": ColorField
18
+ };
215
19
  const AutoField = memo(function AutoField2({
216
20
  field,
217
21
  name,
218
22
  value,
219
23
  onChange
220
24
  }) {
221
- var _a, _b;
25
+ var _a, _b, _c;
222
26
  const label = field.label || name;
27
+ const KeyOverride = KEY_FIELD_MAP[name];
28
+ if (KeyOverride) {
29
+ return /* @__PURE__ */ jsx(KeyOverride, { label, value, onChange });
30
+ }
223
31
  switch (field.type) {
224
32
  case "text":
225
33
  return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
@@ -266,7 +74,7 @@ const AutoField = memo(function AutoField2({
266
74
  onChange
267
75
  }
268
76
  );
269
- case "select":
77
+ case "select": {
270
78
  if ((((_a = field.options) == null ? void 0 : _a.length) || 0) > LARGE_SELECT_THRESHOLD) {
271
79
  return /* @__PURE__ */ jsx(
272
80
  VirtualizedSelectField,
@@ -278,24 +86,29 @@ const AutoField = memo(function AutoField2({
278
86
  }
279
87
  );
280
88
  }
89
+ const stringValue = String(value ?? "");
90
+ const selectedOption = (_b = field.options) == null ? void 0 : _b.find(
91
+ (o) => String(o.value) === stringValue
92
+ );
281
93
  return /* @__PURE__ */ jsxs("div", { className: "mb-4 space-y-1.5", children: [
282
94
  /* @__PURE__ */ jsx(Label, { children: label }),
283
95
  /* @__PURE__ */ jsxs(
284
96
  Select,
285
97
  {
286
- value: String(value ?? ""),
98
+ value: stringValue,
287
99
  onValueChange: (val) => {
288
100
  var _a2;
289
101
  const opt = (_a2 = field.options) == null ? void 0 : _a2.find((o) => String(o.value) === val);
290
102
  onChange(opt ? opt.value : val);
291
103
  },
292
104
  children: [
293
- /* @__PURE__ */ jsx(SelectTrigger, { className: "w-full", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
294
- /* @__PURE__ */ jsx(SelectContent, { children: (_b = field.options) == null ? void 0 : _b.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: String(opt.value), children: opt.label }, String(opt.value))) })
105
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-full", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: label, children: (selectedOption == null ? void 0 : selectedOption.label) ?? stringValue }) }),
106
+ /* @__PURE__ */ jsx(SelectContent, { children: (_c = field.options) == null ? void 0 : _c.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: String(opt.value), children: opt.label }, String(opt.value))) })
295
107
  ]
296
108
  }
297
109
  )
298
110
  ] });
111
+ }
299
112
  case "object":
300
113
  return /* @__PURE__ */ jsx(
301
114
  ObjectField,
@@ -0,0 +1,6 @@
1
+ /** 颜色选择器:color swatch + 文本输入 */
2
+ export declare const ColorField: import("react").NamedExoticComponent<{
3
+ label: string;
4
+ value: any;
5
+ onChange: (value: any) => void;
6
+ }>;
@@ -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,7 @@
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 { ActionField, ACTION_TYPE_FIELD_MAP, ACTION_TYPE_OPTIONS, PageActionFields, ExternalActionFields, EmailActionFields, PhoneActionFields, SectionActionFields, DownloadActionFields, type ActionTypeFieldProps, } from "./action-field";
@@ -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,10 @@
1
+ /** 单选/切换:若选项恰好为 true/false 两个值则渲染开关,否则为 ToggleGroup */
2
+ export declare const RadioToggleField: import("react").NamedExoticComponent<{
3
+ label: string;
4
+ options: {
5
+ label: string;
6
+ value: any;
7
+ }[];
8
+ value: any;
9
+ onChange: (value: any) => void;
10
+ }>;
@@ -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
+ };
@@ -33,7 +33,11 @@ const getCroppedImg = async (imageSrc, pixelCrop, rotation) => {
33
33
  ctx.translate(safeArea / 2, safeArea / 2);
34
34
  ctx.rotate(rotation * Math.PI / 180);
35
35
  ctx.translate(-safeArea / 2, -safeArea / 2);
36
- ctx.drawImage(image2, safeArea / 2 - image2.width * 0.5, safeArea / 2 - image2.height * 0.5);
36
+ ctx.drawImage(
37
+ image2,
38
+ safeArea / 2 - image2.width * 0.5,
39
+ safeArea / 2 - image2.height * 0.5
40
+ );
37
41
  const data = ctx.getImageData(0, 0, safeArea, safeArea);
38
42
  canvas.width = pixelCrop.width;
39
43
  canvas.height = pixelCrop.height;
@@ -48,7 +52,14 @@ const getCroppedImg = async (imageSrc, pixelCrop, rotation) => {
48
52
  };
49
53
  const DEFAULT_LIBRARY_IMAGES = [];
50
54
  function CompoundImageBase(props) {
51
- return /* @__PURE__ */ jsx("img", { src: props.src, alt: props.alt, className: cn("block h-full w-full object-cover", props.className) });
55
+ return /* @__PURE__ */ jsx(
56
+ "img",
57
+ {
58
+ src: props.src,
59
+ alt: props.alt,
60
+ className: cn("block h-full w-full object-cover", props.className)
61
+ }
62
+ );
52
63
  }
53
64
  function ImageEditDialog({
54
65
  open,
@@ -58,7 +69,9 @@ function ImageEditDialog({
58
69
  }) {
59
70
  const [tab, setTab] = useState("library");
60
71
  const [alt, setAlt] = useState(initialData.alt ?? "");
61
- const [libraryImages, setLibraryImages] = useState(DEFAULT_LIBRARY_IMAGES);
72
+ const [libraryImages, setLibraryImages] = useState(
73
+ DEFAULT_LIBRARY_IMAGES
74
+ );
62
75
  const [isLoadingImages, setIsLoadingImages] = useState(false);
63
76
  const { fetchLibraryImages } = useEditorContext();
64
77
  useEffect(() => {
@@ -97,7 +110,11 @@ function ImageEditDialog({
97
110
  const handleCropSave = useCallback(async () => {
98
111
  if (!croppedAreaPixels || !initialData.src) return;
99
112
  try {
100
- const croppedBlob = await getCroppedImg(initialData.src, croppedAreaPixels, rotation);
113
+ const croppedBlob = await getCroppedImg(
114
+ initialData.src,
115
+ croppedAreaPixels,
116
+ rotation
117
+ );
101
118
  const file = new File([croppedBlob], "cropped-image.jpg", {
102
119
  type: "image/jpeg"
103
120
  });
@@ -122,112 +139,166 @@ function ImageEditDialog({
122
139
  }, [selectedImg, alt, onSave, handleClose]);
123
140
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (o) => !o && handleClose(), children: /* @__PURE__ */ jsxs(DialogContent, { className: "flex max-h-[80vh] max-w-lg flex-col gap-0 overflow-hidden p-0", children: [
124
141
  /* @__PURE__ */ jsx(DialogHeader, { className: "px-6 pt-5 pb-0", children: /* @__PURE__ */ jsx(DialogTitle, { className: "text-base font-semibold", children: "Edit Image" }) }),
125
- /* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), className: "flex flex-1 flex-col overflow-hidden", children: [
126
- /* @__PURE__ */ jsxs(TabsList, { className: "mx-6 mt-3 bg-transparent", children: [
127
- /* @__PURE__ */ jsxs(TabsTrigger, { value: "adjust", children: [
128
- /* @__PURE__ */ jsx(Pencil, {}),
129
- "Crop"
130
- ] }),
131
- /* @__PURE__ */ jsxs(TabsTrigger, { value: "library", children: [
132
- /* @__PURE__ */ jsx(Search, {}),
133
- "Library"
134
- ] }),
135
- /* @__PURE__ */ jsxs(TabsTrigger, { value: "upload", children: [
136
- /* @__PURE__ */ jsx(Upload, {}),
137
- "Upload"
138
- ] })
139
- ] }),
140
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex-1 overflow-hidden", children: [
141
- /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col", tab !== "adjust" && "hidden"), children: [
142
- /* @__PURE__ */ jsx("div", { className: "bg-muted relative h-64", children: /* @__PURE__ */ jsx(
143
- Cropper,
144
- {
145
- image: initialData.src,
146
- crop,
147
- zoom,
148
- rotation,
149
- aspect: 16 / 9,
150
- onCropChange: setCrop,
151
- onZoomChange: setZoom,
152
- onRotationChange: setRotation,
153
- onCropComplete: (_, pixels) => setCroppedAreaPixels(pixels)
154
- }
155
- ) }),
156
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 px-6 py-3", children: [
157
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
158
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-gray-500", children: "Zoom" }),
159
- /* @__PURE__ */ jsx(Slider, { value: [zoom], onValueChange: (vals) => setZoom(Array.isArray(vals) ? vals[0] ?? 1 : vals), min: 1, max: 3, step: 0.1 })
142
+ /* @__PURE__ */ jsxs(
143
+ Tabs,
144
+ {
145
+ value: tab,
146
+ onValueChange: (v) => setTab(v),
147
+ className: "flex flex-1 flex-col overflow-hidden",
148
+ children: [
149
+ /* @__PURE__ */ jsxs(TabsList, { className: "mx-6 mt-3 bg-transparent", children: [
150
+ /* @__PURE__ */ jsxs(TabsTrigger, { value: "adjust", children: [
151
+ /* @__PURE__ */ jsx(Pencil, {}),
152
+ "Crop"
153
+ ] }),
154
+ /* @__PURE__ */ jsxs(TabsTrigger, { value: "library", children: [
155
+ /* @__PURE__ */ jsx(Search, {}),
156
+ "Library"
160
157
  ] }),
161
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
162
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-gray-500", children: "Rotation" }),
163
- /* @__PURE__ */ jsx(Slider, { value: [rotation], onValueChange: (vals) => setRotation(Array.isArray(vals) ? vals[0] ?? 0 : vals), min: 0, max: 360, step: 1 })
158
+ /* @__PURE__ */ jsxs(TabsTrigger, { value: "upload", children: [
159
+ /* @__PURE__ */ jsx(Upload, {}),
160
+ "Upload"
164
161
  ] })
165
- ] })
166
- ] }),
167
- /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-3 px-6 py-4", tab !== "library" && "hidden"), children: /* @__PURE__ */ jsx(ScrollArea, { className: "h-[280px]", children: isLoadingImages ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-20", children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }) : libraryImages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center py-20 text-center", children: [
168
- /* @__PURE__ */ jsx(Search, { className: "h-8 w-8 text-gray-300" }),
169
- /* @__PURE__ */ jsx("p", { className: "mt-3 text-sm font-medium text-gray-500", children: "No images available" }),
170
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-400", children: "Try uploading an image instead" })
171
- ] }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: libraryImages.map((src) => {
172
- const isSelected = selectedImg === src;
173
- return /* @__PURE__ */ jsxs(
174
- "button",
175
- {
176
- type: "button",
177
- onClick: () => setSelectedImg(src),
178
- className: `relative aspect-4/3 overflow-hidden rounded-lg border-2 transition-all ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-transparent hover:border-gray-300"}`,
179
- children: [
180
- /* @__PURE__ */ jsx("img", { src, alt: "", className: "h-full w-full object-cover" }),
181
- isSelected && /* @__PURE__ */ jsx("div", { className: "absolute top-1.5 right-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 shadow-sm", children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-white" }) })
182
- ]
183
- },
184
- src
185
- );
186
- }) }) }) }),
187
- /* @__PURE__ */ jsxs(
188
- "div",
189
- {
190
- className: cn("flex flex-col items-center justify-center gap-4 px-6 py-14", tab !== "upload" && "hidden"),
191
- children: [
192
- /* @__PURE__ */ jsx("div", { className: "flex h-14 w-14 items-center justify-center rounded-full bg-gray-100", children: /* @__PURE__ */ jsx(Upload, { className: "h-6 w-6 text-gray-400" }) }),
193
- /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
194
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-700", children: "Upload a new image to replace the current one" }),
195
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-400", children: "Supports JPG, PNG, WebP formats" })
196
- ] }),
197
- /* @__PURE__ */ jsxs(
198
- Button,
162
+ ] }),
163
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex-1 overflow-hidden", children: [
164
+ /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col", tab !== "adjust" && "hidden"), children: [
165
+ /* @__PURE__ */ jsx("div", { className: "bg-muted relative h-64", children: /* @__PURE__ */ jsx(
166
+ Cropper,
199
167
  {
200
- variant: "outline",
201
- size: "sm",
202
- onClick: () => {
203
- var _a;
204
- return (_a = replaceInputRef.current) == null ? void 0 : _a.click();
205
- },
206
- disabled: isReplaceUploading,
207
- children: [
208
- isReplaceUploading ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Upload, { className: "mr-1.5 h-3.5 w-3.5" }),
209
- "Choose File"
210
- ]
168
+ image: initialData.src,
169
+ crop,
170
+ zoom,
171
+ rotation,
172
+ aspect: 16 / 9,
173
+ onCropChange: setCrop,
174
+ onZoomChange: setZoom,
175
+ onRotationChange: setRotation,
176
+ onCropComplete: (_, pixels) => setCroppedAreaPixels(pixels)
211
177
  }
212
- ),
213
- /* @__PURE__ */ jsx(
214
- "input",
215
- {
216
- ref: replaceInputRef,
217
- type: "file",
218
- accept: "image/*",
219
- onChange: handleFileReplace,
220
- className: "hidden"
221
- }
222
- )
223
- ]
224
- }
225
- )
226
- ] })
227
- ] }),
178
+ ) }),
179
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 px-6 py-3", children: [
180
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
181
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-gray-500", children: "Zoom" }),
182
+ /* @__PURE__ */ jsx(
183
+ Slider,
184
+ {
185
+ className: "flex",
186
+ value: [zoom],
187
+ onValueChange: (vals) => setZoom(Array.isArray(vals) ? vals[0] ?? 1 : vals),
188
+ min: 1,
189
+ max: 3,
190
+ step: 0.1
191
+ }
192
+ )
193
+ ] }),
194
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
195
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-gray-500", children: "Rotation" }),
196
+ /* @__PURE__ */ jsx(
197
+ Slider,
198
+ {
199
+ className: "flex",
200
+ value: [rotation],
201
+ onValueChange: (vals) => setRotation(Array.isArray(vals) ? vals[0] ?? 0 : vals),
202
+ min: 0,
203
+ max: 360,
204
+ step: 1
205
+ }
206
+ )
207
+ ] })
208
+ ] })
209
+ ] }),
210
+ /* @__PURE__ */ jsx(
211
+ "div",
212
+ {
213
+ className: cn(
214
+ "flex flex-col gap-3 px-6 py-4",
215
+ tab !== "library" && "hidden"
216
+ ),
217
+ children: /* @__PURE__ */ jsx(ScrollArea, { className: "h-[280px]", children: isLoadingImages ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-20", children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-gray-400" }) }) : libraryImages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center py-20 text-center", children: [
218
+ /* @__PURE__ */ jsx(Search, { className: "h-8 w-8 text-gray-300" }),
219
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-sm font-medium text-gray-500", children: "No images available" }),
220
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-400", children: "Try uploading an image instead" })
221
+ ] }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: libraryImages.map((src) => {
222
+ const isSelected = selectedImg === src;
223
+ return /* @__PURE__ */ jsxs(
224
+ "button",
225
+ {
226
+ type: "button",
227
+ onClick: () => setSelectedImg(src),
228
+ className: `relative aspect-4/3 overflow-hidden rounded-lg border-2 transition-all ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-transparent hover:border-gray-300"}`,
229
+ children: [
230
+ /* @__PURE__ */ jsx(
231
+ "img",
232
+ {
233
+ src,
234
+ alt: "",
235
+ className: "h-full w-full object-cover"
236
+ }
237
+ ),
238
+ isSelected && /* @__PURE__ */ jsx("div", { className: "absolute top-1.5 right-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 shadow-sm", children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-white" }) })
239
+ ]
240
+ },
241
+ src
242
+ );
243
+ }) }) })
244
+ }
245
+ ),
246
+ /* @__PURE__ */ jsxs(
247
+ "div",
248
+ {
249
+ className: cn(
250
+ "flex flex-col items-center justify-center gap-4 px-6 py-14",
251
+ tab !== "upload" && "hidden"
252
+ ),
253
+ children: [
254
+ /* @__PURE__ */ jsx("div", { className: "flex h-14 w-14 items-center justify-center rounded-full bg-gray-100", children: /* @__PURE__ */ jsx(Upload, { className: "h-6 w-6 text-gray-400" }) }),
255
+ /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
256
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-700", children: "Upload a new image to replace the current one" }),
257
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-400", children: "Supports JPG, PNG, WebP formats" })
258
+ ] }),
259
+ /* @__PURE__ */ jsxs(
260
+ Button,
261
+ {
262
+ variant: "outline",
263
+ size: "sm",
264
+ onClick: () => {
265
+ var _a;
266
+ return (_a = replaceInputRef.current) == null ? void 0 : _a.click();
267
+ },
268
+ disabled: isReplaceUploading,
269
+ children: [
270
+ isReplaceUploading ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Upload, { className: "mr-1.5 h-3.5 w-3.5" }),
271
+ "Choose File"
272
+ ]
273
+ }
274
+ ),
275
+ /* @__PURE__ */ jsx(
276
+ "input",
277
+ {
278
+ ref: replaceInputRef,
279
+ type: "file",
280
+ accept: "image/*",
281
+ onChange: handleFileReplace,
282
+ className: "hidden"
283
+ }
284
+ )
285
+ ]
286
+ }
287
+ )
288
+ ] })
289
+ ]
290
+ }
291
+ ),
228
292
  /* @__PURE__ */ jsx("div", { className: "border-t px-6 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
229
293
  /* @__PURE__ */ jsx(Label, { className: "flex-none", children: "Alt Text" }),
230
- /* @__PURE__ */ jsx(Input, { value: alt, onChange: (e) => setAlt(e.target.value), placeholder: "Image description..." })
294
+ /* @__PURE__ */ jsx(
295
+ Input,
296
+ {
297
+ value: alt,
298
+ onChange: (e) => setAlt(e.target.value),
299
+ placeholder: "Image description..."
300
+ }
301
+ )
231
302
  ] }) }),
232
303
  /* @__PURE__ */ jsxs(DialogFooter, { className: "border-t px-6 py-3", children: [
233
304
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: handleClose, children: "Cancel" }),
@@ -8,7 +8,7 @@ export { CompoundForm, type CompoundFormProps, formMethods, type FormMethods } f
8
8
  export { CompoundImage, type CompoundImageProps } from "./image";
9
9
  export { CompoundArticleCard, type CompoundArticleCardProps } from "./article-card";
10
10
  export { ImageUploadField } from "./image-upload-field";
11
- export { ActionField, ACTION_TYPE_FIELD_MAP, ACTION_TYPE_OPTIONS, PageActionFields, ExternalActionFields, EmailActionFields, PhoneActionFields, SectionActionFields, DownloadActionFields, type ActionTypeFieldProps, } from "./fields/action-field";
11
+ export { ActionField, ACTION_TYPE_FIELD_MAP, ACTION_TYPE_OPTIONS, PageActionFields, ExternalActionFields, EmailActionFields, PhoneActionFields, SectionActionFields, DownloadActionFields, type ActionTypeFieldProps, } from "./fields";
12
12
  export { ErrorBoundary } from "./error-boundary";
13
13
  export { GradientText, type GradientTextProps } from "./gradient-text";
14
14
  export { InlineEditable } from "./inline-editable";
@@ -18,6 +18,5 @@ export { usePuckDispatch, usePuckAppState, usePuckHistory, usePuckConfig, usePuc
18
18
  export { findComponentById, getRandomAdjective } from "./core/utils";
19
19
  export { icon, action, actionDefaults, button, buttons, badge, image, images, padding, paddingDefaults, paddingLevel, heading, description, features, contentFields, contentFieldsWithFeatures, card, cards, field, fieldDefaults, form, formDefaults, getPlaceholderImageUrl, image16x9Placeholder, image1x1Placeholder, image9x16Placeholder, sectionStyle, sectionOverlay, backgroundColor, backgroundImage, } from "./core/fields";
20
20
  export { EditorContextProvider, useEditorContext } from "./editor-context";
21
- export { AutoField } from "./fields/auto-field";
22
- export { type FieldDef, type AutoFieldProps, type FieldsPanelProps } from "./fields/types";
21
+ export { AutoField, type FieldDef, type AutoFieldProps, type FieldsPanelProps } from "./fields";
23
22
  export { default as useUpload, type UploadFileItem, type UploadStatus, type UseUploadOptions } from "./use-upload";
@@ -18,7 +18,10 @@ function Slider({
18
18
  return /* @__PURE__ */ jsx(
19
19
  Slider$1.Root,
20
20
  {
21
- className: cn("data-horizontal:w-full data-vertical:h-full", className),
21
+ className: cn(
22
+ "data-horizontal:w-full data-vertical:h-full flex items-center",
23
+ className
24
+ ),
22
25
  "data-slot": "slider",
23
26
  defaultValue,
24
27
  value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dune-react",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -43,6 +43,7 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
+ "@aws-sdk/client-s3": "3.1017.0",
46
47
  "@puckeditor/core": "0.21.1",
47
48
  "@storybook/addon-docs": "^10.2.19",
48
49
  "@storybook/addon-links": "^10.2.19",