@happyvertical/smrt-svelte 0.51.39 → 0.52.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 (114) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +167 -1
  3. package/agents/workspace.md +24 -0
  4. package/dist/browser-ai/adapters/stt/browser-speech.d.ts +11 -0
  5. package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -1
  6. package/dist/browser-ai/adapters/stt/browser-speech.js +20 -4
  7. package/dist/browser-ai/adapters/stt/browser-speech.test.js +113 -0
  8. package/dist/browser-ai/adapters/stt/dictation-source.d.ts +3 -0
  9. package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -0
  10. package/dist/browser-ai/adapters/stt/dictation-source.js +38 -0
  11. package/dist/browser-ai/adapters/stt/dictation-source.test.js +52 -0
  12. package/dist/browser-ai/adapters/stt/index.d.ts +2 -1
  13. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
  14. package/dist/browser-ai/adapters/stt/index.js +2 -1
  15. package/dist/components/forms/Form.svelte +118 -8
  16. package/dist/components/forms/Form.svelte.d.ts +18 -2
  17. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  18. package/dist/components/forms/FormScope.svelte +202 -0
  19. package/dist/components/forms/FormScope.svelte.d.ts +22 -0
  20. package/dist/components/forms/FormScope.svelte.d.ts.map +1 -0
  21. package/dist/components/forms/__tests__/Form.ui-controls.test.js +119 -0
  22. package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -3
  23. package/dist/components/forms/__tests__/FormScope.test.js +39 -0
  24. package/dist/components/forms/__tests__/form-scope.fixture.svelte +30 -0
  25. package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts +10 -0
  26. package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts.map +1 -0
  27. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte +84 -0
  28. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts +14 -0
  29. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts.map +1 -0
  30. package/dist/components/forms/index.d.ts +1 -0
  31. package/dist/components/forms/index.d.ts.map +1 -1
  32. package/dist/components/forms/index.js +1 -0
  33. package/dist/components/workspace/README.md +11 -0
  34. package/dist/components/workspace/__tests__/AdminShell.responsive.test.js +586 -0
  35. package/dist/components/workspace/__tests__/AdminShell.test.js +85 -0
  36. package/dist/components/workspace/__tests__/TenantNav.test.js +49 -0
  37. package/dist/components/workspace/__tests__/admin-shell-header-overflow.test.js +25 -0
  38. package/dist/components/workspace/__tests__/admin-shell-mobile.test.js +217 -0
  39. package/dist/components/workspace/__tests__/admin-shell-page-contracts.test.js +30 -0
  40. package/dist/components/workspace/__tests__/admin-shell-responsive-state.test.js +184 -0
  41. package/dist/components/workspace/__tests__/admin-shell-theme-hooks.test.js +36 -0
  42. package/dist/components/workspace/__tests__/shell-chrome-components.test.js +108 -0
  43. package/dist/components/workspace/admin-shell/AdminShell.svelte +998 -40
  44. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +42 -1
  45. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  46. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte +170 -0
  47. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts +11 -0
  48. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts.map +1 -0
  49. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +93 -0
  50. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts +11 -0
  51. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts.map +1 -0
  52. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte +88 -0
  53. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts +14 -0
  54. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts.map +1 -0
  55. package/dist/components/workspace/admin-shell/ShellTitle.svelte +42 -0
  56. package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts +11 -0
  57. package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts.map +1 -0
  58. package/dist/components/workspace/admin-shell/TenantNav.svelte +46 -1
  59. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  60. package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts +37 -0
  61. package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts.map +1 -0
  62. package/dist/components/workspace/admin-shell/mobile-shell-dom.js +150 -0
  63. package/dist/components/workspace/admin-shell/mobile-shell.d.ts +191 -0
  64. package/dist/components/workspace/admin-shell/mobile-shell.d.ts.map +1 -0
  65. package/dist/components/workspace/admin-shell/mobile-shell.js +165 -0
  66. package/dist/components/workspace/admin-shell/settings.d.ts +27 -2
  67. package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
  68. package/dist/components/workspace/admin-shell/settings.js +84 -2
  69. package/dist/components/workspace/admin-shell/state.svelte.d.ts +55 -1
  70. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  71. package/dist/components/workspace/admin-shell/state.svelte.js +149 -6
  72. package/dist/components/workspace/admin-shell/types.d.ts +97 -0
  73. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  74. package/dist/components/workspace/admin-shell/types.js +13 -0
  75. package/dist/components/workspace/index.d.ts +11 -4
  76. package/dist/components/workspace/index.d.ts.map +1 -1
  77. package/dist/components/workspace/index.js +12 -3
  78. package/dist/i18n/strings.workspace.d.ts +10 -0
  79. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  80. package/dist/i18n/strings.workspace.js +10 -0
  81. package/dist/index.d.ts +1 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -1
  84. package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +2 -2
  85. package/dist/web/__tests__/list-sort-surface.fixture.svelte +41 -0
  86. package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts +15 -0
  87. package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts.map +1 -0
  88. package/dist/web/__tests__/list-sort-surface.svelte.test.js +59 -0
  89. package/dist/web/__tests__/surfaces.child.fixture.svelte +69 -0
  90. package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts +16 -0
  91. package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts.map +1 -0
  92. package/dist/web/__tests__/surfaces.fixture.svelte +26 -0
  93. package/dist/web/__tests__/surfaces.fixture.svelte.d.ts +18 -0
  94. package/dist/web/__tests__/surfaces.fixture.svelte.d.ts.map +1 -0
  95. package/dist/web/__tests__/surfaces.svelte.test.js +140 -0
  96. package/dist/web/__tests__/webmcp-ui.test.js +18 -0
  97. package/dist/web/index.d.ts +4 -1
  98. package/dist/web/index.d.ts.map +1 -1
  99. package/dist/web/index.js +4 -1
  100. package/dist/web/link-surface.svelte.d.ts +14 -0
  101. package/dist/web/link-surface.svelte.d.ts.map +1 -0
  102. package/dist/web/link-surface.svelte.js +49 -0
  103. package/dist/web/list-surface.svelte.d.ts +80 -0
  104. package/dist/web/list-surface.svelte.d.ts.map +1 -0
  105. package/dist/web/list-surface.svelte.js +309 -0
  106. package/dist/web/step-surface.svelte.d.ts +17 -0
  107. package/dist/web/step-surface.svelte.d.ts.map +1 -0
  108. package/dist/web/step-surface.svelte.js +56 -0
  109. package/dist/web/webmcp-ui-context.d.ts +7 -0
  110. package/dist/web/webmcp-ui-context.d.ts.map +1 -1
  111. package/dist/web/webmcp-ui-context.js +10 -0
  112. package/dist/web/webmcp-ui.d.ts.map +1 -1
  113. package/dist/web/webmcp-ui.js +16 -3
  114. package/package.json +16 -16
