@sqlrooms/layout 0.28.0-rc.0 → 0.28.1-rc.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/README.md CHANGED
@@ -1,10 +1,6 @@
1
- This package is part of the SQLRooms framework and provides flexible layout components for building complex, resizable, and draggable interfaces.
1
+ Layout slice and mosaic utilities for SQLRooms panel-based UIs.
2
2
 
3
- ## Overview
4
-
5
- The `@sqlrooms/layout` package offers a set of components and utilities for creating dynamic, multi-pane layouts in SQLRooms applications. It's primarily built around the [react-mosaic](https://nomcopter.github.io/react-mosaic/) library, which provides a powerful windowing system similar to the one found in advanced IDEs.
6
-
7
- > **Note:** This package uses [react-mosaic](https://nomcopter.github.io/react-mosaic/) which should not be confused with [uwdata/mosaic](https://github.com/uwdata/mosaic) used in the [`@sqlrooms/mosaic`](/api/mosaic/) package for data visualization purposes.
3
+ This package uses `react-mosaic` to compose resizable panel layouts.
8
4
 
9
5
  ## Installation
10
6
 
@@ -12,78 +8,91 @@ The `@sqlrooms/layout` package offers a set of components and utilities for crea
12
8
  npm install @sqlrooms/layout
13
9
  ```
14
10
 
15
- ## Mosaic Tree Structure
11
+ ## Main exports
16
12
 
17
- The mosaic layout is defined by a tree structure where each node is either a string (representing a panel ID) or an object with `direction`, `first`, `second`, and optional `splitPercentage` properties. Here's an example of how a mosaic tree might look:
13
+ - `createLayoutSlice()`, `useStoreWithLayout()`
14
+ - `MosaicLayout` component
15
+ - mosaic helpers:
16
+ - `makeMosaicStack`
17
+ - `visitMosaicLeafNodes`
18
+ - `getVisibleMosaicLayoutPanels`
19
+ - `findMosaicNodePathByKey`
20
+ - `removeMosaicNodeByKey`
21
+ - layout config schemas/types re-exported from `@sqlrooms/layout-config`
22
+
23
+ ## Store usage
18
24
 
19
25
  ```tsx
20
- // Simple two-panel layout with 30/70 split
21
- const simpleMosaicTree = {
22
- direction: 'row',
23
- first: 'data-sources', // Left panel (30% width)
24
- second: 'main', // Right panel (70% width)
25
- splitPercentage: 30,
26
- };
27
-
28
- // More complex nested layout
29
- const complexMosaicTree = {
30
- direction: 'row',
31
- first: 'data-sources', // Left panel
32
- second: {
33
- // Right side contains a nested layout
34
- direction: 'column',
35
- first: 'main', // Top panel
36
- second: {
37
- // Bottom contains another nested layout
38
- direction: 'row',
39
- first: 'sql-editor',
40
- second: 'visualization',
41
- splitPercentage: 50,
42
- },
43
- splitPercentage: 60,
44
- },
45
- splitPercentage: 20,
46
- };
26
+ import {
27
+ LayoutSliceState,
28
+ LayoutTypes,
29
+ MAIN_VIEW,
30
+ createLayoutSlice,
31
+ } from '@sqlrooms/layout';
32
+ import {
33
+ BaseRoomStoreState,
34
+ createBaseRoomSlice,
35
+ createRoomStore,
36
+ } from '@sqlrooms/room-store';
37
+
38
+ function DataPanel() {
39
+ return <div>Data</div>;
40
+ }
41
+
42
+ function MainPanel() {
43
+ return <div>Main</div>;
44
+ }
45
+
46
+ type State = BaseRoomStoreState & LayoutSliceState;
47
+
48
+ export const {roomStore, useRoomStore} = createRoomStore<State>(
49
+ (set, get, store) => ({
50
+ ...createBaseRoomSlice()(set, get, store),
51
+ ...createLayoutSlice({
52
+ config: {
53
+ type: LayoutTypes.enum.mosaic,
54
+ nodes: {
55
+ direction: 'row',
56
+ first: 'data',
57
+ second: MAIN_VIEW,
58
+ splitPercentage: 30,
59
+ },
60
+ },
61
+ panels: {
62
+ data: {
63
+ title: 'Data',
64
+ component: DataPanel,
65
+ placement: 'sidebar',
66
+ },
67
+ main: {
68
+ title: 'Main',
69
+ component: MainPanel,
70
+ placement: 'main',
71
+ },
72
+ },
73
+ })(set, get, store),
74
+ }),
75
+ );
47
76
  ```
48
77
 
49
- ## Components
50
-
51
- ### MosaicLayout
52
-
53
- A wrapper around the `Mosaic` component from react-mosaic-component that provides a customized look and feel consistent with SQLRooms styling.
78
+ ## Programmatic panel visibility
54
79
 
55
80
  ```tsx
56
- import {MosaicLayout} from '@sqlrooms/layout';
57
-
58
- // Example usage
59
- <MosaicLayout
60
- renderTile={(id, path) => <YourTileContent id={id} />}
61
- value={yourMosaicTree}
62
- onChange={handleLayoutChange}
63
- />;
81
+ import {Button} from '@sqlrooms/ui';
82
+
83
+ function PanelButtons() {
84
+ const togglePanel = useRoomStore((state) => state.layout.togglePanel);
85
+ const togglePanelPin = useRoomStore((state) => state.layout.togglePanelPin);
86
+
87
+ return (
88
+ <div className="flex gap-2">
89
+ <Button onClick={() => togglePanel('data')}>Toggle Data Panel</Button>
90
+ <Button onClick={() => togglePanelPin('data')}>Pin/Unpin Data Panel</Button>
91
+ </div>
92
+ );
93
+ }
64
94
  ```
65
95
 
66
- ### MosaicTile
67
-
68
- A component for rendering individual tiles within the mosaic layout.
69
-
70
- ## Utility Functions
71
-
72
- The package provides several utility functions for working with mosaic layouts:
73
-
74
- - `makeMosaicStack`: Creates a stack of mosaic nodes with specified weights and direction
75
- - `visitMosaicLeafNodes`: Traverses all leaf nodes in a mosaic tree
76
- - `getVisibleMosaicLayoutPanels`: Gets an array of all visible panel IDs
77
- - `findMosaicNodePathByKey`: Finds the path to a specific node by its key
78
- - `removeMosaicNodeByKey`: Removes a node from the mosaic tree by its key
79
-
80
- ## Learn More
81
-
82
- For more information about the underlying react-mosaic library, visit:
83
-
84
- - [react-mosaic documentation](https://nomcopter.github.io/react-mosaic/)
85
- - [react-mosaic GitHub repository](https://github.com/nomcopter/react-mosaic)
86
-
87
- ## License
96
+ ## Note
88
97
 
89
- MIT
98
+ `@sqlrooms/layout` (react-mosaic layout) is different from `@sqlrooms/mosaic` (UW IDL data visualization package).
@@ -20,6 +20,8 @@ export type LayoutSliceConfig = z.infer<typeof LayoutSliceConfig>;
20
20
  export declare function createDefaultLayoutConfig(): LayoutSliceConfig;
21
21
  export type LayoutSliceState = {
22
22
  layout: {
23
+ initialize?: () => Promise<void>;
24
+ destroy?: () => Promise<void>;
23
25
  config: LayoutSliceConfig;
24
26
  panels: Record<string, RoomPanelInfo>;
25
27
  setConfig(layout: LayoutConfig): void;
@@ -1 +1 @@
1
- {"version":3,"file":"LayoutSlice.d.ts","sourceRoot":"","sources":["../src/LayoutSlice.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EAGb,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAGrC,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IACjD,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC;IAC/B,SAAS,EAAE,SAAS,GAAG,gBAAgB,GAAG,MAAM,GAAG,MAAM,CAAC;CAC3D,CAAC;AAEF,eAAO,MAAM,iBAAiB;;;;;2BAAe,CAAC;AAE9C,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,wBAAgB,yBAAyB,IAAI,iBAAiB,CAE7D;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE;QACN,MAAM,EAAE,iBAAiB,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACtC,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;QACtC,wCAAwC;QACxC,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;QACtC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;QACrD,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;KACzC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EAAE,aAA2C,EACnD,MAAW,GACZ,GAAE,sBAA2B,GAAG,YAAY,CAAC,gBAAgB,CAAC,CA+F9D;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,CAAC,GACvC,CAAC,CAEH"}
1
+ {"version":3,"file":"LayoutSlice.d.ts","sourceRoot":"","sources":["../src/LayoutSlice.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EAGb,MAAM,yBAAyB,CAAC;AAUjC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAwBrC,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IACjD,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC;IAC/B,SAAS,EAAE,SAAS,GAAG,gBAAgB,GAAG,MAAM,GAAG,MAAM,CAAC;CAC3D,CAAC;AAEF,eAAO,MAAM,iBAAiB;;;;;2BAAe,CAAC;AAE9C,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,wBAAgB,yBAAyB,IAAI,iBAAiB,CAE7D;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE;QACN,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,EAAE,iBAAiB,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACtC,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;QACtC,wCAAwC;QACxC,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;QACtC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;QACrD,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;KACzC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EAAE,aAA2C,EACnD,MAAW,GACZ,GAAE,sBAA2B,GAAG,YAAY,CAAC,gBAAgB,CAAC,CA0H9D;AAmGD,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,CAAC,GACvC,CAAC,CAEH"}
@@ -1,92 +1,212 @@
1
1
  import { DEFAULT_MOSAIC_LAYOUT, LayoutConfig, MAIN_VIEW, isMosaicLayoutParent, } from '@sqlrooms/layout-config';
2
- import { createSlice, useBaseRoomStore } from '@sqlrooms/room-store';
2
+ import { createSlice, registerCommandsForOwner, unregisterCommandsForOwner, useBaseRoomStore, } from '@sqlrooms/room-store';
3
3
  import { produce } from 'immer';
4
+ import { z } from 'zod';
4
5
  import { makeMosaicStack, removeMosaicNodeByKey } from './mosaic';
6
+ const LAYOUT_COMMAND_OWNER = '@sqlrooms/layout/panels';
7
+ const ToggleLayoutPanelCommandInput = z.object({
8
+ panelId: z
9
+ .string()
10
+ .describe('ID of the panel to show/hide, e.g. "sql-editor".'),
11
+ show: z
12
+ .boolean()
13
+ .optional()
14
+ .describe('Optional explicit visibility. True shows, false hides, omitted toggles.'),
15
+ });
16
+ const ToggleLayoutPanelPinInput = z.object({
17
+ panelId: z.string().describe('ID of the panel to pin/unpin.'),
18
+ });
5
19
  export const LayoutSliceConfig = LayoutConfig;
6
20
  export function createDefaultLayoutConfig() {
7
21
  return DEFAULT_MOSAIC_LAYOUT;
8
22
  }
9
23
  export function createLayoutSlice({ config: initialConfig = createDefaultLayoutConfig(), panels = {}, } = {}) {
10
- return createSlice((set, get) => ({
11
- layout: {
12
- config: initialConfig,
13
- panels,
14
- setConfig: (config) => set((state) => produce(state, (draft) => {
15
- draft.layout.config = config;
16
- })),
17
- setLayout: (layout) => get().layout.setConfig(layout),
18
- togglePanel: (panel, show) => {
19
- if (get().layout.config?.nodes === panel) {
20
- // don't hide the view if it's the only one
21
- return;
22
- }
23
- const result = removeMosaicNodeByKey(get().layout.config?.nodes, panel);
24
- const isShown = result.success;
25
- if (isShown) {
26
- if (show || panel === MAIN_VIEW /*&& areViewsReadyToRender()*/) {
24
+ return createSlice((set, get, store) => {
25
+ let unsubscribePanelChanges;
26
+ const registerPanelCommands = () => {
27
+ const panelCommands = createLayoutPanelCommands(get().layout.panels);
28
+ registerCommandsForOwner(store, LAYOUT_COMMAND_OWNER, panelCommands);
29
+ };
30
+ return {
31
+ layout: {
32
+ initialize: async () => {
33
+ registerPanelCommands();
34
+ unsubscribePanelChanges?.();
35
+ unsubscribePanelChanges = store.subscribe((state, prevState) => {
36
+ if (state.layout.panels !== prevState.layout.panels) {
37
+ registerPanelCommands();
38
+ }
39
+ });
40
+ },
41
+ destroy: async () => {
42
+ unsubscribePanelChanges?.();
43
+ unsubscribePanelChanges = undefined;
44
+ unregisterCommandsForOwner(store, LAYOUT_COMMAND_OWNER);
45
+ },
46
+ config: initialConfig,
47
+ panels,
48
+ setConfig: (config) => set((state) => produce(state, (draft) => {
49
+ draft.layout.config = config;
50
+ })),
51
+ setLayout: (layout) => get().layout.setConfig(layout),
52
+ togglePanel: (panel, show) => {
53
+ if (get().layout.config?.nodes === panel) {
54
+ // don't hide the view if it's the only one
27
55
  return;
28
56
  }
29
- set((state) => produce(state, (draft) => {
30
- const layout = draft.layout.config;
31
- layout.nodes = result.nextTree;
32
- if (layout.pinned?.includes(panel)) {
33
- layout.pinned = layout.pinned.filter((p) => p !== panel);
57
+ const result = removeMosaicNodeByKey(get().layout.config?.nodes, panel);
58
+ const isShown = result.success;
59
+ if (isShown) {
60
+ if (show || panel === MAIN_VIEW /*&& areViewsReadyToRender()*/) {
61
+ return;
34
62
  }
35
- }));
36
- }
37
- else {
38
- if (show === false) {
39
- return;
63
+ set((state) => produce(state, (draft) => {
64
+ const layout = draft.layout.config;
65
+ layout.nodes = result.nextTree;
66
+ if (layout.pinned?.includes(panel)) {
67
+ layout.pinned = layout.pinned.filter((p) => p !== panel);
68
+ }
69
+ }));
40
70
  }
71
+ else {
72
+ if (show === false) {
73
+ return;
74
+ }
75
+ set((state) => produce(state, (draft) => {
76
+ const layout = draft.layout.config;
77
+ const root = layout.nodes;
78
+ const placement = draft.layout.panels[panel]?.placement;
79
+ const side = placement === 'sidebar' ? 'first' : 'second';
80
+ const toReplace = isMosaicLayoutParent(root)
81
+ ? root[side]
82
+ : undefined;
83
+ if (toReplace &&
84
+ isMosaicLayoutParent(root) &&
85
+ !isMosaicLayoutParent(toReplace) &&
86
+ toReplace !== MAIN_VIEW &&
87
+ !layout.fixed?.includes(toReplace) &&
88
+ !layout.pinned?.includes(toReplace)) {
89
+ // replace first un-pinned leaf
90
+ root[side] = panel;
91
+ }
92
+ else {
93
+ const panelNode = { node: panel, weight: 1 };
94
+ const restNode = {
95
+ node: draft.layout.config?.nodes,
96
+ weight: 3,
97
+ };
98
+ // add to to the left
99
+ layout.nodes = makeMosaicStack(placement === 'sidebar-bottom' ? 'column' : 'row', side === 'first'
100
+ ? [panelNode, restNode]
101
+ : [restNode, panelNode]);
102
+ }
103
+ }));
104
+ }
105
+ },
106
+ /**
107
+ * Toggle the pin state of a panel.
108
+ * @param panel - The panel to toggle the pin state of.
109
+ */
110
+ togglePanelPin: (panel) => {
41
111
  set((state) => produce(state, (draft) => {
42
112
  const layout = draft.layout.config;
43
- const root = layout.nodes;
44
- const placement = draft.layout.panels[panel]?.placement;
45
- const side = placement === 'sidebar' ? 'first' : 'second';
46
- const toReplace = isMosaicLayoutParent(root)
47
- ? root[side]
48
- : undefined;
49
- if (toReplace &&
50
- isMosaicLayoutParent(root) &&
51
- !isMosaicLayoutParent(toReplace) &&
52
- toReplace !== MAIN_VIEW &&
53
- !layout.fixed?.includes(toReplace) &&
54
- !layout.pinned?.includes(toReplace)) {
55
- // replace first un-pinned leaf
56
- root[side] = panel;
113
+ const pinned = layout.pinned ?? [];
114
+ if (pinned.includes(panel)) {
115
+ layout.pinned = pinned.filter((p) => p !== panel);
57
116
  }
58
117
  else {
59
- const panelNode = { node: panel, weight: 1 };
60
- const restNode = {
61
- node: draft.layout.config?.nodes,
62
- weight: 3,
63
- };
64
- // add to to the left
65
- layout.nodes = makeMosaicStack(placement === 'sidebar-bottom' ? 'column' : 'row', side === 'first'
66
- ? [panelNode, restNode]
67
- : [restNode, panelNode]);
118
+ layout.pinned = [...pinned, panel];
68
119
  }
69
120
  }));
70
- }
71
- },
72
- /**
73
- * Toggle the pin state of a panel.
74
- * @param panel - The panel to toggle the pin state of.
75
- */
76
- togglePanelPin: (panel) => {
77
- set((state) => produce(state, (draft) => {
78
- const layout = draft.layout.config;
79
- const pinned = layout.pinned ?? [];
80
- if (pinned.includes(panel)) {
81
- layout.pinned = pinned.filter((p) => p !== panel);
82
- }
83
- else {
84
- layout.pinned = [...pinned, panel];
85
- }
86
- }));
121
+ },
87
122
  },
123
+ };
124
+ });
125
+ }
126
+ function createLayoutPanelCommands(panels) {
127
+ const byIdCommand = {
128
+ id: 'layout.panel.toggle',
129
+ name: 'Toggle panel by ID',
130
+ description: 'Show or hide a panel by its ID',
131
+ group: 'Layout',
132
+ keywords: ['layout', 'panel', 'toggle', 'show', 'hide', 'id'],
133
+ inputSchema: ToggleLayoutPanelCommandInput,
134
+ inputDescription: 'Provide panelId and optional show. If show is omitted, the panel visibility is toggled.',
135
+ metadata: {
136
+ readOnly: false,
137
+ idempotent: true,
138
+ riskLevel: 'low',
139
+ },
140
+ validateInput: (input, { getState }) => {
141
+ const { panelId } = input;
142
+ if (!getState().layout.panels[panelId]) {
143
+ throw new Error(`Unknown panel ID "${panelId}".`);
144
+ }
145
+ },
146
+ execute: ({ getState }, input) => {
147
+ const { panelId, show } = input;
148
+ getState().layout.togglePanel(panelId, show);
149
+ return {
150
+ success: true,
151
+ commandId: 'layout.panel.toggle',
152
+ message: `Toggled panel "${panelId}".`,
153
+ };
88
154
  },
89
- }));
155
+ };
156
+ const pinByIdCommand = {
157
+ id: 'layout.panel.toggle-pin',
158
+ name: 'Toggle panel pin by ID',
159
+ description: 'Pin or unpin a panel by its ID',
160
+ group: 'Layout',
161
+ keywords: ['layout', 'panel', 'pin', 'unpin', 'id'],
162
+ inputSchema: ToggleLayoutPanelPinInput,
163
+ inputDescription: 'Provide panelId to toggle pin state.',
164
+ metadata: {
165
+ readOnly: false,
166
+ idempotent: true,
167
+ riskLevel: 'low',
168
+ },
169
+ validateInput: (input, { getState }) => {
170
+ const { panelId } = input;
171
+ if (!getState().layout.panels[panelId]) {
172
+ throw new Error(`Unknown panel ID "${panelId}".`);
173
+ }
174
+ },
175
+ execute: ({ getState }, input) => {
176
+ const { panelId } = input;
177
+ getState().layout.togglePanelPin(panelId);
178
+ return {
179
+ success: true,
180
+ commandId: 'layout.panel.toggle-pin',
181
+ message: `Toggled pin state for panel "${panelId}".`,
182
+ };
183
+ },
184
+ };
185
+ const panelShortcutCommands = Object.entries(panels).map(([panelId, panelInfo]) => {
186
+ const title = panelInfo.title ?? panelId;
187
+ const keywords = [panelId, panelInfo.title, panelInfo.placement].filter((value) => Boolean(value));
188
+ return {
189
+ id: `layout.panel.toggle.${panelId}`,
190
+ name: `Toggle panel: ${title}`,
191
+ description: `Show or hide the ${title} panel`,
192
+ group: 'Layout',
193
+ keywords,
194
+ metadata: {
195
+ readOnly: false,
196
+ idempotent: true,
197
+ riskLevel: 'low',
198
+ },
199
+ execute: ({ getState }) => {
200
+ getState().layout.togglePanel(panelId);
201
+ return {
202
+ success: true,
203
+ commandId: `layout.panel.toggle.${panelId}`,
204
+ message: `Toggled panel "${panelId}".`,
205
+ };
206
+ },
207
+ };
208
+ });
209
+ return [byIdCommand, pinByIdCommand, ...panelShortcutCommands];
90
210
  }
91
211
  export function useStoreWithLayout(selector) {
92
212
  return useBaseRoomStore((state) => selector(state));
@@ -1 +1 @@
1
- {"version":3,"file":"LayoutSlice.js","sourceRoot":"","sources":["../src/LayoutSlice.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_MOSAIC_LAYOUT,\n LayoutConfig,\n MAIN_VIEW,\n isMosaicLayoutParent,\n} from '@sqlrooms/layout-config';\nimport {createSlice, useBaseRoomStore} from '@sqlrooms/room-store';\nimport {produce} from 'immer';\nimport React from 'react';\nimport {z} from 'zod';\nimport {StateCreator} from 'zustand';\nimport {makeMosaicStack, removeMosaicNodeByKey} from './mosaic';\n\nexport type RoomPanelInfo = {\n title?: string;\n icon?: React.ComponentType<{className?: string}>;\n component: React.ComponentType;\n placement: 'sidebar' | 'sidebar-bottom' | 'main' | string;\n};\n\nexport const LayoutSliceConfig = LayoutConfig;\n\nexport type LayoutSliceConfig = z.infer<typeof LayoutSliceConfig>;\n\nexport function createDefaultLayoutConfig(): LayoutSliceConfig {\n return DEFAULT_MOSAIC_LAYOUT;\n}\n\nexport type LayoutSliceState = {\n layout: {\n config: LayoutSliceConfig;\n panels: Record<string, RoomPanelInfo>;\n setConfig(layout: LayoutConfig): void;\n /** @deprecated Use setConfig instead */\n setLayout(layout: LayoutConfig): void;\n togglePanel: (panel: string, show?: boolean) => void;\n togglePanelPin: (panel: string) => void;\n };\n};\n\nexport type CreateLayoutSliceProps = {\n config?: LayoutSliceConfig;\n panels?: Record<string, RoomPanelInfo>;\n};\n\nexport function createLayoutSlice({\n config: initialConfig = createDefaultLayoutConfig(),\n panels = {},\n}: CreateLayoutSliceProps = {}): StateCreator<LayoutSliceState> {\n return createSlice<LayoutSliceState>((set, get) => ({\n layout: {\n config: initialConfig,\n panels,\n setConfig: (config) =>\n set((state) =>\n produce(state, (draft) => {\n draft.layout.config = config;\n }),\n ),\n setLayout: (layout) => get().layout.setConfig(layout),\n togglePanel: (panel, show) => {\n if (get().layout.config?.nodes === panel) {\n // don't hide the view if it's the only one\n return;\n }\n const result = removeMosaicNodeByKey(get().layout.config?.nodes, panel);\n const isShown = result.success;\n if (isShown) {\n if (show || panel === MAIN_VIEW /*&& areViewsReadyToRender()*/) {\n return;\n }\n set((state) =>\n produce(state, (draft) => {\n const layout = draft.layout.config;\n layout.nodes = result.nextTree;\n if (layout.pinned?.includes(panel)) {\n layout.pinned = layout.pinned.filter(\n (p: string) => p !== panel,\n );\n }\n }),\n );\n } else {\n if (show === false) {\n return;\n }\n set((state) =>\n produce(state, (draft) => {\n const layout = draft.layout.config;\n const root = layout.nodes;\n const placement = draft.layout.panels[panel]?.placement;\n const side = placement === 'sidebar' ? 'first' : 'second';\n const toReplace = isMosaicLayoutParent(root)\n ? root[side]\n : undefined;\n if (\n toReplace &&\n isMosaicLayoutParent(root) &&\n !isMosaicLayoutParent(toReplace) &&\n toReplace !== MAIN_VIEW &&\n !layout.fixed?.includes(toReplace) &&\n !layout.pinned?.includes(toReplace)\n ) {\n // replace first un-pinned leaf\n root[side] = panel;\n } else {\n const panelNode = {node: panel, weight: 1};\n const restNode = {\n node: draft.layout.config?.nodes,\n weight: 3,\n };\n // add to to the left\n layout.nodes = makeMosaicStack(\n placement === 'sidebar-bottom' ? 'column' : 'row',\n side === 'first'\n ? [panelNode, restNode]\n : [restNode, panelNode],\n );\n }\n }),\n );\n }\n },\n\n /**\n * Toggle the pin state of a panel.\n * @param panel - The panel to toggle the pin state of.\n */\n togglePanelPin: (panel: string) => {\n set((state) =>\n produce(state, (draft) => {\n const layout = draft.layout.config;\n const pinned = layout.pinned ?? [];\n if (pinned.includes(panel)) {\n layout.pinned = pinned.filter((p: string) => p !== panel);\n } else {\n layout.pinned = [...pinned, panel];\n }\n }),\n );\n },\n },\n }));\n}\n\nexport function useStoreWithLayout<T>(\n selector: (state: LayoutSliceState) => T,\n): T {\n return useBaseRoomStore<LayoutSliceState, T>((state) => selector(state));\n}\n"]}
1
+ {"version":3,"file":"LayoutSlice.js","sourceRoot":"","sources":["../src/LayoutSlice.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_MOSAIC_LAYOUT,\n LayoutConfig,\n MAIN_VIEW,\n isMosaicLayoutParent,\n} from '@sqlrooms/layout-config';\nimport {\n BaseRoomStoreState,\n createSlice,\n registerCommandsForOwner,\n RoomCommand,\n unregisterCommandsForOwner,\n useBaseRoomStore,\n} from '@sqlrooms/room-store';\nimport {produce} from 'immer';\nimport React from 'react';\nimport {z} from 'zod';\nimport {StateCreator} from 'zustand';\nimport {makeMosaicStack, removeMosaicNodeByKey} from './mosaic';\n\nconst LAYOUT_COMMAND_OWNER = '@sqlrooms/layout/panels';\nconst ToggleLayoutPanelCommandInput = z.object({\n panelId: z\n .string()\n .describe('ID of the panel to show/hide, e.g. \"sql-editor\".'),\n show: z\n .boolean()\n .optional()\n .describe(\n 'Optional explicit visibility. True shows, false hides, omitted toggles.',\n ),\n});\ntype ToggleLayoutPanelCommandInput = z.infer<\n typeof ToggleLayoutPanelCommandInput\n>;\n\nconst ToggleLayoutPanelPinInput = z.object({\n panelId: z.string().describe('ID of the panel to pin/unpin.'),\n});\ntype ToggleLayoutPanelPinInput = z.infer<typeof ToggleLayoutPanelPinInput>;\n\nexport type RoomPanelInfo = {\n title?: string;\n icon?: React.ComponentType<{className?: string}>;\n component: React.ComponentType;\n placement: 'sidebar' | 'sidebar-bottom' | 'main' | string;\n};\n\nexport const LayoutSliceConfig = LayoutConfig;\n\nexport type LayoutSliceConfig = z.infer<typeof LayoutSliceConfig>;\n\nexport function createDefaultLayoutConfig(): LayoutSliceConfig {\n return DEFAULT_MOSAIC_LAYOUT;\n}\n\nexport type LayoutSliceState = {\n layout: {\n initialize?: () => Promise<void>;\n destroy?: () => Promise<void>;\n config: LayoutSliceConfig;\n panels: Record<string, RoomPanelInfo>;\n setConfig(layout: LayoutConfig): void;\n /** @deprecated Use setConfig instead */\n setLayout(layout: LayoutConfig): void;\n togglePanel: (panel: string, show?: boolean) => void;\n togglePanelPin: (panel: string) => void;\n };\n};\n\nexport type CreateLayoutSliceProps = {\n config?: LayoutSliceConfig;\n panels?: Record<string, RoomPanelInfo>;\n};\n\nexport function createLayoutSlice({\n config: initialConfig = createDefaultLayoutConfig(),\n panels = {},\n}: CreateLayoutSliceProps = {}): StateCreator<LayoutSliceState> {\n return createSlice<LayoutSliceState, BaseRoomStoreState & LayoutSliceState>(\n (set, get, store) => {\n let unsubscribePanelChanges: (() => void) | undefined;\n const registerPanelCommands = () => {\n const panelCommands = createLayoutPanelCommands(get().layout.panels);\n registerCommandsForOwner(store, LAYOUT_COMMAND_OWNER, panelCommands);\n };\n\n return {\n layout: {\n initialize: async () => {\n registerPanelCommands();\n unsubscribePanelChanges?.();\n unsubscribePanelChanges = store.subscribe((state, prevState) => {\n if (state.layout.panels !== prevState.layout.panels) {\n registerPanelCommands();\n }\n });\n },\n destroy: async () => {\n unsubscribePanelChanges?.();\n unsubscribePanelChanges = undefined;\n unregisterCommandsForOwner(store, LAYOUT_COMMAND_OWNER);\n },\n config: initialConfig,\n panels,\n setConfig: (config) =>\n set((state) =>\n produce(state, (draft) => {\n draft.layout.config = config;\n }),\n ),\n setLayout: (layout) => get().layout.setConfig(layout),\n togglePanel: (panel, show) => {\n if (get().layout.config?.nodes === panel) {\n // don't hide the view if it's the only one\n return;\n }\n const result = removeMosaicNodeByKey(\n get().layout.config?.nodes,\n panel,\n );\n const isShown = result.success;\n if (isShown) {\n if (show || panel === MAIN_VIEW /*&& areViewsReadyToRender()*/) {\n return;\n }\n set((state) =>\n produce(state, (draft) => {\n const layout = draft.layout.config;\n layout.nodes = result.nextTree;\n if (layout.pinned?.includes(panel)) {\n layout.pinned = layout.pinned.filter(\n (p: string) => p !== panel,\n );\n }\n }),\n );\n } else {\n if (show === false) {\n return;\n }\n set((state) =>\n produce(state, (draft) => {\n const layout = draft.layout.config;\n const root = layout.nodes;\n const placement = draft.layout.panels[panel]?.placement;\n const side = placement === 'sidebar' ? 'first' : 'second';\n const toReplace = isMosaicLayoutParent(root)\n ? root[side]\n : undefined;\n if (\n toReplace &&\n isMosaicLayoutParent(root) &&\n !isMosaicLayoutParent(toReplace) &&\n toReplace !== MAIN_VIEW &&\n !layout.fixed?.includes(toReplace) &&\n !layout.pinned?.includes(toReplace)\n ) {\n // replace first un-pinned leaf\n root[side] = panel;\n } else {\n const panelNode = {node: panel, weight: 1};\n const restNode = {\n node: draft.layout.config?.nodes,\n weight: 3,\n };\n // add to to the left\n layout.nodes = makeMosaicStack(\n placement === 'sidebar-bottom' ? 'column' : 'row',\n side === 'first'\n ? [panelNode, restNode]\n : [restNode, panelNode],\n );\n }\n }),\n );\n }\n },\n\n /**\n * Toggle the pin state of a panel.\n * @param panel - The panel to toggle the pin state of.\n */\n togglePanelPin: (panel: string) => {\n set((state) =>\n produce(state, (draft) => {\n const layout = draft.layout.config;\n const pinned = layout.pinned ?? [];\n if (pinned.includes(panel)) {\n layout.pinned = pinned.filter((p: string) => p !== panel);\n } else {\n layout.pinned = [...pinned, panel];\n }\n }),\n );\n },\n },\n };\n },\n );\n}\n\ntype LayoutCommandStoreState = BaseRoomStoreState & LayoutSliceState;\n\nfunction createLayoutPanelCommands(\n panels: Record<string, RoomPanelInfo>,\n): RoomCommand<LayoutCommandStoreState>[] {\n const byIdCommand: RoomCommand<LayoutCommandStoreState> = {\n id: 'layout.panel.toggle',\n name: 'Toggle panel by ID',\n description: 'Show or hide a panel by its ID',\n group: 'Layout',\n keywords: ['layout', 'panel', 'toggle', 'show', 'hide', 'id'],\n inputSchema: ToggleLayoutPanelCommandInput,\n inputDescription:\n 'Provide panelId and optional show. If show is omitted, the panel visibility is toggled.',\n metadata: {\n readOnly: false,\n idempotent: true,\n riskLevel: 'low',\n },\n validateInput: (input, {getState}) => {\n const {panelId} = input as ToggleLayoutPanelCommandInput;\n if (!getState().layout.panels[panelId]) {\n throw new Error(`Unknown panel ID \"${panelId}\".`);\n }\n },\n execute: ({getState}, input) => {\n const {panelId, show} = input as ToggleLayoutPanelCommandInput;\n getState().layout.togglePanel(panelId, show);\n return {\n success: true,\n commandId: 'layout.panel.toggle',\n message: `Toggled panel \"${panelId}\".`,\n };\n },\n };\n\n const pinByIdCommand: RoomCommand<LayoutCommandStoreState> = {\n id: 'layout.panel.toggle-pin',\n name: 'Toggle panel pin by ID',\n description: 'Pin or unpin a panel by its ID',\n group: 'Layout',\n keywords: ['layout', 'panel', 'pin', 'unpin', 'id'],\n inputSchema: ToggleLayoutPanelPinInput,\n inputDescription: 'Provide panelId to toggle pin state.',\n metadata: {\n readOnly: false,\n idempotent: true,\n riskLevel: 'low',\n },\n validateInput: (input, {getState}) => {\n const {panelId} = input as ToggleLayoutPanelPinInput;\n if (!getState().layout.panels[panelId]) {\n throw new Error(`Unknown panel ID \"${panelId}\".`);\n }\n },\n execute: ({getState}, input) => {\n const {panelId} = input as ToggleLayoutPanelPinInput;\n getState().layout.togglePanelPin(panelId);\n return {\n success: true,\n commandId: 'layout.panel.toggle-pin',\n message: `Toggled pin state for panel \"${panelId}\".`,\n };\n },\n };\n\n const panelShortcutCommands: RoomCommand<LayoutCommandStoreState>[] =\n Object.entries(panels).map(([panelId, panelInfo]) => {\n const title = panelInfo.title ?? panelId;\n const keywords = [panelId, panelInfo.title, panelInfo.placement].filter(\n (value): value is string => Boolean(value),\n );\n return {\n id: `layout.panel.toggle.${panelId}`,\n name: `Toggle panel: ${title}`,\n description: `Show or hide the ${title} panel`,\n group: 'Layout',\n keywords,\n metadata: {\n readOnly: false,\n idempotent: true,\n riskLevel: 'low',\n },\n execute: ({getState}) => {\n getState().layout.togglePanel(panelId);\n return {\n success: true,\n commandId: `layout.panel.toggle.${panelId}`,\n message: `Toggled panel \"${panelId}\".`,\n };\n },\n };\n });\n\n return [byIdCommand, pinByIdCommand, ...panelShortcutCommands];\n}\n\nexport function useStoreWithLayout<T>(\n selector: (state: LayoutSliceState) => T,\n): T {\n return useBaseRoomStore<LayoutSliceState, T>((state) => selector(state));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlrooms/layout",
3
- "version": "0.28.0-rc.0",
3
+ "version": "0.28.1-rc.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/index.js",
@@ -21,9 +21,9 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "@sqlrooms/layout-config": "0.28.0-rc.0",
25
- "@sqlrooms/room-store": "0.28.0-rc.0",
26
- "@sqlrooms/ui": "0.28.0-rc.0",
24
+ "@sqlrooms/layout-config": "0.28.1-rc.0",
25
+ "@sqlrooms/room-store": "0.28.1-rc.0",
26
+ "@sqlrooms/ui": "0.28.1-rc.0",
27
27
  "immer": "^11.0.1",
28
28
  "zod": "^4.1.8",
29
29
  "zustand": "^5.0.8"
@@ -40,5 +40,5 @@
40
40
  "typecheck": "tsc --noEmit",
41
41
  "typedoc": "typedoc"
42
42
  },
43
- "gitHead": "87a478edbff690e04c38cc717db8e11e844565c8"
43
+ "gitHead": "1e0dcae95d1ccdbcd1b32df1d647d0f794b94e5e"
44
44
  }