@almadar/ui 2.50.1 → 2.52.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.
@@ -23,5 +23,17 @@ export interface CodeBlockProps {
23
23
  foldable?: boolean;
24
24
  /** Additional CSS classes */
25
25
  className?: string;
26
+ /**
27
+ * GAP-51: when true, render an editable surface (composes the `Textarea` atom)
28
+ * instead of the syntax-highlighted read-only display. Folding + Prism
29
+ * highlighting are skipped in editable mode for the first cut. Default: false
30
+ * (existing read-only behavior unchanged).
31
+ */
32
+ editable?: boolean;
33
+ /**
34
+ * GAP-51: called with the new code on every keystroke when `editable === true`.
35
+ * Consumers should debounce + parse downstream — `CodeBlock` does not.
36
+ */
37
+ onChange?: (code: string) => void;
26
38
  }
27
39
  export declare const CodeBlock: React.NamedExoticComponent<CodeBlockProps>;
@@ -27,5 +27,28 @@ export interface AvlOrbitalsCosmicZoomProps {
27
27
  width?: number | string;
28
28
  /** Container height */
29
29
  height?: number | string;
30
+ /**
31
+ * GAP-52: name of the orbital to highlight with a persistent ring/glow.
32
+ * Independent from user-driven selection (click). Used by the builder workspace
33
+ * when entering cosmic mode from a focused orbital — the focused orbital is
34
+ * highlighted while the user can still click any other orbital to select it.
35
+ */
36
+ highlightedOrbital?: string;
37
+ /**
38
+ * GAP-55: fired when the user clicks an orbital tile. Consumers (e.g. the
39
+ * builder workspace) use this as the trigger to drill INTO the clicked
40
+ * orbital — typically by switching back to the canvas tab and opening the
41
+ * clicked orbital at L2 expanded. Local `selected` toggle (visual highlight +
42
+ * info panel) still fires regardless of whether the callback is provided.
43
+ */
44
+ onOrbitalSelect?: (orbital: string) => void;
45
+ /**
46
+ * GAP-54: minimum zoom factor when scroll-wheel zooming. Default 0.4.
47
+ */
48
+ minZoom?: number;
49
+ /**
50
+ * GAP-54: maximum zoom factor when scroll-wheel zooming. Default 3.
51
+ */
52
+ maxZoom?: number;
30
53
  }
31
54
  export declare const AvlOrbitalsCosmicZoom: React.FC<AvlOrbitalsCosmicZoomProps>;
@@ -28,6 +28,30 @@ export interface FlowCanvasProps {
28
28
  transition?: string;
29
29
  }) => void;
30
30
  onLevelChange?: (level: ViewLevel, orbital?: string) => void;
31
+ /**
32
+ * GAP-52: fired when the user double-clicks an orbital. Consumers (e.g. the
33
+ * builder workspace) use this as the trigger to enter cosmic mode
34
+ * (`AvlOrbitalsCosmicZoom`) for the focused orbital.
35
+ *
36
+ * The level at which this fires is controlled by `cosmicEntryLevel` (default
37
+ * `'expanded'`). At `'expanded'` the existing overview→expanded drill is
38
+ * preserved — the callback fires only on the second double-click. At
39
+ * `'overview'` the callback fires on the FIRST double-click and the existing
40
+ * drill is suppressed for that interaction. `'both'` fires at either level.
41
+ *
42
+ * The callback runs unconditionally — persona / permission gating is the
43
+ * consumer's responsibility.
44
+ */
45
+ onOrbitalDoubleClick?: (orbital: string) => void;
46
+ /**
47
+ * GAP-53: which level the `onOrbitalDoubleClick` callback fires at.
48
+ * - `'expanded'` (default, non-breaking) — fires only at L2 expanded; the
49
+ * first overview double-click still drills overview→expanded.
50
+ * - `'overview'` — fires at L1 overview on the FIRST double-click. The
51
+ * overview→expanded drill is suppressed when the callback is provided.
52
+ * - `'both'` — fires at either level.
53
+ */
54
+ cosmicEntryLevel?: 'expanded' | 'overview' | 'both';
31
55
  initialOrbital?: string;
