@arronqzy/react-view 1.0.12 → 1.0.14

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.
Files changed (31) hide show
  1. package/dist/styles.css +96 -4
  2. package/package.json +8 -7
  3. package/src/panel/ReactViewOnlinePreview.tsx +72 -20
  4. package/src/panel/ReactViewPanel.tsx +238 -126
  5. package/src/panel/ai/applyAssistantAction.ts +1 -0
  6. package/src/panel/components/BusyOverlay.tsx +25 -0
  7. package/src/panel/components/ElementsLayer.tsx +110 -68
  8. package/src/panel/components/MaterialSidebar.tsx +15 -0
  9. package/src/panel/components/MoveableLayer.tsx +8 -0
  10. package/src/panel/components/PanelCanvas.tsx +3 -0
  11. package/src/panel/components/PanelConfigSidebar.tsx +582 -443
  12. package/src/panel/components/SelectLayer.tsx +5 -0
  13. package/src/panel/components/WorkspaceConfigSidebar.tsx +9 -0
  14. package/src/panel/components/WorkspaceProjectNav.tsx +27 -4
  15. package/src/panel/components/config/PanelConfigScene3dSection.tsx +874 -0
  16. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +1 -2
  17. package/src/panel/components/table/PanelConfigTableSection.tsx +82 -50
  18. package/src/panel/components/table/TableNodeContent.tsx +40 -15
  19. package/src/panel/hooks/usePanelElements.ts +13 -1
  20. package/src/panel/hooks/useWorkspaceProjects.ts +133 -66
  21. package/src/panel/library/workspace-project-cache.ts +8 -2
  22. package/src/panel/library/workspace-project-db.ts +29 -11
  23. package/src/panel/library/workspace-snapshot.ts +21 -4
  24. package/src/panel/scope/view-scope-store.ts +8 -0
  25. package/src/panel/types.ts +4 -3
  26. package/src/panel/utils/async-work.ts +178 -0
  27. package/src/panel/utils/chartOptionBuilder.ts +7 -1
  28. package/src/panel/utils/gridPlacement.ts +29 -0
  29. package/src/panel/utils/outputScale.ts +69 -0
  30. package/src/panel/utils/panelElementDefaults.ts +37 -2
  31. package/src/panel/utils/scope-autocomplete.ts +0 -114
@@ -7,14 +7,13 @@ import {
7
7
  } from "react";
8
8
  import { createPortal } from "react-dom";
9
9
  import { cn } from "@arronqzy/ui";
10
-
11
10
  import {
12
11
  applyScopeAutocompleteSelection,
13
12
  buildScopeExpression,
14
13
  getScopeAutocompleteSuggestions,
15
14
  parseScopeAutocomplete,
16
15
  type ScopeAutocompleteState,
17
- } from "../../utils/scope-autocomplete";
16
+ } from "@arronqzy/blueprint-dsl";
18
17
 
