@happyvertical/smrt-svelte 0.55.0 → 0.55.2

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 (79) hide show
  1. package/README.md +38 -6
  2. package/agents/components.md +44 -1
  3. package/agents/workspace.md +77 -2
  4. package/dist/components/app/AppShell.svelte +122 -24
  5. package/dist/components/app/AppShell.svelte.d.ts +25 -2
  6. package/dist/components/app/AppShell.svelte.d.ts.map +1 -1
  7. package/dist/components/app/DockToggles.svelte +103 -0
  8. package/dist/components/app/DockToggles.svelte.d.ts +8 -0
  9. package/dist/components/app/DockToggles.svelte.d.ts.map +1 -0
  10. package/dist/components/app/dock-toggle.d.ts +14 -0
  11. package/dist/components/app/dock-toggle.d.ts.map +1 -0
  12. package/dist/components/app/dock-toggle.js +1 -0
  13. package/dist/components/app/index.d.ts +2 -0
  14. package/dist/components/app/index.d.ts.map +1 -1
  15. package/dist/components/app/index.js +1 -0
  16. package/dist/components/board/Board.svelte +92 -394
  17. package/dist/components/board/Board.svelte.d.ts.map +1 -1
  18. package/dist/components/forms/RelationInput.svelte +379 -0
  19. package/dist/components/forms/RelationInput.svelte.d.ts +75 -0
  20. package/dist/components/forms/RelationInput.svelte.d.ts.map +1 -0
  21. package/dist/components/forms/index.d.ts +2 -1
  22. package/dist/components/forms/index.d.ts.map +1 -1
  23. package/dist/components/forms/index.js +1 -0
  24. package/dist/components/forms/types.d.ts +9 -0
  25. package/dist/components/forms/types.d.ts.map +1 -1
  26. package/dist/components/sortable/Sortable.svelte +359 -0
  27. package/dist/components/sortable/Sortable.svelte.d.ts +26 -0
  28. package/dist/components/sortable/Sortable.svelte.d.ts.map +1 -0
  29. package/dist/components/sortable/controller.svelte.d.ts +152 -0
  30. package/dist/components/sortable/controller.svelte.d.ts.map +1 -0
  31. package/dist/components/sortable/controller.svelte.js +453 -0
  32. package/dist/components/sortable/index.d.ts +5 -0
  33. package/dist/components/sortable/index.d.ts.map +1 -0
  34. package/dist/components/sortable/index.js +2 -0
  35. package/dist/components/sortable/types.d.ts +80 -0
  36. package/dist/components/sortable/types.d.ts.map +1 -0
  37. package/dist/components/sortable/types.js +1 -0
  38. package/dist/components/workspace/admin-shell/AdminShell.svelte +62 -3
  39. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +5 -0
  40. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  41. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +2 -1
  42. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte +196 -0
  43. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts +17 -0
  44. package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts.map +1 -0
  45. package/dist/components/workspace/admin-shell/TenantNav.svelte +69 -3
  46. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  47. package/dist/components/workspace/admin-shell/dock.d.ts +37 -0
  48. package/dist/components/workspace/admin-shell/dock.d.ts.map +1 -0
  49. package/dist/components/workspace/admin-shell/dock.js +25 -0
  50. package/dist/components/workspace/admin-shell/layout-context.d.ts +7 -0
  51. package/dist/components/workspace/admin-shell/layout-context.d.ts.map +1 -0
  52. package/dist/components/workspace/admin-shell/layout-context.js +17 -0
  53. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts +69 -0
  54. package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts.map +1 -0
  55. package/dist/components/workspace/admin-shell/layout-controller.svelte.js +91 -0
  56. package/dist/components/workspace/admin-shell/layout.d.ts +120 -0
  57. package/dist/components/workspace/admin-shell/layout.d.ts.map +1 -0
  58. package/dist/components/workspace/admin-shell/layout.js +402 -0
  59. package/dist/components/workspace/admin-shell/settings.d.ts +6 -4
  60. package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
  61. package/dist/components/workspace/admin-shell/settings.js +14 -4
  62. package/dist/components/workspace/admin-shell/state.svelte.d.ts +59 -1
  63. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  64. package/dist/components/workspace/admin-shell/state.svelte.js +146 -2
  65. package/dist/components/workspace/admin-shell/types.d.ts +52 -0
  66. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  67. package/dist/components/workspace/index.d.ts +6 -1
  68. package/dist/components/workspace/index.d.ts.map +1 -1
  69. package/dist/components/workspace/index.js +5 -0
  70. package/dist/i18n/server.d.ts +1 -0
  71. package/dist/i18n/server.d.ts.map +1 -1
  72. package/dist/i18n/server.js +1 -0
  73. package/dist/i18n/strings.sortable.d.ts +13 -0
  74. package/dist/i18n/strings.sortable.d.ts.map +1 -0
  75. package/dist/i18n/strings.sortable.js +13 -0
  76. package/dist/i18n/strings.workspace.d.ts +13 -0
  77. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  78. package/dist/i18n/strings.workspace.js +13 -0
  79. package/package.json +26 -15
