@arronqzy/react-view 1.0.22 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/panel/ReactViewPanel.tsx +109 -965
- package/src/panel/components/PanelConfigSidebar.tsx +297 -3712
- package/src/panel/components/PanelHistoryPopover.tsx +163 -0
- package/src/panel/components/PanelLayerDock.tsx +412 -0
- package/src/panel/components/PanelMenubar.tsx +149 -0
- package/src/panel/components/PanelWorkspaceDialogs.tsx +271 -0
- package/src/panel/components/config/PanelConfigAudioSection.tsx +334 -0
- package/src/panel/components/config/PanelConfigChartSection.tsx +799 -0
- package/src/panel/components/config/PanelConfigGeometrySection.tsx +233 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.tsx +70 -0
- package/src/panel/components/config/PanelConfigGridSection.tsx +118 -0
- package/src/panel/components/config/PanelConfigMultiSelect.tsx +1291 -0
- package/src/panel/components/config/PanelConfigNodeInfo.tsx +169 -0
- package/src/panel/components/config/PanelConfigReferenceSection.tsx +99 -0
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +6 -2
- package/src/panel/components/config/PanelConfigStyleSections.tsx +249 -0
- package/src/panel/components/config/PanelConfigTextSection.tsx +268 -0
- package/src/panel/components/config/PanelConfigVideoSection.tsx +133 -0
- package/src/panel/components/config/PanelConfigViewportSection.tsx +139 -0
- package/src/panel/components/config/helpers.ts +85 -0
- package/src/panel/icons.tsx +160 -0
- package/src/panel/utils/panelSelection.ts +57 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
3
|
+
import {
|
|
4
|
+
Checkbox,
|
|
5
|
+
Input,
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "@arronqzy/ui";
|
|
12
|
+
import type { PanelElement } from "../../types";
|
|
13
|
+
import { cssTextLineHeight, cssTextAlignStyle } from "../../utils/panelElementDefaults";
|
|
14
|
+
import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
|
|
15
|
+
|
|
16
|
+
export function PanelConfigTextSection({
|
|
17
|
+
element,
|
|
18
|
+
helpers,
|
|
19
|
+
updateElement,
|
|
20
|
+
}: {
|
|
21
|
+
element: PanelElement;
|
|
22
|
+
helpers: ConfigSectionHelpers;
|
|
23
|
+
updateElement: UpdateElement;
|
|
24
|
+
}) {
|
|
25
|
+
const { t } = useI18n();
|
|
26
|
+
const { renderSection, renderFieldGroup, renderColorField } = helpers;
|
|
27
|
+
const selectedElement = element;
|
|
28
|
+
const textEditorRef = useRef<HTMLDivElement | null>(null);
|
|
29
|
+
|
|
30
|
+
const updateSelectedText = useCallback(
|
|
31
|
+
(patch: Partial<PanelElement>) => {
|
|
32
|
+
if (selectedElement.materialType !== "text") return;
|
|
33
|
+
updateElement(selectedElement.id, patch);
|
|
34
|
+
},
|
|
35
|
+
[selectedElement, updateElement]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (selectedElement.materialType !== "text") return;
|
|
40
|
+
const nextHtml =
|
|
41
|
+
selectedElement.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
|
|
42
|
+
if (textEditorRef.current && textEditorRef.current.innerHTML !== nextHtml) {
|
|
43
|
+
textEditorRef.current.innerHTML = nextHtml;
|
|
44
|
+
}
|
|
45
|
+
}, [selectedElement.id, selectedElement.materialType, selectedElement.textHtml]);
|
|
46
|
+
|
|
47
|
+
const execTextCommand = (cmd: "bold" | "italic" | "underline") => {
|
|
48
|
+
textEditorRef.current?.focus();
|
|
49
|
+
document.execCommand(cmd);
|
|
50
|
+
const next = textEditorRef.current?.innerHTML ?? "";
|
|
51
|
+
if (selectedElement.materialType === "text") {
|
|
52
|
+
updateElement(selectedElement.id, { textHtml: next || "<p><br/></p>" });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{
|
|
59
|
+
renderSection(
|
|
60
|
+
"textConfig",
|
|
61
|
+
t("panel.config.sectionText"),
|
|
62
|
+
<>
|
|
63
|
+
{renderFieldGroup(
|
|
64
|
+
t("panel.config.groupTextContent"),
|
|
65
|
+
<>
|
|
66
|
+
<div className="flex items-center gap-1">
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
70
|
+
onClick={() => execTextCommand("bold")}
|
|
71
|
+
>
|
|
72
|
+
B
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="rounded border border-border px-2 py-1 text-[11px] italic hover:bg-accent"
|
|
77
|
+
onClick={() => execTextCommand("italic")}
|
|
78
|
+
>
|
|
79
|
+
I
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="rounded border border-border px-2 py-1 text-[11px] underline hover:bg-accent"
|
|
84
|
+
onClick={() => execTextCommand("underline")}
|
|
85
|
+
>
|
|
86
|
+
U
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div
|
|
90
|
+
ref={textEditorRef}
|
|
91
|
+
data-config-field="textHtml"
|
|
92
|
+
data-panel-user-text=""
|
|
93
|
+
className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 leading-6 outline-none"
|
|
94
|
+
style={{
|
|
95
|
+
fontFamily: selectedElement.textFontFamily || undefined,
|
|
96
|
+
fontSize: selectedElement.textFontSize
|
|
97
|
+
? `${selectedElement.textFontSize}px`
|
|
98
|
+
: undefined,
|
|
99
|
+
fontWeight: selectedElement.textFontWeight || undefined,
|
|
100
|
+
color: selectedElement.textColor || undefined,
|
|
101
|
+
lineHeight: cssTextLineHeight(selectedElement.textLineHeight),
|
|
102
|
+
...cssTextAlignStyle(selectedElement.textAlign),
|
|
103
|
+
}}
|
|
104
|
+
contentEditable
|
|
105
|
+
suppressContentEditableWarning
|
|
106
|
+
onInput={(e) => {
|
|
107
|
+
const nextHtml = (e.currentTarget as HTMLDivElement).innerHTML;
|
|
108
|
+
updateSelectedText({
|
|
109
|
+
textHtml: nextHtml || "<p><br/></p>",
|
|
110
|
+
});
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
{renderFieldGroup(
|
|
116
|
+
t("panel.config.groupTextStyle"),
|
|
117
|
+
<>
|
|
118
|
+
<label className="block space-y-1">
|
|
119
|
+
<div>{t("panel.config.fontFamily")}</div>
|
|
120
|
+
<Input
|
|
121
|
+
value={selectedElement.textFontFamily ?? ""}
|
|
122
|
+
onChange={(e) =>
|
|
123
|
+
updateSelectedText({
|
|
124
|
+
textFontFamily: e.target.value || undefined,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
placeholder={t("panel.config.fontFamilyPlaceholder")}
|
|
128
|
+
className="h-7"
|
|
129
|
+
/>
|
|
130
|
+
</label>
|
|
131
|
+
<div className="grid grid-cols-2 gap-2">
|
|
132
|
+
<label className="block space-y-1">
|
|
133
|
+
<div>{t("panel.config.fontSizePx")}</div>
|
|
134
|
+
<Input
|
|
135
|
+
type="number"
|
|
136
|
+
min={8}
|
|
137
|
+
max={200}
|
|
138
|
+
step={1}
|
|
139
|
+
value={selectedElement.textFontSize ?? 14}
|
|
140
|
+
onChange={(e) =>
|
|
141
|
+
updateSelectedText({
|
|
142
|
+
textFontSize: Math.max(8, Number(e.target.value) || 14),
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
className="h-7"
|
|
146
|
+
/>
|
|
147
|
+
</label>
|
|
148
|
+
<label className="block space-y-1">
|
|
149
|
+
<div>{t("panel.config.fontWeight")}</div>
|
|
150
|
+
<Select
|
|
151
|
+
value={selectedElement.textFontWeight ?? "400"}
|
|
152
|
+
onValueChange={(value) =>
|
|
153
|
+
updateSelectedText({
|
|
154
|
+
textFontWeight: value,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
>
|
|
158
|
+
<SelectTrigger className="h-7">
|
|
159
|
+
<SelectValue placeholder={t("panel.config.selectFontWeight")} />
|
|
160
|
+
</SelectTrigger>
|
|
161
|
+
<SelectContent>
|
|
162
|
+
<SelectItem value="300">300</SelectItem>
|
|
163
|
+
<SelectItem value="400">400</SelectItem>
|
|
164
|
+
<SelectItem value="500">500</SelectItem>
|
|
165
|
+
<SelectItem value="600">600</SelectItem>
|
|
166
|
+
<SelectItem value="700">700</SelectItem>
|
|
167
|
+
</SelectContent>
|
|
168
|
+
</Select>
|
|
169
|
+
</label>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="grid grid-cols-2 gap-2">
|
|
172
|
+
<label className="block space-y-1">
|
|
173
|
+
<div>{t("panel.config.textAlign")}</div>
|
|
174
|
+
<Select
|
|
175
|
+
value={selectedElement.textAlign ?? "left"}
|
|
176
|
+
onValueChange={(value) =>
|
|
177
|
+
updateSelectedText({
|
|
178
|
+
textAlign: value as PanelElement["textAlign"],
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
>
|
|
182
|
+
<SelectTrigger className="h-7">
|
|
183
|
+
<SelectValue placeholder={t("panel.config.selectTextAlign")} />
|
|
184
|
+
</SelectTrigger>
|
|
185
|
+
<SelectContent>
|
|
186
|
+
<SelectItem value="left">{t("panel.config.alignLeft")}</SelectItem>
|
|
187
|
+
<SelectItem value="center">{t("panel.config.alignCenter")}</SelectItem>
|
|
188
|
+
<SelectItem value="middle">{t("panel.config.alignMiddle")}</SelectItem>
|
|
189
|
+
<SelectItem value="right">{t("panel.config.alignRight")}</SelectItem>
|
|
190
|
+
<SelectItem value="justify">{t("panel.config.alignJustify")}</SelectItem>
|
|
191
|
+
</SelectContent>
|
|
192
|
+
</Select>
|
|
193
|
+
</label>
|
|
194
|
+
<label className="block space-y-1">
|
|
195
|
+
<div className="flex items-center justify-between gap-2">
|
|
196
|
+
<span>{t("panel.config.lineHeight")}</span>
|
|
197
|
+
<label className="flex items-center gap-1 font-normal">
|
|
198
|
+
<Checkbox
|
|
199
|
+
checked={selectedElement.textLineHeight === "auto"}
|
|
200
|
+
className="h-3.5 w-3.5 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
201
|
+
onCheckedChange={(checked) =>
|
|
202
|
+
updateSelectedText({
|
|
203
|
+
textLineHeight: checked === true ? "auto" : 1.6,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
/>
|
|
207
|
+
<span>{t("panel.config.lineHeightAuto")}</span>
|
|
208
|
+
</label>
|
|
209
|
+
</div>
|
|
210
|
+
<Input
|
|
211
|
+
type="number"
|
|
212
|
+
min={1}
|
|
213
|
+
max={3}
|
|
214
|
+
step={0.1}
|
|
215
|
+
disabled={selectedElement.textLineHeight === "auto"}
|
|
216
|
+
placeholder={t("panel.config.lineHeightAuto")}
|
|
217
|
+
value={
|
|
218
|
+
selectedElement.textLineHeight === "auto"
|
|
219
|
+
? ""
|
|
220
|
+
: (selectedElement.textLineHeight ?? 1.6)
|
|
221
|
+
}
|
|
222
|
+
onChange={(e) =>
|
|
223
|
+
updateSelectedText({
|
|
224
|
+
textLineHeight: Math.min(
|
|
225
|
+
3,
|
|
226
|
+
Math.max(1, Number(e.target.value) || 1.6)
|
|
227
|
+
),
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
className="h-7"
|
|
231
|
+
/>
|
|
232
|
+
</label>
|
|
233
|
+
</div>
|
|
234
|
+
{renderColorField(
|
|
235
|
+
t("panel.config.textColor"),
|
|
236
|
+
selectedElement.textColor ?? "",
|
|
237
|
+
(next) =>
|
|
238
|
+
updateSelectedText({
|
|
239
|
+
textColor: next || undefined,
|
|
240
|
+
})
|
|
241
|
+
)}
|
|
242
|
+
</>
|
|
243
|
+
)}
|
|
244
|
+
{renderFieldGroup(
|
|
245
|
+
t("panel.config.groupInputAbility"),
|
|
246
|
+
<label className="flex items-center gap-2">
|
|
247
|
+
<Checkbox
|
|
248
|
+
checked={selectedElement.textAllowInput ?? true}
|
|
249
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
250
|
+
onCheckedChange={(checked) =>
|
|
251
|
+
updateSelectedText({
|
|
252
|
+
textAllowInput: checked !== false,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
/>
|
|
256
|
+
<span>{t("panel.config.allowCanvasInput")}</span>
|
|
257
|
+
</label>,
|
|
258
|
+
undefined,
|
|
259
|
+
{ groupKey: "textInputAbility", defaultOpen: false }
|
|
260
|
+
)}
|
|
261
|
+
</>,
|
|
262
|
+
true,
|
|
263
|
+
[t("panel.material.text"), t("panel.config.searchKwRichText"), t("panel.config.fontFamily"), t("panel.config.color"), t("panel.config.textAlign"), t("panel.config.lineHeight"), t("panel.config.searchKwInput")]
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
3
|
+
import { Checkbox, Input } from "@arronqzy/ui";
|
|
4
|
+
import type { PanelElement } from "../../types";
|
|
5
|
+
import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
|
|
6
|
+
import { getPanelMessages } from "../../constants/messages";
|
|
7
|
+
import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
|
|
8
|
+
|
|
9
|
+
export function PanelConfigVideoSection({
|
|
10
|
+
element,
|
|
11
|
+
helpers,
|
|
12
|
+
updateElement,
|
|
13
|
+
}: {
|
|
14
|
+
element: PanelElement;
|
|
15
|
+
helpers: ConfigSectionHelpers;
|
|
16
|
+
updateElement: UpdateElement;
|
|
17
|
+
}) {
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
const messages = React.useMemo(() => getPanelMessages(t), [t]);
|
|
20
|
+
const { renderSection, renderFieldGroup } = helpers;
|
|
21
|
+
const selectedElement = element;
|
|
22
|
+
const [videoStatus, setVideoStatus] = useState("");
|
|
23
|
+
|
|
24
|
+
const updateSelectedVideo = useCallback(
|
|
25
|
+
(patch: Partial<PanelElement>) => {
|
|
26
|
+
if (selectedElement.materialType !== "video") return;
|
|
27
|
+
updateElement(selectedElement.id, patch);
|
|
28
|
+
},
|
|
29
|
+
[selectedElement, updateElement]
|
|
30
|
+
);
|
|
31
|
+
const handleUploadVideoFile = useCallback(
|
|
32
|
+
async (file: File) => {
|
|
33
|
+
if (selectedElement.materialType !== "video") return;
|
|
34
|
+
try {
|
|
35
|
+
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
36
|
+
const base64 = await readFileAsDataUrl(file, messages.readVideoFailed, "video");
|
|
37
|
+
updateSelectedVideo({ videoSrc: base64 });
|
|
38
|
+
setVideoStatus(messages.videoLocalSaved);
|
|
39
|
+
try {
|
|
40
|
+
const form = new FormData();
|
|
41
|
+
form.append("file", file);
|
|
42
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
43
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
44
|
+
const data = (await resp.json()) as { url?: string };
|
|
45
|
+
if (data.url) {
|
|
46
|
+
updateSelectedVideo({ videoRemoteUrl: data.url });
|
|
47
|
+
setVideoStatus(messages.videoRemoteUploaded);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
setVideoStatus(messages.videoServerUploadFailed);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
setVideoStatus(
|
|
55
|
+
error instanceof Error ? error.message : messages.readVideoFailed
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[messages, selectedElement, t, updateSelectedVideo]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
{
|
|
65
|
+
renderSection(
|
|
66
|
+
"videoConfig",
|
|
67
|
+
t("panel.config.sectionVideo"),
|
|
68
|
+
<>
|
|
69
|
+
{renderFieldGroup(
|
|
70
|
+
t("panel.config.groupVideoSource"),
|
|
71
|
+
<>
|
|
72
|
+
<label className="block space-y-1">
|
|
73
|
+
<div>{t("panel.config.videoUrl")}</div>
|
|
74
|
+
<Input
|
|
75
|
+
value={selectedElement.videoRemoteUrl ?? ""}
|
|
76
|
+
onChange={(e) =>
|
|
77
|
+
updateSelectedVideo({
|
|
78
|
+
videoRemoteUrl: e.target.value || undefined,
|
|
79
|
+
videoSrc: e.target.value || selectedElement.videoSrc,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
placeholder="https://example.com/video.mp4"
|
|
83
|
+
className="h-7"
|
|
84
|
+
/>
|
|
85
|
+
</label>
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
88
|
+
{t("panel.config.uploadVideo")}
|
|
89
|
+
<Input
|
|
90
|
+
type="file"
|
|
91
|
+
accept="video/*"
|
|
92
|
+
className="hidden"
|
|
93
|
+
onChange={async (e) => {
|
|
94
|
+
const file = e.target.files?.[0];
|
|
95
|
+
e.currentTarget.value = "";
|
|
96
|
+
if (!file) return;
|
|
97
|
+
await handleUploadVideoFile(file);
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
{videoStatus ? (
|
|
103
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
104
|
+
{videoStatus}
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
<video
|
|
108
|
+
controls
|
|
109
|
+
className="h-36 w-full rounded border border-border/60 bg-black/80 object-contain"
|
|
110
|
+
src={selectedElement.videoSrc || selectedElement.videoRemoteUrl || ""}
|
|
111
|
+
/>
|
|
112
|
+
<label className="flex items-center gap-2">
|
|
113
|
+
<Checkbox
|
|
114
|
+
checked={selectedElement.mediaAutoPauseOnEdit !== false}
|
|
115
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
116
|
+
onCheckedChange={(checked) =>
|
|
117
|
+
updateSelectedVideo({
|
|
118
|
+
mediaAutoPauseOnEdit: checked !== false,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
<span>{t("panel.config.autoPauseMedia")}</span>
|
|
123
|
+
</label>
|
|
124
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
</>,
|
|
127
|
+
true,
|
|
128
|
+
[t("panel.material.video"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwPreview"), t("panel.config.searchKwAutoPause"), "media"]
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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 { VIEWPORT_OVERFLOW_MODES, normalizeViewportOverflow, type ViewportOverflowMode } from "../../utils/viewportPlacement";
|
|
11
|
+
import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
|
|
12
|
+
|
|
13
|
+
export function PanelConfigViewportSection({
|
|
14
|
+
element,
|
|
15
|
+
helpers,
|
|
16
|
+
updateElement,
|
|
17
|
+
layers,
|
|
18
|
+
setReferenceCopyMode,
|
|
19
|
+
}: {
|
|
20
|
+
element: PanelElement;
|
|
21
|
+
helpers: ConfigSectionHelpers;
|
|
22
|
+
updateElement: UpdateElement;
|
|
23
|
+
layers: PanelLayer[];
|
|
24
|
+
setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
|
|
25
|
+
}) {
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
const { renderSection, renderFieldGroup } = helpers;
|
|
28
|
+
const selectedElement = element;
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{
|
|
32
|
+
renderSection(
|
|
33
|
+
"viewportConfig",
|
|
34
|
+
t("panel.config.sectionViewport"),
|
|
35
|
+
<>
|
|
36
|
+
{renderFieldGroup(
|
|
37
|
+
t("panel.config.groupRefSource"),
|
|
38
|
+
<label className="block space-y-1">
|
|
39
|
+
<div>{t("panel.config.refLayer")}</div>
|
|
40
|
+
<Select
|
|
41
|
+
value={selectedElement.refLayerId ?? "__none__"}
|
|
42
|
+
onValueChange={(value) =>
|
|
43
|
+
updateElement(selectedElement.id, {
|
|
44
|
+
refLayerId: value === "__none__" ? undefined : value,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<SelectTrigger className="h-7">
|
|
49
|
+
<SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
|
|
50
|
+
</SelectTrigger>
|
|
51
|
+
<SelectContent>
|
|
52
|
+
<SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
|
|
53
|
+
{layers
|
|
54
|
+
.filter((l) => l.id !== selectedElement.layerId)
|
|
55
|
+
.map((l) => (
|
|
56
|
+
<SelectItem key={l.id} value={l.id}>
|
|
57
|
+
{l.name}
|
|
58
|
+
</SelectItem>
|
|
59
|
+
))}
|
|
60
|
+
</SelectContent>
|
|
61
|
+
</Select>
|
|
62
|
+
</label>
|
|
63
|
+
)}
|
|
64
|
+
{renderFieldGroup(
|
|
65
|
+
t("panel.config.groupCopyStrategy"),
|
|
66
|
+
<>
|
|
67
|
+
<label className="block space-y-1">
|
|
68
|
+
<div>{t("panel.config.copyMode")}</div>
|
|
69
|
+
<Select
|
|
70
|
+
value={selectedElement.refCopyMode ?? "shallow"}
|
|
71
|
+
onValueChange={(value) =>
|
|
72
|
+
setReferenceCopyMode?.(
|
|
73
|
+
selectedElement.id,
|
|
74
|
+
value as ReferenceCopyMode
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
<SelectTrigger className="h-7">
|
|
79
|
+
<SelectValue placeholder={t("panel.config.selectCopyMode")} />
|
|
80
|
+
</SelectTrigger>
|
|
81
|
+
<SelectContent>
|
|
82
|
+
<SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
|
|
83
|
+
<SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
|
|
84
|
+
</SelectContent>
|
|
85
|
+
</Select>
|
|
86
|
+
</label>
|
|
87
|
+
</>,
|
|
88
|
+
<>{t("panel.config.copyStrategyHint")}</>,
|
|
89
|
+
{ groupKey: "viewportCopyStrategy", defaultOpen: false }
|
|
90
|
+
)}
|
|
91
|
+
{renderFieldGroup(
|
|
92
|
+
t("panel.config.groupViewportOverflow"),
|
|
93
|
+
<label className="block space-y-1">
|
|
94
|
+
<div>{t("panel.config.viewportOverflow")}</div>
|
|
95
|
+
<Select
|
|
96
|
+
value={normalizeViewportOverflow(selectedElement.viewportOverflow)}
|
|
97
|
+
onValueChange={(value) =>
|
|
98
|
+
updateElement(selectedElement.id, {
|
|
99
|
+
viewportOverflow: value as ViewportOverflowMode,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
<SelectTrigger className="h-7">
|
|
104
|
+
<SelectValue placeholder={t("panel.config.viewportOverflow")} />
|
|
105
|
+
</SelectTrigger>
|
|
106
|
+
<SelectContent>
|
|
107
|
+
{VIEWPORT_OVERFLOW_MODES.map((mode) => (
|
|
108
|
+
<SelectItem key={mode} value={mode}>
|
|
109
|
+
{mode === "scroll-x"
|
|
110
|
+
? t("panel.config.viewportOverflowScrollX")
|
|
111
|
+
: mode === "scroll-y"
|
|
112
|
+
? t("panel.config.viewportOverflowScrollY")
|
|
113
|
+
: mode === "scroll"
|
|
114
|
+
? t("panel.config.viewportOverflowScroll")
|
|
115
|
+
: t("panel.config.viewportOverflowClip")}
|
|
116
|
+
</SelectItem>
|
|
117
|
+
))}
|
|
118
|
+
</SelectContent>
|
|
119
|
+
</Select>
|
|
120
|
+
</label>
|
|
121
|
+
)}
|
|
122
|
+
</>,
|
|
123
|
+
true,
|
|
124
|
+
[
|
|
125
|
+
t("panel.config.searchKwViewport"),
|
|
126
|
+
"viewport",
|
|
127
|
+
t("panel.config.refLayer"),
|
|
128
|
+
t("panel.config.viewportOverflow"),
|
|
129
|
+
t("panel.config.viewportOverflowScrollX"),
|
|
130
|
+
t("panel.config.viewportOverflowClip"),
|
|
131
|
+
t("panel.material.shallowCopy"),
|
|
132
|
+
t("panel.material.deepCopy"),
|
|
133
|
+
],
|
|
134
|
+
<>{t("panel.config.viewportHint")}</>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { PanelChartConfig, PanelElement } from "../../types";
|
|
3
|
+
|
|
4
|
+
export type UpdateElement = (
|
|
5
|
+
id: string,
|
|
6
|
+
patch: Partial<PanelElement>,
|
|
7
|
+
options?: { batchId?: string; meta?: Record<string, unknown> }
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
export type RenderSection = (
|
|
11
|
+
key: string,
|
|
12
|
+
title: string,
|
|
13
|
+
children: React.ReactNode,
|
|
14
|
+
defaultOpen?: boolean,
|
|
15
|
+
searchTerms?: string[],
|
|
16
|
+
hint?: React.ReactNode
|
|
17
|
+
) => React.ReactNode;
|
|
18
|
+
|
|
19
|
+
export type RenderFieldGroup = (
|
|
20
|
+
title: string,
|
|
21
|
+
children: React.ReactNode,
|
|
22
|
+
hint?: React.ReactNode,
|
|
23
|
+
options?: { groupKey?: string; defaultOpen?: boolean }
|
|
24
|
+
) => React.ReactNode;
|
|
25
|
+
|
|
26
|
+
export type RenderColorField = (
|
|
27
|
+
label: string,
|
|
28
|
+
value: string,
|
|
29
|
+
onTextChange: (next: string) => void
|
|
30
|
+
) => React.ReactNode;
|
|
31
|
+
|
|
32
|
+
export type ConfigSectionHelpers = {
|
|
33
|
+
renderSection: RenderSection;
|
|
34
|
+
renderFieldGroup: RenderFieldGroup;
|
|
35
|
+
renderColorField: RenderColorField;
|
|
36
|
+
hasSearch: boolean;
|
|
37
|
+
normalizedSearch: string;
|
|
38
|
+
isSectionExpanded: (key: string, defaultValue?: boolean) => boolean;
|
|
39
|
+
setSectionExpanded: (key: string, next: boolean) => void;
|
|
40
|
+
optionCheckboxClass: string;
|
|
41
|
+
optionInputClass: string;
|
|
42
|
+
optionSelectTriggerClass: string;
|
|
43
|
+
renderOptionLabel: (label: string, keyPath: string, desc: string) => React.ReactNode;
|
|
44
|
+
renderFormatterLabel: (label?: string) => React.ReactNode;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const OPTION_CHECKBOX_CLASS =
|
|
48
|
+
"h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40";
|
|
49
|
+
export const OPTION_INPUT_CLASS =
|
|
50
|
+
"h-7 border border-border/60 bg-background/90 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-border/60";
|
|
51
|
+
export const OPTION_SELECT_TRIGGER_CLASS =
|
|
52
|
+
"h-7 border border-border/60 bg-muted/40 shadow-none ring-0 focus:ring-0 focus:ring-offset-0 focus:border-border/60 data-[state=open]:border-border/60";
|
|
53
|
+
|
|
54
|
+
export function mergeOptionPatch(
|
|
55
|
+
base: Record<string, unknown> | undefined,
|
|
56
|
+
patch: Record<string, unknown>
|
|
57
|
+
): Record<string, unknown> {
|
|
58
|
+
const output: Record<string, unknown> = { ...(base ?? {}) };
|
|
59
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
60
|
+
const prev = output[key];
|
|
61
|
+
if (
|
|
62
|
+
value &&
|
|
63
|
+
typeof value === "object" &&
|
|
64
|
+
!Array.isArray(value) &&
|
|
65
|
+
prev &&
|
|
66
|
+
typeof prev === "object" &&
|
|
67
|
+
!Array.isArray(prev)
|
|
68
|
+
) {
|
|
69
|
+
output[key] = mergeOptionPatch(
|
|
70
|
+
prev as Record<string, unknown>,
|
|
71
|
+
value as Record<string, unknown>
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
output[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function patchChart(
|
|
81
|
+
chart: PanelChartConfig | undefined,
|
|
82
|
+
patch: Partial<PanelChartConfig>
|
|
83
|
+
): PanelChartConfig {
|
|
84
|
+
return { ...(chart ?? {}), ...patch };
|
|
85
|
+
}
|