@dxos/plugin-stack 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.
Files changed (88) 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 +4 -2
  4. package/dist/lib/neutral/StackPlugin.mjs.map +3 -3
  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-CTWUHXK5.mjs → chunk-ZOIZTRMG.mjs} +3 -5
  12. package/dist/lib/neutral/chunk-ZOIZTRMG.mjs.map +7 -0
  13. package/dist/lib/neutral/components/index.mjs +188 -145
  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 +8 -6
  25. package/dist/lib/neutral/translations.mjs.map +3 -3
  26. package/dist/lib/neutral/types/index.mjs +4 -5
  27. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  28. package/dist/types/src/components/Stack/Stack.d.ts +67 -0
  29. package/dist/types/src/components/Stack/Stack.d.ts.map +1 -0
  30. package/dist/types/src/components/Stack/index.d.ts +2 -0
  31. package/dist/types/src/components/Stack/index.d.ts.map +1 -0
  32. package/dist/types/src/components/index.d.ts +1 -3
  33. package/dist/types/src/components/index.d.ts.map +1 -1
  34. package/dist/types/src/containers/StackArticle/StackArticle.d.ts +1 -2
  35. package/dist/types/src/containers/StackArticle/StackArticle.d.ts.map +1 -1
  36. package/dist/types/src/containers/StackArticle/StackArticle.stories.d.ts +7 -0
  37. package/dist/types/src/containers/StackArticle/StackArticle.stories.d.ts.map +1 -0
  38. package/dist/types/src/meta.d.ts +4 -0
  39. package/dist/types/src/meta.d.ts.map +1 -1
  40. package/dist/types/src/translations.d.ts +5 -16
  41. package/dist/types/src/translations.d.ts.map +1 -1
  42. package/dist/types/src/types/Stack.d.ts +6 -0
  43. package/dist/types/src/types/Stack.d.ts.map +1 -0
  44. package/dist/types/src/types/index.d.ts +1 -2
  45. package/dist/types/src/types/index.d.ts.map +1 -1
  46. package/dist/types/tsconfig.tsbuildinfo +1 -1
  47. package/dx.config.ts +2 -4
  48. package/package.json +33 -28
  49. package/src/StackPlugin.ts +2 -2
  50. package/src/components/Stack/Stack.tsx +319 -0
  51. package/src/components/{StackContext → Stack}/index.ts +1 -1
  52. package/src/components/index.ts +1 -3
  53. package/src/containers/StackArticle/StackArticle.stories.tsx +122 -0
  54. package/src/containers/StackArticle/StackArticle.tsx +180 -118
  55. package/src/translations.ts +5 -5
  56. package/src/types/Stack.ts +12 -0
  57. package/src/types/index.ts +1 -2
  58. package/dist/lib/neutral/StackArticle-SBM3FBJM.mjs +0 -124
  59. package/dist/lib/neutral/StackArticle-SBM3FBJM.mjs.map +0 -7
  60. package/dist/lib/neutral/chunk-CTWUHXK5.mjs.map +0 -7
  61. package/dist/lib/neutral/chunk-T4NSFSPH.mjs +0 -24
  62. package/dist/lib/neutral/chunk-T4NSFSPH.mjs.map +0 -7
  63. package/dist/types/src/components/StackContext/StackContext.d.ts +0 -11
  64. package/dist/types/src/components/StackContext/StackContext.d.ts.map +0 -1
  65. package/dist/types/src/components/StackContext/index.d.ts +0 -2
  66. package/dist/types/src/components/StackContext/index.d.ts.map +0 -1
  67. package/dist/types/src/components/StackSection/CaretDownUp.d.ts +0 -3
  68. package/dist/types/src/components/StackSection/CaretDownUp.d.ts.map +0 -1
  69. package/dist/types/src/components/StackSection/StackSection.d.ts +0 -5
  70. package/dist/types/src/components/StackSection/StackSection.d.ts.map +0 -1
  71. package/dist/types/src/components/StackSection/index.d.ts +0 -2
  72. package/dist/types/src/components/StackSection/index.d.ts.map +0 -1
  73. package/dist/types/src/components/StackSettings/StackSettings.d.ts +0 -6
  74. package/dist/types/src/components/StackSettings/StackSettings.d.ts.map +0 -1
  75. package/dist/types/src/components/StackSettings/index.d.ts +0 -2
  76. package/dist/types/src/components/StackSettings/index.d.ts.map +0 -1
  77. package/dist/types/src/types/stack.d.ts +0 -18
  78. package/dist/types/src/types/stack.d.ts.map +0 -1
  79. package/dist/types/src/types/types.d.ts +0 -44
  80. package/dist/types/src/types/types.d.ts.map +0 -1
  81. package/src/components/StackContext/StackContext.tsx +0 -25
  82. package/src/components/StackSection/CaretDownUp.tsx +0 -30
  83. package/src/components/StackSection/StackSection.tsx +0 -124
  84. package/src/components/StackSection/index.ts +0 -5
  85. package/src/components/StackSettings/StackSettings.tsx +0 -28
  86. package/src/components/StackSettings/index.ts +0 -5
  87. package/src/types/stack.ts +0 -26
  88. package/src/types/types.ts +0 -46
