@particle-academy/fancy-flow 0.40.3 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -12576,6 +12576,36 @@ function HelperLines({ horizontal, vertical }) {
12576
12576
  )
12577
12577
  ] });
12578
12578
  }
12579
+ function readDocumentTheme() {
12580
+ if (typeof document === "undefined") return "light";
12581
+ const root2 = document.documentElement;
12582
+ const attr = root2.getAttribute("data-theme");
12583
+ if (attr === "dark" || attr === "light") return attr;
12584
+ if (root2.classList.contains("dark")) return "dark";
12585
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
12586
+ }
12587
+ function useResolvedColorMode(explicit) {
12588
+ const [resolved, setResolved] = ReactExports.useState("light");
12589
+ const pinned = explicit === "light" || explicit === "dark" ? explicit : void 0;
12590
+ ReactExports.useEffect(() => {
12591
+ if (pinned) return;
12592
+ if (typeof document === "undefined") return;
12593
+ const sync = () => setResolved(readDocumentTheme());
12594
+ sync();
12595
+ const observer = new MutationObserver(sync);
12596
+ observer.observe(document.documentElement, {
12597
+ attributes: true,
12598
+ attributeFilter: ["class", "data-theme"]
12599
+ });
12600
+ const media = window.matchMedia?.("(prefers-color-scheme: dark)");
12601
+ media?.addEventListener?.("change", sync);
12602
+ return () => {
12603
+ observer.disconnect();
12604
+ media?.removeEventListener?.("change", sync);
12605
+ };
12606
+ }, [pinned]);
12607
+ return pinned ?? resolved;
12608
+ }
12579
12609
  function preventScrollWhileZooming(event) {
12580
12610
  if (event.shiftKey) event.preventDefault();
12581
12611
  }
@@ -12648,6 +12678,7 @@ function FlowCanvas({
12648
12678
  },
12649
12679
  [showHelperLines, onNodesChange]
12650
12680
  );