32
56
  /** Start at Level 2 (expanded) when initialOrbital is set. Default: 'overview'. */
33
57
  initialLevel?: ViewLevel;
@@ -4094,7 +4094,9 @@ var CodeBlock = React114__namespace.default.memo(
4094
4094
  showLanguageBadge = true,
4095
4095
  maxHeight = "60vh",
4096
4096
  foldable: foldableProp,
4097
- className
4097
+ className,
4098
+ editable = false,
4099
+ onChange
4098
4100
  }) => {
4099
4101
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
4100
4102
  const isOrb = language === "orb";
@@ -4265,7 +4267,37 @@ var CodeBlock = React114__namespace.default.memo(
4265
4267
  ]
4266
4268
  }
4267
4269
  ),
4268
- /* @__PURE__ */ jsxRuntime.jsx(
4270
+ editable ? (
4271
+ /* GAP-51: editable mode — composes the Textarea atom. Plain text editing,
4272
+ no Prism highlighting overlay (follow-up). The textarea is uncontrolled
4273
+ on the value side: we pass `code` as the initial value and forward
4274
+ every keystroke via onChange — the consumer is responsible for
4275
+ debouncing and re-deriving `code` only after the user stops typing
4276
+ so the cursor doesn't fight a re-render. */
4277
+ /* @__PURE__ */ jsxRuntime.jsx(
4278
+ Textarea,
4279
+ {
4280
+ defaultValue: code,
4281
+ onChange: (e) => onChange?.(e.target.value),
4282
+ spellCheck: false,
4283
+ style: {
4284
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
4285
+ fontSize: "13px",
4286
+ lineHeight: "1.5",
4287
+ backgroundColor: "#1e1e1e",
4288
+ color: "#e6e6e6",
4289
+ borderRadius: hasHeader ? "0 0 0.5rem 0.5rem" : "0.5rem",
4290
+ border: "none",
4291
+ padding: "1rem",
4292
+ resize: "none",
4293
+ minHeight: "160px",
4294
+ maxHeight,
4295
+ width: "100%",
4296
+ outline: "none"
4297
+ }
4298
+ }
4299
+ )
4300
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4269
4301
  "div",
4270
4302
  {
4271
4303
  ref: scrollRef,
@@ -4285,7 +4317,7 @@ var CodeBlock = React114__namespace.default.memo(
4285
4317
  )
4286
4318
  ] });
4287
4319
  },
4288
- (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable
4320
+ (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable && prev.editable === next.editable && prev.onChange === next.onChange
4289
4321
  );
4290
4322
  CodeBlock.displayName = "CodeBlock";
4291
4323
 
@@ -4049,7 +4049,9 @@ var CodeBlock = React114__default.memo(
4049
4049
  showLanguageBadge = true,
4050
4050
  maxHeight = "60vh",
4051
4051
  foldable: foldableProp,
4052
- className
4052
+ className,
4053
+ editable = false,
4054
+ onChange
4053
4055
  }) => {
4054
4056
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
4055
4057
  const isOrb = language === "orb";
@@ -4220,7 +4222,37 @@ var CodeBlock = React114__default.memo(
4220
4222
  ]
4221
4223
  }
4222
4224
  ),
4223
- /* @__PURE__ */ jsx(
4225
+ editable ? (
4226
+ /* GAP-51: editable mode — composes the Textarea atom. Plain text editing,
4227
+ no Prism highlighting overlay (follow-up). The textarea is uncontrolled
4228
+ on the value side: we pass `code` as the initial value and forward
4229
+ every keystroke via onChange — the consumer is responsible for
4230
+ debouncing and re-deriving `code` only after the user stops typing
4231
+ so the cursor doesn't fight a re-render. */
4232
+ /* @__PURE__ */ jsx(
4233
+ Textarea,
4234
+ {
4235
+ defaultValue: code,
4236
+ onChange: (e) => onChange?.(e.target.value),
4237
+ spellCheck: false,
4238
+ style: {
4239
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
4240
+ fontSize: "13px",
4241
+ lineHeight: "1.5",
4242
+ backgroundColor: "#1e1e1e",
4243
+ color: "#e6e6e6",
4244
+ borderRadius: hasHeader ? "0 0 0.5rem 0.5rem" : "0.5rem",
4245
+ border: "none",
4246
+ padding: "1rem",
4247
+ resize: "none",
4248
+ minHeight: "160px",
4249
+ maxHeight,
4250
+ width: "100%",
4251
+ outline: "none"
4252
+ }
4253
+ }
4254
+ )
4255
+ ) : /* @__PURE__ */ jsx(
4224
4256
  "div",
4225
4257
  {
4226
4258
  ref: scrollRef,
@@ -4240,7 +4272,7 @@ var CodeBlock = React114__default.memo(
4240
4272
  )
4241
4273
  ] });
