@dxos/plugin-deck 0.9.1-main.c7dcc2e112 → 0.10.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/PLUGIN.mdl +3 -1
- package/dist/lib/neutral/{DeckLayout-VX2BP4Q2.mjs → DeckLayout-B3RUUV5P.mjs} +4 -3
- package/dist/lib/neutral/DeckLayout-B3RUUV5P.mjs.map +7 -0
- package/dist/lib/neutral/DeckPlugin.mjs +2 -6
- package/dist/lib/neutral/DeckPlugin.mjs.map +3 -3
- package/dist/lib/neutral/DeckSettings-Z7GMB7XJ.mjs +32 -0
- package/dist/lib/neutral/DeckSettings-Z7GMB7XJ.mjs.map +7 -0
- package/dist/lib/neutral/{add-toast-APKTCLIA.mjs → add-toast-BHNEH5UY.mjs} +2 -2
- package/dist/lib/neutral/{adjust-7CZI4GK3.mjs → adjust-MJB6UMRQ.mjs} +16 -10
- package/dist/lib/neutral/adjust-MJB6UMRQ.mjs.map +7 -0
- package/dist/lib/neutral/{app-graph-builder-6HTXRTU2.mjs → app-graph-builder-2F7P4IFQ.mjs} +4 -3
- package/dist/lib/neutral/app-graph-builder-2F7P4IFQ.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +5 -5
- package/dist/lib/neutral/chunk-FKGYHBUB.mjs +8 -0
- package/dist/lib/neutral/{chunk-CL7JIOI5.mjs → chunk-ICOJ4GMB.mjs} +10 -6
- package/dist/lib/neutral/chunk-ICOJ4GMB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-NGX6RXNV.mjs → chunk-PP32NEVY.mjs} +2 -2
- package/dist/lib/neutral/chunk-PP32NEVY.mjs.map +7 -0
- package/dist/lib/neutral/chunk-SONATB35.mjs +20 -0
- package/dist/lib/neutral/chunk-SONATB35.mjs.map +7 -0
- package/dist/lib/neutral/chunk-Z2B5RSVI.mjs +1096 -0
- package/dist/lib/neutral/chunk-Z2B5RSVI.mjs.map +7 -0
- package/dist/lib/neutral/{close-KJUCFIJ5.mjs → close-2YGB5IIP.mjs} +4 -3
- package/dist/lib/neutral/{close-KJUCFIJ5.mjs.map → close-2YGB5IIP.mjs.map} +1 -1
- package/dist/lib/neutral/components/index.mjs +201 -16
- package/dist/lib/neutral/components/index.mjs.map +4 -4
- package/dist/lib/neutral/containers/index.mjs +6 -11
- package/dist/lib/neutral/containers/index.mjs.map +3 -3
- package/dist/lib/neutral/hooks/index.mjs +101 -17
- package/dist/lib/neutral/hooks/index.mjs.map +4 -4
- package/dist/lib/neutral/index.mjs +7 -1
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/{notification-tracker-CUDFRDCF.mjs → notification-tracker-Y5CRTWJM.mjs} +4 -2
- package/dist/lib/neutral/notification-tracker-Y5CRTWJM.mjs.map +7 -0
- package/dist/lib/neutral/{open-VU7YS3HB.mjs → open-P6XQ56M7.mjs} +4 -3
- package/dist/lib/neutral/{open-VU7YS3HB.mjs.map → open-P6XQ56M7.mjs.map} +1 -1
- package/dist/lib/neutral/operations/index.mjs +1 -1
- package/dist/lib/neutral/plugin.mjs +1 -1
- package/dist/lib/neutral/{react-surface-O4POQWYL.mjs → react-surface-H63FXUK3.mjs} +2 -2
- package/dist/lib/neutral/{react-surface-O4POQWYL.mjs.map → react-surface-H63FXUK3.mjs.map} +1 -1
- package/dist/lib/neutral/{revert-workspace-24TKG3I7.mjs → revert-workspace-RXWP2TEE.mjs} +2 -2
- package/dist/lib/neutral/{scroll-into-view-3VXT6TWC.mjs → scroll-into-view-HFV5EGYZ.mjs} +2 -2
- package/dist/lib/neutral/{set-RFOLTI57.mjs → set-HOOVB7E4.mjs} +4 -3
- package/dist/lib/neutral/{set-RFOLTI57.mjs.map → set-HOOVB7E4.mjs.map} +1 -1
- package/dist/lib/neutral/{set-layout-mode-AZ73W52Z.mjs → set-layout-mode-XAGSZZGB.mjs} +2 -2
- package/dist/lib/neutral/{state-D3YXB5NP.mjs → state-F4X5GPGZ.mjs} +3 -2
- package/dist/lib/neutral/{state-D3YXB5NP.mjs.map → state-F4X5GPGZ.mjs.map} +1 -1
- package/dist/lib/neutral/{switch-workspace-TBPT23CZ.mjs → switch-workspace-PA7N6EUX.mjs} +2 -2
- package/dist/lib/neutral/translations.mjs +2 -2
- package/dist/lib/neutral/translations.mjs.map +2 -2
- package/dist/lib/neutral/types/index.mjs +1 -1
- package/dist/lib/neutral/{update-companion-5LCY6QME.mjs → update-companion-6A2SKQAI.mjs} +14 -6
- package/dist/lib/neutral/update-companion-6A2SKQAI.mjs.map +7 -0
- package/dist/lib/neutral/{update-complementary-HZ7QXZYE.mjs → update-complementary-K55I5EV5.mjs} +2 -2
- package/dist/lib/neutral/{update-dialog-A7W3R5EI.mjs → update-dialog-Z3GEXMVV.mjs} +2 -2
- package/dist/lib/neutral/{update-plank-size-XHTYBLSD.mjs → update-plank-size-BTESVV7N.mjs} +2 -2
- package/dist/lib/neutral/{update-popover-RDIHG73B.mjs → update-popover-QEVDSZSH.mjs} +2 -2
- package/dist/lib/neutral/{update-sidebar-GEI7USA5.mjs → update-sidebar-VSRCN47U.mjs} +2 -2
- package/dist/lib/neutral/{url-handler-PUS4X33T.mjs → url-handler-ERZXXEYR.mjs} +3 -2
- package/dist/lib/neutral/{url-handler-PUS4X33T.mjs.map → url-handler-ERZXXEYR.mjs.map} +1 -1
- package/dist/types/src/DeckPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -2
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/capabilities/notification-tracker.d.ts +1 -1
- package/dist/types/src/capabilities/notification-tracker.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-root.d.ts +1 -5
- package/dist/types/src/capabilities/react-root.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +1 -1
- package/dist/types/src/capabilities/settings.d.ts +1 -3
- package/dist/types/src/capabilities/settings.d.ts.map +1 -1
- package/dist/types/src/capabilities/state.d.ts +1 -187
- package/dist/types/src/capabilities/state.d.ts.map +1 -1
- package/dist/types/src/capabilities/tools.d.ts +1 -1
- package/dist/types/src/capabilities/url-handler.d.ts +1 -1
- package/dist/types/src/components/Companion/Companion.d.ts +17 -0
- package/dist/types/src/components/Companion/Companion.d.ts.map +1 -0
- package/dist/types/src/components/Companion/index.d.ts +2 -0
- package/dist/types/src/components/Companion/index.d.ts.map +1 -0
- package/dist/types/src/components/Matrix/Matrix.d.ts +1 -1
- package/dist/types/src/components/Matrix/Matrix.d.ts.map +1 -1
- package/dist/types/src/components/Matrix/Matrix.stories.d.ts +2 -2
- package/dist/types/src/components/Matrix/Matrix.stories.d.ts.map +1 -1
- package/dist/types/src/components/Matrix/index.d.ts +1 -1
- package/dist/types/src/components/Matrix/index.d.ts.map +1 -1
- package/dist/types/src/components/Pane/Pane.d.ts +45 -0
- package/dist/types/src/components/Pane/Pane.d.ts.map +1 -0
- package/dist/types/src/components/Pane/Pane.stories.d.ts +7 -0
- package/dist/types/src/components/Pane/Pane.stories.d.ts.map +1 -0
- package/dist/types/src/components/Pane/index.d.ts +2 -0
- package/dist/types/src/components/Pane/index.d.ts.map +1 -0
- package/dist/types/src/components/Plank/Plank.d.ts +74 -0
- package/dist/types/src/components/Plank/Plank.d.ts.map +1 -0
- package/dist/types/src/components/Plank/Plank.stories.d.ts +6 -0
- package/dist/types/src/components/Plank/Plank.stories.d.ts.map +1 -0
- package/dist/types/src/components/Plank/index.d.ts +2 -0
- package/dist/types/src/components/Plank/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/Deck/Deck.d.ts +1 -1
- package/dist/types/src/containers/Deck/Deck.d.ts.map +1 -1
- package/dist/types/src/containers/Deck/Deck.stories.d.ts +2 -2
- package/dist/types/src/containers/Deck/DeckPlank.d.ts +21 -0
- package/dist/types/src/containers/Deck/DeckPlank.d.ts.map +1 -0
- package/dist/types/src/containers/Deck/DeckViewport.d.ts +3 -1
- package/dist/types/src/containers/Deck/DeckViewport.d.ts.map +1 -1
- package/dist/types/src/containers/{Plank → Deck}/PlankControls.d.ts +3 -10
- package/dist/types/src/containers/Deck/PlankControls.d.ts.map +1 -0
- package/dist/types/src/containers/Deck/PlankFallback.d.ts +6 -0
- package/dist/types/src/containers/Deck/PlankFallback.d.ts.map +1 -0
- package/dist/types/src/containers/Deck/useDeckPlank.d.ts +49 -0
- package/dist/types/src/containers/Deck/useDeckPlank.d.ts.map +1 -0
- package/dist/types/src/containers/DeckLayout/DeckLayout.stories.d.ts +2 -2
- package/dist/types/src/containers/DeckLayout/DeckLayout.stories.d.ts.map +1 -1
- package/dist/types/src/{components → containers}/DeckSettings/DeckSettings.d.ts +1 -1
- package/dist/types/src/containers/DeckSettings/DeckSettings.d.ts.map +1 -0
- package/dist/types/src/{components → containers}/DeckSettings/DeckSettings.stories.d.ts +2 -2
- package/dist/types/src/{components → containers}/DeckSettings/DeckSettings.stories.d.ts.map +1 -1
- package/dist/types/src/containers/DeckSettings/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +1 -1
- package/dist/types/src/containers/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useCompanionSplit.d.ts +28 -0
- package/dist/types/src/hooks/useCompanionSplit.d.ts.map +1 -0
- package/dist/types/src/hooks/useCompanions.d.ts +11 -7
- package/dist/types/src/hooks/useCompanions.d.ts.map +1 -1
- package/dist/types/src/hooks/useDeckCompanions.d.ts +2 -2
- package/dist/types/src/hooks/useDeckCompanions.d.ts.map +1 -1
- package/dist/types/src/hooks/useDeckState.d.ts +1 -1
- package/dist/types/src/hooks/useDeckState.d.ts.map +1 -1
- package/dist/types/src/hooks/useSelectedCompanionVariant.d.ts +6 -0
- package/dist/types/src/hooks/useSelectedCompanionVariant.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +4 -0
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/operations/adjust.d.ts.map +1 -1
- package/dist/types/src/operations/update-companion.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/DeckCapabilities.d.ts +2 -2
- package/dist/types/src/types/DeckCapabilities.d.ts.map +1 -1
- package/dist/types/src/types/DeckOperation.d.ts +2 -2
- package/dist/types/src/types/DeckOperation.d.ts.map +1 -1
- package/dist/types/src/types/schema.d.ts +7 -7
- package/dist/types/src/types/schema.d.ts.map +1 -1
- package/dist/types/src/util/companion-view-state.d.ts +16 -0
- package/dist/types/src/util/companion-view-state.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/plank-url-params.d.ts +2 -0
- package/dist/types/src/util/plank-url-params.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +49 -47
- package/src/DeckPlugin.ts +1 -2
- package/src/capabilities/app-graph-builder.ts +3 -2
- package/src/capabilities/notification-tracker.ts +3 -1
- package/src/capabilities/react-surface.tsx +1 -1
- package/src/components/Companion/Companion.tsx +91 -0
- package/src/components/Companion/index.ts +5 -0
- package/src/components/Matrix/Matrix.stories.tsx +13 -26
- package/src/components/Matrix/Matrix.tsx +3 -3
- package/src/components/Matrix/SPEC.md +1 -1
- package/src/components/Matrix/index.ts +1 -1
- package/src/components/Pane/Pane.stories.tsx +92 -0
- package/src/components/Pane/Pane.tsx +178 -0
- package/src/components/Pane/index.ts +5 -0
- package/src/components/Plank/Plank.stories.tsx +103 -0
- package/src/components/Plank/Plank.tsx +149 -0
- package/src/components/Plank/index.ts +5 -0
- package/src/components/index.ts +3 -4
- package/src/containers/Deck/Deck.tsx +3 -3
- package/src/containers/Deck/DeckPlank.tsx +187 -0
- package/src/containers/Deck/DeckViewport.tsx +116 -243
- package/src/containers/{Plank → Deck}/PlankControls.tsx +19 -17
- package/src/containers/Deck/PlankFallback.tsx +42 -0
- package/src/containers/Deck/useDeckPlank.ts +219 -0
- package/src/containers/DeckLayout/DeckLayout.stories.tsx +38 -51
- package/src/containers/DeckLayout/Dialog.tsx +1 -1
- package/src/{components → containers}/DeckSettings/DeckSettings.stories.tsx +1 -1
- package/src/containers/DeckSettings/DeckSettings.tsx +42 -0
- package/src/containers/Sidebar/ComplementarySidebar.tsx +2 -2
- package/src/containers/index.ts +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCompanionSplit.ts +78 -0
- package/src/hooks/useCompanions.ts +39 -7
- package/src/hooks/useDeckCompanions.ts +3 -3
- package/src/hooks/useDeckState.ts +1 -1
- package/src/hooks/useSelectedCompanionVariant.ts +14 -0
- package/src/index.ts +7 -0
- package/src/operations/adjust.ts +23 -12
- package/src/operations/update-companion.ts +10 -6
- package/src/translations.ts +2 -2
- package/src/types/DeckCapabilities.ts +1 -1
- package/src/types/DeckOperation.ts +2 -1
- package/src/types/schema.ts +5 -4
- package/src/util/companion-view-state.ts +28 -0
- package/src/util/index.ts +1 -0
- package/src/util/plank-url-params.test.ts +5 -0
- package/src/util/plank-url-params.ts +3 -1
- package/src/util/sanitize-persisted-state.test.ts +1 -1
- package/src/util/set-active.test.ts +1 -1
- package/dist/lib/neutral/DeckLayout-VX2BP4Q2.mjs.map +0 -7
- package/dist/lib/neutral/DeckSettings-CQQZJ2YS.mjs +0 -27
- package/dist/lib/neutral/DeckSettings-CQQZJ2YS.mjs.map +0 -7
- package/dist/lib/neutral/adjust-7CZI4GK3.mjs.map +0 -7
- package/dist/lib/neutral/app-graph-builder-6HTXRTU2.mjs.map +0 -7
- package/dist/lib/neutral/chunk-CL7JIOI5.mjs.map +0 -7
- package/dist/lib/neutral/chunk-DNG3L5QN.mjs +0 -1304
- package/dist/lib/neutral/chunk-DNG3L5QN.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NGX6RXNV.mjs.map +0 -7
- package/dist/lib/neutral/chunk-UZLAR4DR.mjs +0 -8
- package/dist/lib/neutral/notification-tracker-CUDFRDCF.mjs.map +0 -7
- package/dist/lib/neutral/update-companion-5LCY6QME.mjs.map +0 -7
- package/dist/types/src/components/DeckSettings/DeckSettings.d.ts.map +0 -1
- package/dist/types/src/components/DeckSettings/index.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/Plank.d.ts +0 -17
- package/dist/types/src/containers/Plank/Plank.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/Plank.stories.d.ts +0 -67
- package/dist/types/src/containers/Plank/Plank.stories.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankComponent.d.ts +0 -17
- package/dist/types/src/containers/Plank/PlankComponent.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankContent.d.ts +0 -10
- package/dist/types/src/containers/Plank/PlankContent.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankControls.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankError.d.ts +0 -15
- package/dist/types/src/containers/Plank/PlankError.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankHeading.d.ts +0 -22
- package/dist/types/src/containers/Plank/PlankHeading.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankLoading.d.ts +0 -3
- package/dist/types/src/containers/Plank/PlankLoading.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/PlankRoot.d.ts +0 -37
- package/dist/types/src/containers/Plank/PlankRoot.d.ts.map +0 -1
- package/dist/types/src/containers/Plank/index.d.ts +0 -4
- package/dist/types/src/containers/Plank/index.d.ts.map +0 -1
- package/src/components/DeckSettings/DeckSettings.tsx +0 -34
- package/src/containers/Plank/Plank.stories.tsx +0 -108
- package/src/containers/Plank/Plank.tsx +0 -22
- package/src/containers/Plank/PlankComponent.tsx +0 -223
- package/src/containers/Plank/PlankContent.tsx +0 -45
- package/src/containers/Plank/PlankError.tsx +0 -82
- package/src/containers/Plank/PlankHeading.tsx +0 -236
- package/src/containers/Plank/PlankLoading.tsx +0 -10
- package/src/containers/Plank/PlankRoot.tsx +0 -49
- package/src/containers/Plank/index.ts +0 -7
- /package/dist/lib/neutral/{add-toast-APKTCLIA.mjs.map → add-toast-BHNEH5UY.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-UZLAR4DR.mjs.map → chunk-FKGYHBUB.mjs.map} +0 -0
- /package/dist/lib/neutral/{revert-workspace-24TKG3I7.mjs.map → revert-workspace-RXWP2TEE.mjs.map} +0 -0
- /package/dist/lib/neutral/{scroll-into-view-3VXT6TWC.mjs.map → scroll-into-view-HFV5EGYZ.mjs.map} +0 -0
- /package/dist/lib/neutral/{set-layout-mode-AZ73W52Z.mjs.map → set-layout-mode-XAGSZZGB.mjs.map} +0 -0
- /package/dist/lib/neutral/{switch-workspace-TBPT23CZ.mjs.map → switch-workspace-PA7N6EUX.mjs.map} +0 -0
- /package/dist/lib/neutral/{update-complementary-HZ7QXZYE.mjs.map → update-complementary-K55I5EV5.mjs.map} +0 -0
- /package/dist/lib/neutral/{update-dialog-A7W3R5EI.mjs.map → update-dialog-Z3GEXMVV.mjs.map} +0 -0
- /package/dist/lib/neutral/{update-plank-size-XHTYBLSD.mjs.map → update-plank-size-BTESVV7N.mjs.map} +0 -0
- /package/dist/lib/neutral/{update-popover-RDIHG73B.mjs.map → update-popover-QEVDSZSH.mjs.map} +0 -0
- /package/dist/lib/neutral/{update-sidebar-GEI7USA5.mjs.map → update-sidebar-VSRCN47U.mjs.map} +0 -0
- /package/dist/types/src/{components → containers}/DeckSettings/index.d.ts +0 -0
- /package/src/{components → containers}/DeckSettings/index.ts +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
6
|
+
import React, { type ComponentPropsWithRef, forwardRef } from 'react';
|
|
7
|
+
|
|
8
|
+
import { AttentionSigilButton } from '@dxos/app-toolkit/ui';
|
|
9
|
+
import { DensityProvider, IconButton, type ThemedClassName, composableProps, slottable } from '@dxos/react-ui';
|
|
10
|
+
import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
|
|
11
|
+
import { iconSize, mx } from '@dxos/ui-theme';
|
|
12
|
+
import type { Merge } from '@dxos/util';
|
|
13
|
+
|
|
14
|
+
//
|
|
15
|
+
// Pane
|
|
16
|
+
//
|
|
17
|
+
// A presentational, radix-style component for a single deck pane: a toolbar rail above a content body.
|
|
18
|
+
// A plank always has a toolbar, so it implements that layout directly rather than wrapping the
|
|
19
|
+
// generalized `Panel`. A companion pane is just a Pane whose toolbar holds `Pane.Tabs` instead of a
|
|
20
|
+
// sigil/title. Intentionally free of app-framework concepts (capabilities, operations, Surface) —
|
|
21
|
+
// containers compose the sigil, controls, tabs and content surface into the slots.
|
|
22
|
+
//
|
|
23
|
+
|
|
24
|
+
//
|
|
25
|
+
// Root
|
|
26
|
+
//
|
|
27
|
+
|
|
28
|
+
// Root accepts arbitrary div attributes (the container wires tabIndex, data-* and key handlers), so it
|
|
29
|
+
// is a plain forwarding div rather than the narrowly-typed `slottable` used by the toolbar/content slots.
|
|
30
|
+
type PaneRootProps = ThemedClassName<ComponentPropsWithRef<'div'>>;
|
|
31
|
+
|
|
32
|
+
const PaneRoot = forwardRef<HTMLDivElement, PaneRootProps>(({ children, ...props }, forwardedRef) => (
|
|
33
|
+
<div
|
|
34
|
+
{...composableProps(props, {
|
|
35
|
+
role: 'article',
|
|
36
|
+
classNames:
|
|
37
|
+
'dx-container flex flex-col dx-attention-surface relative dx-focus-ring-inset-over-all dx-density-lg min-w-0',
|
|
38
|
+
})}
|
|
39
|
+
ref={forwardedRef}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
));
|
|
44
|
+
|
|
45
|
+
PaneRoot.displayName = 'Pane.Root';
|
|
46
|
+
|
|
47
|
+
//
|
|
48
|
+
// Toolbar
|
|
49
|
+
//
|
|
50
|
+
|
|
51
|
+
const PaneToolbar = slottable<HTMLDivElement>(({ children, asChild, ...props }, forwardedRef) => {
|
|
52
|
+
const Comp = asChild ? Slot : 'div';
|
|
53
|
+
return (
|
|
54
|
+
<Comp
|
|
55
|
+
{...composableProps(props, {
|
|
56
|
+
style: iconSize(5),
|
|
57
|
+
classNames: 'flex items-center gap-1 px-1 shrink-0 h-(--dx-rail-content) bg-header-surface',
|
|
58
|
+
})}
|
|
59
|
+
ref={forwardedRef}
|
|
60
|
+
>
|
|
61
|
+
<DensityProvider density='lg'>{children}</DensityProvider>
|
|
62
|
+
</Comp>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
PaneToolbar.displayName = 'Pane.Toolbar';
|
|
67
|
+
|
|
68
|
+
//
|
|
69
|
+
// Content
|
|
70
|
+
//
|
|
71
|
+
|
|
72
|
+
const PaneContent = slottable<HTMLDivElement>(({ children, asChild, ...props }, forwardedRef) => {
|
|
73
|
+
const Comp = asChild ? Slot : 'div';
|
|
74
|
+
return (
|
|
75
|
+
<Comp {...composableProps(props, { classNames: 'flex-1 min-h-0' })} ref={forwardedRef}>
|
|
76
|
+
{children}
|
|
77
|
+
</Comp>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
PaneContent.displayName = 'Pane.Content';
|
|
82
|
+
|
|
83
|
+
//
|
|
84
|
+
// Title
|
|
85
|
+
//
|
|
86
|
+
|
|
87
|
+
type PaneTitleProps = ThemedClassName<ComponentPropsWithRef<'h1'>> & AttendableId & Related;
|
|
88
|
+
|
|
89
|
+
/** Attention-aware plank title; colors to the accent when the plank (or a related companion) is attended. */
|
|
90
|
+
const PaneTitle = forwardRef<HTMLHeadingElement, PaneTitleProps>(
|
|
91
|
+
({ attendableId, related, classNames, ...props }, forwardedRef) => {
|
|
92
|
+
const { hasAttention, isAncestor, isRelated } = useAttention(attendableId);
|
|
93
|
+
return (
|
|
94
|
+
<h1
|
|
95
|
+
{...props}
|
|
96
|
+
data-attention={((related && isRelated) || hasAttention || isAncestor).toString()}
|
|
97
|
+
className={mx(
|
|
98
|
+
'px-1 min-w-0 w-0 grow truncate font-medium text-base-fg data-[attention=true]:text-accent-text self-center',
|
|
99
|
+
classNames,
|
|
100
|
+
)}
|
|
101
|
+
ref={forwardedRef}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
PaneTitle.displayName = 'Pane.Title';
|
|
108
|
+
|
|
109
|
+
//
|
|
110
|
+
// Tabs
|
|
111
|
+
//
|
|
112
|
+
|
|
113
|
+
export type PaneTab = {
|
|
114
|
+
id: string;
|
|
115
|
+
icon: string;
|
|
116
|
+
/** Already-localized label. */
|
|
117
|
+
label: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type PaneTabsProps = Merge<
|
|
121
|
+
ThemedClassName<{
|
|
122
|
+
tabs: ReadonlyArray<PaneTab>;
|
|
123
|
+
value?: string;
|
|
124
|
+
onValueChange?: (id: string) => void;
|
|
125
|
+
/** Collapse inactive tabs to icon-only once the tab count exceeds this. */
|
|
126
|
+
maxTabs?: number;
|
|
127
|
+
}>,
|
|
128
|
+
AttendableId,
|
|
129
|
+
Related
|
|
130
|
+
>;
|
|
131
|
+
|
|
132
|
+
/** Full-height tab strip for a companion plank's toolbar; selects among available companions. */
|
|
133
|
+
const PaneTabs = forwardRef<HTMLDivElement, PaneTabsProps>(
|
|
134
|
+
({ tabs, value, onValueChange, maxTabs = 5, attendableId, related, classNames }, forwardedRef) => {
|
|
135
|
+
// The active tab only reads as primary when the plank (shared with its companion) is attended.
|
|
136
|
+
const { hasAttention, isAncestor, isRelated } = useAttention(attendableId);
|
|
137
|
+
const attended = (related && isRelated) || hasAttention || isAncestor;
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
role='tablist'
|
|
141
|
+
className={mx('flex-1 min-w-0 overflow-x-auto scrollbar-none flex items-center gap-1', classNames)}
|
|
142
|
+
ref={forwardedRef}
|
|
143
|
+
>
|
|
144
|
+
{tabs.map(({ id, icon, label }) => (
|
|
145
|
+
<IconButton
|
|
146
|
+
key={id}
|
|
147
|
+
role='tab'
|
|
148
|
+
aria-selected={value === id}
|
|
149
|
+
tabIndex={value === id ? 0 : -1}
|
|
150
|
+
data-id={id}
|
|
151
|
+
icon={icon}
|
|
152
|
+
iconOnly={tabs.length > maxTabs && value !== id}
|
|
153
|
+
label={label}
|
|
154
|
+
variant={value === id && attended ? 'primary' : 'ghost'}
|
|
155
|
+
onClick={() => onValueChange?.(id)}
|
|
156
|
+
/>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
PaneTabs.displayName = 'Pane.Tabs';
|
|
164
|
+
|
|
165
|
+
//
|
|
166
|
+
// Pane
|
|
167
|
+
//
|
|
168
|
+
|
|
169
|
+
export const Pane = {
|
|
170
|
+
Root: PaneRoot,
|
|
171
|
+
Toolbar: PaneToolbar,
|
|
172
|
+
Sigil: AttentionSigilButton,
|
|
173
|
+
Title: PaneTitle,
|
|
174
|
+
Tabs: PaneTabs,
|
|
175
|
+
Content: PaneContent,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export type { PaneRootProps, PaneTabsProps, PaneTitleProps };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Capabilities, Capability, Plugin } from '@dxos/app-framework';
|
|
9
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
10
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
11
|
+
import { AppActivationEvents, AppPlugin } from '@dxos/app-toolkit';
|
|
12
|
+
import { AppSurface } from '@dxos/app-toolkit/ui';
|
|
13
|
+
import { Obj } from '@dxos/echo';
|
|
14
|
+
import { type Node } from '@dxos/plugin-graph';
|
|
15
|
+
import { corePlugins } from '@dxos/plugin-testing';
|
|
16
|
+
import { random } from '@dxos/random';
|
|
17
|
+
import { withAttention } from '@dxos/react-ui-attention/testing';
|
|
18
|
+
import { Syntax } from '@dxos/react-ui-syntax-highlighter';
|
|
19
|
+
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
20
|
+
import { Organization, Person } from '@dxos/types';
|
|
21
|
+
|
|
22
|
+
import { DeckState, OperationHandler } from '#capabilities';
|
|
23
|
+
import { meta as pluginMeta } from '#meta';
|
|
24
|
+
import { translations } from '#translations';
|
|
25
|
+
|
|
26
|
+
import { Plank } from './Plank';
|
|
27
|
+
|
|
28
|
+
random.seed(99);
|
|
29
|
+
|
|
30
|
+
const TestPlugin = Plugin.define(pluginMeta).pipe(
|
|
31
|
+
Plugin.addModule({
|
|
32
|
+
id: Capability.getModuleTag(DeckState),
|
|
33
|
+
activatesOn: AppActivationEvents.AppGraphReady,
|
|
34
|
+
activate: () => DeckState(),
|
|
35
|
+
}),
|
|
36
|
+
AppPlugin.addOperationHandlerModule({
|
|
37
|
+
activate: OperationHandler,
|
|
38
|
+
}),
|
|
39
|
+
Plugin.make,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const TestExtension = Capability.contributes(
|
|
43
|
+
Capabilities.ReactSurface,
|
|
44
|
+
Surface.create({
|
|
45
|
+
id: 'storyArticle',
|
|
46
|
+
filter: Surface.makeFilter(AppSurface.Article),
|
|
47
|
+
component: ({ data: { subject } }) =>
|
|
48
|
+
subject ? (
|
|
49
|
+
<Syntax.Root data={subject}>
|
|
50
|
+
<Syntax.Content>
|
|
51
|
+
<Syntax.Viewport>
|
|
52
|
+
<Syntax.Code />
|
|
53
|
+
</Syntax.Viewport>
|
|
54
|
+
</Syntax.Content>
|
|
55
|
+
</Syntax.Root>
|
|
56
|
+
) : (
|
|
57
|
+
<Loading />
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// A border makes each plank's bounds obvious against the deck surface.
|
|
63
|
+
const PLANK_CLASSNAMES = 'border border-separator';
|
|
64
|
+
|
|
65
|
+
const useNode = (data: Obj.Any, icon: string): Node.Node =>
|
|
66
|
+
useMemo(
|
|
67
|
+
() => ({ id: data.id, type: 'test', data, properties: { label: Obj.getLabel(data) ?? 'Untitled', icon } }),
|
|
68
|
+
[data, icon],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Two planks side by side; focus either to move attention (sigil/title take the accent color).
|
|
72
|
+
const DefaultStory = () => {
|
|
73
|
+
const [organization, person] = useMemo(
|
|
74
|
+
() => [Organization.make({ name: random.company.name() }), Person.make({ fullName: random.person.fullName() })],
|
|
75
|
+
[],
|
|
76
|
+
);
|
|
77
|
+
const organizationNode = useNode(organization, 'ph--building-office--regular');
|
|
78
|
+
const personNode = useNode(person, 'ph--user--regular');
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className='flex h-full gap-3 p-3 bg-deck-surface'>
|
|
82
|
+
<Plank node={organizationNode} classNames={PLANK_CLASSNAMES} />
|
|
83
|
+
<Plank node={personNode} classNames={PLANK_CLASSNAMES} />
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const meta: Meta = {
|
|
89
|
+
title: 'plugins/plugin-deck/components/Plank',
|
|
90
|
+
decorators: [
|
|
91
|
+
withPluginManager({ plugins: [...corePlugins(), TestPlugin()], capabilities: [TestExtension] }),
|
|
92
|
+
withAttention(),
|
|
93
|
+
withTheme(),
|
|
94
|
+
withLayout({ layout: 'fullscreen' }),
|
|
95
|
+
],
|
|
96
|
+
parameters: { layout: 'fullscreen', translations },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default meta;
|
|
100
|
+
|
|
101
|
+
type Story = StoryObj;
|
|
102
|
+
|
|
103
|
+
export const Default: Story = { render: () => <DefaultStory /> };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, {
|
|
6
|
+
type ComponentProps,
|
|
7
|
+
Fragment,
|
|
8
|
+
type KeyboardEventHandler,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
forwardRef,
|
|
11
|
+
useMemo,
|
|
12
|
+
} from 'react';
|
|
13
|
+
|
|
14
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
15
|
+
import { AppSurface, AttentionSigil, type AttentionSigilAction } from '@dxos/app-toolkit/ui';
|
|
16
|
+
import { type Node } from '@dxos/plugin-graph';
|
|
17
|
+
import { Icon, Popover, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
18
|
+
import { useAttentionAttributes } from '@dxos/react-ui-attention';
|
|
19
|
+
|
|
20
|
+
import { meta } from '#meta';
|
|
21
|
+
|
|
22
|
+
import { Pane } from '../Pane';
|
|
23
|
+
|
|
24
|
+
type SurfaceProps = ComponentProps<typeof Surface.Surface>;
|
|
25
|
+
|
|
26
|
+
export type PlankProps = ThemedClassName<{
|
|
27
|
+
node: Node.Node;
|
|
28
|
+
/** Attendable id; defaults to the node id. */
|
|
29
|
+
attendableId?: string;
|
|
30
|
+
/** Grouped sigil menu actions; when present the sigil opens a menu, otherwise it is a plain button. */
|
|
31
|
+
actions?: AttentionSigilAction[][];
|
|
32
|
+
onAction?: (action: AttentionSigilAction) => void;
|
|
33
|
+
/** Toolbar controls rendered after the title (e.g. close/solo/fullscreen). */
|
|
34
|
+
controls?: ReactNode;
|
|
35
|
+
/** Toolbar content rendered between the title and the controls (e.g. a NavbarEnd surface). */
|
|
36
|
+
navbarEnd?: ReactNode;
|
|
37
|
+
/** Sigil-menu footer content (e.g. a MenuFooter surface). */
|
|
38
|
+
sigilFooter?: ReactNode;
|
|
39
|
+
/** Highlight the title/sigil when a related (companion) region is attended. */
|
|
40
|
+
related?: boolean;
|
|
41
|
+
/** Render the title in a muted style (e.g. while the subject is loading). */
|
|
42
|
+
pending?: boolean;
|
|
43
|
+
/** When it matches `${meta.key}:${node.id}`, anchors a popover to the sigil. */
|
|
44
|
+
popoverAnchorId?: string;
|
|
45
|
+
/** Extra Article surface data merged over the computed defaults (e.g. companionTo/variant/path). */
|
|
46
|
+
articleData?: Partial<AppSurface.ArticleData>;
|
|
47
|
+
/** Error fallback for the content surface. */
|
|
48
|
+
fallback?: SurfaceProps['fallback'];
|
|
49
|
+
/** Loading placeholder for the content surface. */
|
|
50
|
+
placeholder?: SurfaceProps['placeholder'];
|
|
51
|
+
/** Render only the content surface, omitting the toolbar (e.g. fullscreen). */
|
|
52
|
+
headless?: boolean;
|
|
53
|
+
// TODO(burdon): Why is this required?
|
|
54
|
+
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A higher-level deck pane bound to a graph {@link Node}: renders the node's sigil, title and content
|
|
59
|
+
* Surface inside a {@link Pane}, and makes itself the node's attendable region. Toolbar controls
|
|
60
|
+
* (close/solo/etc.) and framework surfaces (NavbarEnd/MenuFooter) are supplied by the container via
|
|
61
|
+
* slots so this component stays free of capabilities/operations.
|
|
62
|
+
*/
|
|
63
|
+
export const Plank = forwardRef<HTMLDivElement, PlankProps>(
|
|
64
|
+
(
|
|
65
|
+
{
|
|
66
|
+
classNames,
|
|
67
|
+
node,
|
|
68
|
+
attendableId = node.id,
|
|
69
|
+
actions,
|
|
70
|
+
onAction,
|
|
71
|
+
controls,
|
|
72
|
+
navbarEnd,
|
|
73
|
+
sigilFooter,
|
|
74
|
+
related,
|
|
75
|
+
pending,
|
|
76
|
+
popoverAnchorId,
|
|
77
|
+
articleData,
|
|
78
|
+
fallback,
|
|
79
|
+
placeholder,
|
|
80
|
+
headless,
|
|
81
|
+
onKeyDown,
|
|
82
|
+
},
|
|
83
|
+
forwardedRef,
|
|
84
|
+
) => {
|
|
85
|
+
const { t } = useTranslation(meta.profile.key);
|
|
86
|
+
const attentionAttrs = useAttentionAttributes(attendableId);
|
|
87
|
+
const icon = node.properties?.icon ?? 'ph--circle-dashed--regular';
|
|
88
|
+
const label = toLocalizedString(node.properties?.label ?? '', t);
|
|
89
|
+
const data = useMemo<AppSurface.ArticleData>(
|
|
90
|
+
() => ({ attendableId, subject: node.data, properties: node.properties, popoverAnchorId, ...articleData }),
|
|
91
|
+
[attendableId, node.data, node.properties, popoverAnchorId, articleData],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Anchor the sigil's popover only when this plank's menu is the active popover target.
|
|
95
|
+
const ActionRoot = popoverAnchorId === `${meta.profile.key}:${node.id}` ? Popover.Anchor : Fragment;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Pane.Root
|
|
99
|
+
ref={forwardedRef}
|
|
100
|
+
classNames={classNames}
|
|
101
|
+
tabIndex={0}
|
|
102
|
+
onKeyDown={onKeyDown}
|
|
103
|
+
{...attentionAttrs}
|
|
104
|
+
data-testid='deck.plank'
|
|
105
|
+
>
|
|
106
|
+
{!headless && (
|
|
107
|
+
<Pane.Toolbar>
|
|
108
|
+
<ActionRoot>
|
|
109
|
+
{actions && actions.length > 0 ? (
|
|
110
|
+
<AttentionSigil
|
|
111
|
+
icon={icon}
|
|
112
|
+
related={related}
|
|
113
|
+
attendableId={attendableId}
|
|
114
|
+
actions={actions}
|
|
115
|
+
onAction={onAction}
|
|
116
|
+
triggerLabel={label}
|
|
117
|
+
>
|
|
118
|
+
{sigilFooter}
|
|
119
|
+
</AttentionSigil>
|
|
120
|
+
) : (
|
|
121
|
+
<Pane.Sigil attendableId={attendableId}>
|
|
122
|
+
<span className='sr-only'>{label}</span>
|
|
123
|
+
<Icon icon={icon} />
|
|
124
|
+
</Pane.Sigil>
|
|
125
|
+
)}
|
|
126
|
+
</ActionRoot>
|
|
127
|
+
<Pane.Title attendableId={attendableId} related={related} classNames={pending && 'text-description'}>
|
|
128
|
+
{label}
|
|
129
|
+
</Pane.Title>
|
|
130
|
+
{navbarEnd}
|
|
131
|
+
{controls}
|
|
132
|
+
</Pane.Toolbar>
|
|
133
|
+
)}
|
|
134
|
+
<Pane.Content>
|
|
135
|
+
<Surface.Surface
|
|
136
|
+
key={node.id}
|
|
137
|
+
type={AppSurface.Article}
|
|
138
|
+
data={data}
|
|
139
|
+
limit={1}
|
|
140
|
+
fallback={fallback}
|
|
141
|
+
placeholder={placeholder}
|
|
142
|
+
/>
|
|
143
|
+
</Pane.Content>
|
|
144
|
+
</Pane.Root>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
Plank.displayName = 'Plank';
|
package/src/components/index.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const DeckSettings: ComponentType<any> = lazy(() => import('./DeckSettings'));
|
|
8
|
-
|
|
5
|
+
export { Companion, type CompanionProps } from './Companion';
|
|
9
6
|
export { Matrix } from './Matrix';
|
|
7
|
+
export { Pane, type PaneTab } from './Pane';
|
|
8
|
+
export { Plank, type PlankProps } from './Plank';
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { DeckContent, DeckContentProps } from './DeckContent';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { DeckLayoutChangeRequest, DeckRoot, DeckRootProps } from './DeckRoot';
|
|
7
|
+
import { DeckContentEmpty, DeckMultiMode, DeckSoloMode, DeckViewport, DeckViewportProps } from './DeckViewport';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Radix-style composite Deck component.
|
|
@@ -18,4 +18,4 @@ export const Deck = {
|
|
|
18
18
|
SoloMode: DeckSoloMode,
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export type { DeckLayoutChangeRequest, DeckRootProps,
|
|
21
|
+
export type { DeckContentProps, DeckLayoutChangeRequest, DeckRootProps, DeckViewportProps };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useFocusFinders } from '@fluentui/react-tabster';
|
|
6
|
+
import React, { type KeyboardEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
9
|
+
import { AppSurface } from '@dxos/app-toolkit/ui';
|
|
10
|
+
import { Splitter, type ThemedClassName } from '@dxos/react-ui';
|
|
11
|
+
|
|
12
|
+
import { Companion, Plank } from '#components';
|
|
13
|
+
import { useCompanionSplit } from '#hooks';
|
|
14
|
+
import { type LayoutMode, type ResolvedPart, type Settings } from '#types';
|
|
15
|
+
|
|
16
|
+
import { PlankCompanionControls, PlankControls } from './PlankControls';
|
|
17
|
+
import { PlankErrorFallback, PlankLoading } from './PlankFallback';
|
|
18
|
+
import { useDeckPlank } from './useDeckPlank';
|
|
19
|
+
|
|
20
|
+
const PLANK_LOADING = <PlankLoading />;
|
|
21
|
+
|
|
22
|
+
export type DeckPlankProps = ThemedClassName<{
|
|
23
|
+
id: string;
|
|
24
|
+
part: ResolvedPart;
|
|
25
|
+
layoutMode: LayoutMode;
|
|
26
|
+
active?: string[];
|
|
27
|
+
/** Whether the companion pane should be shown for this plank (gated further by attention in multi-mode). */
|
|
28
|
+
companionShown?: boolean;
|
|
29
|
+
settings?: Settings.Settings;
|
|
30
|
+
path?: string[];
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Connected deck plank: resolves the node, companions, actions and operation handlers via
|
|
35
|
+
* {@link useDeckPlank} and renders the presentational {@link Plank} — wrapped in a {@link Splitter}
|
|
36
|
+
* beside a {@link Companion} when a companion is open. Replaces the legacy
|
|
37
|
+
* `PlankContainer`/`PlankComponent`/`PlankHeading` tree.
|
|
38
|
+
*/
|
|
39
|
+
export const DeckPlank = memo(
|
|
40
|
+
({ id, part, layoutMode, active, companionShown, settings, path, classNames }: DeckPlankProps) => {
|
|
41
|
+
const { findFirstFocusable } = useFocusFinders();
|
|
42
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
const {
|
|
44
|
+
node,
|
|
45
|
+
companions,
|
|
46
|
+
resolvedCompanionId,
|
|
47
|
+
hasCompanion,
|
|
48
|
+
companionOrientation,
|
|
49
|
+
capabilities,
|
|
50
|
+
sigilActions,
|
|
51
|
+
popoverAnchorId,
|
|
52
|
+
scrollIntoView,
|
|
53
|
+
onAction,
|
|
54
|
+
onAdjust,
|
|
55
|
+
onScrollIntoView,
|
|
56
|
+
onUpdateCompanion,
|
|
57
|
+
} = useDeckPlank({ id, part, layoutMode, active, companionShown, deckEnabled: settings?.enableDeck });
|
|
58
|
+
|
|
59
|
+
// Memoize the split point per orientation so toggling side-by-side ↔ stacked restores each one.
|
|
60
|
+
const { size: companionSize, onSizeChange: onCompanionSizeChange } = useCompanionSplit(companionOrientation);
|
|
61
|
+
|
|
62
|
+
// Newly opened/navigated planks are flagged via `scrollIntoView`; focus the pane so it gains
|
|
63
|
+
// attention, then clear the one-shot flag.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (scrollIntoView === id) {
|
|
66
|
+
rootRef.current?.focus();
|
|
67
|
+
onScrollIntoView(undefined);
|
|
68
|
+
}
|
|
69
|
+
}, [scrollIntoView, id, onScrollIntoView]);
|
|
70
|
+
|
|
71
|
+
// Tabster's focus group should move focus to Main on Escape, but something blocks it; handle directly.
|
|
72
|
+
const handleKeyDown = useCallback(
|
|
73
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
74
|
+
if (event.target === event.currentTarget) {
|
|
75
|
+
switch (event.key) {
|
|
76
|
+
case 'Escape':
|
|
77
|
+
event.currentTarget.closest('main')?.focus();
|
|
78
|
+
break;
|
|
79
|
+
case 'Enter':
|
|
80
|
+
findFirstFocusable(event.currentTarget)?.focus();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[findFirstFocusable],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Stable reference so Plank's useMemo on articleData doesn't bust every render.
|
|
89
|
+
const articleData = useMemo(() => ({ path }), [path]);
|
|
90
|
+
|
|
91
|
+
// Keep the companion panel mounted for 250ms after hasCompanion goes false so the Splitter's
|
|
92
|
+
// CSS transition can complete before the subtree is removed from the DOM.
|
|
93
|
+
const [companionMounted, setCompanionMounted] = useState(hasCompanion);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (hasCompanion) {
|
|
96
|
+
setCompanionMounted(true);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const timer = setTimeout(() => setCompanionMounted(false), 250);
|
|
100
|
+
return () => clearTimeout(timer);
|
|
101
|
+
}, [hasCompanion]);
|
|
102
|
+
|
|
103
|
+
const companionControls = useMemo(() => <PlankCompanionControls primary={id} />, [id]);
|
|
104
|
+
|
|
105
|
+
if (!node) {
|
|
106
|
+
return PLANK_LOADING;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const controls = (
|
|
110
|
+
<PlankControls
|
|
111
|
+
capabilities={capabilities}
|
|
112
|
+
layoutMode={layoutMode}
|
|
113
|
+
close={part === 'complementary' ? 'minify-end' : true}
|
|
114
|
+
onClick={onAdjust}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const navbarEnd =
|
|
119
|
+
part !== 'complementary' ? (
|
|
120
|
+
<Surface.Surface type={AppSurface.NavbarEnd} data={{ subject: node.data } satisfies AppSurface.NavbarEndData} />
|
|
121
|
+
) : undefined;
|
|
122
|
+
|
|
123
|
+
const sigilFooter = (
|
|
124
|
+
<Surface.Surface type={AppSurface.MenuFooter} data={{ subject: node.data } satisfies AppSurface.MenuFooterData} />
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// In fullscreen the toolbar is hidden so the content fills the viewport.
|
|
128
|
+
const headless = layoutMode === 'solo--fullscreen';
|
|
129
|
+
|
|
130
|
+
// Splitter.Root is always the outer element so <Plank> stays at the same tree position regardless of
|
|
131
|
+
// whether the companion is open. A root-element-type change (Plank ↔ Splitter.Root) would force React
|
|
132
|
+
// to unmount and remount the entire subtree on every companion toggle, resetting article state.
|
|
133
|
+
//
|
|
134
|
+
// `mode` drives the animated open/close: switching between 'split' and 'start' triggers the Splitter's
|
|
135
|
+
// built-in 250ms CSS transition. `companionMounted` trails `hasCompanion` by 250ms on close so the
|
|
136
|
+
// panel stays in the DOM long enough for the collapse animation to finish before being unmounted.
|
|
137
|
+
return (
|
|
138
|
+
<Splitter.Root
|
|
139
|
+
orientation={companionOrientation}
|
|
140
|
+
anchor='end'
|
|
141
|
+
mode={hasCompanion ? 'split' : 'start'}
|
|
142
|
+
resizable={hasCompanion}
|
|
143
|
+
size={companionSize}
|
|
144
|
+
minSize={20}
|
|
145
|
+
onSizeChange={onCompanionSizeChange}
|
|
146
|
+
classNames={classNames}
|
|
147
|
+
>
|
|
148
|
+
<Splitter.Panel position='start'>
|
|
149
|
+
<Plank
|
|
150
|
+
ref={rootRef}
|
|
151
|
+
node={node}
|
|
152
|
+
attendableId={id}
|
|
153
|
+
related={part === 'complementary'}
|
|
154
|
+
actions={sigilActions}
|
|
155
|
+
onAction={onAction}
|
|
156
|
+
popoverAnchorId={popoverAnchorId}
|
|
157
|
+
articleData={articleData}
|
|
158
|
+
controls={controls}
|
|
159
|
+
navbarEnd={navbarEnd}
|
|
160
|
+
sigilFooter={sigilFooter}
|
|
161
|
+
fallback={PlankErrorFallback}
|
|
162
|
+
placeholder={PLANK_LOADING}
|
|
163
|
+
headless={headless}
|
|
164
|
+
onKeyDown={handleKeyDown}
|
|
165
|
+
/>
|
|
166
|
+
</Splitter.Panel>
|
|
167
|
+
{companionMounted && (
|
|
168
|
+
<>
|
|
169
|
+
<Splitter.Handle />
|
|
170
|
+
<Splitter.Panel position='end'>
|
|
171
|
+
<Companion
|
|
172
|
+
companions={companions}
|
|
173
|
+
value={resolvedCompanionId}
|
|
174
|
+
onValueChange={onUpdateCompanion}
|
|
175
|
+
attendableId={id}
|
|
176
|
+
companionTo={node.data}
|
|
177
|
+
controls={companionControls}
|
|
178
|
+
/>
|
|
179
|
+
</Splitter.Panel>
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
</Splitter.Root>
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
DeckPlank.displayName = 'DeckPlank';
|