12681
+ const resolvedColorMode = useResolvedColorMode(colorMode);
12651
12682
  return /* @__PURE__ */ jsxRuntime.jsxs(
12652
12683
  "div",
12653
12684
  {
@@ -12655,8 +12686,8 @@ function FlowCanvas({
12655
12686
  "ff-canvas",
12656
12687
  // colorMode drives BOTH react-flow's chrome (below) and our `ff-` styles
12657
12688
  // via the shared `.dark` class / light opt-out — one theme signal.
12658
- colorMode === "dark" ? "dark" : "",
12659
- colorMode === "light" ? "ff-canvas--light" : "",
12689
+ resolvedColorMode === "dark" ? "dark" : "",
12690
+ resolvedColorMode === "light" ? "ff-canvas--light" : "",
12660
12691
  className ?? ""
12661
12692
  ].filter(Boolean).join(" "),
12662
12693
  style: { height, ...style2 },
@@ -12668,7 +12699,7 @@ function FlowCanvas({
12668
12699
  nodes: orderedNodes,
12669
12700
  edges,
12670
12701
  onNodesChange: handleNodesChange,
12671
- colorMode,
12702
+ colorMode: resolvedColorMode,
12672
12703
  nodeTypes: mergedNodeTypes,
12673
12704
  edgeTypes: mergedEdgeTypes,
12674
12705
  fitView: true,
@@ -12866,7 +12897,7 @@ function ConfigFieldRenderer({
12866
12897
  }
12867
12898
  );
12868
12899
  case "json":
12869
- return /* @__PURE__ */ jsxRuntime.jsx(JsonField, { value, onChange, rows: field.rows });
12900
+ return /* @__PURE__ */ jsxRuntime.jsx(JsonField, { value, onChange, rows: field.rows, handle });
12870
12901
  case "expression":
12871
12902
  return /* @__PURE__ */ jsxRuntime.jsx(
12872
12903
  "textarea",
@@ -12898,6 +12929,7 @@ function ConfigFieldRenderer({
12898
12929
  return /* @__PURE__ */ jsxRuntime.jsx(
12899
12930
  RepeaterField,
12900
12931
  {
12932
+ id: id2,
12901
12933
  field,
12902
12934
  value,
12903
12935
  onChange,
@@ -12907,7 +12939,7 @@ function ConfigFieldRenderer({
12907
12939
  }
12908
12940
  );
12909
12941
  case "keyvalue":
12910
- return /* @__PURE__ */ jsxRuntime.jsx(KeyValueField, { field, value, onChange });
12942
+ return /* @__PURE__ */ jsxRuntime.jsx(KeyValueField, { id: id2, field, value, onChange });
12911
12943
  case "document":
12912
12944
  if (renderDocumentField) {
12913
12945
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderDocumentField({ documentType: field.documentType, value, onChange }) });
@@ -12952,6 +12984,7 @@ function ChoiceField({
12952
12984
  );
12953
12985
  }
12954
12986
  function RepeaterField({
12987
+ id: id2,
12955
12988
  field,
12956
12989
  value,
12957
12990
  onChange,
@@ -12986,7 +13019,7 @@ function RepeaterField({
12986
13019
  if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
12987
13020
  return `Item ${i + 1}`;
12988
13021
  };
12989
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater", "data-ff-repeater": field.key, children: [
13022
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: id2, role: "group", className: "ff-repeater", "data-ff-repeater": field.key, "data-ff-field": field.key, children: [
12990
13023
  rows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-repeater__empty", children: "None yet." }),
12991
13024
  rows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater__row", "data-ff-repeater-row": i, children: [
12992
13025
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-repeater__row-head", children: [
@@ -13061,6 +13094,7 @@ function RepeaterField({
13061
13094
  ] });
13062
13095
  }
13063
13096
  function KeyValueField({
13097
+ id: id2,
13064
13098
  field,
13065
13099
  value,
13066
13100
  onChange
@@ -13079,7 +13113,7 @@ function KeyValueField({
13079
13113
  const setVal = (i, val) => commit(entries.map(([k2, v2], idx) => idx === i ? [k2, val] : [k2, v2]));
13080
13114
  const remove2 = (i) => commit(entries.filter((_2, idx) => idx !== i));
13081
13115
  const add = () => commit([...entries, ["", ""]]);
13082
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-keyvalue", "data-ff-keyvalue": field.key, children: [
13116
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: id2, role: "group", className: "ff-keyvalue", "data-ff-keyvalue": field.key, "data-ff-field": field.key, children: [
13083
13117
  entries.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-keyvalue__head", children: [
13084
13118
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: field.keyLabel ?? "Key" }),
13085
13119
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: field.valueLabel ?? "Value" }),
@@ -13135,34 +13169,76 @@ function KeyValueField({
13135
13169
  ] })
13136
13170
  ] });
13137
13171
  }
13138
- function JsonField({ value, onChange, rows }) {
13139
- const initial = ReactExports.useMemo(() => {
13172
+ function JsonField({
13173
+ value,
13174
+ onChange,
13175
+ rows,
13176
+ handle
13177
+ }) {
13178
+ const serialized = ReactExports.useMemo(() => {
13140
13179
  try {
13141
13180
  return value === void 0 ? "" : JSON.stringify(value, null, 2);
13142
13181
  } catch {
13143
13182
  return "";
13144
13183
  }
13145
13184
  }, [value]);
13146
- return /* @__PURE__ */ jsxRuntime.jsx(
13147
- "textarea",
13148
- {
13149
- className: "ff-panel__input ff-panel__input--json",
13150
- rows: rows ?? 6,
13151
- defaultValue: initial,
13152
- spellCheck: false,
13153
- onBlur: (e) => {
13154
- const text = e.target.value.trim();
13155
- if (text === "") {
13156
- onChange(void 0);
13157
- return;
13158
- }
13159
- try {
13160
- onChange(JSON.parse(text));
13161
- } catch {
13185
+ const [text, setText] = ReactExports.useState(serialized);
13186
+ const [error, setError] = ReactExports.useState(null);
13187
+ const [touched, setTouched] = ReactExports.useState(false);
13188
+ const lastProps = ReactExports.useRef(serialized);
13189
+ const committed = ReactExports.useRef(null);
13190
+ if (serialized !== lastProps.current) {
13191
+ lastProps.current = serialized;
13192
+ if (serialized !== committed.current) {
13193
+ setText(serialized);
13194
+ setError(null);
13195
+ setTouched(false);
13196
+ }
13197
+ }
13198
+ const commit = (next) => {
13199
+ const trimmed = next.trim();
13200
+ if (trimmed === "") {
13201
+ committed.current = "";
13202
+ onChange(void 0);
13203
+ return null;
13204
+ }
13205
+ try {
13206
+ const parsed = JSON.parse(trimmed);
13207
+ committed.current = JSON.stringify(parsed, null, 2);
13208
+ onChange(parsed);
13209
+ return null;
13210
+ } catch (e) {
13211
+ return e instanceof Error ? e.message : "Invalid JSON";
13212
+ }
13213
+ };
13214
+ const errorId = handle.id ? `${handle.id}-error` : void 0;
13215
+ const showError = touched && error !== null;
13216
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13217
+ /* @__PURE__ */ jsxRuntime.jsx(
13218
+ "textarea",
13219
+ {
13220
+ ...handle,
13221
+ className: "ff-panel__input ff-panel__input--json",
13222
+ rows: rows ?? 6,
13223
+ value: text,
13224
+ spellCheck: false,
13225
+ "aria-invalid": showError || void 0,
13226
+ "aria-describedby": showError ? errorId : void 0,
13227
+ onChange: (e) => {
13228
+ setText(e.target.value);
13229
+ setError(commit(e.target.value));
13230
+ },
13231
+ onBlur: (e) => {
13232
+ setTouched(true);
13233
+ setError(commit(e.target.value));
13162
13234
  }
13163
13235
  }
13164
- }
13165
- );
13236
+ ),
13237
+ showError && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "ff-panel__issue ff-panel__issue--field", id: errorId, role: "alert", children: [
13238
+ "\u26A0 ",
13239
+ error
13240
+ ] })
13241
+ ] });
13166
13242
  }
13167
13243
  function NodeConfigPanel({
13168
13244
  node,
@@ -14030,6 +14106,7 @@ function FlowEditorInner({
14030
14106
  metadata,
14031
14107
  showPalette = true,
14032
14108
  showPanel = true,
14109
+ fieldRenderers,
14033
14110
  showFeed = true,
14034
14111
  extraToolbar,
14035
14112
  actions = [],
@@ -14589,6 +14666,7 @@ function FlowEditorInner({
14589
14666
  className: "ff-editor__panel",
14590
14667
  node: api.selected,
14591
14668
  onChange: api.updateNode,
14669
+ fieldRenderers,
14592
14670
  onDelete: builtins.delete === false ? void 0 : (n) => api.deleteNodes([n.id])
14593
14671
  }
14594
14672
  ),