4242
4274
  },
4243
- (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable
4275
+ (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable && prev.editable === next.editable && prev.onChange === next.onChange
4244
4276
  );
4245
4277
  CodeBlock.displayName = "CodeBlock";
4246
4278
 
@@ -10014,7 +10014,9 @@ var CodeBlock = React114__namespace.default.memo(
10014
10014
  showLanguageBadge = true,
10015
10015
  maxHeight = "60vh",
10016
10016
  foldable: foldableProp,
10017
- className
10017
+ className,
10018
+ editable = false,
10019
+ onChange
10018
10020
  }) => {
10019
10021
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
10020
10022
  const isOrb = language === "orb";
@@ -10185,7 +10187,37 @@ var CodeBlock = React114__namespace.default.memo(
10185
10187
  ]
10186
10188
  }
10187
10189
  ),
10188
- /* @__PURE__ */ jsxRuntime.jsx(
10190
+ editable ? (
10191
+ /* GAP-51: editable mode — composes the Textarea atom. Plain text editing,
10192
+ no Prism highlighting overlay (follow-up). The textarea is uncontrolled
10193
+ on the value side: we pass `code` as the initial value and forward
10194
+ every keystroke via onChange — the consumer is responsible for
10195
+ debouncing and re-deriving `code` only after the user stops typing
10196
+ so the cursor doesn't fight a re-render. */
10197
+ /* @__PURE__ */ jsxRuntime.jsx(
10198
+ Textarea,
10199
+ {
10200
+ defaultValue: code,
10201
+ onChange: (e) => onChange?.(e.target.value),
10202
+ spellCheck: false,
10203
+ style: {
10204
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
10205
+ fontSize: "13px",
10206
+ lineHeight: "1.5",
10207
+ backgroundColor: "#1e1e1e",
10208
+ color: "#e6e6e6",
10209
+ borderRadius: hasHeader ? "0 0 0.5rem 0.5rem" : "0.5rem",
10210
+ border: "none",
10211
+ padding: "1rem",
10212
+ resize: "none",
10213
+ minHeight: "160px",
10214
+ maxHeight,
10215
+ width: "100%",
10216
+ outline: "none"
10217
+ }
10218
+ }
10219
+ )
10220
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
10189
10221
  "div",
10190
10222
  {
10191
10223
  ref: scrollRef,
@@ -10205,7 +10237,7 @@ var CodeBlock = React114__namespace.default.memo(
10205
10237
  )
10206
10238
  ] });
10207
10239
  },
10208
- (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable
10240
+ (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable && prev.editable === next.editable && prev.onChange === next.onChange
10209
10241
  );
10210
10242
  CodeBlock.displayName = "CodeBlock";
10211
10243
 
@@ -33657,6 +33689,9 @@ function UISlotRenderer({
33657
33689
  }
33658
33690
  UISlotRenderer.displayName = "UISlotRenderer";
33659
33691
 
33692
+ // runtime/OrbPreview.tsx
33693
+ init_useEventBus();
33694
+
33660
33695
  // runtime/ServerBridge.tsx
33661
33696
  init_useEventBus();
33662
33697
  var ServerBridgeContext = React114.createContext(null);
@@ -33889,7 +33924,7 @@ function SlotBridge() {
33889
33924
  }, [slots, render, clear]);
33890
33925
  return null;
33891
33926
  }
