@dxos/plugin-stack 0.9.0 → 0.9.1-staging.ee54ba693a

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.
Files changed (95) hide show
  1. package/dist/lib/neutral/StackArticle-DDKRSUE7.mjs +199 -0
  2. package/dist/lib/neutral/StackArticle-DDKRSUE7.mjs.map +7 -0
  3. package/dist/lib/neutral/StackPlugin.mjs +6 -4
  4. package/dist/lib/neutral/StackPlugin.mjs.map +4 -4
  5. package/dist/lib/neutral/capabilities/index.mjs +3 -1
  6. package/dist/lib/neutral/capabilities/index.mjs.map +1 -1
  7. package/dist/lib/neutral/chunk-FDNICBZZ.mjs +18 -0
  8. package/dist/lib/neutral/chunk-FDNICBZZ.mjs.map +7 -0
  9. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  10. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  11. package/dist/lib/neutral/chunk-ZOIZTRMG.mjs +44 -0
  12. package/dist/lib/neutral/chunk-ZOIZTRMG.mjs.map +7 -0
  13. package/dist/lib/neutral/components/index.mjs +191 -147
  14. package/dist/lib/neutral/components/index.mjs.map +4 -4
  15. package/dist/lib/neutral/containers/index.mjs +3 -1
  16. package/dist/lib/neutral/containers/index.mjs.map +1 -1
  17. package/dist/lib/neutral/index.mjs +5 -6
  18. package/dist/lib/neutral/meta.json +1 -1
  19. package/dist/lib/neutral/meta.mjs +2 -1
  20. package/dist/lib/neutral/plugin.mjs +2 -1
  21. package/dist/lib/neutral/plugin.mjs.map +1 -1
  22. package/dist/lib/neutral/{react-surface-MD3CVY2N.mjs → react-surface-IL4CW7ND.mjs} +3 -1
  23. package/dist/lib/neutral/{react-surface-MD3CVY2N.mjs.map → react-surface-IL4CW7ND.mjs.map} +1 -1
  24. package/dist/lib/neutral/translations.mjs +9 -7
  25. package/dist/lib/neutral/translations.mjs.map +3 -3
  26. package/dist/lib/neutral/types/index.mjs +4 -5
  27. package/dist/types/dx.config.d.ts +28 -0
  28. package/dist/types/dx.config.d.ts.map +1 -0
  29. package/dist/types/src/capabilities/index.d.ts +1 -1
  30. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/react-surface.d.ts +2 -2
  32. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  33. package/dist/types/src/components/Stack/Stack.d.ts +67 -0
  34. package/dist/types/src/components/Stack/Stack.d.ts.map +1 -0
  35. package/dist/types/src/components/Stack/index.d.ts +2 -0
  36. package/dist/types/src/components/Stack/index.d.ts.map +1 -0
  37. package/dist/types/src/components/index.d.ts +1 -3
  38. package/dist/types/src/components/index.d.ts.map +1 -1
  39. package/dist/types/src/containers/StackArticle/StackArticle.d.ts +1 -2
  40. package/dist/types/src/containers/StackArticle/StackArticle.d.ts.map +1 -1
  41. package/dist/types/src/containers/StackArticle/StackArticle.stories.d.ts +7 -0
  42. package/dist/types/src/containers/StackArticle/StackArticle.stories.d.ts.map +1 -0
  43. package/dist/types/src/meta.d.ts +32 -2
  44. package/dist/types/src/meta.d.ts.map +1 -1
  45. package/dist/types/src/translations.d.ts +5 -16
  46. package/dist/types/src/translations.d.ts.map +1 -1
  47. package/dist/types/src/types/Stack.d.ts +6 -0
  48. package/dist/types/src/types/Stack.d.ts.map +1 -0
  49. package/dist/types/src/types/index.d.ts +1 -2
  50. package/dist/types/src/types/index.d.ts.map +1 -1
  51. package/dist/types/tsconfig.tsbuildinfo +1 -1
  52. package/dx.config.ts +32 -0
  53. package/package.json +34 -28
  54. package/src/StackPlugin.ts +3 -3
  55. package/src/components/Stack/Stack.tsx +319 -0
  56. package/src/components/{StackContext → Stack}/index.ts +1 -1
  57. package/src/components/index.ts +1 -3
  58. package/src/containers/StackArticle/StackArticle.stories.tsx +122 -0
  59. package/src/containers/StackArticle/StackArticle.tsx +181 -119
  60. package/src/meta.ts +3 -26
  61. package/src/translations.ts +6 -6
  62. package/src/types/Stack.ts +12 -0
  63. package/src/types/index.ts +1 -2
  64. package/dist/lib/neutral/StackArticle-R7IS6TYR.mjs +0 -124
  65. package/dist/lib/neutral/StackArticle-R7IS6TYR.mjs.map +0 -7
  66. package/dist/lib/neutral/chunk-RGJSGQGF.mjs +0 -36
  67. package/dist/lib/neutral/chunk-RGJSGQGF.mjs.map +0 -7
  68. package/dist/lib/neutral/chunk-T4NSFSPH.mjs +0 -24
  69. package/dist/lib/neutral/chunk-T4NSFSPH.mjs.map +0 -7
  70. package/dist/types/src/components/StackContext/StackContext.d.ts +0 -11
  71. package/dist/types/src/components/StackContext/StackContext.d.ts.map +0 -1
  72. package/dist/types/src/components/StackContext/index.d.ts +0 -2
  73. package/dist/types/src/components/StackContext/index.d.ts.map +0 -1
  74. package/dist/types/src/components/StackSection/CaretDownUp.d.ts +0 -3
  75. package/dist/types/src/components/StackSection/CaretDownUp.d.ts.map +0 -1
  76. package/dist/types/src/components/StackSection/StackSection.d.ts +0 -5
  77. package/dist/types/src/components/StackSection/StackSection.d.ts.map +0 -1
  78. package/dist/types/src/components/StackSection/index.d.ts +0 -2
  79. package/dist/types/src/components/StackSection/index.d.ts.map +0 -1
  80. package/dist/types/src/components/StackSettings/StackSettings.d.ts +0 -6
  81. package/dist/types/src/components/StackSettings/StackSettings.d.ts.map +0 -1
  82. package/dist/types/src/components/StackSettings/index.d.ts +0 -2
  83. package/dist/types/src/components/StackSettings/index.d.ts.map +0 -1
  84. package/dist/types/src/types/stack.d.ts +0 -18
  85. package/dist/types/src/types/stack.d.ts.map +0 -1
  86. package/dist/types/src/types/types.d.ts +0 -44
  87. package/dist/types/src/types/types.d.ts.map +0 -1
  88. package/src/components/StackContext/StackContext.tsx +0 -25
  89. package/src/components/StackSection/CaretDownUp.tsx +0 -30
  90. package/src/components/StackSection/StackSection.tsx +0 -123
  91. package/src/components/StackSection/index.ts +0 -5
  92. package/src/components/StackSettings/StackSettings.tsx +0 -28
  93. package/src/components/StackSettings/index.ts +0 -5
  94. package/src/types/stack.ts +0 -26
  95. package/src/types/types.ts +0 -46