@@ -0,0 +1,120 @@
1
+ /**
2
+ * User-customizable shell layout: a small, versioned, JSON-serializable delta
3
+ * applied over the navigation and panel defaults the host generates.
4
+ *
5
+ * This module is pure (no Svelte, no DOM) so a host can validate, store,
6
+ * export, or apply a layout anywhere, including on the server. It is also
7
+ * published as `@happyvertical/smrt-svelte/workspace/layout`.
8
+ *
9
+ * Ids: sections are identified by `ShellNavGroup.id ?? heading`, items by
10
+ * `ShellNavItem.id ?? href`. Sections and items share one id namespace (the
11
+ * `hidden` list holds both), so keep them unique; later duplicates get a
12
+ * `#2`, `#3`, ... suffix. The flat `nav` items form an implicit first
13
+ * section, {@link SHELL_NAV_ROOT_SECTION_ID}, which always renders first and
14
+ * cannot be reordered or hidden as a whole.
15
+ */
16
+ import type { PanelEdge, ShellNavGroup, ShellNavItem, ShellPanelDefaults } from './types.js';
17
+ /** The only layout format version this package reads and writes. */
18
+ export declare const SHELL_LAYOUT_VERSION: 1;
19
+ /** Section id of the flat `nav` items rendered above all groups. */
20
+ export declare const SHELL_NAV_ROOT_SECTION_ID = "@root";
21
+ /** A user's choice for one edge panel. */
22
+ export interface ShellLayoutPanel {
23
+ /** `false` hides the edge; absent or `true` keeps it as configured. */
24
+ visible?: boolean;
25
+ /** The state the edge starts in. */
26
+ initial?: 'collapsed' | 'expanded';
27
+ }
28
+ /**
29
+ * A user's customization of the shell, stored as a sparse delta over what the
30
+ * host generates. Every field is optional; unknown ids are ignored.
31
+ */
32
+ export interface ShellLayout {
33
+ version: 1;
34
+ /** Section ids in display order. Sections not listed keep their slot. */
35
+ sectionOrder?: string[];
36
+ /** Per section id, item ids in display order. */
37
+ itemOrder?: Record<string, string[]>;
38
+ /** Ids of hidden sections and items. */
39
+ hidden?: string[];
40
+ /** Item id to the id of the section it was moved into. */
41
+ moved?: Record<string, string>;
42
+ /** Per edge panel overrides. */
43
+ panels?: Partial<Record<PanelEdge, ShellLayoutPanel>>;
44
+ }
45
+ /** An item as the layout sees it. */
46
+ export interface ShellNavModelItem {
47
+ id: string;
48
+ item: ShellNavItem;
49
+ /** The section the host put the item in. */
50
+ nativeSectionId: string;
51
+ /** The section it is displayed in (differs when moved). */
52
+ sectionId: string;
53
+ /** Hidden by its own id. */
54
+ hidden: boolean;
55
+ }
56
+ /** A section as the layout sees it. */
57
+ export interface ShellNavModelSection {
58
+ id: string;
59
+ /** `null` for the implicit root section of flat `nav` items. */
60
+ group: ShellNavGroup | null;
61
+ heading: string | null;
62
+ hidden: boolean;
63
+ /** Items in display order, including hidden ones. */
64
+ items: ShellNavModelItem[];
65
+ }
66
+ /** Result of {@link applyShellLayout}. */
67
+ export interface AppliedShellLayout {
68
+ nav: ShellNavItem[];
69
+ groups: ShellNavGroup[];
70
+ panels: ShellPanelDefaults;
71
+ }
72
+ export declare function createShellLayout(): ShellLayout;
73
+ /** A layout with no customization (nothing to apply). */
74
+ export declare function isShellLayoutEmpty(layout: ShellLayout | null | undefined): boolean;
75
+ /**
76
+ * Read an untrusted value (stored JSON, an imported blueprint) as a layout.
77
+ * Never throws: anything unreadable, including a version this package does not
78
+ * know, yields an empty layout, and unknown or malformed fields are dropped.
79
+ */
80
+ export declare function normalizeShellLayout(input: unknown): ShellLayout;
81
+ export declare function shellNavGroupId(group: ShellNavGroup): string;
82
+ export declare function shellNavItemId(item: ShellNavItem): string;
83
+ /**
84
+ * The navigation as the layout sees it: every section and item (hidden ones
85
+ * flagged, not removed) in display order. An editor renders this; the shell
86
+ * renders {@link applyShellLayout}'s filtered result.
87
+ */
88
+ export declare function resolveShellNavModel(nav?: readonly ShellNavItem[], groups?: readonly ShellNavGroup[], layout?: ShellLayout | null): ShellNavModelSection[];
89
+ /** The state an edge starts in once a layout override is applied. */
90
+ export declare function applyShellLayoutPanel(base: ShellPanelDefaults[PanelEdge], panel: ShellLayoutPanel | undefined): ShellPanelDefaults[PanelEdge];
91
+ /**
92
+ * Apply a layout over host-generated navigation and panel defaults.
93
+ *
94
+ * Pure and total: ids the host no longer has are ignored, items and sections
95
+ * the layout does not mention keep their default positions, and an empty or
96
+ * missing layout returns the inputs unchanged. Sections the layout empties
97
+ * (every item hidden or moved away) are dropped from `groups`.
98
+ */
99
+ export declare function applyShellLayout(nav: ShellNavItem[], groups: ShellNavGroup[], panels: ShellPanelDefaults | undefined, layout: ShellLayout | null | undefined): AppliedShellLayout;
100
+ /**
101
+ * Move a group section to `toIndex` among the groups (the root section is not
102
+ * counted). Returns the layout unchanged when `sectionId` is not a group.
103
+ */
104
+ export declare function moveShellSection(nav: readonly ShellNavItem[], groups: readonly ShellNavGroup[], layout: ShellLayout | null | undefined, sectionId: string, toIndex: number): ShellLayout;
105
+ /**
106
+ * Move an item into `toSectionId` (a group or the root section) at `toIndex`
107
+ * among that section's items, hidden ones included. `toIndex` defaults to the
108
+ * end. Returns the layout unchanged when either id is unknown.
109
+ */
110
+ export declare function moveShellItem(nav: readonly ShellNavItem[], groups: readonly ShellNavGroup[], layout: ShellLayout | null | undefined, itemId: string, toSectionId: string, toIndex?: number): ShellLayout;
111
+ /** Hide a section or item. Unknown ids and the root section are ignored. */
112
+ export declare function hideShellEntry(nav: readonly ShellNavItem[], groups: readonly ShellNavGroup[], layout: ShellLayout | null | undefined, id: string): ShellLayout;
113
+ /** Show a hidden section or item again. */
114
+ export declare function showShellEntry(layout: ShellLayout | null | undefined, id: string): ShellLayout;
115
+ /**
116
+ * Set an edge panel's `visible` and/or `initial`. Values equal to the host's
117
+ * defaults (`defaults`, when given) are dropped to keep the layout sparse.
118
+ */
119
+ export declare function setShellLayoutPanel(layout: ShellLayout | null | undefined, edge: PanelEdge, patch: ShellLayoutPanel, defaults?: ShellPanelDefaults): ShellLayout;
120
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,402 @@
1
+ /** The only layout format version this package reads and writes. */
2
+ export const SHELL_LAYOUT_VERSION = 1;
3
+ /** Section id of the flat `nav` items rendered above all groups. */
4
+ export const SHELL_NAV_ROOT_SECTION_ID = '@root';
5
+ const EDGES = ['top', 'left', 'right', 'bottom'];
6
+ export function createShellLayout() {
7
+ return { version: SHELL_LAYOUT_VERSION };
8
+ }
9
+ /** A layout with no customization (nothing to apply). */
10
+ export function isShellLayoutEmpty(layout) {
11
+ if (!layout)
12
+ return true;
13
+ return (!layout.sectionOrder?.length &&
14
+ !hasKeys(layout.itemOrder) &&
15
+ !layout.hidden?.length &&
16
+ !hasKeys(layout.moved) &&
17
+ !hasKeys(layout.panels));
18
+ }
19
+ function hasKeys(value) {
20
+ return (typeof value === 'object' && value !== null && Object.keys(value).length > 0);
21
+ }
22
+ function isRecord(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+ function stringList(value) {
26
+ if (!Array.isArray(value))
27
+ return undefined;
28
+ const seen = new Set();
29
+ for (const entry of value) {
30
+ if (typeof entry === 'string' && entry !== '')
31
+ seen.add(entry);
32
+ }
33
+ return seen.size > 0 ? [...seen] : undefined;
34
+ }
35
+ /**
36
+ * Read an untrusted value (stored JSON, an imported blueprint) as a layout.
37
+ * Never throws: anything unreadable, including a version this package does not
38
+ * know, yields an empty layout, and unknown or malformed fields are dropped.
39
+ */
40
+ export function normalizeShellLayout(input) {
41
+ const layout = createShellLayout();
42
+ if (!isRecord(input) || input.version !== SHELL_LAYOUT_VERSION)
43
+ return layout;
44
+ const sectionOrder = stringList(input.sectionOrder);
45
+ if (sectionOrder)
46
+ layout.sectionOrder = sectionOrder;
47
+ if (isRecord(input.itemOrder)) {
48
+ const itemOrder = {};
49
+ for (const [section, ids] of Object.entries(input.itemOrder)) {
50
+ const list = stringList(ids);
51
+ if (list)
52
+ itemOrder[section] = list;
53
+ }
54
+ if (hasKeys(itemOrder))
55
+ layout.itemOrder = itemOrder;
56
+ }
57
+ const hidden = stringList(input.hidden);
58
+ if (hidden)
59
+ layout.hidden = hidden;
60
+ if (isRecord(input.moved)) {
61
+ const moved = {};
62
+ for (const [id, section] of Object.entries(input.moved)) {
63
+ if (typeof section === 'string' && section !== '')
64
+ moved[id] = section;
65
+ }
66
+ if (hasKeys(moved))
67
+ layout.moved = moved;
68
+ }
69
+ if (isRecord(input.panels)) {
70
+ const panels = {};
71
+ for (const edge of EDGES) {
72
+ const raw = input.panels[edge];
73
+ if (!isRecord(raw))
74
+ continue;
75
+ const panel = {};
76
+ if (typeof raw.visible === 'boolean')
77
+ panel.visible = raw.visible;
78
+ if (raw.initial === 'collapsed' || raw.initial === 'expanded') {
79
+ panel.initial = raw.initial;
80
+ }
81
+ if (hasKeys(panel))
82
+ panels[edge] = panel;
83
+ }
84
+ if (hasKeys(panels))
85
+ layout.panels = panels;
86
+ }
87
+ return layout;
88
+ }
89
+ export function shellNavGroupId(group) {
90
+ return group.id ?? group.heading;
91
+ }
92
+ export function shellNavItemId(item) {
93
+ return item.id ?? item.href;
94
+ }
95
+ /**
96
+ * Keep every listed id that is present, in the listed order, in the slots the
97
+ * listed ids already occupy. Ids the list does not mention (for example new
98
+ * ones the host added later) stay exactly where the defaults put them.
99
+ */
100
+ function mergeOrder(defaults, preferred) {
101
+ if (!preferred || preferred.length === 0)
102
+ return [...defaults];
103
+ const present = new Set(defaults);
104
+ const listed = [];
105
+ const listedSet = new Set();
106
+ for (const id of preferred) {
107
+ if (present.has(id) && !listedSet.has(id)) {
108
+ listedSet.add(id);
109
+ listed.push(id);
110
+ }
111
+ }
112
+ if (listed.length === 0)
113
+ return [...defaults];
114
+ let next = 0;
115
+ return defaults.map((id) => (listedSet.has(id) ? listed[next++] : id));
116
+ }
117
+ /**
118
+ * The navigation as the layout sees it: every section and item (hidden ones
119
+ * flagged, not removed) in display order. An editor renders this; the shell
120
+ * renders {@link applyShellLayout}'s filtered result.
121
+ */
122
+ export function resolveShellNavModel(nav = [], groups = [], layout) {
123
+ const used = new Set([SHELL_NAV_ROOT_SECTION_ID]);
124
+ const unique = (id) => {
125
+ let candidate = id;
126
+ for (let n = 2; used.has(candidate); n += 1)
127
+ candidate = `${id}#${n}`;
128
+ used.add(candidate);
129
+ return candidate;
130
+ };
131
+ const natives = [
132
+ {
133
+ id: SHELL_NAV_ROOT_SECTION_ID,
134
+ group: null,
135
+ items: nav.map((item) => ({ id: unique(shellNavItemId(item)), item })),
136
+ },
137
+ ];
138
+ for (const group of groups) {
139
+ const id = unique(shellNavGroupId(group));
140
+ natives.push({
141
+ id,
142
+ group,
143
+ items: group.items.map((item) => ({
144
+ id: unique(shellNavItemId(item)),
145
+ item,
146
+ })),
147
+ });
148
+ }
149
+ const hidden = new Set(stringList(layout?.hidden) ?? []);
150
+ const moved = isRecord(layout?.moved)
151
+ ? layout?.moved
152
+ : {};
153
+ const sectionIds = new Set(natives.map((section) => section.id));
154
+ const itemIds = new Set(natives.flatMap((section) => section.items.map((entry) => entry.id)));
155
+ /** Where an item is displayed: its moved-to section when that exists. */
156
+ const displayedIn = (id, native) => {
157
+ const target = itemIds.has(id) ? moved[id] : undefined;
158
+ return typeof target === 'string' && sectionIds.has(target)
159
+ ? target
160
+ : native;
161
+ };
162
+ const groupSections = natives.slice(1).map((section) => section.id);
163
+ const orderedGroupIds = mergeOrder(groupSections, stringList(layout?.sectionOrder));
164
+ const byId = new Map(natives.map((section) => [section.id, section]));
165
+ const orderedSections = [
166
+ natives[0],
167
+ ...orderedGroupIds.map((id) => byId.get(id)),
168
+ ];
169
+ const itemOrder = isRecord(layout?.itemOrder)
170
+ ? layout?.itemOrder
171
+ : {};
172
+ return orderedSections.map((section) => {
173
+ const defaults = [];
174
+ for (const native of natives) {
175
+ for (const entry of native.items) {
176
+ if (displayedIn(entry.id, native.id) !== section.id)
177
+ continue;
178
+ defaults.push({
179
+ id: entry.id,
180
+ item: entry.item,
181
+ nativeSectionId: native.id,
182
+ sectionId: section.id,
183
+ hidden: hidden.has(entry.id),
184
+ });
185
+ }
186
+ }
187
+ // Items the host placed here keep their order; moved-in items follow.
188
+ defaults.sort((a, b) => Number(a.nativeSectionId !== section.id) -
189
+ Number(b.nativeSectionId !== section.id));
190
+ const byItem = new Map(defaults.map((entry) => [entry.id, entry]));
191
+ const order = mergeOrder(defaults.map((entry) => entry.id), stringList(itemOrder[section.id]));
192
+ return {
193
+ id: section.id,
194
+ group: section.group,
195
+ heading: section.group ? section.group.heading : null,
196
+ hidden: section.group ? hidden.has(section.id) : false,
197
+ items: order.map((id) => byItem.get(id)),
198
+ };
199
+ });
200
+ }
201
+ /** The state an edge starts in once a layout override is applied. */
202
+ export function applyShellLayoutPanel(base, panel) {
203
+ if (!panel)
204
+ return base;
205
+ // `false` and `initial: 'hidden'` mean the host has no such panel; a
206
+ // layout can hide a panel but never conjures one the host removed.
207
+ if (base === false || base?.initial === 'hidden')
208
+ return base;
209
+ if (panel.visible === false)
210
+ return false;
211
+ if (panel.initial === 'collapsed' || panel.initial === 'expanded') {
212
+ return { ...base, initial: panel.initial };
213
+ }
214
+ return base;
215
+ }
216
+ /**
217
+ * Apply a layout over host-generated navigation and panel defaults.
218
+ *
219
+ * Pure and total: ids the host no longer has are ignored, items and sections
220
+ * the layout does not mention keep their default positions, and an empty or
221
+ * missing layout returns the inputs unchanged. Sections the layout empties
222
+ * (every item hidden or moved away) are dropped from `groups`.
223
+ */
224
+ export function applyShellLayout(nav, groups, panels, layout) {
225
+ const basePanels = panels ?? {};
226
+ if (isShellLayoutEmpty(layout)) {
227
+ return { nav, groups, panels: basePanels };
228
+ }
229
+ const model = resolveShellNavModel(nav, groups, layout);
230
+ const visible = (section) => section.hidden
231
+ ? []
232
+ : section.items.filter((entry) => !entry.hidden).map((e) => e.item);
233
+ const [root, ...sections] = model;
234
+ const nextGroups = [];
235
+ for (const section of sections) {
236
+ const items = visible(section);
237
+ const group = section.group;
238
+ if (section.hidden || (items.length === 0 && group.items.length > 0)) {
239
+ continue;
240
+ }
241
+ nextGroups.push({ ...group, items });
242
+ }
243
+ const nextPanels = { ...basePanels };
244
+ const overrides = isRecord(layout?.panels) ? layout?.panels : undefined;
245
+ for (const edge of EDGES) {
246
+ const override = overrides?.[edge];
247
+ if (!isRecord(override))
248
+ continue;
249
+ const next = applyShellLayoutPanel(basePanels[edge], override);
250
+ if (next !== undefined)
251
+ nextPanels[edge] = next;
252
+ }
253
+ return { nav: visible(root), groups: nextGroups, panels: nextPanels };
254
+ }
255
+ // Mutations. Each returns a new sparse layout and never mutates its input.
256
+ function compact(layout) {
257
+ const next = { version: SHELL_LAYOUT_VERSION };
258
+ if (layout.sectionOrder?.length)
259
+ next.sectionOrder = layout.sectionOrder;
260
+ if (hasKeys(layout.itemOrder))
261
+ next.itemOrder = layout.itemOrder;
262
+ if (layout.hidden?.length)
263
+ next.hidden = layout.hidden;
264
+ if (hasKeys(layout.moved))
265
+ next.moved = layout.moved;
266
+ if (hasKeys(layout.panels))
267
+ next.panels = layout.panels;
268
+ return next;
269
+ }
270
+ function sameList(a, b) {
271
+ return a.length === b.length && a.every((value, i) => value === b[i]);
272
+ }
273
+ function clampIndex(index, max) {
274
+ if (!Number.isFinite(index))
275
+ return max;
276
+ return Math.max(0, Math.min(Math.trunc(index), max));
277
+ }
278
+ /**
279
+ * Move a group section to `toIndex` among the groups (the root section is not
280
+ * counted). Returns the layout unchanged when `sectionId` is not a group.
281
+ */
282
+ export function moveShellSection(nav, groups, layout, sectionId, toIndex) {
283
+ const current = normalizeShellLayout(layout ?? createShellLayout());
284
+ const ids = resolveShellNavModel(nav, groups, current)
285
+ .slice(1)
286
+ .map((section) => section.id);
287
+ if (!ids.includes(sectionId))
288
+ return current;
289
+ const next = ids.filter((id) => id !== sectionId);
290
+ next.splice(clampIndex(toIndex, next.length), 0, sectionId);
291
+ const defaults = resolveShellNavModel(nav, groups)
292
+ .slice(1)
293
+ .map((section) => section.id);
294
+ const sectionOrder = sameList(next, defaults) ? undefined : next;
295
+ return compact({ ...current, sectionOrder });
296
+ }
297
+ /**
298
+ * Move an item into `toSectionId` (a group or the root section) at `toIndex`
299
+ * among that section's items, hidden ones included. `toIndex` defaults to the
300
+ * end. Returns the layout unchanged when either id is unknown.
301
+ */
302
+ export function moveShellItem(nav, groups, layout, itemId, toSectionId, toIndex) {
303
+ const current = normalizeShellLayout(layout ?? createShellLayout());
304
+ const model = resolveShellNavModel(nav, groups, current);
305
+ const source = model.find((section) => section.items.some((entry) => entry.id === itemId));
306
+ const target = model.find((section) => section.id === toSectionId);
307
+ if (!source || !target)
308
+ return current;
309
+ const entry = source.items.find((candidate) => candidate.id === itemId);
310
+ if (!entry)
311
+ return current;
312
+ const lists = new Map(model.map((section) => [
313
+ section.id,
314
+ section.items.map((candidate) => candidate.id),
315
+ ]));
316
+ lists.set(source.id, (lists.get(source.id) ?? []).filter((id) => id !== itemId));
317
+ const targetList = [...(lists.get(target.id) ?? [])];
318
+ targetList.splice(clampIndex(toIndex ?? targetList.length, targetList.length), 0, itemId);
319
+ lists.set(target.id, targetList);
320
+ const moved = { ...(current.moved ?? {}) };
321
+ if (target.id === entry.nativeSectionId)
322
+ delete moved[itemId];
323
+ else
324
+ moved[itemId] = target.id;
325
+ // With `moved` settled, a section's default order is what an empty
326
+ // itemOrder would give; write an order only where it differs.
327
+ const settled = { ...current, moved };
328
+ const defaultModel = resolveShellNavModel(nav, groups, {
329
+ version: SHELL_LAYOUT_VERSION,
330
+ moved,
331
+ sectionOrder: current.sectionOrder,
332
+ });
333
+ const itemOrder = { ...(current.itemOrder ?? {}) };
334
+ for (const id of new Set([source.id, target.id])) {
335
+ const defaults = defaultModel
336
+ .find((section) => section.id === id)
337
+ ?.items.map((candidate) => candidate.id);
338
+ const list = lists.get(id) ?? [];
339
+ if (defaults && sameList(list, defaults))
340
+ delete itemOrder[id];
341
+ else
342
+ itemOrder[id] = list;
343
+ }
344
+ return compact({ ...settled, itemOrder });
345
+ }
346
+ /** Hide a section or item. Unknown ids and the root section are ignored. */
347
+ export function hideShellEntry(nav, groups, layout, id) {
348
+ const current = normalizeShellLayout(layout ?? createShellLayout());
349
+ if (!isKnownEntry(nav, groups, id) || current.hidden?.includes(id)) {
350
+ return current;
351
+ }
352
+ return compact({ ...current, hidden: [...(current.hidden ?? []), id] });
353
+ }
354
+ /** Show a hidden section or item again. */
355
+ export function showShellEntry(layout, id) {
356
+ const current = normalizeShellLayout(layout ?? createShellLayout());
357
+ if (!current.hidden?.includes(id))
358
+ return current;
359
+ return compact({
360
+ ...current,
361
+ hidden: current.hidden.filter((entry) => entry !== id),
362
+ });
363
+ }
364
+ function isKnownEntry(nav, groups, id) {
365
+ return resolveShellNavModel(nav, groups).some((section) => (section.group !== null && section.id === id) ||
366
+ section.items.some((entry) => entry.id === id));
367
+ }
368
+ /**
369
+ * Set an edge panel's `visible` and/or `initial`. Values equal to the host's
370
+ * defaults (`defaults`, when given) are dropped to keep the layout sparse.
371
+ */
372
+ export function setShellLayoutPanel(layout, edge, patch, defaults) {
373
+ const current = normalizeShellLayout(layout ?? createShellLayout());
374
+ if (!EDGES.includes(edge))
375
+ return current;
376
+ const panel = { ...(current.panels?.[edge] ?? {}) };
377
+ if ('visible' in patch) {
378
+ if (patch.visible === false)
379
+ panel.visible = false;
380
+ else
381
+ delete panel.visible;
382
+ }
383
+ if ('initial' in patch) {
384
+ if (patch.initial === 'collapsed' || patch.initial === 'expanded') {
385
+ panel.initial = patch.initial;
386
+ }
387
+ else
388
+ delete panel.initial;
389
+ }
390
+ const base = defaults?.[edge];
391
+ if (panel.initial !== undefined &&
392
+ base !== false &&
393
+ panel.initial === (base?.initial ?? 'collapsed')) {
394
+ delete panel.initial;
395
+ }
396
+ const panels = { ...(current.panels ?? {}) };
397
+ if (hasKeys(panel))
398
+ panels[edge] = panel;
399
+ else
400
+ delete panels[edge];
401
+ return compact({ ...current, panels });
402
+ }
@@ -1,12 +1,14 @@
1
+ import { type ShellLayoutPanel } from './layout.js';
1
2
  import type { PanelEdge, PanelState, ResolvedShellConfig, ShellHotkeyBinding, ShellPanelConfig, ShellPanelDefaults, ShellPanelResize, ShellSettingsAdapter, ShellSettingsDelta, ShellViewport } from './types.js';
2
3
  export declare const DEFAULT_SHELL_KEYMAP: Record<PanelEdge, ShellHotkeyBinding>;
3
4
  export declare function resolveShellConfig(appDefaults?: ShellPanelDefaults): ResolvedShellConfig;
4
5
  /**
5
- * An edge's state: hidden edges stay hidden; otherwise the user's setting,
6
- * then the edge's default for `viewport` (when it declares
7
- * `viewportDefaults`), then its configured initial state.
6
+ * An edge's state: hidden edges (by the host or the user's layout) stay
7
+ * hidden; otherwise the user's setting, then the edge's default for
8
+ * `viewport` (when it declares `viewportDefaults`), then the layout's
9
+ * `initial`, then the configured initial state.
8
10
  */
9
- export declare function resolveInitialPanelState(edge: PanelEdge, config: ShellPanelConfig, settings?: ShellSettingsDelta, viewport?: ShellViewport): PanelState;
11
+ export declare function resolveInitialPanelState(edge: PanelEdge, config: ShellPanelConfig, settings?: ShellSettingsDelta, viewport?: ShellViewport, layoutPanel?: ShellLayoutPanel): PanelState;
10
12
  /** Default limits for a resizable side edge (CSS px). */
11
13
  export declare const DEFAULT_SHELL_PANEL_RESIZE: Required<ShellPanelResize>;
12
14
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,SAAS,EAAE,kBAAkB,CAKtE,CAAC;AA6CF,wBAAgB,kBAAkB,CAChC,WAAW,GAAE,kBAAuB,GACnC,mBAAmB,CA2BrB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,EACxB,QAAQ,GAAE,kBAAuB,EACjC,QAAQ,CAAC,EAAE,aAAa,GACvB,UAAU,CAOZ;AAED,yDAAyD;AACzD,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,gBAAgB,CAIjE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,GACvB,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAUnC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACjC,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,OAAO,GAAG,MAAM,GACrB,OAAO,CAKT;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,mBAAmB,GAC1B,kBAAkB,CAqBpB;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,EACxB,QAAQ,GAAE,kBAAuB,GAChC,kBAAkB,GAAG,IAAI,CAM3B;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,kBAAkB,GACvB,kBAAkB,CAiBpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAuBpB;AAED,qBAAa,gCAAiC,YAAW,oBAAoB;IAC/D,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAExC,IAAI,IAAI,kBAAkB,GAAG,IAAI;IAWjC,KAAK,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;CAOvC"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,SAAS,EAAE,kBAAkB,CAKtE,CAAC;AA6CF,wBAAgB,kBAAkB,CAChC,WAAW,GAAE,kBAAuB,GACnC,mBAAmB,CA2BrB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,EACxB,QAAQ,GAAE,kBAAuB,EACjC,QAAQ,CAAC,EAAE,aAAa,EACxB,WAAW,CAAC,EAAE,gBAAgB,GAC7B,UAAU,CASZ;AAED,yDAAyD;AACzD,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,gBAAgB,CAIjE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,GACvB,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAUnC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACjC,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,OAAO,GAAG,MAAM,GACrB,OAAO,CAKT;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,mBAAmB,GAC1B,kBAAkB,CAqBpB;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,EACxB,QAAQ,GAAE,kBAAuB,GAChC,kBAAkB,GAAG,IAAI,CAM3B;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,kBAAkB,GACvB,kBAAkB,CAiBpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CA2BpB;AAED,qBAAa,gCAAiC,YAAW,oBAAoB;IAC/D,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAExC,IAAI,IAAI,kBAAkB,GAAG,IAAI;IAWjC,KAAK,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;CAOvC"}
@@ -1,3 +1,4 @@
1
+ import { isShellLayoutEmpty, normalizeShellLayout, } from './layout.js';
1
2
  import { EDGE_SCOPES, PANEL_EDGES } from './types.js';