33892
- function TraitInitializer({ traits: traits2, orbitalNames, onNavigate }) {
33927
+ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate, onLocalFallback }) {
33893
33928
  const slotsActions = useSlotsActions();
33894
33929
  const bridge = useServerBridge();
33895
33930
  const entityStore = providers.useEntityStore();
@@ -33930,10 +33965,11 @@ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate }) {
33930
33965
  const fallback = setTimeout(() => {
33931
33966
  if (!initSentRef.current) {
33932
33967
  sendEvent("INIT");
33968
+ onLocalFallback?.();
33933
33969
  }
33934
33970
  }, 5e3);
33935
33971
  return () => clearTimeout(fallback);
33936
- }, [traits2, orbitalNames, sendEvent]);
33972
+ }, [traits2, orbitalNames, sendEvent, onLocalFallback]);
33937
33973
  React114.useEffect(() => {
33938
33974
  if (!bridge.connected || !orbitalNames?.length || initSentRef.current) return;
33939
33975
  initSentRef.current = true;
@@ -33979,7 +34015,7 @@ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate }) {
33979
34015
  }, [bridge.connected, orbitalNames, bridge.sendEvent, slotsActions]);
33980
34016
  return null;
33981
34017
  }
33982
- function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate }) {
34018
+ function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate, onLocalFallback }) {
33983
34019
  const { traits: traits2, allEntities, ir } = useResolvedSchema(schema, pageName);
33984
34020
  const allPageTraits = React114.useMemo(() => {
33985
34021
  if (pageName && traits2.length > 0) return traits2;
@@ -34019,7 +34055,15 @@ function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate }) {
34019
34055
  }
34020
34056
  }, [mockKey, serverUrl, mockData, entityStore]);
34021
34057
  const inner = /* @__PURE__ */ jsxRuntime.jsx(providers.VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsxRuntime.jsx(SlotsProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs(EntitySchemaProvider, { entities: Array.from(allEntities.values()), children: [
34022
- /* @__PURE__ */ jsxRuntime.jsx(TraitInitializer, { traits: allPageTraits, orbitalNames: serverUrl ? orbitalNames : void 0, onNavigate }),
34058
+ /* @__PURE__ */ jsxRuntime.jsx(
34059
+ TraitInitializer,
34060
+ {
34061
+ traits: allPageTraits,
34062
+ orbitalNames: serverUrl ? orbitalNames : void 0,
34063
+ onNavigate,
34064
+ onLocalFallback
34065
+ }
34066
+ ),
34023
34067
  /* @__PURE__ */ jsxRuntime.jsx(SlotBridge, {}),
34024
34068
  /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-h-full p-4", children: /* @__PURE__ */ jsxRuntime.jsx(UISlotRenderer, { includeHud: true, hudMode: "inline", includeFloating: true }) })
34025
34069
  ] }) }) });
