@arkcit/engine-react 0.3.9 → 0.3.10

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/engine.js CHANGED
@@ -58,7 +58,7 @@ var NodeErrorBoundary = class extends React.Component {
58
58
  };
59
59
 
60
60
  // src/engine/ReactWebEngineRoot.tsx
61
- import React4, { useRef, useState as useState2 } from "react";
61
+ import React4, { useEffect, useRef, useState as useState2 } from "react";
62
62
 
63
63
  // src/rendering/resolveResolvedReactNode.ts
64
64
  import {
@@ -387,6 +387,111 @@ var useUIEngineState = () => {
387
387
  };
388
388
  };
389
389
 
390
+ // src/engine/tablistController.ts
391
+ var TAB_ACTION_SELECTOR = '[role="tab"],[data-saas-preview-tab-panel-id]';
392
+ var asHTMLElement = (value) => value instanceof HTMLElement ? value : null;
393
+ var closestHTMLElement = (target, selector) => {
394
+ const element = target;
395
+ return typeof (element == null ? void 0 : element.closest) === "function" ? asHTMLElement(element.closest(selector)) : null;
396
+ };
397
+ var queryByIdOrNodeId = (root, id) => {
398
+ var _a, _b;
399
+ if (!id) return null;
400
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
401
+ const escapedId = CSS.escape(id);
402
+ return asHTMLElement(root.querySelector(`#${escapedId},[data-node-id="${escapedId}"]`));
403
+ }
404
+ return (_b = (_a = Array.from(root.querySelectorAll("[id]")).find((element) => element.id === id)) != null ? _a : Array.from(root.querySelectorAll("[data-node-id]")).find(
405
+ (element) => element.getAttribute("data-node-id") === id
406
+ )) != null ? _b : null;
407
+ };
408
+ var readHashTargetId = (element) => {
409
+ var _a, _b;
410
+ const href = (_b = (_a = element.getAttribute("href")) == null ? void 0 : _a.trim()) != null ? _b : "";
411
+ const hashIndex = href.indexOf("#");
412
+ return hashIndex >= 0 ? href.slice(hashIndex + 1).trim() : "";
413
+ };
414
+ var readTabPanelId = (element) => {
415
+ var _a, _b;
416
+ return ((_a = element.getAttribute("data-saas-preview-tab-panel-id")) == null ? void 0 : _a.trim()) || ((_b = element.getAttribute("aria-controls")) == null ? void 0 : _b.trim()) || readHashTargetId(element);
417
+ };
418
+ var readTabSectionId = (element) => {
419
+ var _a, _b, _c;
420
+ return ((_a = element.getAttribute("data-saas-preview-tab-section-id")) == null ? void 0 : _a.trim()) || ((_c = (_b = element.closest("[data-saas-preview-tab-section-id]")) == null ? void 0 : _b.getAttribute("data-saas-preview-tab-section-id")) == null ? void 0 : _c.trim()) || "";
421
+ };
422
+ var isDisabledTabAction = (element) => element.getAttribute("aria-disabled") === "true" || element instanceof HTMLButtonElement && element.disabled;
423
+ var getTabListTabs = (tabList) => Array.from(tabList.querySelectorAll('[role="tab"]')).filter(
424
+ (tab) => tab.closest('[role="tablist"]') === tabList
425
+ );
426
+ var findMatchingTab = (root, action) => {
427
+ var _a;
428
+ const panelId = readTabPanelId(action);
429
+ if (!panelId) return null;
430
+ const sectionId = readTabSectionId(action);
431
+ return (_a = Array.from(root.querySelectorAll('[role="tab"]')).find((tab) => {
432
+ if (readTabPanelId(tab) !== panelId) return false;
433
+ return !sectionId || readTabSectionId(tab) === sectionId;
434
+ })) != null ? _a : null;
435
+ };
436
+ var applyTablistSelection = (root, activeTab) => {
437
+ const tabList = activeTab.closest('[role="tablist"]');
438
+ if (!tabList || !root.contains(tabList)) return false;
439
+ const tabs = getTabListTabs(tabList);
440
+ if (!tabs.includes(activeTab) || isDisabledTabAction(activeTab)) return true;
441
+ const panelByTab = /* @__PURE__ */ new Map();
442
+ for (const tab of tabs) {
443
+ const panel = queryByIdOrNodeId(root, readTabPanelId(tab));
444
+ if (panel) {
445
+ panelByTab.set(tab, panel);
446
+ }
447
+ }
448
+ const activePanel = panelByTab.get(activeTab);
449
+ if (!activePanel || panelByTab.size === 0) return false;
450
+ for (const tab of tabs) {
451
+ const selected = tab === activeTab;
452
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
453
+ tab.tabIndex = selected ? 0 : -1;
454
+ }
455
+ for (const panel of panelByTab.values()) {
456
+ const selected = panel === activePanel;
457
+ panel.hidden = !selected;
458
+ panel.setAttribute("aria-hidden", selected ? "false" : "true");
459
+ if (selected) {
460
+ panel.style.removeProperty("display");
461
+ } else {
462
+ panel.style.display = "none";
463
+ }
464
+ }
465
+ return true;
466
+ };
467
+ var initializeTablistPanels = (root) => {
468
+ var _a, _b;
469
+ const tabLists = Array.from(root.querySelectorAll('[role="tablist"]'));
470
+ for (const tabList of tabLists) {
471
+ const tabs = getTabListTabs(tabList);
472
+ const activeTab = (_b = (_a = tabs.find(
473
+ (tab) => tab.getAttribute("aria-selected") === "true" && !isDisabledTabAction(tab)
474
+ )) != null ? _a : tabs.find((tab) => !isDisabledTabAction(tab))) != null ? _b : null;
475
+ if (activeTab) {
476
+ applyTablistSelection(root, activeTab);
477
+ }
478
+ }
479
+ };
480
+ var bindTablistController = (root) => {
481
+ initializeTablistPanels(root);
482
+ const handleClick = (event) => {
483
+ const action = closestHTMLElement(event.target, TAB_ACTION_SELECTOR);
484
+ if (!action || !root.contains(action) || isDisabledTabAction(action)) return;
485
+ const tab = action.getAttribute("role") === "tab" ? action : findMatchingTab(root, action);
486
+ if (!tab || !applyTablistSelection(root, tab)) return;
487
+ event.preventDefault();
488
+ };
489
+ root.addEventListener("click", handleClick, true);
490
+ return () => {
491
+ root.removeEventListener("click", handleClick, true);
492
+ };
493
+ };
494
+
390
495
  // src/engine/ReactWebEngineRoot.tsx