2
3
  export const DEFAULT_SHELL_KEYMAP = {
3
4
  top: { code: 'KeyW' },
@@ -73,15 +74,19 @@ export function resolveShellConfig(appDefaults = {}) {
73
74
  return { panels };
74
75
  }
75
76
  /**
76
- * An edge's state: hidden edges stay hidden; otherwise the user's setting,
77
- * then the edge's default for `viewport` (when it declares
78
- * `viewportDefaults`), then its configured initial state.
77
+ * An edge's state: hidden edges (by the host or the user's layout) stay
78
+ * hidden; otherwise the user's setting, then the edge's default for
79
+ * `viewport` (when it declares `viewportDefaults`), then the layout's
80
+ * `initial`, then the configured initial state.
79
81
  */
80
- export function resolveInitialPanelState(edge, config, settings = {}, viewport) {
82
+ export function resolveInitialPanelState(edge, config, settings = {}, viewport, layoutPanel) {
81
83
  if (config.initial === 'hidden')
82
84
  return 'hidden';
85
+ if (layoutPanel?.visible === false)
86
+ return 'hidden';
83
87
  return (settings.panels?.[edge] ??
84
88
  (viewport ? config.viewportDefaults?.[viewport] : undefined) ??
89
+ layoutPanel?.initial ??
85
90
  config.initial);
86
91
  }
87
92
  /** Default limits for a resizable side edge (CSS px). */
@@ -182,6 +187,11 @@ export function pruneShellSettingsDelta(delta) {
182
187
  if (delta.activeFocusToolId !== undefined) {
183
188
  pruned.activeFocusToolId = delta.activeFocusToolId;
184
189
  }
190
+ if (delta.layout !== undefined) {
191
+ const layout = normalizeShellLayout(delta.layout);
192
+ if (!isShellLayoutEmpty(layout))
193
+ pruned.layout = layout;
194
+ }
185
195
  if (delta.keymap && Object.keys(delta.keymap).length > 0) {
186
196
  pruned.keymap = delta.keymap;
187
197
  }