package/dx.config.ts ADDED
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Config2 } from '@dxos/app-framework/config';
6
+ import { trim } from '@dxos/util';
7
+
8
+ export default Config2.make({
9
+ plugin: {
10
+ key: 'org.dxos.plugin.stack',
11
+ name: 'Stacks',
12
+ author: 'DXOS',
13
+ description: trim`
14
+ The Stack plugin renders any Collection as a vertically scrollable stack of collapsible sections in the
15
+ Composer article surface. Each section wraps an arbitrary ECHO object and delegates its content rendering
16
+ to the section surface role, enabling any other plugin to contribute its own section renderer.
17
+
18
+ Sections can be individually collapsed to show only their title, expanded to reveal full content, navigated
19
+ to directly, or removed from the collection. New sections can be inserted before or after any existing
20
+ section via a per-section dropdown menu, or appended via the toolbar Add button.
21
+
22
+ The plugin registers StackViewType with the ECHO schema module so that per-section view metadata
23
+ (height overrides, custom display data) can be stored and replicated alongside the collection.
24
+
25
+ A settings panel exposes a separation toggle that controls whether visual dividers are rendered between
26
+ sections.
27
+ `,
28
+ source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-stack',
29
+ icon: { key: 'ph--stack-simple--regular' },
30
+ spec: 'PLUGIN.mdl',
31
+ },
32
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-stack",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-staging.ee54ba693a",
4
4
  "description": "DXOS Surface plugin for Stack entities",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -70,47 +70,53 @@
70
70
  "types": "dist/types/src/index.d.ts",
71
71
  "files": [
72
72
  "dist",
73
+ "dx.config.ts",
73
74
  "src",
74
75
  "PLUGIN.mdl"
75
76
  ],
76
77
  "dependencies": {
77
78
  "@effect-atom/atom-react": "^0.5.0",
78
79
  "@radix-ui/react-collapsible": "1.1.3",
79
- "effect": "3.21.3",
80
- "@dxos/app-framework": "0.9.0",
81
- "@dxos/invariant": "0.9.0",
82
- "@dxos/app-toolkit": "0.9.0",
83
- "@dxos/echo": "0.9.0",
84
- "@dxos/keys": "0.9.0",
85
- "@dxos/operation": "0.9.0",
86
- "@dxos/plugin-client": "0.9.0",
87
- "@dxos/plugin-deck": "0.9.0",
88
- "@dxos/react-ui-attention": "0.9.0",
89
- "@dxos/react-ui-form": "0.9.0",
90
- "@dxos/plugin-graph": "0.9.0",
91
- "@dxos/react-ui-stack": "0.9.0",
92
- "@dxos/util": "0.9.0",
93
- "@dxos/plugin-space": "0.9.0"
80
+ "effect": "3.21.4",
81
+ "@dxos/app-framework": "0.9.1-staging.ee54ba693a",
82
+ "@dxos/echo": "0.9.1-staging.ee54ba693a",
83
+ "@dxos/app-toolkit": "0.9.1-staging.ee54ba693a",
84
+ "@dxos/keys": "0.9.1-staging.ee54ba693a",
85
+ "@dxos/invariant": "0.9.1-staging.ee54ba693a",
86
+ "@dxos/operation": "0.9.1-staging.ee54ba693a",
87
+ "@dxos/plugin-client": "0.9.1-staging.ee54ba693a",
88
+ "@dxos/plugin-deck": "0.9.1-staging.ee54ba693a",
89
+ "@dxos/plugin-graph": "0.9.1-staging.ee54ba693a",
90
+ "@dxos/plugin-space": "0.9.1-staging.ee54ba693a",
91
+ "@dxos/react-ui-attention": "0.9.1-staging.ee54ba693a",
92
+ "@dxos/react-ui-form": "0.9.1-staging.ee54ba693a",
93
+ "@dxos/util": "0.9.1-staging.ee54ba693a",
94
+ "@dxos/react-ui-mosaic": "0.9.1-staging.ee54ba693a",
95
+ "@dxos/react-ui-menu": "0.9.1-staging.ee54ba693a"
94
96
  },
95
97
  "devDependencies": {
96
98
  "@babel/core": "^7.18.13",
97
- "@types/react": "~19.2.7",
99
+ "@types/react": "~19.2.17",
98
100
  "@types/react-dom": "~19.2.3",
99
- "react": "~19.2.3",
100
- "react-dom": "~19.2.3",
101
+ "react": "~19.2.7",
102
+ "react-dom": "~19.2.7",
101
103
  "vite": "^8.0.16",
102
- "@dxos/plugin-testing": "0.9.0",
103
- "@dxos/react-ui": "0.9.0",
104
- "@dxos/storybook-utils": "0.9.0",
105
- "@dxos/ui-theme": "0.9.0"
104
+ "@dxos/plugin-sketch": "0.9.1-staging.ee54ba693a",
105
+ "@dxos/plugin-markdown": "0.9.1-staging.ee54ba693a",
106
+ "@dxos/react-client": "0.9.1-staging.ee54ba693a",
107
+ "@dxos/plugin-testing": "0.9.1-staging.ee54ba693a",
108
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
109
+ "@dxos/storybook-utils": "0.9.1-staging.ee54ba693a",
110
+ "@dxos/random": "0.9.1-staging.ee54ba693a",
111
+ "@dxos/react-ui": "0.9.1-staging.ee54ba693a"
106
112
  },
107
113
  "peerDependencies": {
108
114
  "@effect-atom/atom-react": "^0.5.0",
109
- "effect": "3.21.3",
110
- "react": "~19.2.3",
111
- "react-dom": "~19.2.3",
112
- "@dxos/react-ui": "0.9.0",
113
- "@dxos/ui-theme": "0.9.0"
115
+ "effect": "3.21.4",
116
+ "react": "~19.2.7",
117
+ "react-dom": "~19.2.7",
118
+ "@dxos/react-ui": "0.9.1-staging.ee54ba693a",
119
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
114
120
  },
115
121
  "publishConfig": {
116
122
  "access": "public"
@@ -8,17 +8,17 @@ import { AppPlugin } from '@dxos/app-toolkit';
8
8
  import { ReactSurface } from '#capabilities';
9
9
  import { meta } from '#meta';
10
10
  import { translations } from '#translations';
11
- import { StackViewType } from '#types';
11
+ import { Stack } from '#types';
12
12
 
13
13
  // eslint-disable-next-line import/no-relative-packages
14
14
  import pluginSpec from '../PLUGIN.mdl?raw';
15
15
 
16
16
  export const StackPlugin = Plugin.define(meta).pipe(
17
- AppPlugin.addSchemaModule({ schema: [StackViewType] }),
17
+ AppPlugin.addSchemaModule({ schema: [Stack.Stack] }),
18
18
  AppPlugin.addSurfaceModule({ activate: ReactSurface }),
19
19
  AppPlugin.addTranslationsModule({ translations }),
20
20
  AppPlugin.addPluginAssetModule({
21
- asset: { pluginId: meta.id, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },
21
+ asset: { pluginId: meta.profile.key, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },
22
22
  }),
23
23
  Plugin.make,
24
24
  );
@@ -0,0 +1,319 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, {
6
+ type ComponentPropsWithoutRef,
7
+ type ForwardedRef,
8
+ type PropsWithChildren,
9
+ createContext,
10
+ forwardRef,
11
+ useCallback,
12
+ useContext,
13
+ useMemo,
14
+ useState,
15
+ } from 'react';
16
+
17
+ import { Surface } from '@dxos/app-framework/ui';
18
+ import { Paths } from '@dxos/app-toolkit';
19
+ import { AppSurface, AttentionSigilButton } from '@dxos/app-toolkit/ui';
20
+ import { Obj } from '@dxos/echo';
21
+ import {
22
+ DropdownMenu,
23
+ Icon,
24
+ ScrollArea,
25
+ ScrollAreaRootProps,
26
+ type ThemedClassName,
27
+ useTranslation,
28
+ } from '@dxos/react-ui';
29
+ import { useAttentionAttributes } from '@dxos/react-ui-attention';
30
+ import { Mosaic, type MosaicEventHandler, type MosaicTileProps } from '@dxos/react-ui-mosaic';
31
+
32
+ import { meta } from '#meta';
33
+
34
+ //
35
+ // Types
36
+ //
37
+
38
+ export type StackSectionItem = {
39
+ id: string;
40
+ object: Obj.Unknown;
41
+ };
42
+
43
+ //
44
+ // Context
45
+ //
46
+
47
+ /** Section-level callbacks consumed by stack sections. */
48
+ export type StackContextValue = {
49
+ attendableId: string;
50
+ /** Ids of sections that are currently collapsed. */
51
+ collapsed: ReadonlySet<string>;
52
+ onCollapse: (id: string, collapsed: boolean) => void;
53
+ /** Add a new section immediately after the given section. */
54
+ onAdd: (id: string) => void;
55
+ onMoveUp: (id: string) => void;
56
+ onMoveDown: (id: string) => void;
57
+ onDelete: (id: string) => void;
58
+ };
59
+
60
+ type StackContextType = StackContextValue & {
61
+ /** Container id used to scope Mosaic drag/drop. */
62
+ id: string;
63
+ eventHandler: MosaicEventHandler;
64
+ /** Scroll viewport element; threaded to the Mosaic container for autoscroll. */
65
+ viewport: HTMLElement | null;
66
+ setViewport: (element: HTMLElement | null) => void;
67
+ };
68
+
69
+ const StackContext = createContext<StackContextType | undefined>(undefined);
70
+
71
+ const useStackContext = (consumer: string): StackContextType => {
72
+ const context = useContext(StackContext);
73
+ if (!context) {
74
+ throw new Error(`\`${consumer}\` must be used within \`Stack.Root\`.`);
75
+ }
76
+ return context;
77
+ };
78
+
79
+ /** Section-level callbacks consumed by stack sections. */
80
+ export const useStack = (): StackContextValue => useStackContext('useStack');
81
+
82
+ //
83
+ // Root
84
+ //
85
+
86
+ type StackRootProps = PropsWithChildren<
87
+ StackContextValue & {
88
+ /** Container id used to scope Mosaic drag/drop. */
89
+ id: string;
90
+ /** Drag/drop event handler; defaults to a read-only (no-drop) handler. */
91
+ eventHandler?: MosaicEventHandler;
92
+ }
93
+ >;
94
+
95
+ const StackRoot = ({
96
+ id,
97
+ eventHandler,
98
+ children,
99
+ attendableId,
100
+ collapsed,
101
+ onCollapse,
102
+ onAdd,
103
+ onMoveUp,
104
+ onMoveDown,
105
+ onDelete,
106
+ }: StackRootProps) => {
107
+ const [viewport, setViewport] = useState<HTMLElement | null>(null);
108
+ const value = useMemo<StackContextType>(
109
+ () => ({
110
+ id,
111
+ eventHandler: eventHandler ?? { id, canDrop: () => false },
112
+ viewport,
113
+ setViewport,
114
+ attendableId,
115
+ collapsed,
116
+ onCollapse,
117
+ onAdd,
118
+ onMoveUp,
119
+ onMoveDown,
120
+ onDelete,
121
+ }),
122
+ [id, eventHandler, viewport, attendableId, collapsed, onCollapse, onAdd, onMoveUp, onMoveDown, onDelete],
123
+ );
124
+
125
+ return (
126
+ <StackContext.Provider value={value}>
127
+ <Mosaic.Root>{children}</Mosaic.Root>
128
+ </StackContext.Provider>
129
+ );
130
+ };
131
+
132
+ StackRoot.displayName = 'Stack.Root';
133
+
134
+ //
135
+ // Content
136
+ //
137
+
138
+ type StackContentProps = ThemedClassName<
139
+ ComponentPropsWithoutRef<'div'> & Pick<ScrollAreaRootProps, 'centered' | 'thin' | 'padding'>
140
+ >;
141
+
142
+ const StackContent = forwardRef<HTMLDivElement, StackContentProps>(({ children, ...props }, forwardedRef) => {
143
+ const { eventHandler, viewport } = useStackContext('Stack.Content');
144
+ return (
145
+ <Mosaic.Container asChild orientation='vertical' autoScroll={viewport} eventHandler={eventHandler}>
146
+ <ScrollArea.Root {...props} orientation='vertical' ref={forwardedRef}>
147
+ {children}
148
+ </ScrollArea.Root>
149
+ </Mosaic.Container>
150
+ );
151
+ });
152
+
153
+ StackContent.displayName = 'Stack.Content';
154
+
155
+ //
156
+ // Viewport
157
+ //
158
+
159
+ type StackViewportProps = ThemedClassName<PropsWithChildren>;
160
+
161
+ const StackViewport = forwardRef<HTMLDivElement, StackViewportProps>(
162
+ ({ classNames, children }, forwardedRef: ForwardedRef<HTMLDivElement>) => {
163
+ const { setViewport } = useStackContext('Stack.Viewport');
164
+ // Capture the viewport element for autoscroll while still honouring a forwarded ref.
165
+ const setRefs = useCallback(
166
+ (element: HTMLDivElement | null) => {
167
+ setViewport(element);
168
+ if (typeof forwardedRef === 'function') {
169
+ forwardedRef(element);
170
+ } else if (forwardedRef) {
171
+ forwardedRef.current = element;
172
+ }
173
+ },
174
+ [setViewport, forwardedRef],
175
+ );
176
+
177
+ return (
178
+ <ScrollArea.Viewport classNames={classNames} ref={setRefs}>
179
+ {children}
180
+ </ScrollArea.Viewport>
181
+ );
182
+ },
183
+ );
184
+
185
+ StackViewport.displayName = 'Stack.Viewport';
186
+
187
+ //
188
+ // Section
189
+ //
190
+
191
+ type StackSectionProps = MosaicTileProps<StackSectionItem>;
192
+
193
+ /**
194
+ * A stack section rendered as a Mosaic tile: a 40px left rail (drag handle + section menu) beside the
195
+ * main content area, which renders the object's content surface (or its title when collapsed). The
196
+ * rail sticks to the top of the scroll viewport while the section is in view.
197
+ */
198
+ // TODO(burdon): All sections are intrinsic (content-sized) for now. Extrinsic content (e.g. a sketch
199
+ // with no intrinsic height) should later get a Mosaic-native resizable height affordance.
200
+
201
+ // Inline grip glyph for the drag preview. The native drag image does not rasterize external SVG sprite
202
+ // `<use>` icons (e.g. `@dxos/react-ui` `Icon`), so the preview uses plain inline SVG circles instead.
203
+ const DragHandleGlyph = () => (
204
+ <svg width={10} height={16} viewBox='0 0 10 16' aria-hidden className='shrink-0 text-description'>
205
+ {[3, 8, 13].flatMap((cy) =>
206
+ [2.5, 7.5].map((cx) => <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r={1.1} fill='currentColor' />),
207
+ )}
208
+ </svg>
209
+ );
210
+
211
+ const StackSection = ({ data, ...tileProps }: StackSectionProps) => {
212
+ const { id, object } = data;
213
+ const { t } = useTranslation(meta.profile.key);
214
+ const { attendableId: parentAttendableId, collapsed, onAdd, onMoveUp, onMoveDown, onCollapse, onDelete } = useStack();
215
+ const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
216
+ const attendableId = Paths.getCollectionObjectPath(parentAttendableId, object.id);
217
+ const attentionAttrs = useAttentionAttributes(attendableId);
218
+ const surfaceData = useMemo(() => ({ attendableId, subject: object }), [object, attendableId]);
219
+ const isCollapsed = collapsed.has(id);
220
+ const icon = Obj.getIcon(object)?.icon ?? 'ph--circle-dashed--regular';
221
+ const title = Obj.getLabel(object, { fallback: 'typename' }) ?? t('untitled-section.title');
222
+
223
+ const rail = (
224
+ <div className='grid grid-rows-[min-content_1fr]'>
225
+ <div className='p-1 bg-toolbar-surface'>
226
+ <DropdownMenu.Root open={optionsMenuOpen} onOpenChange={setOptionsMenuOpen}>
227
+ <DropdownMenu.Trigger asChild>
228
+ <AttentionSigilButton size='md' attendableId={attendableId}>
229
+ <Icon icon={icon} classNames='transition-opacity' />
230
+ </AttentionSigilButton>
231
+ </DropdownMenu.Trigger>
232
+ <DropdownMenu.Portal>
233
+ <DropdownMenu.Content>
234
+ <DropdownMenu.Viewport>
235
+ {isCollapsed ? (
236
+ <DropdownMenu.Item onClick={() => onCollapse(id, false)} data-testid='section.expand'>
237
+ <Icon icon='ph--arrows-out-line-vertical--regular' />
238
+ <span className='ms-2 grow'>{t('expand.label')}</span>
239
+ </DropdownMenu.Item>
240
+ ) : (
241
+ <DropdownMenu.Item onClick={() => onCollapse(id, true)} data-testid='section.collapse'>
242
+ <Icon icon='ph--arrows-in-line-vertical--regular' />
243
+ <span className='ms-2 grow'>{t('collapse.label')}</span>
244
+ </DropdownMenu.Item>
245
+ )}
246
+ <DropdownMenu.Separator />
247
+ <DropdownMenu.Item onClick={() => onAdd(id)} data-testid='section.add'>
248
+ <Icon icon='ph--plus--regular' />
249
+ <span className='ms-2 grow'>{t('add-section.label')}</span>
250
+ </DropdownMenu.Item>
251
+ <DropdownMenu.Item onClick={() => onMoveUp(id)} data-testid='section.move-up'>
252
+ <Icon icon='ph--arrow-line-up--regular' />
253
+ <span className='ms-2 grow'>{t('move-up.label')}</span>
254
+ </DropdownMenu.Item>
255
+ <DropdownMenu.Item onClick={() => onMoveDown(id)} data-testid='section.move-down'>
256
+ <Icon icon='ph--arrow-line-down--regular' />
257
+ <span className='ms-2 grow'>{t('move-down.label')}</span>
258
+ </DropdownMenu.Item>
259
+ <DropdownMenu.Separator />
260
+ <DropdownMenu.Item onClick={() => onDelete(id)} data-testid='section.remove'>
261
+ <Icon icon='ph--trash--regular' />
262
+ <span className='ms-2 grow'>{t('remove-section.label')}</span>
263
+ </DropdownMenu.Item>
264
+ </DropdownMenu.Viewport>
265
+ <DropdownMenu.Arrow />
266
+ </DropdownMenu.Content>
267
+ </DropdownMenu.Portal>
268
+ </DropdownMenu.Root>
269
+ </div>
270
+ <div className='p-1'>
271
+ {/* Inline glyph (not a sprite `Icon`) so the handle stays visible in the tile's native drag image. */}
272
+ <Mosaic.DragHandle
273
+ label={t('drag-handle.label')}
274
+ classNames='p-1 min-h-0 w-(--dx-rail-item) h-(--dx-rail-item)'
275
+ testId='section.drag-handle'
276
+ >
277
+ <DragHandleGlyph />
278
+ </Mosaic.DragHandle>
279
+ </div>
280
+ </div>
281
+ );
282
+
283
+ return (
284
+ <Mosaic.Tile
285
+ {...tileProps}
286
+ data={data}
287
+ classNames='grid grid-cols-[var(--dx-rail-action)_1fr] dx-attention-surface border border-subdued-separator'
288
+ >
289
+ <div className='border-e border-subdued-separator'>
290
+ <div className='sticky top-0 flex flex-col items-center'>{rail}</div>
291
+ </div>
292
+ <div {...attentionAttrs} className='min-w-0'>
293
+ <span className='sr-only'>{title}</span>
294
+ {isCollapsed ? (
295
+ <div className='h-(--dx-toolbar-size) flex p-1'>
296
+ <h2 className='flex items-center font-medium'>{title}</h2>
297
+ </div>
298
+ ) : (
299
+ <Surface.Surface type={AppSurface.Section} data={surfaceData} limit={1} />
300
+ )}
301
+ </div>
302
+ </Mosaic.Tile>
303
+ );
304
+ };
305
+
306
+ StackSection.displayName = 'Stack.Section';
307
+
308
+ //
309
+ // Stack
310
+ //
311
+
312
+ export const Stack = {
313
+ Root: StackRoot,
314
+ Content: StackContent,
315
+ Viewport: StackViewport,
316
+ Section: StackSection,
317
+ };
318
+
319
+ export type { StackContentProps, StackRootProps, StackSectionProps, StackViewportProps };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './StackContext';
5
+ export * from './Stack';
@@ -2,6 +2,4 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './StackContext';
6
- export * from './StackSection';
7
- export * from './StackSettings';
5
+ export * from './Stack';
@@ -0,0 +1,122 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Effect from 'effect/Effect';
7
+ import React from 'react';
8
+
9
+ import { Capability } from '@dxos/app-framework';
10
+ import { withPluginManager } from '@dxos/app-framework/testing';
11
+ import { AppActivationEvents, AppCapabilities } from '@dxos/app-toolkit';
12
+ import { AppSurface } from '@dxos/app-toolkit/ui';
13
+ import { Collection, Filter, Ref } from '@dxos/echo';
14
+ import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
15
+ import { Markdown, MarkdownEvents } from '@dxos/plugin-markdown';
16
+ import { MarkdownPlugin } from '@dxos/plugin-markdown/testing';
17
+ import { Sketch } from '@dxos/plugin-sketch';
18
+ import { SketchPlugin } from '@dxos/plugin-sketch/plugin';
19
+ import { SketchBuilder } from '@dxos/plugin-sketch/testing';
20
+ import { SpacePlugin } from '@dxos/plugin-space/testing';
21
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
22
+ import { random } from '@dxos/random';
23
+ import { useClient } from '@dxos/react-client';
24
+ import { useQuery } from '@dxos/react-client/echo';
25
+ import { Loading, withLayout } from '@dxos/react-ui/testing';
26
+
27
+ import { translations } from '#translations';
28
+
29
+ import { StackArticle, type StackArticleProps } from './StackArticle';
30
+
31
+ // A minimal sketch (tldraw `tldraw.com/2`) snapshot, used as a test image.
32
+ const SKETCH_CONTENT = new SketchBuilder()
33
+ .rectangle({ id: 'rect', x: 0, y: 0, text: 'DXOS', color: 'blue', fill: 'solid', size: 'l' })
34
+ .ellipse({ id: 'echo', x: 320, y: 0, text: 'ECHO', color: 'green' })
35
+ .arrow({ from: 'rect', to: 'echo' })
36
+ .build();
37
+
38
+ const DefaultStory = (args: StackArticleProps) => {
39
+ const client = useClient();
40
+ const [space] = client.spaces.get();
41
+ const [collection] = useQuery(space?.db, Filter.type(Collection.Collection));
42
+ if (!collection) {
43
+ return <Loading />;
44
+ }
45
+
46
+ return <StackArticle {...args} subject={collection} attendableId='test' />;
47
+ };
48
+
49
+ const meta: Meta<typeof StackArticle> = {
50
+ title: 'plugins/plugin-stack/containers/StackArticle',
51
+ component: StackArticle,
52
+ render: DefaultStory,
53
+ decorators: [
54
+ withLayout({ layout: 'fullscreen' }),
55
+ withPluginManager({
56
+ setupEvents: [AppActivationEvents.SetupSettings, MarkdownEvents.SetupExtensions],
57
+ capabilities: [Capability.contributes(AppCapabilities.Translations, translations)],
58
+ plugins: [
59
+ ...corePlugins(),
60
+ ClientPlugin({
61
+ types: [Collection.Collection, Markdown.Document, Sketch.Sketch, Sketch.Canvas],
62
+ onClientInitialized: ({ client }) =>
63
+ Effect.gen(function* () {
64
+ const { personalSpace: space } = yield* initializeIdentity(client);
65
+
66
+ const documents = Array.from({ length: 5 }).map(() =>
67
+ Ref.make(
68
+ space.db.add(
69
+ Markdown.make({
70
+ name: random.lorem.sentence(5),
71
+ content:
72
+ [
73
+ `# ${random.lorem.sentence(5)}`,
74
+ random.lorem.paragraph(),
75
+ random.lorem.paragraph(),
76
+ random.lorem.paragraph(),
77
+ ].join('\n\n') + '\n',
78
+ }),
79
+ ),
80
+ ),
81
+ );
82
+
83
+ const sketches = [
84
+ Ref.make(
85
+ space.db.add(
86
+ Sketch.make({
87
+ name: random.lorem.sentence(2),
88
+ canvas: {
89
+ content: SKETCH_CONTENT,
90
+ },
91
+ }),
92
+ ),
93
+ ),
94
+ ];
95
+
96
+ const insertAt = 1;
97
+ space.db.add(
98
+ Collection.make({
99
+ name: random.lorem.sentence(5),
100
+ objects: [...documents.slice(0, insertAt), ...sketches, ...documents.slice(insertAt + 1)],
101
+ }),
102
+ );
103
+ }),
104
+ }),
105
+ MarkdownPlugin(),
106
+ SketchPlugin(),
107
+ SpacePlugin({}),
108
+ StorybookPlugin({}),
109
+ ],
110
+ }),
111
+ ],
112
+ };
113
+
114
+ export default meta;
115
+
116
+ type Story = StoryObj<typeof meta>;
117
+
118
+ export const Default: Story = {
119
+ args: {
120
+ role: AppSurface.Article.role,
121
+ },
122
+ };