19
18
  function isAutocompleteTarget(
20
19
  target: EventTarget | null
@@ -17,6 +17,7 @@ import {
17
17
  generateMockTableRows,
18
18
  stringifyMockTableRows,
19
19
  type TableMockLocale,
20
+ isCssTransparent,
20
21
  } from "@arronqzy/view-table";
21
22
  import type { PanelElement } from "../../types";
22
23
  import { ConfigHintIcon } from "../ConfigHintIcon";
@@ -24,6 +25,7 @@ import { ConditionEditor } from "./ConditionEditor";
24
25
  import { JsonCodeEditor } from "./JsonCodeEditor";
25
26
  import { useI18n } from "@arronqzy/i18n/react";
26
27
  import React, { useMemo, useRef, useState } from "react";
28
+ import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
27
29
  import {
28
30
  Button,
29
31
  Dialog,
@@ -217,35 +219,34 @@ export function PanelConfigTableSection({
217
219
 
218
220
  const uploadImageForColumn = async (index: number, file: File) => {
219
221
  try {
220
- const base64 = await new Promise<string>((resolve, reject) => {
221
- const reader = new FileReader();
222
- reader.onload = () => resolve(String(reader.result ?? ""));
223
- reader.onerror = () => reject(new Error("read failed"));
224
- reader.readAsDataURL(file);
225
- });
226
- patchWidgetProps(index, {
227
- imageUrlMode: "static",
228
- imageUrl: base64,
229
- });
230
- setImageUploadHint(t("panel.config.uploadWrittenBase64"));
231
- try {
232
- const form = new FormData();
233
- form.append("file", file);
234
- const resp = await fetch("/api/upload", { method: "POST", body: form });
235
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
236
- const data = (await resp.json()) as { url?: string };
237
- if (data.url) {
238
- patchWidgetProps(index, {
239
- imageUrlMode: "static",
240
- imageUrl: data.url,
241
- });
242
- setImageUploadHint(t("panel.config.uploadServerAndBase64"));
222
+ await runBusyTask(t("common.uploadingFile"), async () => {
223
+ const base64 = await readFileAsDataUrl(file, t("panel.messages.readImageFailed"), "image");
224
+ patchWidgetProps(index, {
225
+ imageUrlMode: "static",
226
+ imageUrl: base64,
227
+ });
228
+ setImageUploadHint(t("panel.config.uploadWrittenBase64"));
229
+ try {
230
+ const form = new FormData();
231
+ form.append("file", file);
232
+ const resp = await fetch("/api/upload", { method: "POST", body: form });
233
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
234
+ const data = (await resp.json()) as { url?: string };
235
+ if (data.url) {
236
+ patchWidgetProps(index, {
237
+ imageUrlMode: "static",
238
+ imageUrl: data.url,
239
+ });
240
+ setImageUploadHint(t("panel.config.uploadServerAndBase64"));
241
+ }
242
+ } catch {
243
+ setImageUploadHint(t("panel.config.uploadServerFailedKeepBase64"));
243
244
  }
244
- } catch {
245
- setImageUploadHint(t("panel.config.uploadServerFailedKeepBase64"));
246
- }
247
- } catch {
248
- setImageUploadHint(t("panel.messages.readImageFailed"));
245
+ });
246
+ } catch (error) {
247
+ setImageUploadHint(
248
+ error instanceof Error ? error.message : t("panel.messages.readImageFailed")
249
+ );
249
250
  }
250
251
  };
251
252
 
@@ -321,27 +322,45 @@ export function PanelConfigTableSection({
321
322
  value: string,
322
323
  onChange: (next: string) => void,
323
324
  hintKey?: string,
324
- placeholder = "#000000"
325
+ placeholder = "#000000",
326
+ allowTransparent = false
325
327
  ) => {
326
328
  const label = t(labelKey);
329
+ const transparent = allowTransparent && isCssTransparent(value);
330
+ const fallbackHex = placeholder.startsWith("#") ? placeholder : "#ffffff";
327
331
  return (
328
332
  <div className={fieldClass}>
329
333
  {fieldLabel(labelKey, hintKey)}
330
- <div className="flex items-center gap-2">
331
- <Input
332
- value={value}
333
- onChange={(e) => onChange(e.target.value)}
334
- placeholder={placeholder}
335
- className={`${inputClass} min-w-0 flex-1`}
336
- />
337
- <Input
338
- type="color"
339
- value={toPickerHex(value, placeholder.startsWith("#") ? placeholder : "#000000")}
340
- onChange={(e) => onChange(e.target.value)}
341
- className="h-7 w-10 shrink-0 cursor-pointer p-1"
342
- aria-label={t("common.colorPickerAria", { label })}
343
- />
344
- </div>
334
+ {allowTransparent ? (
335
+ <label className="flex items-center gap-2 text-[11px]">
336
+ <Switch
337
+ checked={transparent}
338
+ onCheckedChange={(checked) => onChange(checked ? "transparent" : fallbackHex)}
339
+ />
340
+ <span>{t("panel.config.tableBgTransparent")}</span>
341
+ </label>
342
+ ) : null}
343
+ {transparent ? (
344
+ <div className="text-[10px] text-muted-foreground">
345
+ {t("panel.config.tableBgTransparentHint")}
346
+ </div>
347
+ ) : (
348
+ <div className="flex items-center gap-2">
349
+ <Input
350
+ value={value}
351
+ onChange={(e) => onChange(e.target.value)}
352
+ placeholder={placeholder}
353
+ className={`${inputClass} min-w-0 flex-1`}
354
+ />
355
+ <Input
356
+ type="color"
357
+ value={toPickerHex(value, fallbackHex)}
358
+ onChange={(e) => onChange(e.target.value)}
359
+ className="h-7 w-10 shrink-0 cursor-pointer p-1"
360
+ aria-label={t("common.colorPickerAria", { label })}
361
+ />
362
+ </div>
363
+ )}
345
364
  </div>
346
365
  );
347
366
  };
@@ -705,9 +724,8 @@ export function PanelConfigTableSection({
705
724
  </ConfigHintIcon>
706
725
  </div>
707
726
 
708
- {(selectedColumn.widget ?? "text") === "text" ? (
709
- <>
710
- <div className="grid grid-cols-2 gap-2">
727
+ {(selectedColumn.widget ?? "text") !== "image" ? (
728
+ <div className="grid grid-cols-2 gap-2">
711
729
  <label className={fieldClass}>
712
730
  {fieldLabel("panel.config.tableTextFontSize")}
713
731
  <Input
@@ -849,6 +867,10 @@ export function PanelConfigTableSection({
849
867
  </Select>
850
868
  </label>
851
869
  </div>
870
+ ) : null}
871
+
872
+ {(selectedColumn.widget ?? "text") === "text" ? (
873
+ <>
852
874
  <label className={fieldClass}>
853
875
  {fieldLabel("panel.config.tableActionOnClick", "panel.config.tableActionBlueprintHint")}
854
876
  <BlueprintNodeSelect
@@ -1849,6 +1871,14 @@ export function PanelConfigTableSection({
1849
1871
  />
1850
1872
  </label>
1851
1873
  <div className="grid grid-cols-2 gap-2">
1874
+ {colorField(
1875
+ "panel.config.tableBodyBg",
1876
+ table.tableStyle?.backgroundColor ?? "",
1877
+ (next) => patchTableStyle({ backgroundColor: next || undefined }),
1878
+ "panel.config.tableBodyBgHint",
1879
+ "#ffffff",
1880
+ true
1881
+ )}
1852
1882
  {colorField(
1853
1883
  "panel.config.tableStyleColor",
1854
1884
  table.tableStyle?.color ?? "",
@@ -1875,8 +1905,9 @@ export function PanelConfigTableSection({
1875
1905
  "panel.config.tableHeaderBg",
1876
1906
  table.headerStyle?.backgroundColor ?? "",
1877
1907
  (next) => patchHeaderStyle({ backgroundColor: next || undefined }),
1878
- undefined,
1879
- "#f3f4f6"
1908
+ "panel.config.tableHeaderBgHint",
1909
+ "#f3f4f6",
1910
+ true
1880
1911
  )}
1881
1912
  <label className={fieldClass}>
1882
1913
  {fieldLabel("panel.config.tableHeaderFontSize")}
@@ -1969,7 +2000,8 @@ export function PanelConfigTableSection({
1969
2000
  patchTable({ rowStyleRules: next });
1970
2001
  },
1971
2002
  "panel.config.tableBgColorHint",
1972
- "#ecfccb"
2003
+ "#ecfccb",
2004
+ true
1973
2005
  )}
1974
2006
  </div>
1975
2007
  ))}
@@ -5,8 +5,11 @@ import {
5
5
  resolveCellDisplay,
6
6
  resolveRowDisplay,
7
7
  stylePropsToCss,
8
+ tableTextStyleToCss,
8
9
  resolveRawTableInput,
9
10
  resolveProgressDisplay,
11
+ resolveTableBodyBackground,
12
+ resolveTableHeaderBackground,
10
13
  type PanelTableConfig,
11
14
  type NormalizedColumn,
12
15
  type NormalizedRow,
@@ -64,8 +67,12 @@ function TableCellView({
64
67
  };
65
68
 
66
69
  const style = stylePropsToCss(cell.style);
70
+ const textCss = tableTextStyleToCss(cell.widgetProps?.textStyle);
67
71
  const textOverflow = cell.widgetProps?.textStyle?.overflow ?? "ellipsis";
68
- const isWrap = cell.widget === "text" && textOverflow === "wrap";
72
+ const isWrap =
73
+ textOverflow === "wrap" &&
74
+ cell.widget !== "image" &&
75
+ cell.widget !== "progress";
69
76
  const common: React.CSSProperties = {
70
77
  ...style,
71
78
  overflow: "hidden",
@@ -98,6 +105,7 @@ function TableCellView({
98
105
  lineHeight: "20px",
99
106
  color: "#fff",
100
107
  background: bg,
108
+ ...textCss,
101
109
  }}
102
110
  >
103
111
  {cell.widget === "badge" ? (
@@ -126,7 +134,13 @@ function TableCellView({
126
134
  e.stopPropagation();
127
135
  emit("click", cell.widgetProps?.actions?.onClickBlueprintNodeId);
128
136
  }}
129
- style={{ color: "#2563eb", textDecoration: "underline", overflow: "hidden", textOverflow: "ellipsis" }}
137
+ style={{
138
+ color: "#2563eb",
139
+ textDecoration: "underline",
140
+ overflow: "hidden",
141
+ textOverflow: "ellipsis",
142
+ ...textCss,
143
+ }}
130
144
  >
131
145
  {cell.text || cell.href}
132
146
  </a>
@@ -137,7 +151,14 @@ function TableCellView({
137
151
  const display = resolveProgressDisplay(cell.widgetProps);
138
152
  const trackColor = cell.color ?? "#3b82f6";
139
153
  const label = (
140
- <span style={{ fontSize: 11, color: "rgba(0,0,0,0.55)", flex: "0 0 auto" }}>
154
+ <span
155
+ style={{
156
+ fontSize: 11,
157
+ color: "rgba(0,0,0,0.55)",
158
+ flex: "0 0 auto",
159
+ ...textCss,
160
+ }}
161
+ >
141
162
  {cell.text}
142
163
  </span>
143
164
  );
@@ -178,8 +199,15 @@ function TableCellView({
178
199
  y="50%"
179
200
  dominantBaseline="central"
180
201
  textAnchor="middle"
181
- fontSize={Math.max(8, Math.round(size * 0.32))}
182
- fill="rgba(0,0,0,0.65)"
202
+ fontSize={
203
+ typeof textCss.fontSize === "number"
204
+ ? textCss.fontSize
205
+ : Math.max(8, Math.round(size * 0.32))
206
+ }
207
+ fill={typeof textCss.color === "string" ? textCss.color : "rgba(0,0,0,0.65)"}
208
+ fontFamily={typeof textCss.fontFamily === "string" ? textCss.fontFamily : undefined}
209
+ fontWeight={textCss.fontWeight}
210
+ fontStyle={typeof textCss.fontStyle === "string" ? textCss.fontStyle : undefined}
183
211
  >
184
212
  {Math.round(pct)}
185
213
  </text>
@@ -227,7 +255,7 @@ function TableCellView({
227
255
  draggable={false}
228
256
  />
229
257
  ) : (
230
- <span style={{ fontSize: 11, opacity: 0.5 }}>—</span>
258
+ <span style={{ fontSize: 11, opacity: 0.5, ...textCss }}>—</span>
231
259
  )}
232
260
  </div>
233
261
  );
@@ -275,21 +303,18 @@ function TableCellView({
275
303
  }}
276
304
  />
277
305
  </button>
278
- <span style={{ marginLeft: 6, fontSize: 12 }}>{cell.text}</span>
306
+ <span style={{ marginLeft: 6, fontSize: 12, ...textCss }}>{cell.text}</span>
279
307
  </div>
280
308
  );
281
309
  } else {
282
- const textStyle = cell.widgetProps?.textStyle;
283
310
  const canClickText = Boolean(cell.widgetProps?.actions?.onClickBlueprintNodeId);
284
311
  content = (
285
312
  <div
286
313
  style={{
287
314
  ...common,
288
- fontFamily: textStyle?.fontFamily,
289
- fontStyle: textStyle?.fontStyle,
290
- textDecoration: textStyle?.textDecoration === "none" ? undefined : textStyle?.textDecoration,
315
+ ...textCss,
291
316
  cursor: canClickText ? "pointer" : undefined,
292
- color: canClickText && !common.color ? "#2563eb" : common.color,
317
+ color: canClickText && !common.color && !textCss.color ? "#2563eb" : (textCss.color as string | undefined) ?? common.color,
293
318
  }}
294
319
  onClick={(e) => {
295
320
  if (!canClickText) return;
@@ -431,7 +456,7 @@ export function TableNodeContent({
431
456
  <div
432
457
  className="rv-table-node h-full w-full overflow-hidden"
433
458
  style={{
434
- background: (tableConfig.tableStyle?.backgroundColor as string) || "#fff",
459
+ background: resolveTableBodyBackground(tableConfig.tableStyle?.backgroundColor),
435
460
  border:
436
461
  tableConfig.tableStyle?.borderWidth != null
437
462
  ? `${tableConfig.tableStyle.borderWidth}px solid ${tableConfig.tableStyle.borderColor ?? "rgba(0,0,0,0.12)"}`
@@ -455,7 +480,7 @@ export function TableNodeContent({
455
480
  position: tableConfig.tableStyle?.stickyHeader === false ? "relative" : "sticky",
456
481
  top: 0,
457
482
  zIndex: 2,
458
- background: (tableConfig.headerStyle?.backgroundColor as string) || "rgba(0,0,0,0.04)",
483
+ background: resolveTableHeaderBackground(tableConfig.headerStyle?.backgroundColor),
459
484
  borderBottom: "1px solid rgba(0,0,0,0.1)",
460
485
  ...headerStyle,
461
486
  color: headerTextColor,
@@ -489,7 +514,7 @@ export function TableNodeContent({
489
514
  </div>
490
515
  ) : null}
491
516
 
492
- <div ref={parentRef} className="min-h-0 flex-1 overflow-auto">
517
+ <div ref={parentRef} className="min-h-0 flex-1 overflow-auto" style={{ background: "transparent" }}>
493
518
  {model.rows.length === 0 ? (
494
519
  <div className="flex h-full flex-col items-center justify-center gap-2 px-3 text-center text-xs text-muted-foreground">
495
520
  <svg
@@ -9,6 +9,7 @@ import type {
9
9
  ReferenceCopyMode,
10
10
  } from "../types";
11
11
  import { getPanelMessages } from "../constants/messages";
12
+ import { removeViewElementScopes } from "../scope/view-scope-store";
12
13
  import {
13
14
  DEFAULT_LAYER,
14
15
  DEFAULT_LAYER_ID,
@@ -17,6 +18,7 @@ import {
17
18
  getDefaultGridConfig,
18
19
  getDefaultLayer,
19
20
  getDefaultNodeName,
21
+ getDefaultScene3dConfigForMaterial,
20
22
  getDefaultSizeByMaterial,
21
23
  getDefaultTextContent,
22
24
  normalizePrimaryLayer,
@@ -367,6 +369,7 @@ export function usePanelElements() {
367
369
  });
368
370
  const idSet = new Set(unlockedIds);
369
371
  if (idSet.size === 0) return;
372
+ const familyIds = new Set<string>();
370
373
  store.update(
371
374
  (draft) => {
372
375
  const deletedSourceIds = new Set<string>();
@@ -376,7 +379,7 @@ export function usePanelElements() {
376
379
  const props = n.props as PanelElement;
377
380
  deletedSourceIds.add(props.mappingSourceNodeId ?? props.id);
378
381
  });
379
- const familyIds = new Set<string>();
382
+ familyIds.clear();
380
383
  (draft.root.children ?? []).forEach((n) => {
381
384
  if (!isPanelElementNode(n) || !n.props) return;
382
385
  const props = n.props as PanelElement;
@@ -389,6 +392,7 @@ export function usePanelElements() {
389
392
  },
390
393
  { meta: { type: "node.batch-delete", ids: Array.from(idSet) } }
391
394
  );
395
+ removeViewElementScopes([...familyIds]);
392
396
  }, [byId, layerById]);
393
397
 
394
398
  const bringElementsToFront = useCallback((ids: string[]) => {
@@ -598,6 +602,7 @@ export function usePanelElements() {
598
602
  geometryShape: materialType === "geometry" ? "rect" : undefined,
599
603
  geometryColor: materialType === "geometry" ? "#3b82f6" : undefined,
600
604
  geometryScript: undefined,
605
+ ...getDefaultScene3dConfigForMaterial(materialType),
601
606
  x: Math.round(x - size.width / 2),
602
607
  y: Math.round(y - size.height / 2),
603
608
  width: size.width,
@@ -1059,6 +1064,13 @@ export function usePanelElements() {
1059
1064
 
1060
1065
  const exportPanelData = useCallback(() => {
1061
1066
  const current = store.getState();
1067
+ if (typeof structuredClone === "function") {
1068
+ try {
1069
+ return structuredClone(current) as State;
1070
+ } catch {
1071
+ /* fall through */
1072
+ }
1073
+ }
1062
1074
  return JSON.parse(JSON.stringify(current)) as State;
1063
1075
  }, []);
1064
1076