@@ -34036,6 +34080,16 @@ function OrbPreview({
34036
34080
  className,
34037
34081
  serverUrl
34038
34082
  }) {
34083
+ const [localFallback, setLocalFallback] = React114.useState(false);
34084
+ const eventBus = useEventBus();
34085
+ const handleLocalFallback = React114.useCallback(() => {
34086
+ if (localFallback) return;
34087
+ setLocalFallback(true);
34088
+ eventBus.emit("UI:NOTIFY", {
34089
+ message: "Preview server unreachable \u2014 running locally without server-side state.",
34090
+ severity: "warning"
34091
+ });
34092
+ }, [localFallback, eventBus]);
34039
34093
  const parseResult = React114.useMemo(() => {
34040
34094
  let parsed;
34041
34095
  if (typeof schema === "string") {
@@ -34093,13 +34147,26 @@ function OrbPreview({
34093
34147
  el.addEventListener("click", handler, true);
34094
34148
  return () => el.removeEventListener("click", handler, true);
34095
34149
  }, [pages, handleNavigate]);
34096
- return /* @__PURE__ */ jsxRuntime.jsx(
34150
+ return /* @__PURE__ */ jsxRuntime.jsxs(
34097
34151
  Box,
34098
34152
  {
34099
34153
  ref: containerRef,
34100
34154
  className: `overflow-auto border border-[var(--color-border)] rounded-[var(--radius-md)] ${className ?? ""}`,
34101
34155
  style: { height },
34102
- children: /* @__PURE__ */ jsxRuntime.jsx(providers.OrbitalProvider, { initialData: effectiveMockData, skipTheme: true, verification: true, children: /* @__PURE__ */ jsxRuntime.jsx(context.UISlotProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(SchemaRunner, { schema: parsedSchema, serverUrl, mockData: effectiveMockData, pageName: currentPage, onNavigate: handleNavigate }) }) })
34156
+ children: [
34157
+ localFallback && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-3 py-2 bg-[var(--color-warning)] bg-opacity-10 border-b border-[var(--color-warning)] flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-[var(--color-warning-foreground)] flex-1", children: "Preview server unreachable \u2014 running locally. Server-side state and persistence are disabled." }) }),
34158
+ /* @__PURE__ */ jsxRuntime.jsx(providers.OrbitalProvider, { initialData: effectiveMockData, skipTheme: true, verification: true, children: /* @__PURE__ */ jsxRuntime.jsx(context.UISlotProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
34159
+ SchemaRunner,
34160
+ {
34161
+ schema: parsedSchema,
34162
+ serverUrl,
34163
+ mockData: effectiveMockData,
34164
+ pageName: currentPage,
34165
+ onNavigate: handleNavigate,
34166
+ onLocalFallback: handleLocalFallback
34167
+ }
34168
+ ) }) })
34169
+ ]
34103
34170
  }
34104
34171
  );
34105
34172
  }
@@ -9969,7 +9969,9 @@ var CodeBlock = React114__default.memo(
9969
9969
  showLanguageBadge = true,
9970
9970
  maxHeight = "60vh",
9971
9971
  foldable: foldableProp,
9972
- className
9972
+ className,
9973
+ editable = false,
9974
+ onChange
9973
9975
  }) => {
9974
9976
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
9975
9977
  const isOrb = language === "orb";
@@ -10140,7 +10142,37 @@ var CodeBlock = React114__default.memo(
10140
10142
  ]
10141
10143
  }
10142
10144
  ),
10143
- /* @__PURE__ */ jsx(
10145
+ editable ? (
10146
+ /* GAP-51: editable mode — composes the Textarea atom. Plain text editing,
10147
+ no Prism highlighting overlay (follow-up). The textarea is uncontrolled
10148
+ on the value side: we pass `code` as the initial value and forward
10149
+ every keystroke via onChange — the consumer is responsible for
10150
+ debouncing and re-deriving `code` only after the user stops typing
10151
+ so the cursor doesn't fight a re-render. */
10152
+ /* @__PURE__ */ jsx(
10153
+ Textarea,
10154
+ {
10155
+ defaultValue: code,
10156
+ onChange: (e) => onChange?.(e.target.value),
10157
+ spellCheck: false,
10158
+ style: {
10159
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
10160
+ fontSize: "13px",
10161
+ lineHeight: "1.5",
10162
+ backgroundColor: "#1e1e1e",
10163
+ color: "#e6e6e6",
10164
+ borderRadius: hasHeader ? "0 0 0.5rem 0.5rem" : "0.5rem",
10165
+ border: "none",
10166
+ padding: "1rem",
10167
+ resize: "none",
10168
+ minHeight: "160px",
10169
+ maxHeight,
10170
+ width: "100%",
10171
+ outline: "none"
10172
+ }
10173
+ }
10174
+ )
10175
+ ) : /* @__PURE__ */ jsx(
10144
10176
  "div",
10145
10177
  {
10146
10178
  ref: scrollRef,
@@ -10160,7 +10192,7 @@ var CodeBlock = React114__default.memo(
10160
10192
  )
10161
10193
  ] });
10162
10194
  },