@@ -0,0 +1,150 @@
1
+ /**
2
+ * DOM wiring for AdminShell's responsive chrome. Every installer returns a
3
+ * disposer and is a no-op without the browser API it needs, so callers can
4
+ * run them from `onMount` without guards. The decisions live in
5
+ * `./mobile-shell.ts`.
6
+ */
7
+ import { isEditableTarget } from './hotkeys.js';
8
+ import { FORM_ACTION_BAR_SELECTOR, keyboardLikelyOpen, ScrollChrome, viewportFor, } from './mobile-shell.js';
9
+ import { ADMIN_SHELL_DESKTOP_QUERY, ADMIN_SHELL_PHONE_QUERY, } from './state.svelte.js';
10
+ /**
11
+ * Keep `shell.viewport` in step with the phone/desktop media queries (which
12
+ * applies each edge's `viewportDefaults` when the class changes).
13
+ */
14
+ export function installShellViewport(shell) {
15
+ if (typeof window === 'undefined' ||
16
+ typeof window.matchMedia !== 'function') {
17
+ return () => { };
18
+ }
19
+ const phone = window.matchMedia(ADMIN_SHELL_PHONE_QUERY);
20
+ const desktop = window.matchMedia(ADMIN_SHELL_DESKTOP_QUERY);
21
+ const sync = () => shell.setViewport(viewportFor({ phone: phone.matches, desktop: desktop.matches }));
22
+ sync();
23
+ phone.addEventListener('change', sync);
24
+ desktop.addEventListener('change', sync);
25
+ return () => {
26
+ phone.removeEventListener('change', sync);
27
+ desktop.removeEventListener('change', sync);
28
+ };
29
+ }
30
+ /**
31
+ * Keep `shell.overlayMatches` in step with each side edge's `overlayMedia`
32
+ * query (edges without one are skipped).
33
+ */
34
+ export function installShellOverlayMedia(shell) {
35
+ if (typeof window === 'undefined' ||
36
+ typeof window.matchMedia !== 'function') {
37
+ return () => { };
38
+ }
39
+ const disposers = [];
40
+ for (const edge of ['left', 'right']) {
41
+ const query = shell.config.panels[edge].overlayMedia;
42
+ if (!query)
43
+ continue;
44
+ const media = window.matchMedia(query);
45
+ const sync = () => shell.setOverlayMatch(edge, media.matches);
46
+ sync();
47
+ media.addEventListener('change', sync);
48
+ disposers.push(() => media.removeEventListener('change', sync));
49
+ }
50
+ return () => {
51
+ for (const dispose of disposers)
52
+ dispose();
53
+ };
54
+ }
55
+ /**
56
+ * Hide-on-scroll for one scroller (AdminShell's main region). Samples once
57
+ * per animation frame and reports changes of the hidden state.
58
+ */
59
+ export function installScrollChrome(scroller, options) {
60
+ const chrome = new ScrollChrome(options.tuning);
61
+ let frame = 0;
62
+ let last = false;
63
+ function sample() {
64
+ frame = 0;
65
+ const hidden = chrome.update({
66
+ y: scroller.scrollTop,
67
+ maxY: scroller.scrollHeight - scroller.clientHeight,
68
+ pinned: options.isPinned(),
69
+ });
70
+ if (hidden !== last) {
71
+ last = hidden;
72
+ options.onChange(hidden);
73
+ }
74
+ }
75
+ function onScroll() {
76
+ if (!frame)
77
+ frame = requestAnimationFrame(sample);
78
+ }
79
+ scroller.addEventListener('scroll', onScroll, { passive: true });
80
+ return {
81
+ recheck: sample,
82
+ reset() {
83
+ chrome.reset();
84
+ if (last) {
85
+ last = false;
86
+ options.onChange(false);
87
+ }
88
+ },
89
+ destroy() {
90
+ if (frame)
91
+ cancelAnimationFrame(frame);
92
+ scroller.removeEventListener('scroll', onScroll);
93
+ },
94
+ };
95
+ }
96
+ /**
97
+ * Watch for the on-screen keyboard. Sets `data-keyboard-open` on `<html>`
98
+ * (phone bars and `FormActionBar` hide on it) and keeps the focused field in
99
+ * view when the keyboard opens.
100
+ */
101
+ export function installKeyboardWatcher(onChange) {
102
+ if (typeof window === 'undefined' || !window.visualViewport) {
103
+ return () => { };
104
+ }
105
+ const viewport = window.visualViewport;
106
+ let open = false;
107
+ function sync() {
108
+ const next = keyboardLikelyOpen({
109
+ layoutHeight: window.innerHeight,
110
+ visualHeight: viewport.height,
111
+ scale: viewport.scale,
112
+ });
113
+ if (next === open)
114
+ return;
115
+ open = next;
116
+ if (open)
117
+ document.documentElement.dataset.keyboardOpen = '';
118
+ else
119
+ delete document.documentElement.dataset.keyboardOpen;
120
+ onChange(open);
121
+ const active = document.activeElement;
122
+ if (open && active instanceof HTMLElement && isEditableTarget(active)) {
123
+ active.scrollIntoView({ block: 'center' });
124
+ }
125
+ }
126
+ viewport.addEventListener('resize', sync);
127
+ return () => {
128
+ viewport.removeEventListener('resize', sync);
129
+ delete document.documentElement.dataset.keyboardOpen;
130
+ };
131
+ }
132
+ /**
133
+ * Report whether `container` holds a form action bar
134
+ * (`[data-form-action-bar]`), now and whenever its subtree changes.
135
+ */
136
+ export function watchFormActionBar(container, onChange) {
137
+ let present = Boolean(container.querySelector(FORM_ACTION_BAR_SELECTOR));
138
+ onChange(present);
139
+ if (typeof MutationObserver === 'undefined')
140
+ return () => { };
141
+ const observer = new MutationObserver(() => {
142
+ const next = Boolean(container.querySelector(FORM_ACTION_BAR_SELECTOR));
143
+ if (next === present)
144
+ return;
145
+ present = next;
146
+ onChange(present);
147
+ });
148
+ observer.observe(container, { childList: true, subtree: true });
149
+ return () => observer.disconnect();
150
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Pure logic behind AdminShell's phone chrome: the hide-on-scroll state
3
+ * machine, the on-screen keyboard heuristic, the phone top-bar model, and the
4
+ * bottom-bar mode. DOM-free and SSR-safe so it can be unit tested; the DOM
5
+ * wiring lives in `./mobile-shell-dom.ts`.
6
+ */
7
+ import type { Component, ComponentType, SvelteComponent } from 'svelte';
8
+ import type { ShellViewport } from './types.js';
9
+ /** Classify a viewport from the phone and desktop media-query matches. */
10
+ export declare function viewportFor(matches: {
11
+ phone: boolean;
12
+ desktop: boolean;
13
+ }): ShellViewport;
14
+ /** Tuning for {@link ScrollChrome}. */
15
+ export interface ScrollChromeOptions {
16
+ /** Always show the chrome within this many px of the top (default 56). */
17
+ revealWithin?: number;
18
+ /** Downward travel in px before the chrome hides (default 12). */
19
+ hideAfter?: number;
20
+ /** Upward travel in px before it comes back (default 4, "first scroll up"). */
21
+ showAfter?: number;
22
+ }
23
+ /** One scroll position fed to {@link ScrollChrome.update}. */
24
+ export interface ScrollSample {
25
+ /** Current scrollTop. */
26
+ y: number;
27
+ /** Largest reachable scrollTop, to ignore iOS overscroll bounce. */
28
+ maxY?: number;
29
+ /** Keep the chrome shown (a banner is visible, a drawer is open, …). */
30
+ pinned?: boolean;
31
+ }
32
+ /**
33
+ * Hide-on-scroll state machine for the phone top bar (and anything that
34
+ * slides with it). Feed it every scroll position; read `hidden`. Travel
35
+ * accumulates in one direction and resets when the direction reverses.
36
+ */
37
+ export declare class ScrollChrome {
38
+ #private;
39
+ hidden: boolean;
40
+ constructor(options?: ScrollChromeOptions);
41
+ /** Feed a scroll sample; returns whether the chrome is now hidden. */
42
+ update(sample: ScrollSample): boolean;
43
+ /** Show the chrome and forget the scroll history (e.g. after navigating). */
44
+ reset(): void;
45
+ }
46
+ /**
47
+ * `visualViewport` heuristic: the keyboard is open when the visible viewport
48
+ * is much shorter than the layout viewport. `scale` undoes pinch zoom, which
49
+ * also shrinks `visualViewport.height`.
50
+ */
51
+ export declare function keyboardLikelyOpen(input: {
52
+ layoutHeight: number;
53
+ visualHeight: number;
54
+ scale?: number;
55
+ threshold?: number;
56
+ }): boolean;
57
+ /** A nav entry as far as path matching is concerned. */
58
+ export interface ShellNavPathItem {
59
+ href: string;
60
+ exact?: boolean;
61
+ children?: ShellNavPathItem[];
62
+ }
63
+ /** Drop trailing slashes (`/` stays `/`). */
64
+ export declare function normalizeShellPath(path: string): string;
65
+ /** Whether `path` is the item's page or (unless `exact`) below it. */
66
+ export declare function shellNavItemMatches(item: ShellNavPathItem, path: string): boolean;
67
+ /** The chain of nav items from the top level down to the one matching `path`. */
68
+ export declare function findShellNavTrail<T extends ShellNavPathItem>(items: T[], path: string): T[];
69
+ /** One ancestor of the current page: a breadcrumb and a possible back target. */
70
+ export interface ShellCrumb {
71
+ label: string;
72
+ href: string;
73
+ }
74
+ /** Input for {@link shellPageTrailFor}. */
75
+ export interface ShellPageTrailInput<T extends ShellNavPathItem & {
76
+ label: string;
77
+ }> {
78
+ /** Current pathname. */
79
+ path: string;
80
+ /** The workspace home (overview) page. */
81
+ homeHref: string;
82
+ /** Workspace name: the first ancestor of every non-home page. */
83
+ homeTitle: string;
84
+ /** The workspace nav. */
85
+ navItems: T[];
86
+ /**
87
+ * Ancestors deeper than the nav knows (an article above its video, a
88
+ * council above its meeting), nearest last. They follow the nav trail.
89
+ */
90
+ parents?: readonly ShellCrumb[] | null;
91
+ }
92
+ /** Where a page sits: a section home, or a page with ancestors. */
93
+ export interface ShellPageTrail {
94
+ /** The home page or a top-level nav page (and no page-given parents). */
95
+ sectionHome: boolean;
96
+ /** Ancestors only, never the current page; empty on section homes. */
97
+ crumbs: ShellCrumb[];
98
+ /** The nav item for the current page itself, if it is one. */
99
+ current: {
100
+ label: string;
101
+ href: string;
102
+ } | null;
103
+ }
104
+ /**
105
+ * The single source for a page's place in the workspace: breadcrumbs on
106
+ * wider screens and the phone top bar's back target both come from here.
107
+ *
108
+ * Section homes (the home page and top-level nav pages) have no ancestors.
109
+ * Anything else lists the workspace home, the nav items above it (a nested
110
+ * nav page's parent item counts, even when its URL is not below it), then the
111
+ * page-given `parents`. The current page is never a crumb.
112
+ */
113
+ export declare function shellPageTrailFor<T extends ShellNavPathItem & {
114
+ label: string;
115
+ }>(input: ShellPageTrailInput<T>): ShellPageTrail;
116
+ /**
117
+ * What the phone top bar shows: the workspace name on section home pages, or
118
+ * a back arrow (to the parent, never `history.back()`) plus the page's
119
+ * title on every other page.
120
+ */
121
+ export type PhoneTopBarModel = {
122
+ kind: 'home';
123
+ title: string;
124
+ } | {
125
+ kind: 'detail';
126
+ title: string;
127
+ backHref: string;
128
+ backLabel: string;
129
+ };
130
+ /** Input for {@link phoneTopBarFor}. */
131
+ export interface PhoneTopBarInput<T extends ShellNavPathItem & {
132
+ label: string;
133
+ }> extends ShellPageTrailInput<T> {
134
+ /** Title the page provided (article headline, person's name, …). */
135
+ pageTitle?: string | null;
136
+ /**
137
+ * Explicit back target. Prefer `parents` (the same ancestors the page's
138
+ * breadcrumbs show); this overrides the last one.
139
+ */
140
+ backHref?: string | null;
141
+ /** Back label used with an explicit `backHref` (default "Back"). */
142
+ backLabel?: string;
143
+ }
144
+ /**
145
+ * Section homes (see {@link shellPageTrailFor}) show the workspace name.
146
+ * Every other page shows a back arrow to its nearest ancestor (the last
147
+ * breadcrumb) and the page's title.
148
+ */
149
+ export declare function phoneTopBarFor<T extends ShellNavPathItem & {
150
+ label: string;
151
+ }>(input: PhoneTopBarInput<T>): PhoneTopBarModel;
152
+ /**
153
+ * What sits at the bottom of a phone screen: the nav bar, a form's own
154
+ * action bar (which replaces it), or nothing while the keyboard is open.
155
+ */
156
+ export type BottomBarMode = 'nav' | 'form' | 'none';
157
+ /** Decide the {@link BottomBarMode}. */
158
+ export declare function bottomBarMode(input: {
159
+ formActions: boolean;
160
+ keyboardOpen: boolean;
161
+ }): BottomBarMode;
162
+ /** Selector a form's action row carries to replace the phone bottom bar. */
163
+ export declare const FORM_ACTION_BAR_SELECTOR = "[data-form-action-bar]";
164
+ /**
165
+ * An icon component: a Svelte 5 function component or a legacy class
166
+ * component (e.g. lucide-svelte icons).
167
+ */
168
+ export type ShellIcon = Component<{
169
+ size?: number | string;
170
+ }> | ComponentType<SvelteComponent<{
171
+ size?: number | string;
172
+ }>>;
173
+ /** One phone bottom bar entry: a link (`href`; `onclick` also runs) or a button. */
174
+ export interface PhoneBottomBarItem {
175
+ id: string;
176
+ label: string;
177
+ icon: ShellIcon;
178
+ href?: string;
179
+ onclick?: () => void;
180
+ active?: boolean;
181
+ /** Count badge (hidden at 0). */
182
+ badge?: number;
183
+ /** Small dot, e.g. an unread reply. */
184
+ dot?: boolean;
185
+ /** Accessible description of the dot (default "new"). */
186
+ dotLabel?: string;
187
+ /** For items that open a panel: its expanded state and id. */
188
+ expanded?: boolean;
189
+ controls?: string;
190
+ }
191
+ //# sourceMappingURL=mobile-shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-shell.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/mobile-shell.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ /** Classify a viewport from the phone and desktop media-query matches. */
2
+ export function viewportFor(matches) {
3
+ if (matches.phone)
4
+ return 'phone';
5
+ return matches.desktop ? 'desktop' : 'tablet';
6
+ }
7
+ /**
8
+ * Hide-on-scroll state machine for the phone top bar (and anything that
9
+ * slides with it). Feed it every scroll position; read `hidden`. Travel
10
+ * accumulates in one direction and resets when the direction reverses.
11
+ */
12
+ export class ScrollChrome {
13
+ hidden = false;
14
+ #lastY = 0;
15
+ #travel = 0;
16
+ #revealWithin;
17
+ #hideAfter;
18
+ #showAfter;
19
+ constructor(options = {}) {
20
+ this.#revealWithin = options.revealWithin ?? 56;
21
+ this.#hideAfter = options.hideAfter ?? 12;
22
+ this.#showAfter = options.showAfter ?? 4;
23
+ }
24
+ /** Feed a scroll sample; returns whether the chrome is now hidden. */
25
+ update(sample) {
26
+ const max = sample.maxY ?? Number.POSITIVE_INFINITY;
27
+ const y = Math.min(Math.max(sample.y, 0), Math.max(max, 0));
28
+ if (sample.pinned || y <= this.#revealWithin) {
29
+ this.hidden = false;
30
+ this.#lastY = y;
31
+ this.#travel = 0;
32
+ return this.hidden;
33
+ }
34
+ const delta = y - this.#lastY;
35
+ this.#lastY = y;
36
+ if (delta === 0)
37
+ return this.hidden;
38
+ this.#travel =
39
+ Math.sign(delta) === Math.sign(this.#travel)
40
+ ? this.#travel + delta
41
+ : delta;
42
+ if (this.#travel >= this.#hideAfter)
43
+ this.hidden = true;
44
+ else if (-this.#travel >= this.#showAfter)
45
+ this.hidden = false;
46
+ return this.hidden;
47
+ }
48
+ /** Show the chrome and forget the scroll history (e.g. after navigating). */
49
+ reset() {
50
+ this.hidden = false;
51
+ this.#lastY = 0;
52
+ this.#travel = 0;
53
+ }
54
+ }
55
+ // ---- On-screen keyboard ---------------------------------------------------
56
+ /**
57
+ * `visualViewport` heuristic: the keyboard is open when the visible viewport
58
+ * is much shorter than the layout viewport. `scale` undoes pinch zoom, which
59
+ * also shrinks `visualViewport.height`.
60
+ */
61
+ export function keyboardLikelyOpen(input) {
62
+ const visible = input.visualHeight * (input.scale ?? 1);
63
+ return input.layoutHeight - visible > (input.threshold ?? 150);
64
+ }
65
+ /** Drop trailing slashes (`/` stays `/`). */
66
+ export function normalizeShellPath(path) {
67
+ return path.replace(/\/+$/, '') || '/';
68
+ }
69
+ /** Whether `path` is the item's page or (unless `exact`) below it. */
70
+ export function shellNavItemMatches(item, path) {
71
+ const href = normalizeShellPath(item.href);
72
+ const current = normalizeShellPath(path);
73
+ if (href === current)
74
+ return true;
75
+ if (item.exact || href === '/')
76
+ return false;
77
+ return current.startsWith(`${href}/`);
78
+ }
79
+ /** The chain of nav items from the top level down to the one matching `path`. */
80
+ export function findShellNavTrail(items, path) {
81
+ for (const item of items) {
82
+ const childTrail = item.children
83
+ ? findShellNavTrail(item.children, path)
84
+ : [];
85
+ if (childTrail.length > 0)
86
+ return [item, ...childTrail];
87
+ if (shellNavItemMatches(item, path))
88
+ return [item];
89
+ }
90
+ return [];
91
+ }
92
+ /**
93
+ * The single source for a page's place in the workspace: breadcrumbs on
94
+ * wider screens and the phone top bar's back target both come from here.
95
+ *
96
+ * Section homes (the home page and top-level nav pages) have no ancestors.
97
+ * Anything else lists the workspace home, the nav items above it (a nested
98
+ * nav page's parent item counts, even when its URL is not below it), then the
99
+ * page-given `parents`. The current page is never a crumb.
100
+ */
101
+ export function shellPageTrailFor(input) {
102
+ const path = normalizeShellPath(input.path);
103
+ const home = normalizeShellPath(input.homeHref);
104
+ const parents = [...(input.parents ?? [])];
105
+ const trail = findShellNavTrail(input.navItems, path);
106
+ const last = trail.at(-1);
107
+ const currentItem = last && normalizeShellPath(last.href) === path ? last : null;
108
+ const current = currentItem
109
+ ? { label: currentItem.label, href: currentItem.href }
110
+ : null;
111
+ const topLevelHome = path === home ||
112
+ input.navItems.some((item) => normalizeShellPath(item.href) === path);
113
+ if (topLevelHome && parents.length === 0) {
114
+ return { sectionHome: true, crumbs: [], current };
115
+ }
116
+ const crumbs = [
117
+ { label: input.homeTitle, href: input.homeHref },
118
+ ];
119
+ for (const item of trail) {
120
+ if (item === currentItem)
121
+ continue;
122
+ const href = normalizeShellPath(item.href);
123
+ if (href === home || href === path)
124
+ continue;
125
+ crumbs.push({ label: item.label, href: item.href });
126
+ }
127
+ for (const parent of parents) {
128
+ const href = normalizeShellPath(parent.href);
129
+ if (href === path)
130
+ continue;
131
+ // A page that names an ancestor the nav already supplied (a form under
132
+ // its list's nav item) must not show that crumb twice.
133
+ if (crumbs.some((crumb) => normalizeShellPath(crumb.href) === href))
134
+ continue;
135
+ crumbs.push({ label: parent.label, href: parent.href });
136
+ }
137
+ return { sectionHome: false, crumbs, current };
138
+ }
139
+ /**
140
+ * Section homes (see {@link shellPageTrailFor}) show the workspace name.
141
+ * Every other page shows a back arrow to its nearest ancestor (the last
142
+ * breadcrumb) and the page's title.
143
+ */
144
+ export function phoneTopBarFor(input) {
145
+ const trail = shellPageTrailFor(input);
146
+ if (trail.sectionHome && !input.backHref) {
147
+ return { kind: 'home', title: input.homeTitle };
148
+ }
149
+ const parent = trail.crumbs.at(-1) ?? {
150
+ label: input.homeTitle,
151
+ href: input.homeHref,
152
+ };
153
+ const backHref = input.backHref ?? parent.href;
154
+ const backLabel = input.backHref ? (input.backLabel ?? 'Back') : parent.label;
155
+ const title = input.pageTitle?.trim() || trail.current?.label || parent.label;
156
+ return { kind: 'detail', title, backHref, backLabel };
157
+ }
158
+ /** Decide the {@link BottomBarMode}. */
159
+ export function bottomBarMode(input) {
160
+ if (input.keyboardOpen)
161
+ return 'none';
162
+ return input.formActions ? 'form' : 'nav';
163
+ }
164
+ /** Selector a form's action row carries to replace the phone bottom bar. */
165
+ export const FORM_ACTION_BAR_SELECTOR = '[data-form-action-bar]';
@@ -1,7 +1,32 @@
1
- import type { PanelEdge, PanelState, ResolvedShellConfig, ShellHotkeyBinding, ShellPanelConfig, ShellPanelDefaults, ShellSettingsAdapter, ShellSettingsDelta } from './types.js';
1
+ import type { PanelEdge, PanelState, ResolvedShellConfig, ShellHotkeyBinding, ShellPanelConfig, ShellPanelDefaults, ShellPanelResize, ShellSettingsAdapter, ShellSettingsDelta, ShellViewport } from './types.js';
2
2
  export declare const DEFAULT_SHELL_KEYMAP: Record<PanelEdge, ShellHotkeyBinding>;
3
3
  export declare function resolveShellConfig(appDefaults?: ShellPanelDefaults): ResolvedShellConfig;
4
- export declare function resolveInitialPanelState(edge: PanelEdge, config: ShellPanelConfig, settings?: ShellSettingsDelta): PanelState;
4
+ /**
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.
8
+ */
9
+ export declare function resolveInitialPanelState(edge: PanelEdge, config: ShellPanelConfig, settings?: ShellSettingsDelta, viewport?: ShellViewport): PanelState;
10
+ /** Default limits for a resizable side edge (CSS px). */
11
+ export declare const DEFAULT_SHELL_PANEL_RESIZE: Required<ShellPanelResize>;
12
+ /**
13
+ * Resolved resize limits for an edge, or `null` when it is not resizable.
14
+ * Only `left` and `right` edges resize.
15
+ */
16
+ export declare function resolvePanelResize(edge: PanelEdge, config: ShellPanelConfig): Required<ShellPanelResize> | null;
17
+ /** Clamp a requested width into an edge's resize limits (rounded to px). */
18
+ export declare function clampPanelSize(size: number, limits: Required<ShellPanelResize>): number;
19
+ /**
20
+ * Whether the settings adapter stores an edge's `state` (open/closed) or
21
+ * `size`. Edges with `viewportDefaults` never store their state: it follows
22
+ * the viewport class.
23
+ */
24
+ export declare function panelPersists(config: ShellPanelConfig, what: 'state' | 'size'): boolean;
25
+ /**
26
+ * Drop the panel states and sizes the config says not to store, so they are
27
+ * neither written by nor read back from a settings adapter.
28
+ */
29
+ export declare function stripUnpersistedSettings(delta: ShellSettingsDelta, config: ResolvedShellConfig): ShellSettingsDelta;
5
30
  export declare function resolveHotkey(edge: PanelEdge, config: ShellPanelConfig, settings?: ShellSettingsDelta): ShellHotkeyBinding | null;
6
31
  export declare function mergeShellSettingsDelta(base: ShellSettingsDelta, next: ShellSettingsDelta): ShellSettingsDelta;
7
32
  export declare function pruneShellSettingsDelta(delta: ShellSettingsDelta): ShellSettingsDelta;
@@ -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,oBAAoB,EACpB,kBAAkB,EACnB,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,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,gBAAgB,EACxB,QAAQ,GAAE,kBAAuB,GAChC,UAAU,CAGZ;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,CAapB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAepB;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,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"}
@@ -72,10 +72,84 @@ export function resolveShellConfig(appDefaults = {}) {
72
72
  }));
73
73
  return { panels };
74
74
  }
75
- export function resolveInitialPanelState(edge, config, settings = {}) {
75
+ /**
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.
79
+ */
80
+ export function resolveInitialPanelState(edge, config, settings = {}, viewport) {
76
81
  if (config.initial === 'hidden')
77
82
  return 'hidden';
78
- return settings.panels?.[edge] ?? config.initial;
83
+ return (settings.panels?.[edge] ??
84
+ (viewport ? config.viewportDefaults?.[viewport] : undefined) ??
85
+ config.initial);
86
+ }
87
+ /** Default limits for a resizable side edge (CSS px). */
88
+ export const DEFAULT_SHELL_PANEL_RESIZE = {
89
+ min: 240,
90
+ max: 720,
91
+ step: 16,
92
+ };
93
+ /**
94
+ * Resolved resize limits for an edge, or `null` when it is not resizable.
95
+ * Only `left` and `right` edges resize.
96
+ */
97
+ export function resolvePanelResize(edge, config) {
98
+ if (edge !== 'left' && edge !== 'right')
99
+ return null;
100
+ if (!config.resizable || config.initial === 'hidden')
101
+ return null;
102
+ const limits = config.resizable === true ? {} : config.resizable;
103
+ const min = Math.max(0, limits.min ?? DEFAULT_SHELL_PANEL_RESIZE.min);
104
+ return {
105
+ min,
106
+ max: Math.max(min, limits.max ?? DEFAULT_SHELL_PANEL_RESIZE.max),
107
+ step: Math.max(1, limits.step ?? DEFAULT_SHELL_PANEL_RESIZE.step),
108
+ };
109
+ }
110
+ /** Clamp a requested width into an edge's resize limits (rounded to px). */
111
+ export function clampPanelSize(size, limits) {
112
+ return Math.round(Math.min(limits.max, Math.max(limits.min, size)));
113
+ }
114
+ /**
115
+ * Whether the settings adapter stores an edge's `state` (open/closed) or
116
+ * `size`. Edges with `viewportDefaults` never store their state: it follows
117
+ * the viewport class.
118
+ */
119
+ export function panelPersists(config, what) {
120
+ if (what === 'state' && config.viewportDefaults)
121
+ return false;
122
+ const persist = config.persist ?? true;
123
+ if (typeof persist === 'boolean')
124
+ return persist;
125
+ return persist[what] ?? true;
126
+ }
127
+ /**
128
+ * Drop the panel states and sizes the config says not to store, so they are
129
+ * neither written by nor read back from a settings adapter.
130
+ */
131
+ export function stripUnpersistedSettings(delta, config) {
132
+ const stripped = { ...delta };
133
+ for (const key of ['panels', 'sizes']) {
134
+ const values = delta[key];
135
+ if (!values)
136
+ continue;
137
+ const kept = {};
138
+ for (const edge of PANEL_EDGES) {
139
+ if (!(edge in values))
140
+ continue;
141
+ if (panelPersists(config.panels[edge], key === 'panels' ? 'state' : 'size')) {
142
+ kept[edge] = values[edge];
143
+ }
144
+ }
145
+ if (Object.keys(kept).length > 0) {
146
+ stripped[key] = kept;
147
+ }
148
+ else {
149
+ delete stripped[key];
150
+ }
151
+ }
152
+ return stripped;
79
153
  }
80
154
  export function resolveHotkey(edge, config, settings = {}) {
81
155
  if (config.initial === 'hidden')
@@ -95,6 +169,9 @@ export function mergeShellSettingsDelta(base, next) {
95
169
  panels: base.panels || next.panels
96
170
  ? { ...(base.panels ?? {}), ...(next.panels ?? {}) }
97
171
  : undefined,
172
+ sizes: base.sizes || next.sizes
173
+ ? { ...(base.sizes ?? {}), ...(next.sizes ?? {}) }
174
+ : undefined,
98
175
  };
99
176
  }
100
177
  export function pruneShellSettingsDelta(delta) {
@@ -111,6 +188,11 @@ export function pruneShellSettingsDelta(delta) {
111
188
  if (delta.panels && Object.keys(delta.panels).length > 0) {
112
189
  pruned.panels = delta.panels;
113
190
  }
191
+ if (delta.sizes) {
192
+ const sizes = Object.fromEntries(Object.entries(delta.sizes).filter(([, size]) => typeof size === 'number' && Number.isFinite(size)));
193
+ if (Object.keys(sizes).length > 0)
194
+ pruned.sizes = sizes;
195
+ }
114
196
  return pruned;
115
197
  }
116
198
  export class LocalStorageShellSettingsAdapter {