@parhelia/core 0.1.12884 → 0.1.12886
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/config/config.js +13 -1
- package/dist/config/config.js.map +1 -1
- package/dist/editor/LinkEditorDialog.js +25 -25
- package/dist/editor/LinkEditorDialog.js.map +1 -1
- package/dist/editor/client/EditorShell.js +1 -0
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/operations.d.ts +2 -1
- package/dist/editor/client/operations.js +61 -5
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +42 -32
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.js +25 -1
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.js.map +1 -1
- package/dist/editor/menubar/VersionSelector.js +16 -1
- package/dist/editor/menubar/VersionSelector.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/ManualBrowser.js +189 -79
- package/dist/editor/menubar/toolbar-sections/ManualBrowser.js.map +1 -1
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js +80 -32
- package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +77 -0
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/reviews/Comments.js +7 -0
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/SuggestedEdit.js +9 -4
- package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
- package/dist/editor/reviews/suggestionContentChange.d.ts +2 -0
- package/dist/editor/reviews/suggestionContentChange.js +9 -0
- package/dist/editor/reviews/suggestionContentChange.js.map +1 -0
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/package.json +1 -1
|
@@ -355,6 +355,16 @@ export function deriveSelectorState(params) {
|
|
|
355
355
|
}
|
|
356
356
|
return "disabled";
|
|
357
357
|
}
|
|
358
|
+
// Finishers for every highlight overlay currently on screen. Lets a superseding
|
|
359
|
+
// reveal wipe the previous reveal's highlight immediately so it does not keep
|
|
360
|
+
// pulsing over the close/re-open transition (the "uncoordinated animation").
|
|
361
|
+
const activeHighlightFinishers = new Set();
|
|
362
|
+
// Remove all active highlight overlays right now (resolving their lifetimes).
|
|
363
|
+
function clearActiveHighlightOverlays() {
|
|
364
|
+
for (const finish of [...activeHighlightFinishers]) {
|
|
365
|
+
finish();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
358
368
|
// Create a temporary highlight overlay. Resolves when its finite "life"
|
|
359
369
|
// animation ends and the overlay has been removed (event-driven, not a timer).
|
|
360
370
|
function showHighlightOverlay(x, y, width, height, duration) {
|
|
@@ -415,6 +425,7 @@ function showHighlightOverlay(x, y, width, height, duration) {
|
|
|
415
425
|
if (finished)
|
|
416
426
|
return;
|
|
417
427
|
finished = true;
|
|
428
|
+
activeHighlightFinishers.delete(finish);
|
|
418
429
|
overlay.removeEventListener("animationend", onAnimationEnd);
|
|
419
430
|
overlay.remove();
|
|
420
431
|
resolve();
|
|
@@ -426,6 +437,8 @@ function showHighlightOverlay(x, y, width, height, duration) {
|
|
|
426
437
|
}
|
|
427
438
|
};
|
|
428
439
|
overlay.addEventListener("animationend", onAnimationEnd);
|
|
440
|
+
// Allow a superseding reveal to wipe this overlay immediately.
|
|
441
|
+
activeHighlightFinishers.add(finish);
|
|
429
442
|
// Guardrail: if animationend is ever missed, remove shortly after duration.
|
|
430
443
|
window.setTimeout(finish, duration + 500);
|
|
431
444
|
});
|
|
@@ -463,9 +476,25 @@ function clearActiveReveal(id) {
|
|
|
463
476
|
activeRevealController = null;
|
|
464
477
|
}
|
|
465
478
|
}
|
|
479
|
+
// The surface key for the Agents panel depends on HOW it opens (mirrors the
|
|
480
|
+
// open logic in config.tsx `openAgentsPanel`): the dedicated far-right panel on
|
|
481
|
+
// desktop ("agents-panel") vs. a left sidebar stack on mobile / contexts without
|
|
482
|
+
// `setShowAgentsPanel` ("sidebar:agents-panel"). Used for de-duplication within
|
|
483
|
+
// a reveal and to keep the surface key self-documenting.
|
|
484
|
+
function agentsPanelSurfaceKey(editContext) {
|
|
485
|
+
if (editContext &&
|
|
486
|
+
!editContext.isMobile &&
|
|
487
|
+
typeof editContext.setShowAgentsPanel === "function") {
|
|
488
|
+
return "agents-panel";
|
|
489
|
+
}
|
|
490
|
+
return "sidebar:agents-panel";
|
|
491
|
+
}
|
|
466
492
|
// Close the More-panels flyout if it is currently open (inverse of open-more-sidebars).
|
|
493
|
+
function isMorePanelsExpanded() {
|
|
494
|
+
return !!document.querySelector('[data-testid="more-sidebars-button"][aria-expanded="true"]');
|
|
495
|
+
}
|
|
467
496
|
function closeMorePanelsIfOpen() {
|
|
468
|
-
if (!
|
|
497
|
+
if (!isMorePanelsExpanded())
|
|
469
498
|
return;
|
|
470
499
|
const trigger = document.querySelector('[data-testid="more-sidebars-button"]');
|
|
471
500
|
trigger?.click();
|
|
@@ -487,6 +516,40 @@ function closeVersionSelectorIfOpen() {
|
|
|
487
516
|
document.querySelector('[data-testid="version-selector"]');
|
|
488
517
|
trigger?.click();
|
|
489
518
|
}
|
|
519
|
+
// Resolve once the More-panels popover is fully closed: its list is gone AND
|
|
520
|
+
// the trigger no longer reports aria-expanded="true". This lets a superseding
|
|
521
|
+
// More-panels reveal reopen from a truly closed state instead of sampling
|
|
522
|
+
// present-but-closing menu items.
|
|
523
|
+
async function waitForMorePanelsClosed(timeoutMs = 1000) {
|
|
524
|
+
const isStillOpen = () => isElementPresent("@more-sidebars-panel") || isMorePanelsExpanded();
|
|
525
|
+
const waitUntilClosed = async () => {
|
|
526
|
+
const startedAt = Date.now();
|
|
527
|
+
while (Date.now() - startedAt < timeoutMs && isStillOpen()) {
|
|
528
|
+
await waitForDelay(50);
|
|
529
|
+
}
|
|
530
|
+
return !isStillOpen();
|
|
531
|
+
};
|
|
532
|
+
if (await waitUntilClosed())
|
|
533
|
+
return;
|
|
534
|
+
// If the first close signal was missed, try one guarded close again. The
|
|
535
|
+
// guard prevents toggling a panel that is already closing or closed.
|
|
536
|
+
closeMorePanelsIfOpen();
|
|
537
|
+
await waitForSelectorAnimations('[data-testid="more-sidebars-panel"]');
|
|
538
|
+
await waitUntilClosed();
|
|
539
|
+
}
|
|
540
|
+
// Resolve once the version-selector popover is fully closed: its list is gone
|
|
541
|
+
// AND the trigger no longer reports aria-expanded="true". The supersede close
|
|
542
|
+
// awaits this so the next reveal's open-version-selector (which early-returns
|
|
543
|
+
// when it believes the popover is still open/visible) always starts from a
|
|
544
|
+
// truly closed state, making the close-then-reopen deterministic.
|
|
545
|
+
async function waitForVersionSelectorClosed(timeoutMs = 1000) {
|
|
546
|
+
const startedAt = Date.now();
|
|
547
|
+
const isStillOpen = () => isElementPresent("@version-selector-list") ||
|
|
548
|
+
!!document.querySelector('[data-testid="version-selector"][aria-expanded="true"]');
|
|
549
|
+
while (Date.now() - startedAt < timeoutMs && isStillOpen()) {
|
|
550
|
+
await waitForDelay(50);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
490
553
|
function isAgentsPanelOpen() {
|
|
491
554
|
const trigger = document.querySelector('[data-testid="agents-panel-button"]');
|
|
492
555
|
return (trigger?.getAttribute("aria-pressed") === "true" ||
|
|
@@ -546,21 +609,32 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
546
609
|
revealRef.current = null;
|
|
547
610
|
setIsActiveReveal(false);
|
|
548
611
|
}, []);
|
|
549
|
-
//
|
|
550
|
-
//
|
|
612
|
+
// Fully tear down this reveal: wipe its highlight, close every owned surface
|
|
613
|
+
// (reverse open order so nested surfaces close first) and reset the button.
|
|
614
|
+
// Used for explicit close (2nd click, auto timer), external-close handling,
|
|
615
|
+
// and being superseded by another reveal.
|
|
551
616
|
const runRestore = useCallback(async () => {
|
|
552
617
|
const reveal = revealRef.current;
|
|
618
|
+
// Remove the highlight immediately so it does not keep pulsing over the
|
|
619
|
+
// close/re-open transition while the next reveal builds up.
|
|
620
|
+
clearActiveHighlightOverlays();
|
|
553
621
|
// Reset button state immediately (button turns blue) while the surface
|
|
554
|
-
// teardown animation plays out; the
|
|
622
|
+
// teardown animation plays out; the promise resolves once settled.
|
|
555
623
|
clearActiveReveal(reveal?.id ?? controllerIdRef.current);
|
|
556
624
|
revealRef.current = null;
|
|
557
625
|
setIsActiveReveal(false);
|
|
558
|
-
if (reveal)
|
|
626
|
+
if (!reveal)
|
|
627
|
+
return;
|
|
628
|
+
// Close in reverse open order (nested surfaces close first).
|
|
629
|
+
for (let i = reveal.surfaceOps.length - 1; i >= 0; i -= 1) {
|
|
630
|
+
const op = reveal.surfaceOps[i];
|
|
631
|
+
if (!op)
|
|
632
|
+
continue;
|
|
559
633
|
try {
|
|
560
|
-
await
|
|
634
|
+
await op.close();
|
|
561
635
|
}
|
|
562
636
|
catch {
|
|
563
|
-
// ignore
|
|
637
|
+
// ignore individual close failures
|
|
564
638
|
}
|
|
565
639
|
}
|
|
566
640
|
}, []);
|
|
@@ -647,7 +721,7 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
647
721
|
: isDisabled
|
|
648
722
|
? selectorDef.notFoundMessage || selectorDef.description
|
|
649
723
|
: selectorDef.description;
|
|
650
|
-
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { "aria-label": isActiveReveal ? CLOSE_PANE_LABEL : "Show me", onClick: async () => {
|
|
724
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { "data-manual-show-me": "true", "data-manual-show-me-before-action": selectorDef.beforeAction || undefined, "aria-label": isActiveReveal ? CLOSE_PANE_LABEL : "Show me", onClick: async () => {
|
|
651
725
|
// Second click while active -> restore (close=click behaviour).
|
|
652
726
|
if (isActiveReveal) {
|
|
653
727
|
runRestore();
|
|
@@ -675,16 +749,17 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
675
749
|
return;
|
|
676
750
|
}
|
|
677
751
|
// State-machine semantics: any new Show-me action supersedes the
|
|
678
|
-
// currently active State 3 reveal from another button,
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
752
|
+
// currently active State 3 reveal from another button, fully
|
|
753
|
+
// tearing it down (clearing its highlight and closing its surfaces)
|
|
754
|
+
// BEFORE this reveal starts opening. Await the teardown so the close
|
|
755
|
+
// animation completes first - the transition is a clean, sequenced
|
|
756
|
+
// close-then-open - and so this reveal samples fresh DOM and avoids
|
|
757
|
+
// a stale "was this surface open?" read. Because clicks are
|
|
758
|
+
// serialized above, the superseded controller is always the
|
|
759
|
+
// previous fully-settled reveal (no race here).
|
|
683
760
|
const superseded = supersedeActiveReveal(controllerIdRef.current);
|
|
684
761
|
if (superseded) {
|
|
685
762
|
try {
|
|
686
|
-
// Await the close so A's surface finishes its teardown
|
|
687
|
-
// animation before B starts revealing (event-driven, not timed).
|
|
688
763
|
await superseded.close();
|
|
689
764
|
}
|
|
690
765
|
catch {
|
|
@@ -711,28 +786,40 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
711
786
|
};
|
|
712
787
|
await action(actionProps);
|
|
713
788
|
};
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
const
|
|
789
|
+
// Surfaces this reveal opens, in open order. Closed in reverse on
|
|
790
|
+
// restore. The previous reveal was fully torn down above, so we
|
|
791
|
+
// always start from an empty set.
|
|
792
|
+
const surfaceOps = [];
|
|
793
|
+
// Register a surface unless one with the same key was already
|
|
794
|
+
// pushed by an earlier step in this same reveal.
|
|
795
|
+
const pushSurface = (op) => {
|
|
796
|
+
if (surfaceOps.some((existing) => existing.key === op.key)) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
surfaceOps.push(op);
|
|
800
|
+
};
|
|
718
801
|
// 1. Sidebar-only: open the sidebar.
|
|
719
802
|
if (isSidebarOnly &&
|
|
720
803
|
editContext?.openSidebar &&
|
|
721
804
|
selectorDef.location) {
|
|
722
805
|
const sidebarId = selectorDef.location;
|
|
806
|
+
const surfaceKey = `sidebar:${sidebarId}`;
|
|
723
807
|
const wasOpen = !!editContext.openSidebars?.includes(sidebarId);
|
|
724
808
|
if (!wasOpen) {
|
|
725
809
|
editContext.openSidebar(sidebarId);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
810
|
+
pushSurface({
|
|
811
|
+
key: surfaceKey,
|
|
812
|
+
close: async () => {
|
|
813
|
+
editContextRef.current?.toggleSidebar?.(sidebarId, {
|
|
814
|
+
forceClose: true,
|
|
815
|
+
});
|
|
816
|
+
await editContextRef.current?.waitForSurfaceSettled?.(`sidebar:${sidebarId}`);
|
|
817
|
+
},
|
|
818
|
+
isOpen: () => !!editContextRef.current?.openSidebars?.includes(sidebarId),
|
|
731
819
|
});
|
|
732
820
|
// Wait for the sidebar's open animation to finish before querying.
|
|
733
821
|
await editContext.waitForSurfaceSettled?.(`sidebar:${sidebarId}`);
|
|
734
822
|
}
|
|
735
|
-
openPredicates.push(() => !!editContextRef.current?.openSidebars?.includes(sidebarId));
|
|
736
823
|
}
|
|
737
824
|
const availabilityElements = anchorSelector
|
|
738
825
|
? resolveElements(anchorSelector)
|
|
@@ -751,11 +838,14 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
751
838
|
if (shouldOpenEditorForm && editContext) {
|
|
752
839
|
const slotId = editContext.getActiveSlotId();
|
|
753
840
|
editContext.setEditorFormHiddenForSlot(slotId, false);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
841
|
+
pushSurface({
|
|
842
|
+
key: "editor-form",
|
|
843
|
+
close: async () => {
|
|
844
|
+
editContextRef.current?.setEditorFormHiddenForSlot(slotId, true);
|
|
845
|
+
await editContextRef.current?.waitForSurfaceSettled?.("editor-form");
|
|
846
|
+
},
|
|
847
|
+
isOpen: () => !isEditorFormSliderHidden(getEditorFormSlider()),
|
|
757
848
|
});
|
|
758
|
-
openPredicates.push(() => !isEditorFormSliderHidden(getEditorFormSlider()));
|
|
759
849
|
await editContext.waitForSurfaceSettled?.("editor-form");
|
|
760
850
|
}
|
|
761
851
|
else if (selectorDef.tab) {
|
|
@@ -780,21 +870,32 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
780
870
|
if (selectorDef.beforeAction === "open-more-sidebars") {
|
|
781
871
|
const morePanelsNowOpen = isElementPresent("@more-sidebars-panel");
|
|
782
872
|
if (!morePanelsWasOpen && morePanelsNowOpen) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
873
|
+
pushSurface({
|
|
874
|
+
key: "more-sidebars-panel",
|
|
875
|
+
close: async () => {
|
|
876
|
+
closeMorePanelsIfOpen();
|
|
877
|
+
await waitForSelectorAnimations('[data-testid="more-sidebars-panel"]');
|
|
878
|
+
await waitForMorePanelsClosed();
|
|
879
|
+
},
|
|
880
|
+
isOpen: () => isElementPresent("@more-sidebars-panel"),
|
|
786
881
|
});
|
|
787
|
-
openPredicates.push(() => isElementPresent("@more-sidebars-panel"));
|
|
788
882
|
}
|
|
789
883
|
}
|
|
790
884
|
if (selectorDef.beforeAction === "open-version-selector") {
|
|
791
885
|
const versionSelectorNowOpen = isElementPresent("@version-selector-list");
|
|
792
886
|
if (!versionSelectorWasOpen && versionSelectorNowOpen) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
887
|
+
pushSurface({
|
|
888
|
+
key: "version-selector",
|
|
889
|
+
close: async () => {
|
|
890
|
+
closeVersionSelectorIfOpen();
|
|
891
|
+
await waitForSelectorAnimations('[data-testid="version-selector-list"]');
|
|
892
|
+
// Gate until the popover is truly gone so the next
|
|
893
|
+
// reveal's open-version-selector reopens from a closed
|
|
894
|
+
// state (deterministic close-then-reopen).
|
|
895
|
+
await waitForVersionSelectorClosed();
|
|
896
|
+
},
|
|
897
|
+
isOpen: () => isElementPresent("@version-selector-list"),
|
|
796
898
|
});
|
|
797
|
-
openPredicates.push(() => isElementPresent("@version-selector-list"));
|
|
798
899
|
}
|
|
799
900
|
}
|
|
800
901
|
if (selectorDef.beforeAction === "open-agents-panel" ||
|
|
@@ -803,46 +904,59 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
803
904
|
// animation (see config openAgentsPanel/openAgentSettings), so
|
|
804
905
|
// no polling here.
|
|
805
906
|
const agentsPanelNowOpen = isAgentsPanelOpen();
|
|
806
|
-
// Push the panel
|
|
807
|
-
//
|
|
907
|
+
// Push the panel surface first so that, since surfaces close in
|
|
908
|
+
// reverse, the settings popover is closed before the panel.
|
|
808
909
|
if (!agentsPanelWasOpen && agentsPanelNowOpen) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
910
|
+
pushSurface({
|
|
911
|
+
key: agentsPanelSurfaceKey(editContext),
|
|
912
|
+
close: async () => {
|
|
913
|
+
if (editContextRef.current?.isMobile) {
|
|
914
|
+
editContextRef.current?.toggleSidebar?.("agents-panel", {
|
|
915
|
+
forceClose: true,
|
|
916
|
+
});
|
|
917
|
+
await editContextRef.current?.waitForSurfaceSettled?.("sidebar:agents-panel");
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
editContextRef.current?.setShowAgentsPanel?.(false);
|
|
921
|
+
await editContextRef.current?.waitForSurfaceSettled?.("agents-panel");
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
isOpen: () => isAgentsPanelOpen(),
|
|
820
925
|
});
|
|
821
|
-
openPredicates.push(() => isAgentsPanelOpen());
|
|
822
926
|
}
|
|
823
927
|
if (selectorDef.beforeAction === "open-agent-settings") {
|
|
824
928
|
const settingsTargetNowOpen = !!selectorDef.selector &&
|
|
825
929
|
isElementPresent(selectorDef.selector);
|
|
826
930
|
if (!agentSettingsWasOpen && settingsTargetNowOpen) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
931
|
+
pushSurface({
|
|
932
|
+
key: "agent-settings",
|
|
933
|
+
close: async () => {
|
|
934
|
+
const trigger = document.querySelector('[data-testid="agent-settings-popover-trigger"]');
|
|
935
|
+
if (trigger &&
|
|
936
|
+
selectorDef.selector &&
|
|
937
|
+
isElementPresent(selectorDef.selector)) {
|
|
938
|
+
trigger.click();
|
|
939
|
+
await waitForSelectorAnimations('[data-testid="agent-settings-popover-content"]');
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
isOpen: () => selectorDef.selector
|
|
943
|
+
? isElementPresent(selectorDef.selector)
|
|
944
|
+
: false,
|
|
835
945
|
});
|
|
836
|
-
openPredicates.push(() => selectorDef.selector
|
|
837
|
-
? isElementPresent(selectorDef.selector)
|
|
838
|
-
: false);
|
|
839
946
|
}
|
|
840
947
|
}
|
|
841
948
|
}
|
|
842
|
-
// afterAction is the declared inverse for custom reveals (e.g.
|
|
949
|
+
// afterAction is the declared inverse for custom reveals (e.g.
|
|
950
|
+
// field actions). It is a restore-only step with no open surface,
|
|
951
|
+
// so it uses a "custom:" key and is excluded from the still-open
|
|
952
|
+
// check below.
|
|
843
953
|
if (selectorDef.afterAction) {
|
|
844
|
-
|
|
845
|
-
|
|
954
|
+
pushSurface({
|
|
955
|
+
key: "custom:afterAction",
|
|
956
|
+
close: async () => {
|
|
957
|
+
await runManualAction(selectorDef.afterAction, !isSidebarOnly ? resolveElements() : availabilityElements);
|
|
958
|
+
},
|
|
959
|
+
isOpen: () => true,
|
|
846
960
|
});
|
|
847
961
|
}
|
|
848
962
|
// Highlight the target. For sidebar-only selectors there is no
|
|
@@ -866,26 +980,22 @@ function SelectorButton({ selectorDef, keyProp, }) {
|
|
|
866
980
|
highlightLifetime = highlightElement(highlightSelector, HIGHLIGHT_DURATION_MS);
|
|
867
981
|
}
|
|
868
982
|
}
|
|
869
|
-
// If we opened
|
|
983
|
+
// If we opened any surface, this was a State-3 reveal: keep the
|
|
870
984
|
// button active (green) and register it for mutual exclusion.
|
|
871
|
-
if (
|
|
985
|
+
if (surfaceOps.length > 0) {
|
|
872
986
|
const id = ++revealControllerSeq;
|
|
873
987
|
controllerIdRef.current = id;
|
|
874
988
|
revealRef.current = {
|
|
875
989
|
id,
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
990
|
+
surfaceOps,
|
|
991
|
+
// Restore-only "custom:" surfaces (e.g. afterAction) have no
|
|
992
|
+
// open state and must not keep the reveal "open" forever.
|
|
993
|
+
isStillOpen: () => {
|
|
994
|
+
const tracked = surfaceOps.filter((op) => !op.key.startsWith("custom:"));
|
|
995
|
+
return tracked.length === 0
|
|
996
|
+
? true
|
|
997
|
+
: tracked.some((op) => op.isOpen());
|
|
885
998
|
},
|
|
886
|
-
isStillOpen: () => openPredicates.length === 0
|
|
887
|
-
? true
|
|
888
|
-
: openPredicates.some((predicate) => predicate()),
|
|
889
999
|
};
|
|
890
1000
|
registerActiveReveal({
|
|
891
1001
|
id,
|