10163
- (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable
10195
+ (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable && prev.editable === next.editable && prev.onChange === next.onChange
10164
10196
  );
10165
10197
  CodeBlock.displayName = "CodeBlock";
10166
10198
 
@@ -33612,6 +33644,9 @@ function UISlotRenderer({
33612
33644
  }
33613
33645
  UISlotRenderer.displayName = "UISlotRenderer";
33614
33646
 
33647
+ // runtime/OrbPreview.tsx
33648
+ init_useEventBus();
33649
+
33615
33650
  // runtime/ServerBridge.tsx
33616
33651
  init_useEventBus();
33617
33652
  var ServerBridgeContext = createContext(null);
@@ -33844,7 +33879,7 @@ function SlotBridge() {
33844
33879
  }, [slots, render, clear]);
33845
33880
  return null;
33846
33881
  }
33847
- function TraitInitializer({ traits: traits2, orbitalNames, onNavigate }) {
33882
+ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate, onLocalFallback }) {
33848
33883
  const slotsActions = useSlotsActions();
33849
33884
  const bridge = useServerBridge();
33850
33885
  const entityStore = useEntityStore();
@@ -33885,10 +33920,11 @@ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate }) {
33885
33920
  const fallback = setTimeout(() => {
33886
33921
  if (!initSentRef.current) {
33887
33922
  sendEvent("INIT");
33923
+ onLocalFallback?.();
33888
33924
  }
33889
33925
  }, 5e3);
33890
33926
  return () => clearTimeout(fallback);
33891
- }, [traits2, orbitalNames, sendEvent]);
33927
+ }, [traits2, orbitalNames, sendEvent, onLocalFallback]);
33892
33928
  useEffect(() => {
33893
33929
  if (!bridge.connected || !orbitalNames?.length || initSentRef.current) return;
33894
33930
  initSentRef.current = true;
@@ -33934,7 +33970,7 @@ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate }) {
33934
33970
  }, [bridge.connected, orbitalNames, bridge.sendEvent, slotsActions]);
33935
33971
  return null;
33936
33972
  }
33937
- function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate }) {
33973
+ function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate, onLocalFallback }) {
33938
33974
  const { traits: traits2, allEntities, ir } = useResolvedSchema(schema, pageName);
33939
33975
  const allPageTraits = useMemo(() => {
33940
33976
  if (pageName && traits2.length > 0) return traits2;
@@ -33974,7 +34010,15 @@ function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate }) {
33974
34010
  }
33975
34011
  }, [mockKey, serverUrl, mockData, entityStore]);
33976
34012
  const inner = /* @__PURE__ */ jsx(VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsx(SlotsProvider, { children: /* @__PURE__ */ jsxs(EntitySchemaProvider, { entities: Array.from(allEntities.values()), children: [
33977
- /* @__PURE__ */ jsx(TraitInitializer, { traits: allPageTraits, orbitalNames: serverUrl ? orbitalNames : void 0, onNavigate }),
34013
+ /* @__PURE__ */ jsx(
34014
+ TraitInitializer,
34015
+ {
34016
+ traits: allPageTraits,
34017
+ orbitalNames: serverUrl ? orbitalNames : void 0,
34018
+ onNavigate,
34019
+ onLocalFallback
34020
+ }
34021
+ ),
33978
34022
  /* @__PURE__ */ jsx(SlotBridge, {}),
33979
34023
  /* @__PURE__ */ jsx(Box, { className: "min-h-full p-4", children: /* @__PURE__ */ jsx(UISlotRenderer, { includeHud: true, hudMode: "inline", includeFloating: true }) })
33980
34024
  ] }) }) });