391
496
  import { jsx as jsx6 } from "react/jsx-runtime";
392
497
  var INTERNAL_STUDIO_NODE_TYPES = /* @__PURE__ */ new Set();
@@ -494,6 +599,7 @@ var ReactWebEngineRoot = ({
494
599
  const resizeStateRef = useRef(null);
495
600
  const pinchStateRef = useRef(null);
496
601
  const pendingFieldFocusRef = useRef(null);
602
+ const rootRef = useRef(null);
497
603
  const { ancestorTypeMembership, overlaysByNodeId, captureFieldFocus } = dependencies.useUIEngineEffects({
498
604
  schema,
499
605
  runtime,
@@ -558,7 +664,19 @@ var ReactWebEngineRoot = ({
558
664
  InlineTextEditorComponent: InlineTextEditor,
559
665
  isFormInteractiveTarget
560
666
  });
561
- return /* @__PURE__ */ jsx6("div", { className: "flex h-full min-w-0 w-full flex-wrap content-start items-start gap-3", children: schema.nodes.map((node) => /* @__PURE__ */ jsx6(React4.Fragment, { children: renderSafeEngineNode(node) }, node.id)) });
667
+ useEffect(() => {
668
+ const root = rootRef.current;
669
+ return root ? bindTablistController(root) : void 0;
670
+ }, [schema]);
671
+ return /* @__PURE__ */ jsx6(
672
+ "div",
673
+ {
674
+ ref: rootRef,
675
+ "data-arkcit-engine-root": true,
676
+ className: "flex min-w-0 w-full flex-wrap content-start items-start gap-3",
677
+ children: schema.nodes.map((node) => /* @__PURE__ */ jsx6(React4.Fragment, { children: renderSafeEngineNode(node) }, node.id))
678
+ }
679
+ );
562
680
  };
563
681
 
564
682
  // src/engine/StaticReactWebEngineRoot.tsx
@@ -653,7 +771,14 @@ var StaticReactWebEngineRoot = ({
653
771
  NodeWrapper,
654
772
  isFormInteractiveTarget: isFormInteractiveTarget2
655
773
  });
656
- return /* @__PURE__ */ jsx7("div", { className: "flex h-full min-w-0 w-full flex-wrap content-start items-start gap-3", children: schema.nodes.map((node) => /* @__PURE__ */ jsx7(React5.Fragment, { children: renderSafeEngineNode(node) }, node.id)) });
774
+ return /* @__PURE__ */ jsx7(
775
+ "div",
776
+ {
777
+ "data-arkcit-engine-root": true,
778
+ className: "flex min-w-0 w-full flex-wrap content-start items-start gap-3",
779
+ children: schema.nodes.map((node) => /* @__PURE__ */ jsx7(React5.Fragment, { children: renderSafeEngineNode(node) }, node.id))
780
+ }
781
+ );
657
782
  };
658
783
  export {
659
784
  EngineWarningFallback_default as EngineWarningFallback,
package/dist/index.js CHANGED
@@ -436,7 +436,7 @@ var NodeErrorBoundary = class extends React4.Component {
436
436
  };
437
437
 
438
438
  // src/engine/ReactWebEngineRoot.tsx
439
- import React7, { useRef, useState as useState2 } from "react";
439
+ import React7, { useEffect, useRef, useState as useState2 } from "react";
440
440
 
441
441
  // src/rendering/resolveResolvedReactNode.ts
442
442
  import {
@@ -765,6 +765,111 @@ var useUIEngineState = () => {
765
765
  };
766
766
  };
767
767
 
768
+ // src/engine/tablistController.ts
769
+ var TAB_ACTION_SELECTOR = '[role="tab"],[data-saas-preview-tab-panel-id]';
770
+ var asHTMLElement = (value) => value instanceof HTMLElement ? value : null;
771
+ var closestHTMLElement = (target, selector) => {
772
+ const element = target;
773
+ return typeof (element == null ? void 0 : element.closest) === "function" ? asHTMLElement(element.closest(selector)) : null;
774
+ };
775
+ var queryByIdOrNodeId = (root, id) => {
776
+ var _a, _b;
777
+ if (!id) return null;
778
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
779
+ const escapedId = CSS.escape(id);
780
+ return asHTMLElement(root.querySelector(`#${escapedId},[data-node-id="${escapedId}"]`));
781
+ }
782
+ return (_b = (_a = Array.from(root.querySelectorAll("[id]")).find((element) => element.id === id)) != null ? _a : Array.from(root.querySelectorAll("[data-node-id]")).find(
783
+ (element) => element.getAttribute("data-node-id") === id
784
+ )) != null ? _b : null;
785
+ };
786
+ var readHashTargetId = (element) => {
787
+ var _a, _b;
788
+ const href = (_b = (_a = element.getAttribute("href")) == null ? void 0 : _a.trim()) != null ? _b : "";
789
+ const hashIndex = href.indexOf("#");
790
+ return hashIndex >= 0 ? href.slice(hashIndex + 1).trim() : "";
791
+ };
792
+ var readTabPanelId = (element) => {
793
+ var _a, _b;
794
+ return ((_a = element.getAttribute("data-saas-preview-tab-panel-id")) == null ? void 0 : _a.trim()) || ((_b = element.getAttribute("aria-controls")) == null ? void 0 : _b.trim()) || readHashTargetId(element);
795
+ };
796
+ var readTabSectionId = (element) => {
797
+ var _a, _b, _c;
798
+ return ((_a = element.getAttribute("data-saas-preview-tab-section-id")) == null ? void 0 : _a.trim()) || ((_c = (_b = element.closest("[data-saas-preview-tab-section-id]")) == null ? void 0 : _b.getAttribute("data-saas-preview-tab-section-id")) == null ? void 0 : _c.trim()) || "";
799
+ };
800
+ var isDisabledTabAction = (element) => element.getAttribute("aria-disabled") === "true" || element instanceof HTMLButtonElement && element.disabled;
801
+ var getTabListTabs = (tabList) => Array.from(tabList.querySelectorAll('[role="tab"]')).filter(
802
+ (tab) => tab.closest('[role="tablist"]') === tabList
803
+ );
804
+ var findMatchingTab = (root, action) => {
805
+ var _a;
806
+ const panelId = readTabPanelId(action);
807
+ if (!panelId) return null;
808
+ const sectionId = readTabSectionId(action);
809
+ return (_a = Array.from(root.querySelectorAll('[role="tab"]')).find((tab) => {
810
+ if (readTabPanelId(tab) !== panelId) return false;
811
+ return !sectionId || readTabSectionId(tab) === sectionId;
812
+ })) != null ? _a : null;
813
+ };
814
+ var applyTablistSelection = (root, activeTab) => {
815
+ const tabList = activeTab.closest('[role="tablist"]');
816
+ if (!tabList || !root.contains(tabList)) return false;
817
+ const tabs = getTabListTabs(tabList);
818
+ if (!tabs.includes(activeTab) || isDisabledTabAction(activeTab)) return true;
819
+ const panelByTab = /* @__PURE__ */ new Map();
820
+ for (const tab of tabs) {
821
+ const panel = queryByIdOrNodeId(root, readTabPanelId(tab));
822
+ if (panel) {
823
+ panelByTab.set(tab, panel);
824
+ }
825
+ }
826
+ const activePanel = panelByTab.get(activeTab);
827
+ if (!activePanel || panelByTab.size === 0) return false;
828
+ for (const tab of tabs) {
829
+ const selected = tab === activeTab;
830
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
831
+ tab.tabIndex = selected ? 0 : -1;
832
+ }
833
+ for (const panel of panelByTab.values()) {
834
+ const selected = panel === activePanel;
835
+ panel.hidden = !selected;
836
+ panel.setAttribute("aria-hidden", selected ? "false" : "true");
837
+ if (selected) {
838
+ panel.style.removeProperty("display");
839
+ } else {
840
+ panel.style.display = "none";
841
+ }
842
+ }
843
+ return true;
844
+ };
845
+ var initializeTablistPanels = (root) => {
846
+ var _a, _b;
847
+ const tabLists = Array.from(root.querySelectorAll('[role="tablist"]'));
848
+ for (const tabList of tabLists) {
849
+ const tabs = getTabListTabs(tabList);
850
+ const activeTab = (_b = (_a = tabs.find(
851
+ (tab) => tab.getAttribute("aria-selected") === "true" && !isDisabledTabAction(tab)
852
+ )) != null ? _a : tabs.find((tab) => !isDisabledTabAction(tab))) != null ? _b : null;
853
+ if (activeTab) {
854
+ applyTablistSelection(root, activeTab);
855
+ }
856
+ }
857
+ };
858
+ var bindTablistController = (root) => {
859
+ initializeTablistPanels(root);
860
+ const handleClick = (event) => {
861
+ const action = closestHTMLElement(event.target, TAB_ACTION_SELECTOR);
862
+ if (!action || !root.contains(action) || isDisabledTabAction(action)) return;
863
+ const tab = action.getAttribute("role") === "tab" ? action : findMatchingTab(root, action);
864
+ if (!tab || !applyTablistSelection(root, tab)) return;
865
+ event.preventDefault();
866
+ };
867
+ root.addEventListener("click", handleClick, true);
868
+ return () => {
869
+ root.removeEventListener("click", handleClick, true);
870
+ };
871
+ };
872
+
768
873
  // src/engine/ReactWebEngineRoot.tsx
769
874
  import { jsx as jsx9 } from "react/jsx-runtime";
770
875
  var INTERNAL_STUDIO_NODE_TYPES = /* @__PURE__ */ new Set();
@@ -872,6 +977,7 @@ var ReactWebEngineRoot = ({
872
977
  const resizeStateRef = useRef(null);
873
978
  const pinchStateRef = useRef(null);
874
979
  const pendingFieldFocusRef = useRef(null);
980
+ const rootRef = useRef(null);
875
981
  const { ancestorTypeMembership, overlaysByNodeId, captureFieldFocus } = dependencies.useUIEngineEffects({
876
982
  schema,
877
983
  runtime,
@@ -936,7 +1042,19 @@ var ReactWebEngineRoot = ({
936
1042
  InlineTextEditorComponent: InlineTextEditor,
937
1043
  isFormInteractiveTarget
938
1044
  });
939
- return /* @__PURE__ */ jsx9("div", { className: "flex h-full min-w-0 w-full flex-wrap content-start items-start gap-3", children: schema.nodes.map((node) => /* @__PURE__ */ jsx9(React7.Fragment, { children: renderSafeEngineNode(node) }, node.id)) });
1045
+ useEffect(() => {
1046
+ const root = rootRef.current;
1047
+ return root ? bindTablistController(root) : void 0;
1048
+ }, [schema]);
1049
+ return /* @__PURE__ */ jsx9(
1050
+ "div",
1051
+ {
1052
+ ref: rootRef,
1053
+ "data-arkcit-engine-root": true,
1054
+ className: "flex min-w-0 w-full flex-wrap content-start items-start gap-3",
1055
+ children: schema.nodes.map((node) => /* @__PURE__ */ jsx9(React7.Fragment, { children: renderSafeEngineNode(node) }, node.id))
1056
+ }
1057
+ );
940
1058
  };
941
1059
 
942
1060
  // src/engine/StaticReactWebEngineRoot.tsx
@@ -1031,11 +1149,18 @@ var StaticReactWebEngineRoot = ({
1031
1149
  NodeWrapper,
1032
1150
  isFormInteractiveTarget: isFormInteractiveTarget2
1033
1151
  });
1034
- return /* @__PURE__ */ jsx10("div", { className: "flex h-full min-w-0 w-full flex-wrap content-start items-start gap-3", children: schema.nodes.map((node) => /* @__PURE__ */ jsx10(React8.Fragment, { children: renderSafeEngineNode(node) }, node.id)) });
1152
+ return /* @__PURE__ */ jsx10(
1153
+ "div",
1154
+ {
1155
+ "data-arkcit-engine-root": true,
1156
+ className: "flex min-w-0 w-full flex-wrap content-start items-start gap-3",
1157
+ children: schema.nodes.map((node) => /* @__PURE__ */ jsx10(React8.Fragment, { children: renderSafeEngineNode(node) }, node.id))
1158
+ }
1159
+ );
1035
1160
  };
1036
1161
 
1037
1162
  // src/hooks/useUIEngineEffects.ts
1038
- import { useEffect, useLayoutEffect, useMemo } from "react";
1163
+ import { useEffect as useEffect2, useLayoutEffect, useMemo } from "react";
1039
1164
  var useUIEngineEffects = ({
1040
1165
  schema,
1041
1166
  runtime,
@@ -1063,13 +1188,13 @@ var useUIEngineEffects = ({
1063
1188
  pendingFieldFocusRef.current = null;
1064
1189
  dependencies.restorePendingFieldFocus(pending);
1065
1190
  }, [dependencies, schema, pendingFieldFocusRef]);
1066
- useEffect(() => {
1191
+ useEffect2(() => {
1067
1192
  if (!runtime.subscribe) return;
1068
1193
  return runtime.subscribe(() => {
1069
1194
  setVersion((previous) => previous + 1);
1070
1195
  });
1071
1196
  }, [runtime, setVersion]);
1072
- useEffect(() => {
1197
+ useEffect2(() => {
1073
1198
  if (!onNodeResize) return;
1074
1199
  const handleMouseMove = (event) => {
1075
1200
  const state = resizeStateRef.current;
@@ -427,7 +427,14 @@ var StaticReactWebEngineRoot = ({
427
427
  NodeWrapper,
428
428
  isFormInteractiveTarget
429
429
  });
430
- return /* @__PURE__ */ jsx5("div", { className: "flex h-full min-w-0 w-full flex-wrap content-start items-start gap-3", children: schema.nodes.map((node) => /* @__PURE__ */ jsx5(React3.Fragment, { children: renderSafeEngineNode(node) }, node.id)) });
430
+ return /* @__PURE__ */ jsx5(
431
+ "div",
432
+ {
433
+ "data-arkcit-engine-root": true,
434
+ className: "flex min-w-0 w-full flex-wrap content-start items-start gap-3",
435
+ children: schema.nodes.map((node) => /* @__PURE__ */ jsx5(React3.Fragment, { children: renderSafeEngineNode(node) }, node.id))
436
+ }
437
+ );
431
438
  };
432
439
  export {
433
440
  StaticReactWebEngineRoot
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arkcit/engine-react",
3
3
  "private": false,
4
- "version": "0.3.9",
4
+ "version": "0.3.10",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "React-specific renderer package for the Arkcit engine platform.",