@arronqzy/react-view 1.0.22 → 1.0.23

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.
@@ -0,0 +1,169 @@
1
+ import { useI18n } from "@arronqzy/i18n/react";
2
+ import { Input } from "@arronqzy/ui";
3
+ import type { PanelElement } from "../../types";
4
+ import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
5
+
6
+ export function PanelConfigNodeInfo({
7
+ element,
8
+ helpers,
9
+ updateElement,
10
+ nodeZOrderLabel,
11
+ onAdjustNodeZOrder,
12
+ }: {
13
+ element: PanelElement;
14
+ helpers: ConfigSectionHelpers;
15
+ updateElement: UpdateElement;
16
+ nodeZOrderLabel?: string;
17
+ onAdjustNodeZOrder?: (
18
+ nodeId: string,
19
+ action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
20
+ ) => void;
21
+ }) {
22
+ const { t } = useI18n();
23
+ const { renderSection, renderFieldGroup } = helpers;
24
+ const selectedElement = element;
25
+ return (
26
+ <>
27
+ {renderSection(
28
+ "nodeInfo",
29
+ t("panel.config.sectionNodeInfo"),
30
+ <>
31
+ <label className="block space-y-1" data-config-field="name">
32
+ <div>{t("panel.config.nodeName")}</div>
33
+ <Input
34
+ value={selectedElement.name ?? ""}
35
+ onChange={(e) =>
36
+ updateElement(selectedElement.id, {
37
+ name: e.target.value || undefined,
38
+ })
39
+ }
40
+ placeholder={t("panel.config.nodeNamePlaceholder")}
41
+ className="h-7"
42
+ />
43
+ </label>
44
+ <div className="grid grid-cols-2 gap-2">
45
+ <label className="block space-y-1" data-config-field="x">
46
+ <div>X</div>
47
+ <Input
48
+ type="number"
49
+ value={selectedElement.x}
50
+ onChange={(e) =>
51
+ updateElement(selectedElement.id, {
52
+ x: Number(e.target.value) || 0,
53
+ })
54
+ }
55
+ className="h-7"
56
+ />
57
+ </label>
58
+ <label className="block space-y-1" data-config-field="y">
59
+ <div>Y</div>
60
+ <Input
61
+ type="number"
62
+ value={selectedElement.y}
63
+ onChange={(e) =>
64
+ updateElement(selectedElement.id, {
65
+ y: Number(e.target.value) || 0,
66
+ })
67
+ }
68
+ className="h-7"
69
+ />
70
+ </label>
71
+ </div>
72
+ <div className="grid grid-cols-2 gap-2">
73
+ <label className="block space-y-1" data-config-field="width">
74
+ <div>{t("panel.config.width")}</div>
75
+ <Input
76
+ type="number"
77
+ min={1}
78
+ value={selectedElement?.width ?? 1}
79
+ onChange={(e) => {
80
+ if (!selectedElement) return;
81
+ updateElement(selectedElement.id, {
82
+ width: Math.max(1, Number(e.target.value) || 1),
83
+ });
84
+ }}
85
+ className="h-7"
86
+ />
87
+ </label>
88
+ <label className="block space-y-1" data-config-field="height">
89
+ <div>{t("panel.config.height")}</div>
90
+ <Input
91
+ type="number"
92
+ min={1}
93
+ value={selectedElement.height}
94
+ onChange={(e) =>
95
+ updateElement(selectedElement.id, {
96
+ height: Math.max(1, Number(e.target.value) || 1),
97
+ })
98
+ }
99
+ className="h-7"
100
+ />
101
+ </label>
102
+ </div>
103
+ {renderFieldGroup(
104
+ t("panel.config.groupNodeMore"),
105
+ <>
106
+ <div className="grid grid-cols-2 gap-2">
107
+ <label className="block space-y-1" data-config-field="rotate">
108
+ <div>{t("panel.config.rotate")}</div>
109
+ <Input
110
+ type="number"
111
+ value={selectedElement.rotate ?? 0}
112
+ onChange={(e) =>
113
+ updateElement(selectedElement.id, {
114
+ rotate: Number(e.target.value) || 0,
115
+ })
116
+ }
117
+ className="h-7"
118
+ />
119
+ </label>
120
+ </div>
121
+ <div className="space-y-1.5">
122
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.nodeZOrder")}</div>
123
+ <div className="text-[11px] text-muted-foreground/90">
124
+ {t("panel.config.currentZIndex", { value: nodeZOrderLabel ?? "-" })}
125
+ </div>
126
+ <div className="grid grid-cols-2 gap-2">
127
+ <button
128
+ type="button"
129
+ className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
130
+ onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringForward")}
131
+ >
132
+ {t("panel.menubar.bringForward")}
133
+ </button>
134
+ <button
135
+ type="button"
136
+ className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
137
+ onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendBackward")}
138
+ >
139
+ {t("panel.menubar.sendBackward")}
140
+ </button>
141
+ <button
142
+ type="button"
143
+ className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
144
+ onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringToFront")}
145
+ >
146
+ {t("panel.menubar.bringToFront")}
147
+ </button>
148
+ <button
149
+ type="button"
150
+ className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
151
+ onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendToBack")}
152
+ >
153
+ {t("panel.menubar.sendToBack")}
154
+ </button>
155
+ </div>
156
+ </div>
157
+ <div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
158
+ <div className="text-muted-foreground">{t("panel.config.type")}: {selectedElement.materialType ?? selectedElement.id}</div>
159
+ </>,
160
+ undefined,
161
+ { groupKey: "nodeMore", defaultOpen: false }
162
+ )}
163
+ </>,
164
+ true,
165
+ [t("panel.config.name"), "id", t("panel.config.type"), t("panel.config.rotate"), t("panel.config.nodeZOrder"), t("panel.layers.lockShort"), "locked", "name"]
166
+ )}
167
+ </>
168
+ );
169
+ }
@@ -0,0 +1,99 @@
1
+ import { useI18n } from "@arronqzy/i18n/react";
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "@arronqzy/ui";
9
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
10
+ import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
11
+
12
+ export function PanelConfigReferenceSection({
13
+ element,
14
+ helpers,
15
+ updateElement,
16
+ layers,
17
+ setReferenceCopyMode,
18
+ }: {
19
+ element: PanelElement;
20
+ helpers: ConfigSectionHelpers;
21
+ updateElement: UpdateElement;
22
+ layers: PanelLayer[];
23
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
24
+ }) {
25
+ const { t } = useI18n();
26
+ const { renderSection, renderFieldGroup } = helpers;
27
+ const selectedElement = element;
28
+ return (
29
+ <>
30
+ {
31
+ renderSection(
32
+ "reference",
33
+ t("panel.config.sectionReference"),
34
+ <>
35
+ {renderFieldGroup(
36
+ t("panel.config.groupRefSource"),
37
+ <label className="block space-y-1">
38
+ <div>{t("panel.config.refLayer")}</div>
39
+ <Select
40
+ value={selectedElement.refLayerId ?? "__none__"}
41
+ onValueChange={(value) =>
42
+ updateElement(selectedElement.id, {
43
+ refLayerId: value === "__none__" ? undefined : value,
44
+ })
45
+ }
46
+ >
47
+ <SelectTrigger className="h-7">
48
+ <SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
49
+ </SelectTrigger>
50
+ <SelectContent>
51
+ <SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
52
+ {layers
53
+ .filter((l) => l.id !== selectedElement.layerId)
54
+ .map((l) => (
55
+ <SelectItem key={l.id} value={l.id}>
56
+ {l.name}
57
+ </SelectItem>
58
+ ))}
59
+ </SelectContent>
60
+ </Select>
61
+ </label>
62
+ )}
63
+ {renderFieldGroup(
64
+ t("panel.config.groupCopyStrategy"),
65
+ <>
66
+ <label className="block space-y-1">
67
+ <div>{t("panel.config.copyMode")}</div>
68
+ <Select
69
+ value={selectedElement.refCopyMode ?? "shallow"}
70
+ onValueChange={(value) =>
71
+ setReferenceCopyMode?.(
72
+ selectedElement.id,
73
+ value as ReferenceCopyMode
74
+ )
75
+ }
76
+ >
77
+ <SelectTrigger className="h-7">
78
+ <SelectValue placeholder={t("panel.config.selectCopyMode")} />
79
+ </SelectTrigger>
80
+ <SelectContent>
81
+ <SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
82
+ <SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
83
+ </SelectContent>
84
+ </Select>
85
+ </label>
86
+ </>,
87
+ <>
88
+ {t("panel.config.copyStrategyHint")}
89
+ </>,
90
+ { groupKey: "referenceCopyStrategy", defaultOpen: false }
91
+ )}
92
+ </>,
93
+ true,
94
+ [t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
95
+ )
96
+ }
97
+ </>
98
+ );
99
+ }
@@ -1,4 +1,9 @@
1
- import React, { useCallback, useMemo, useRef, useState } from "react";
1
+ import {
2
+ useCallback,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ } from "react";
2
7
  import { useI18n } from "@arronqzy/i18n/react";
3
8
  import {
4
9
  Button,
@@ -31,7 +36,6 @@ import type { PanelElement } from "../../types";
31
36
  import { readFileAsDataUrl } from "../../utils/async-work";
32
37
  import { randomId } from "../../utils/panelElementDefaults";
33
38
  import { ConfigHintIcon } from "../ConfigHintIcon";
34
-
35
39
  type UpdateElement = (
36
40
  id: string,
37
41
  patch: Partial<PanelElement>,
@@ -0,0 +1,249 @@
1
+ import React, { useCallback, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import {
4
+ Input,
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "@arronqzy/ui";
11
+ import type { PanelElement, PanelElementStyle } from "../../types";
12
+ import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
13
+ import { getPanelMessages } from "../../constants/messages";
14
+ import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
15
+
16
+ export function PanelConfigStyleSections({
17
+ element,
18
+ helpers,
19
+ updateElement,
20
+ }: {
21
+ element: PanelElement;
22
+ helpers: ConfigSectionHelpers;
23
+ updateElement: UpdateElement;
24
+ }) {
25
+ const { t } = useI18n();
26
+ const messages = React.useMemo(() => getPanelMessages(t), [t]);
27
+ const { renderSection, renderFieldGroup, renderColorField } = helpers;
28
+ const selectedElement = element;
29
+ const [uploadStatus, setUploadStatus] = useState("");
30
+
31
+ const updateSelectedStyle = useCallback(
32
+ (patch: Partial<PanelElementStyle>) => {
33
+ updateElement(selectedElement.id, {
34
+ style: { ...(selectedElement.style ?? {}), ...patch },
35
+ });
36
+ },
37
+ [selectedElement, updateElement]
38
+ );
39
+
40
+ const handleUploadBackgroundImage = useCallback(
41
+ async (file: File) => {
42
+ try {
43
+ await runBusyTask(t("common.uploadingFile"), async () => {
44
+ const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
45
+ updateSelectedStyle({
46
+ backgroundImage: `url("${base64}")`,
47
+ });
48
+ setUploadStatus(t("panel.config.uploadWrittenBase64"));
49
+ try {
50
+ const form = new FormData();
51
+ form.append("file", file);
52
+ const resp = await fetch("/api/upload", { method: "POST", body: form });
53
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
54
+ const data = (await resp.json()) as { url?: string };
55
+ if (data.url) {
56
+ updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
57
+ setUploadStatus(t("panel.config.uploadServerAndBase64"));
58
+ }
59
+ } catch {
60
+ setUploadStatus(t("panel.config.uploadServerFailedKeepBase64"));
61
+ }
62
+ });
63
+ } catch (error) {
64
+ setUploadStatus(
65
+ error instanceof Error ? error.message : messages.readImageFailed
66
+ );
67
+ }
68
+ },
69
+ [messages.readImageFailed, t, updateSelectedStyle]
70
+ );
71
+
72
+ return (
73
+ <>
74
+ {renderSection(
75
+ "styleBackground",
76
+ t("panel.config.sectionStyleBackground"),
77
+ <>
78
+ {renderFieldGroup(
79
+ t("panel.config.groupBgFill"),
80
+ <>
81
+ {renderColorField(
82
+ t("panel.config.backgroundColor"),
83
+ selectedElement.style?.backgroundColor ?? "",
84
+ (next) => updateSelectedStyle({ backgroundColor: next || undefined })
85
+ )}
86
+ </>
87
+ )}
88
+ {renderFieldGroup(
89
+ t("panel.config.groupBgLayout"),
90
+ <>
91
+ <label className="block space-y-1">
92
+ <div>{t("panel.config.backgroundImage")}</div>
93
+ <Input
94
+ value={selectedElement.style?.backgroundImage ?? ""}
95
+ onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
96
+ placeholder='url("https://...") / linear-gradient(...)'
97
+ className="h-7"
98
+ />
99
+ </label>
100
+ <div className="flex items-center gap-2">
101
+ <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
102
+ {t("panel.config.uploadImage")}
103
+ <Input
104
+ type="file"
105
+ accept="image/*"
106
+ className="hidden"
107
+ onChange={async (e) => {
108
+ const file = e.target.files?.[0];
109
+ e.currentTarget.value = "";
110
+ if (!file) return;
111
+ await handleUploadBackgroundImage(file);
112
+ }}
113
+ />
114
+ </label>
115
+ {uploadStatus ? (
116
+ <span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
117
+ ) : null}
118
+ </div>
119
+ <div className="grid grid-cols-2 gap-2">
120
+ <label className="block space-y-1">
121
+ <div>{t("panel.config.backgroundSize")}</div>
122
+ <Select
123
+ value={selectedElement.style?.backgroundSize ?? "__none__"}
124
+ onValueChange={(value) =>
125
+ updateSelectedStyle({
126
+ backgroundSize: value === "__none__" ? undefined : value,
127
+ })
128
+ }
129
+ >
130
+ <SelectTrigger className="h-7">
131
+ <SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
132
+ </SelectTrigger>
133
+ <SelectContent>
134
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
135
+ <SelectItem value="cover">cover</SelectItem>
136
+ <SelectItem value="contain">contain</SelectItem>
137
+ <SelectItem value="100% 100%">100% 100%</SelectItem>
138
+ <SelectItem value="auto">auto</SelectItem>
139
+ </SelectContent>
140
+ </Select>
141
+ </label>
142
+ <label className="block space-y-1">
143
+ <div>{t("panel.config.backgroundPosition")}</div>
144
+ <Select
145
+ value={selectedElement.style?.backgroundPosition ?? "__none__"}
146
+ onValueChange={(value) =>
147
+ updateSelectedStyle({
148
+ backgroundPosition: value === "__none__" ? undefined : value,
149
+ })
150
+ }
151
+ >
152
+ <SelectTrigger className="h-7">
153
+ <SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
154
+ </SelectTrigger>
155
+ <SelectContent>
156
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
157
+ <SelectItem value="center">center</SelectItem>
158
+ <SelectItem value="top">top</SelectItem>
159
+ <SelectItem value="bottom">bottom</SelectItem>
160
+ <SelectItem value="left">left</SelectItem>
161
+ <SelectItem value="right">right</SelectItem>
162
+ <SelectItem value="top left">top left</SelectItem>
163
+ <SelectItem value="top right">top right</SelectItem>
164
+ <SelectItem value="bottom left">bottom left</SelectItem>
165
+ <SelectItem value="bottom right">bottom right</SelectItem>
166
+ </SelectContent>
167
+ </Select>
168
+ </label>
169
+ </div>
170
+ </>,
171
+ undefined,
172
+ { groupKey: "styleBgLayout", defaultOpen: false }
173
+ )}
174
+ </>,
175
+ false,
176
+ [t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
177
+ )}
178
+ {renderSection(
179
+ "styleBorder",
180
+ t("panel.config.sectionStyleBorder"),
181
+ <>
182
+ {renderFieldGroup(
183
+ t("panel.config.groupBorderGeometry"),
184
+ <div className="grid grid-cols-2 gap-2">
185
+ <label className="block space-y-1">
186
+ <div>{t("panel.config.borderWidth")}</div>
187
+ <Input
188
+ type="number"
189
+ min={0}
190
+ value={selectedElement.style?.borderWidth ?? 0}
191
+ onChange={(e) =>
192
+ updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
193
+ }
194
+ className="h-7"
195
+ />
196
+ </label>
197
+ <label className="block space-y-1">
198
+ <div>{t("panel.config.borderRadius")}</div>
199
+ <Input
200
+ type="number"
201
+ min={0}
202
+ value={selectedElement.style?.borderRadius ?? 0}
203
+ onChange={(e) =>
204
+ updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
205
+ }
206
+ className="h-7"
207
+ />
208
+ </label>
209
+ </div>
210
+ )}
211
+ {renderFieldGroup(
212
+ t("panel.config.groupBorderVisual"),
213
+ <div className="grid grid-cols-2 gap-2">
214
+ <label className="block space-y-1">
215
+ <div>{t("panel.config.borderStyle")}</div>
216
+ <Select
217
+ value={selectedElement.style?.borderStyle ?? "solid"}
218
+ onValueChange={(value) =>
219
+ updateSelectedStyle({
220
+ borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
221
+ })
222
+ }
223
+ >
224
+ <SelectTrigger className="h-7">
225
+ <SelectValue placeholder={t("panel.config.selectBorderStyle")} />
226
+ </SelectTrigger>
227
+ <SelectContent>
228
+ <SelectItem value="none">none</SelectItem>
229
+ <SelectItem value="solid">solid</SelectItem>
230
+ <SelectItem value="dashed">dashed</SelectItem>
231
+ <SelectItem value="dotted">dotted</SelectItem>
232
+ <SelectItem value="double">double</SelectItem>
233
+ </SelectContent>
234
+ </Select>
235
+ </label>
236
+ {renderColorField(
237
+ t("panel.config.borderColor"),
238
+ selectedElement.style?.borderColor ?? "",
239
+ (next) => updateSelectedStyle({ borderColor: next || undefined })
240
+ )}
241
+ </div>
242
+ )}
243
+ </>,
244
+ false,
245
+ [t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
246
+ )}
247
+ </>
248
+ );
249
+ }