package/dx.config.ts CHANGED
@@ -8,12 +8,10 @@ import { trim } from '@dxos/util';
8
8
  export default Config2.make({
9
9
  plugin: {
10
10
  key: 'org.dxos.plugin.stack',
11
- // TODO(wittjosiah): Prevents "stacks" language from being exposed to users in settings panel.
12
- // Conside renaming this to the collection plugin and trying to factor more collections logic from the space plugin.
13
- name: 'Collections',
11
+ name: 'Stacks',
14
12
  author: 'DXOS',
15
13
  description: trim`
16
- StackPlugin renders any Collection object as a vertically scrollable stack of collapsible sections in the
14
+ The Stack plugin renders any Collection as a vertically scrollable stack of collapsible sections in the
17
15
  Composer article surface. Each section wraps an arbitrary ECHO object and delegates its content rendering
18
16
  to the section surface role, enabling any other plugin to contribute its own section renderer.
19
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-stack",
3
- "version": "0.9.1-main.c7dcc2e112",
3
+ "version": "0.10.0",
4
4
  "description": "DXOS Surface plugin for Stack entities",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -77,41 +77,46 @@
77
77
  "dependencies": {
78
78
  "@effect-atom/atom-react": "^0.5.0",
79
79
  "@radix-ui/react-collapsible": "1.1.3",
80
- "effect": "3.21.3",
81
- "@dxos/app-framework": "0.9.1-main.c7dcc2e112",
82
- "@dxos/app-toolkit": "0.9.1-main.c7dcc2e112",
83
- "@dxos/invariant": "0.9.1-main.c7dcc2e112",
84
- "@dxos/operation": "0.9.1-main.c7dcc2e112",
85
- "@dxos/keys": "0.9.1-main.c7dcc2e112",
86
- "@dxos/plugin-graph": "0.9.1-main.c7dcc2e112",
87
- "@dxos/plugin-deck": "0.9.1-main.c7dcc2e112",
88
- "@dxos/echo": "0.9.1-main.c7dcc2e112",
89
- "@dxos/react-ui-attention": "0.9.1-main.c7dcc2e112",
90
- "@dxos/plugin-space": "0.9.1-main.c7dcc2e112",
91
- "@dxos/react-ui-stack": "0.9.1-main.c7dcc2e112",
92
- "@dxos/react-ui-form": "0.9.1-main.c7dcc2e112",
93
- "@dxos/plugin-client": "0.9.1-main.c7dcc2e112",
94
- "@dxos/util": "0.9.1-main.c7dcc2e112"
80
+ "effect": "3.21.4",
81
+ "@dxos/app-framework": "0.10.0",
82
+ "@dxos/app-toolkit": "0.10.0",
83
+ "@dxos/echo": "0.10.0",
84
+ "@dxos/invariant": "0.10.0",
85
+ "@dxos/keys": "0.10.0",
86
+ "@dxos/operation": "0.10.0",
87
+ "@dxos/plugin-client": "0.10.0",
88
+ "@dxos/plugin-graph": "0.10.0",
89
+ "@dxos/plugin-deck": "0.10.0",
90
+ "@dxos/plugin-space": "0.10.0",
91
+ "@dxos/react-ui-attention": "0.10.0",
92
+ "@dxos/react-ui-menu": "0.10.0",
93
+ "@dxos/react-ui-form": "0.10.0",
94
+ "@dxos/util": "0.10.0",
95
+ "@dxos/react-ui-mosaic": "0.10.0"
95
96
  },
96
97
  "devDependencies": {
97
98
  "@babel/core": "^7.18.13",
98
- "@types/react": "~19.2.7",
99
+ "@types/react": "~19.2.17",
99
100
  "@types/react-dom": "~19.2.3",
100
- "react": "~19.2.3",
101
- "react-dom": "~19.2.3",
101
+ "react": "~19.2.7",
102
+ "react-dom": "~19.2.7",
102
103
  "vite": "^8.0.16",
103
- "@dxos/storybook-utils": "0.9.1-main.c7dcc2e112",
104
- "@dxos/react-ui": "0.9.1-main.c7dcc2e112",
105
- "@dxos/ui-theme": "0.9.1-main.c7dcc2e112",
106
- "@dxos/plugin-testing": "0.9.1-main.c7dcc2e112"
104
+ "@dxos/plugin-markdown": "0.10.0",
105
+ "@dxos/plugin-testing": "0.10.0",
106
+ "@dxos/plugin-sketch": "0.10.0",
107
+ "@dxos/react-client": "0.10.0",
108
+ "@dxos/random": "0.10.0",
109
+ "@dxos/storybook-utils": "0.10.0",
110
+ "@dxos/ui-theme": "0.10.0",
111
+ "@dxos/react-ui": "0.10.0"
107
112
  },
108
113
  "peerDependencies": {
109
114
  "@effect-atom/atom-react": "^0.5.0",
110
- "effect": "3.21.3",
111
- "react": "~19.2.3",
112
- "react-dom": "~19.2.3",
113
- "@dxos/react-ui": "0.9.1-main.c7dcc2e112",
114
- "@dxos/ui-theme": "0.9.1-main.c7dcc2e112"
115
+ "effect": "3.21.4",
116
+ "react": "~19.2.7",
117
+ "react-dom": "~19.2.7",
118
+ "@dxos/react-ui": "0.10.0",
119
+ "@dxos/ui-theme": "0.10.0"
115
120
  },
116
121
  "publishConfig": {
117
122
  "access": "public"
@@ -8,13 +8,13 @@ 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({
@@ -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
+ };