@@ -33991,6 +34035,16 @@ function OrbPreview({
33991
34035
  className,
33992
34036
  serverUrl
33993
34037
  }) {
34038
+ const [localFallback, setLocalFallback] = useState(false);
34039
+ const eventBus = useEventBus();
34040
+ const handleLocalFallback = useCallback(() => {
34041
+ if (localFallback) return;
34042
+ setLocalFallback(true);
34043
+ eventBus.emit("UI:NOTIFY", {
34044
+ message: "Preview server unreachable \u2014 running locally without server-side state.",
34045
+ severity: "warning"
34046
+ });
34047
+ }, [localFallback, eventBus]);
33994
34048
  const parseResult = useMemo(() => {
33995
34049
  let parsed;
33996
34050
  if (typeof schema === "string") {
@@ -34048,13 +34102,26 @@ function OrbPreview({
34048
34102
  el.addEventListener("click", handler, true);
34049
34103
  return () => el.removeEventListener("click", handler, true);
34050
34104
  }, [pages, handleNavigate]);
34051
- return /* @__PURE__ */ jsx(
34105
+ return /* @__PURE__ */ jsxs(
34052
34106
  Box,
34053
34107
  {
34054
34108
  ref: containerRef,
34055
34109
  className: `overflow-auto border border-[var(--color-border)] rounded-[var(--radius-md)] ${className ?? ""}`,
34056
34110
  style: { height },
34057
- children: /* @__PURE__ */ jsx(OrbitalProvider, { initialData: effectiveMockData, skipTheme: true, verification: true, children: /* @__PURE__ */ jsx(UISlotProvider, { children: /* @__PURE__ */ jsx(SchemaRunner, { schema: parsedSchema, serverUrl, mockData: effectiveMockData, pageName: currentPage, onNavigate: handleNavigate }) }) })
34111
+ children: [
34112
+ localFallback && /* @__PURE__ */ jsx(Box, { className: "px-3 py-2 bg-[var(--color-warning)] bg-opacity-10 border-b border-[var(--color-warning)] flex items-center gap-2", children: /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-[var(--color-warning-foreground)] flex-1", children: "Preview server unreachable \u2014 running locally. Server-side state and persistence are disabled." }) }),
34113
+ /* @__PURE__ */ jsx(OrbitalProvider, { initialData: effectiveMockData, skipTheme: true, verification: true, children: /* @__PURE__ */ jsx(UISlotProvider, { children: /* @__PURE__ */ jsx(
34114
+ SchemaRunner,
34115
+ {
34116
+ schema: parsedSchema,
34117
+ serverUrl,
34118
+ mockData: effectiveMockData,
34119
+ pageName: currentPage,
34120
+ onNavigate: handleNavigate,
34121
+ onLocalFallback: handleLocalFallback
34122
+ }
34123
+ ) }) })
34124
+ ]
34058
34125
  }
34059
34126
  );
34060
34127
  }
package/index.css CHANGED
@@ -135,6 +135,46 @@ body {
135
135
  background: var(--color-foreground);
136
136
  }
137
137
 
138
+ /**
139
+ * ReactFlow (@xyflow/react) Controls — theme overrides for FlowCanvas zoom buttons
140
+ *
141
+ * Without these, the bottom-left zoom in/out/fit buttons render as plain white
142
+ * squares from ReactFlow's default stylesheet, ignoring the active theme.
143
+ * Targets: .react-flow__controls + .react-flow__controls-button
144
+ */
145
+ .react-flow__controls {
146
+ background: var(--color-card) !important;
147
+ border: 1px solid var(--color-border) !important;
148
+ border-radius: var(--radius-md) !important;
149
+ box-shadow: var(--shadow-main) !important;
150
+ overflow: hidden;
151
+ }
152
+
153
+ .react-flow__controls-button {
154
+ background: transparent !important;
155
+ border: none !important;
156
+ border-bottom: 1px solid var(--color-border) !important;
157
+ color: var(--color-foreground) !important;
158
+ fill: var(--color-foreground) !important;
159
+ width: 28px;
160
+ height: 28px;
161
+ transition: background var(--transition-fast) var(--transition-timing);
162
+ }
163
+
164
+ .react-flow__controls-button:last-child {
165
+ border-bottom: none !important;
166
+ }
167
+
168
+ .react-flow__controls-button:hover {
169
+ background: var(--color-muted) !important;
170
+ }
171
+
172
+ .react-flow__controls-button svg {
173
+ fill: var(--color-foreground) !important;
174
+ width: 14px;
175
+ height: 14px;
176
+ }
177
+
138
178
  @layer utilities {
139
179
  .shadow-theme { box-shadow: var(--shadow-main); }
140
180
  .shadow-theme-sm { box-shadow: var(--shadow-sm); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "2.50.1",
3
+ "version": "2.52.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "main": "./dist/components/index.js",