@lapismd/design-core 0.1.0 → 0.2.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/CHANGELOG.md +44 -0
- package/LICENSE.md +3 -3
- package/README.md +21 -1
- package/package.json +34 -15
- package/spec/src/styling-and-themes.md +11 -9
- package/src/shared/ai/chat.css +25 -1
- package/src/shared/ai/chat.ts +12 -0
- package/src/shared/ai/composer-input/ComposerInput.svelte +3 -2
- package/src/shared/ai/context.svelte.ts +2 -0
- package/src/shared/ai/experimental/reaction-bar/ReactionBar.svelte +1 -1
- package/src/shared/ai/layout/Layout.svelte +116 -10
- package/src/shared/ai/message-list/MessageList.svelte +123 -43
- package/src/shared/ai/message-list/MessageListHistoryFixture.svelte +112 -0
- package/src/shared/ai/message-list/MessageListLegacyFixture.svelte +52 -0
- package/src/shared/ai/message-list/VirtualizedMessageRows.svelte +299 -0
- package/src/shared/ai/message-list/position.ts +7 -0
- package/src/shared/ai/new-messages.svelte.ts +24 -5
- package/src/shared/ai/stream-scroll.svelte.ts +135 -6
- package/src/shared/ai/tool-call-detail/ToolCallDetail.svelte +71 -0
- package/src/shared/ai/tool-call-detail/tool-call-detail.ts +209 -0
- package/src/shared/ai/tool-calls/ToolCalls.svelte +1 -1
- package/src/shared/ai/trigger-menu.ts +2 -3
- package/src/shared/diff/core/merge/actions.ts +4 -3
- package/src/shared/diff/core/merge/render-model.ts +14 -10
- package/src/shared/diff/file-diff/FileDiff.css +68 -1
- package/src/shared/diff/file-diff/FileDiff.svelte +27 -14
- package/src/shared/diff/file-diff/FileDiffComposer.svelte +98 -16
- package/src/shared/diff/file-diff/index.ts +2 -0
- package/src/shared/diff/file-diff/types.ts +13 -0
- package/src/shared/diff/file-listing/FileListingViewModeToggle.css +42 -5
- package/src/shared/diff/index.ts +2 -0
- package/src/shared/diff/merge-editor/MergeEditor.svelte +2 -7
- package/src/shared/filter/filter-query/filters.ts +2 -2
- package/src/shared/filter/filter-query/parser.ts +9 -8
- package/src/shared/filter/power-search/PowerSearch.svelte +1 -1
- package/src/shared/filter/search-filter-bar/SearchFilterBar.svelte +2 -2
- package/src/shared/filter/search-filter-bar/SearchFilterInput.svelte +1 -1
- package/src/shared/filter/search-filter-bar/SearchFilterPredicateEditor.svelte +1 -1
- package/src/shared/filter/search-filter-bar/search-filter-syntax.ts +1 -1
- package/src/shared/forms/autocomplete-input/AutocompleteInput.svelte +19 -7
- package/src/shared/forms/confirm-button/ConfirmButton.svelte +38 -0
- package/src/shared/forms/confirm-button/ConfirmButtonExample.svelte +14 -0
- package/src/shared/forms/confirm-button/index.ts +1 -0
- package/src/shared/forms/confirm-dialog/ConfirmDialog.css +21 -0
- package/src/shared/forms/confirm-dialog/ConfirmDialog.svelte +116 -0
- package/src/shared/forms/confirm-dialog/ConfirmDialogExample.svelte +17 -0
- package/src/shared/forms/confirm-dialog/index.ts +2 -0
- package/src/shared/forms/dialog-frame/DialogFrame.css +130 -0
- package/src/shared/forms/dialog-frame/DialogFrame.svelte +108 -0
- package/src/shared/forms/dialog-frame/DialogFrameExample.svelte +37 -0
- package/src/shared/forms/dialog-frame/index.ts +6 -0
- package/src/shared/forms/dialog.ts +8 -0
- package/src/shared/forms/index.ts +11 -0
- package/src/shared/shadcn/button/button.svelte +4 -0
- package/src/shared/shadcn/code-block/highlightRanges.ts +10 -6
- package/src/shared/shadcn/code-block/syntax/defineSyntaxTheme.ts +4 -2
- package/src/shared/shadcn/code-block/tokenizer.ts +3 -3
- package/src/shared/shadcn/command/command-dialog.svelte +1 -1
- package/src/shared/shadcn/command/command-group.svelte +3 -1
- package/src/shared/shadcn/command-view/command-view.svelte +1 -6
- package/src/shared/shadcn/dialog/dialog-content.svelte +3 -4
- package/src/shared/shadcn/dialog/dialog-portal.svelte +10 -2
- package/src/shared/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -691
- package/src/shared/shadcn/dropdown-menu/dropdown-menu-content.svelte +691 -0
- package/src/shared/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +1 -1
- package/src/shared/shadcn/scroll-area/scroll-area.svelte +2 -0
- package/src/shared/shadcn/spinner/spinner.svelte +3 -3
- package/src/shared/shadcn/tooltip/tooltip-content.svelte +10 -4
- package/src/shared/shadcn/tooltip/tooltip.tokens.ts +4 -0
- package/src/shared/shell/app-shell/AppShell.css +112 -32
- package/src/shared/shell/app-shell/AppShell.demo.css +37 -0
- package/src/shared/shell/app-shell/AppShellBodyContent.svelte +5 -1
- package/src/shared/shell/app-shell/AppShellBodySidebar.svelte +5 -1
- package/src/shared/shell/app-shell/AppShellMain.svelte +12 -1
- package/src/shared/shell/app-shell/AppShellMobilePanelSelector.svelte +1 -1
- package/src/shared/shell/app-shell/AppShellRoot.svelte +27 -13
- package/src/shared/shell/app-shell/AppShellSidebar.svelte +121 -19
- package/src/shared/shell/app-shell/AppShellSidebarClose.svelte +17 -17
- package/src/shared/shell/app-shell/AppShellSidebarToggle.svelte +38 -58
- package/src/shared/shell/app-shell/AppShellSurfaceLayer.svelte +173 -0
- package/src/shared/shell/app-shell/AppShellSurfaceLayerBody.svelte +32 -0
- package/src/shared/shell/app-shell/AppShellSurfaceLayerHeader.svelte +26 -0
- package/src/shared/shell/app-shell/app-shell-context.svelte.ts +6 -3
- package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +269 -11
- package/src/shared/shell/app-shell/app-shell-sidebar-context.svelte.ts +1 -0
- package/src/shared/shell/app-shell/examples/AppShellBodyDemo.svelte +4 -0
- package/src/shared/shell/app-shell/examples/AppShellCompleteDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellConversationDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellExamples.css +624 -0
- package/src/shared/shell/app-shell/examples/AppShellFilesSidebarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellMarkdownDocumentDemo.svelte +6 -1
- package/src/shared/shell/app-shell/examples/AppShellMarkdownFilesDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellModeSwitchDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellMultiPanelDemo.svelte +187 -0
- package/src/shared/shell/app-shell/examples/AppShellProjectSidebarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellSidebarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/examples/AppShellToolbarDemo.svelte +1 -0
- package/src/shared/shell/app-shell/index.ts +14 -0
- package/src/shared/shell/shell.tokens.css +2 -0
- package/src/shared/shell/shell.tokens.ts +5 -0
- package/src/shared/workspace/menu/WorkspaceContextMenuItems.svelte +2 -1
- package/src/shared/workspace/menu/WorkspaceMenuItems.svelte +2 -1
- package/src/shared/workspace/problems/ProblemsQuickFix.svelte +1 -4
- package/src/shared/workspace/settings/AppSettingsContent.svelte +6 -0
- package/src/shared/workspace/settings/AppSettingsNavigation.svelte +7 -0
- package/src/shared/workspace/settings/CopyableValue.svelte +81 -0
- package/src/shared/workspace/settings/DeclarativeSettingsSection.svelte +20 -0
- package/src/shared/workspace/settings/WorkspaceSettingField.svelte +110 -41
- package/src/shared/workspace/settings/WorkspaceSettingMultiSelect.svelte +7 -6
- package/src/shared/workspace/settings/WorkspaceSettingObjectTable.svelte +187 -23
- package/src/shared/workspace/settings/WorkspaceSettingSelect.svelte +29 -29
- package/src/shared/workspace/settings/WorkspaceSettings.css +190 -4
- package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomField.svelte +2 -2
- package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomPage.svelte +13 -0
- package/src/shared/workspace/settings/WorkspaceSettingsSurface.svelte +50 -0
- package/src/shared/workspace/settings/configuration.ts +5 -0
- package/src/shared/workspace/settings/copyable-value.ts +10 -0
- package/src/shared/workspace/settings/index.ts +5 -0
- package/src/shared/workspace/settings/settings-controller.svelte.ts +349 -15
- package/src/shared/workspace/settings/types.ts +90 -8
- package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.css +25 -0
- package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.svelte +48 -0
- package/src/storybook/app-shell-reference/index.ts +1 -0
- package/src/theme.css +10 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { WithElementRef } from "../../../lib/utils.js";
|
|
4
|
+
import { useAppShell } from "./app-shell-context.svelte.js";
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
ref = $bindable(null),
|
|
8
|
+
class: className,
|
|
9
|
+
children,
|
|
10
|
+
...restProps
|
|
11
|
+
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
|
12
|
+
|
|
13
|
+
useAppShell();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<header
|
|
17
|
+
bind:this={ref}
|
|
18
|
+
{...restProps}
|
|
19
|
+
class={["ui-minimal-app-shell__surface-layer-header", className]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join(" ")}
|
|
22
|
+
data-ui-component="app-shell"
|
|
23
|
+
data-ui-part="surface-layer-header"
|
|
24
|
+
>
|
|
25
|
+
{@render children?.()}
|
|
26
|
+
</header>
|
|
@@ -15,9 +15,7 @@ export function setAppShellContext(
|
|
|
15
15
|
* Call this during component initialization, as required by Svelte context.
|
|
16
16
|
*/
|
|
17
17
|
export function useAppShell(): AppShellController {
|
|
18
|
-
const controller =
|
|
19
|
-
APP_SHELL_CONTEXT,
|
|
20
|
-
);
|
|
18
|
+
const controller = useOptionalAppShell();
|
|
21
19
|
if (!controller) {
|
|
22
20
|
throw new Error(
|
|
23
21
|
"AppShell components must be rendered inside AppShell.Root",
|
|
@@ -25,3 +23,8 @@ export function useAppShell(): AppShellController {
|
|
|
25
23
|
}
|
|
26
24
|
return controller;
|
|
27
25
|
}
|
|
26
|
+
|
|
27
|
+
/** Return the nearest root controller when a compound action may live outside it. */
|
|
28
|
+
export function useOptionalAppShell(): AppShellController | undefined {
|
|
29
|
+
return getContext<AppShellController | undefined>(APP_SHELL_CONTEXT);
|
|
30
|
+
}
|
|
@@ -14,6 +14,7 @@ export type AppShellDisplayMode = "auto" | "desktop" | "mobile";
|
|
|
14
14
|
export type AppShellResolvedDisplayMode = Exclude<AppShellDisplayMode, "auto">;
|
|
15
15
|
export type AppShellMobileStage = "left" | "main" | "right";
|
|
16
16
|
export type AppShellMobilePanelKind = "sidebar" | "body-sidebar";
|
|
17
|
+
export type AppShellConstrainedPresentation = "preview" | "replace-main";
|
|
17
18
|
|
|
18
19
|
export interface AppShellMobilePanelRegistration {
|
|
19
20
|
/** Stable id used by mobile selectors and targeted toggle actions. */
|
|
@@ -26,6 +27,19 @@ export interface AppShellMobilePanelRegistration {
|
|
|
26
27
|
readonly kind: AppShellMobilePanelKind;
|
|
27
28
|
/** Current panel landmark, when mounted in a browser. */
|
|
28
29
|
readonly element?: HTMLElement | null;
|
|
30
|
+
/** Lower values leave inline desktop flow before higher values. */
|
|
31
|
+
readonly constraintPriority?: number;
|
|
32
|
+
/** Presentation used when protected main width displaces this panel. */
|
|
33
|
+
readonly constrainedPresentation?: AppShellConstrainedPresentation;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AppShellSurfaceLayerRegistration {
|
|
37
|
+
/** Unique mounted layer owner. Only one may be active per root. */
|
|
38
|
+
readonly token: symbol;
|
|
39
|
+
/** Sidebar ids included in the covered, inert surface. */
|
|
40
|
+
readonly coverPanelIds: readonly string[];
|
|
41
|
+
/** Sidebar ids removed transiently without changing durable layout. */
|
|
42
|
+
readonly suspendPanelIds: readonly string[];
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
export const APP_SHELL_DEFAULT_SIDEBAR_WIDTH = 288;
|
|
@@ -171,14 +185,17 @@ export class AppShellMobileController {
|
|
|
171
185
|
if (returnFocus) this.#returnFocus = returnFocus;
|
|
172
186
|
this.#rootElement?.focus({ preventScroll: true });
|
|
173
187
|
this.stage = side;
|
|
174
|
-
queueMicrotask(() =>
|
|
188
|
+
queueMicrotask(() =>
|
|
189
|
+
this.activePanel(side)?.element?.focus({ preventScroll: true }),
|
|
190
|
+
);
|
|
175
191
|
}
|
|
176
192
|
|
|
177
193
|
showMain(restoreFocus = true): void {
|
|
178
194
|
const focusTarget = this.#returnFocus ?? this.#mainElement;
|
|
179
195
|
this.#rootElement?.focus({ preventScroll: true });
|
|
180
196
|
this.stage = "main";
|
|
181
|
-
if (restoreFocus)
|
|
197
|
+
if (restoreFocus)
|
|
198
|
+
queueMicrotask(() => focusTarget?.focus({ preventScroll: true }));
|
|
182
199
|
this.#returnFocus = null;
|
|
183
200
|
}
|
|
184
201
|
|
|
@@ -208,11 +225,21 @@ export class AppShellMobileController {
|
|
|
208
225
|
this.#mainElement = element;
|
|
209
226
|
}
|
|
210
227
|
|
|
228
|
+
/** @internal Return the mounted main landmark. */
|
|
229
|
+
getMainElement(): HTMLElement | null {
|
|
230
|
+
return this.#mainElement;
|
|
231
|
+
}
|
|
232
|
+
|
|
211
233
|
/** @internal Register the root as a safe focus waypoint between lanes. */
|
|
212
234
|
setRootElement(element: HTMLElement | null): void {
|
|
213
235
|
this.#rootElement = element;
|
|
214
236
|
}
|
|
215
237
|
|
|
238
|
+
/** @internal Return the mounted shell root. */
|
|
239
|
+
getRootElement(): HTMLElement | null {
|
|
240
|
+
return this.#rootElement;
|
|
241
|
+
}
|
|
242
|
+
|
|
216
243
|
#ensureActivePanel(side: AppShellSide): void {
|
|
217
244
|
const panels = this.panelsFor(side);
|
|
218
245
|
const current = this.activePanelId(side);
|
|
@@ -231,6 +258,7 @@ type AppShellSidebarLayoutChangeSource = Exclude<
|
|
|
231
258
|
type AppShellSidebarLayoutChangeListener = (
|
|
232
259
|
source: AppShellSidebarLayoutChangeSource,
|
|
233
260
|
) => void;
|
|
261
|
+
type AppShellProjectionChangeListener = () => void;
|
|
234
262
|
|
|
235
263
|
/** Reactive state for one side of an App Shell. */
|
|
236
264
|
export class AppShellSidebarController {
|
|
@@ -449,17 +477,28 @@ export class AppShellController {
|
|
|
449
477
|
readonly right: AppShellSidebarController;
|
|
450
478
|
readonly mobile = new AppShellMobileController();
|
|
451
479
|
layoutReady = $state(false);
|
|
480
|
+
constrainedPanelIds = $state<string[]>([]);
|
|
481
|
+
desktopPreviewPanelIds = $state<string[]>([]);
|
|
482
|
+
activeSurfaceLayer = $state<AppShellSurfaceLayerRegistration | null>(null);
|
|
483
|
+
projectionVersion = $state(0);
|
|
484
|
+
panelElementsVersion = $state(0);
|
|
452
485
|
|
|
453
|
-
readonly #persistence
|
|
486
|
+
readonly #persistence: AppShellLayoutPersistence | undefined;
|
|
454
487
|
readonly #saveDebounceMs: number;
|
|
455
|
-
readonly #onPersistenceError
|
|
488
|
+
readonly #onPersistenceError:
|
|
489
|
+
| ((event: AppShellPersistenceErrorEvent) => void)
|
|
490
|
+
| undefined;
|
|
456
491
|
readonly #panels = new Map<string, AppShellSidebarController>();
|
|
457
492
|
readonly #panelIds = new Map<AppShellSidebarController, string>();
|
|
458
493
|
readonly #panelDisposers = new Map<string, () => void>();
|
|
494
|
+
readonly #panelElements = new Map<string, HTMLElement>();
|
|
495
|
+
readonly #projectionChangeListeners =
|
|
496
|
+
new Set<AppShellProjectionChangeListener>();
|
|
459
497
|
#restoredPanels = new Map<string, AppShellSidebarLayout>();
|
|
460
498
|
#hydrating = false;
|
|
461
499
|
#restorePromise: Promise<void> | null = null;
|
|
462
500
|
#saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
501
|
+
#projectionFrame: number | null = null;
|
|
463
502
|
#pendingSaveEvent: AppShellLayoutChangeEvent | null = null;
|
|
464
503
|
#saveChain: Promise<void> = Promise.resolve();
|
|
465
504
|
|
|
@@ -467,17 +506,25 @@ export class AppShellController {
|
|
|
467
506
|
this.#persistence = options.persistence;
|
|
468
507
|
this.#saveDebounceMs = Math.max(0, options.saveDebounceMs ?? 200);
|
|
469
508
|
this.#onPersistenceError = options.onPersistenceError;
|
|
470
|
-
const sidebarOptions = {
|
|
471
|
-
|
|
472
|
-
|
|
509
|
+
const sidebarOptions: AppShellSidebarControllerOptions = {
|
|
510
|
+
...(options.sidebarMinWidth === undefined
|
|
511
|
+
? {}
|
|
512
|
+
: { minWidth: options.sidebarMinWidth }),
|
|
513
|
+
...(options.sidebarMaxWidth === undefined
|
|
514
|
+
? {}
|
|
515
|
+
: { maxWidth: options.sidebarMaxWidth }),
|
|
473
516
|
};
|
|
474
517
|
this.left = new AppShellSidebarController(
|
|
475
518
|
"left",
|
|
476
519
|
options.leftCollapsed ?? false,
|
|
477
520
|
{
|
|
478
521
|
...sidebarOptions,
|
|
479
|
-
|
|
480
|
-
|
|
522
|
+
...(options.leftWidth === undefined
|
|
523
|
+
? {}
|
|
524
|
+
: { width: options.leftWidth }),
|
|
525
|
+
...(options.leftClosed === undefined
|
|
526
|
+
? {}
|
|
527
|
+
: { closed: options.leftClosed }),
|
|
481
528
|
},
|
|
482
529
|
);
|
|
483
530
|
this.right = new AppShellSidebarController(
|
|
@@ -485,8 +532,12 @@ export class AppShellController {
|
|
|
485
532
|
options.rightCollapsed ?? false,
|
|
486
533
|
{
|
|
487
534
|
...sidebarOptions,
|
|
488
|
-
|
|
489
|
-
|
|
535
|
+
...(options.rightWidth === undefined
|
|
536
|
+
? {}
|
|
537
|
+
: { width: options.rightWidth }),
|
|
538
|
+
...(options.rightClosed === undefined
|
|
539
|
+
? {}
|
|
540
|
+
: { closed: options.rightClosed }),
|
|
490
541
|
},
|
|
491
542
|
);
|
|
492
543
|
this.#attachPanel("left", this.left);
|
|
@@ -508,6 +559,133 @@ export class AppShellController {
|
|
|
508
559
|
return this.#panelIds.get(sidebar);
|
|
509
560
|
}
|
|
510
561
|
|
|
562
|
+
/** Whether protected desktop width has displaced this panel from inline flow. */
|
|
563
|
+
isPanelConstrained(id: string | undefined): boolean {
|
|
564
|
+
return id !== undefined && this.constrainedPanelIds.includes(id);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Return the mounted panel's configured constrained presentation. */
|
|
568
|
+
getConstrainedPresentation(
|
|
569
|
+
id: string | undefined,
|
|
570
|
+
): AppShellConstrainedPresentation {
|
|
571
|
+
if (!id) return "preview";
|
|
572
|
+
return (
|
|
573
|
+
this.mobile.panels.find((panel) => panel.id === id)
|
|
574
|
+
?.constrainedPresentation ?? "preview"
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Whether a constrained preview panel is currently presented. */
|
|
579
|
+
isPanelDesktopPreviewed(id: string | undefined): boolean {
|
|
580
|
+
return id !== undefined && this.desktopPreviewPanelIds.includes(id);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Present or dismiss one constrained desktop preview without durable changes. */
|
|
584
|
+
setPanelDesktopPreviewed(id: string, previewed: boolean): void {
|
|
585
|
+
if (!this.isPanelConstrained(id)) previewed = false;
|
|
586
|
+
const current = this.desktopPreviewPanelIds.includes(id);
|
|
587
|
+
if (current === previewed) return;
|
|
588
|
+
this.desktopPreviewPanelIds = previewed
|
|
589
|
+
? [...this.desktopPreviewPanelIds, id]
|
|
590
|
+
: this.desktopPreviewPanelIds.filter((candidate) => candidate !== id);
|
|
591
|
+
this.#projectionChanged();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** @internal Install the current constrained projection from AppShell.Root. */
|
|
595
|
+
setConstrainedPanelIds(ids: readonly string[]): void {
|
|
596
|
+
const next = [...new Set(ids)];
|
|
597
|
+
if (
|
|
598
|
+
next.length === this.constrainedPanelIds.length &&
|
|
599
|
+
next.every((id, index) => this.constrainedPanelIds[index] === id)
|
|
600
|
+
) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
this.constrainedPanelIds = next;
|
|
604
|
+
this.desktopPreviewPanelIds = this.desktopPreviewPanelIds.filter((id) =>
|
|
605
|
+
next.includes(id),
|
|
606
|
+
);
|
|
607
|
+
this.#projectionChanged();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @internal Subscribe mounted shell parts to transient projection changes. */
|
|
611
|
+
onProjectionChange(listener: AppShellProjectionChangeListener): () => void {
|
|
612
|
+
this.#projectionChangeListeners.add(listener);
|
|
613
|
+
return () => this.#projectionChangeListeners.delete(listener);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** @internal Register a mounted sidebar landmark for shell geometry. */
|
|
617
|
+
setPanelElement(id: string | undefined, element: HTMLElement | null): void {
|
|
618
|
+
if (!id) return;
|
|
619
|
+
if (element) {
|
|
620
|
+
if (this.#panelElements.get(id) === element) return;
|
|
621
|
+
this.#panelElements.set(id, element);
|
|
622
|
+
this.#syncPanelProjection(id);
|
|
623
|
+
} else {
|
|
624
|
+
if (!this.#panelElements.delete(id)) return;
|
|
625
|
+
}
|
|
626
|
+
this.panelElementsVersion += 1;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** @internal Return a mounted sidebar landmark. */
|
|
630
|
+
getPanelElement(id: string): HTMLElement | null {
|
|
631
|
+
void this.panelElementsVersion;
|
|
632
|
+
return this.#panelElements.get(id) ?? null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** @internal Reconcile every mounted panel with durable and transient state. */
|
|
636
|
+
syncPanelProjections(): void {
|
|
637
|
+
for (const id of this.#panelElements.keys()) {
|
|
638
|
+
this.#syncPanelProjection(id);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Whether the active structural layer temporarily removes this panel. */
|
|
643
|
+
isPanelSuspended(id: string | undefined): boolean {
|
|
644
|
+
return (
|
|
645
|
+
id !== undefined &&
|
|
646
|
+
(this.activeSurfaceLayer?.suspendPanelIds.includes(id) ?? false)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Whether the active structural layer covers this mounted panel. */
|
|
651
|
+
isPanelCovered(id: string | undefined): boolean {
|
|
652
|
+
return (
|
|
653
|
+
id !== undefined &&
|
|
654
|
+
(this.activeSurfaceLayer?.coverPanelIds.includes(id) ?? false)
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Whether a structural layer or replacement panel currently covers main. */
|
|
659
|
+
get mainOccluded(): boolean {
|
|
660
|
+
if (this.activeSurfaceLayer) return true;
|
|
661
|
+
return this.constrainedPanelIds.some((id) => {
|
|
662
|
+
const panel = this.getPanel(id);
|
|
663
|
+
return (
|
|
664
|
+
panel !== undefined &&
|
|
665
|
+
!panel.closed &&
|
|
666
|
+
!this.isPanelSuspended(id) &&
|
|
667
|
+
this.getConstrainedPresentation(id) === "replace-main"
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** @internal Activate the root's single structural surface layer. */
|
|
673
|
+
activateSurfaceLayer(registration: AppShellSurfaceLayerRegistration): void {
|
|
674
|
+
const current = this.activeSurfaceLayer;
|
|
675
|
+
if (current && current.token !== registration.token) {
|
|
676
|
+
throw new Error("AppShell.Root supports one active SurfaceLayer.");
|
|
677
|
+
}
|
|
678
|
+
this.activeSurfaceLayer = registration;
|
|
679
|
+
this.#projectionChanged();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** @internal Remove a mounted structural surface layer. */
|
|
683
|
+
deactivateSurfaceLayer(token: symbol): void {
|
|
684
|
+
if (this.activeSurfaceLayer?.token !== token) return;
|
|
685
|
+
this.activeSurfaceLayer = null;
|
|
686
|
+
this.#projectionChanged();
|
|
687
|
+
}
|
|
688
|
+
|
|
511
689
|
/** Create and register an independently persisted same-side panel. */
|
|
512
690
|
createSidebar(
|
|
513
691
|
id: string,
|
|
@@ -549,6 +727,15 @@ export class AppShellController {
|
|
|
549
727
|
this.#panelDisposers.delete(panelId);
|
|
550
728
|
this.#panels.delete(panelId);
|
|
551
729
|
this.#panelIds.delete(sidebar);
|
|
730
|
+
this.#panelElements.delete(panelId);
|
|
731
|
+
this.constrainedPanelIds = this.constrainedPanelIds.filter(
|
|
732
|
+
(id) => id !== panelId,
|
|
733
|
+
);
|
|
734
|
+
this.desktopPreviewPanelIds = this.desktopPreviewPanelIds.filter(
|
|
735
|
+
(id) => id !== panelId,
|
|
736
|
+
);
|
|
737
|
+
this.panelElementsVersion += 1;
|
|
738
|
+
this.#projectionChanged();
|
|
552
739
|
this.#requestSave({ source: "unregister", panelId });
|
|
553
740
|
};
|
|
554
741
|
}
|
|
@@ -605,6 +792,18 @@ export class AppShellController {
|
|
|
605
792
|
this.#panelDisposers.clear();
|
|
606
793
|
this.#panels.clear();
|
|
607
794
|
this.#panelIds.clear();
|
|
795
|
+
this.#panelElements.clear();
|
|
796
|
+
this.constrainedPanelIds = [];
|
|
797
|
+
this.desktopPreviewPanelIds = [];
|
|
798
|
+
this.activeSurfaceLayer = null;
|
|
799
|
+
this.#projectionChangeListeners.clear();
|
|
800
|
+
if (
|
|
801
|
+
this.#projectionFrame !== null &&
|
|
802
|
+
typeof cancelAnimationFrame !== "undefined"
|
|
803
|
+
) {
|
|
804
|
+
cancelAnimationFrame(this.#projectionFrame);
|
|
805
|
+
this.#projectionFrame = null;
|
|
806
|
+
}
|
|
608
807
|
}
|
|
609
808
|
|
|
610
809
|
#attachPanel(id: string, sidebar: AppShellSidebarController): void {
|
|
@@ -613,6 +812,7 @@ export class AppShellController {
|
|
|
613
812
|
this.#panelDisposers.set(
|
|
614
813
|
id,
|
|
615
814
|
sidebar.onLayoutChange((source) => {
|
|
815
|
+
this.#projectionChanged();
|
|
616
816
|
this.#requestSave({ source, panelId: id });
|
|
617
817
|
}),
|
|
618
818
|
);
|
|
@@ -645,6 +845,64 @@ export class AppShellController {
|
|
|
645
845
|
void this.flushSave();
|
|
646
846
|
}, this.#saveDebounceMs);
|
|
647
847
|
}
|
|
848
|
+
|
|
849
|
+
#projectionChanged(): void {
|
|
850
|
+
this.projectionVersion += 1;
|
|
851
|
+
for (const listener of this.#projectionChangeListeners) listener();
|
|
852
|
+
const sync = () => {
|
|
853
|
+
this.#projectionFrame = null;
|
|
854
|
+
this.syncPanelProjections();
|
|
855
|
+
};
|
|
856
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
857
|
+
queueMicrotask(sync);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (this.#projectionFrame !== null)
|
|
861
|
+
cancelAnimationFrame(this.#projectionFrame);
|
|
862
|
+
this.#projectionFrame = requestAnimationFrame(sync);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
#syncPanelProjection(id: string): void {
|
|
866
|
+
const element = this.#panelElements.get(id);
|
|
867
|
+
const sidebar = this.#panels.get(id);
|
|
868
|
+
if (!element || !sidebar) return;
|
|
869
|
+
const suspended = this.isPanelSuspended(id);
|
|
870
|
+
const covered = suspended || this.isPanelCovered(id);
|
|
871
|
+
const constrained = this.isPanelConstrained(id);
|
|
872
|
+
const mobile = this.mobile.resolvedMode === "mobile";
|
|
873
|
+
const presentation = this.getConstrainedPresentation(id);
|
|
874
|
+
const previewed = this.isPanelDesktopPreviewed(id) || sidebar.previewed;
|
|
875
|
+
const consumerAriaHidden = element.getAttribute(
|
|
876
|
+
"data-consumer-aria-hidden",
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
element.toggleAttribute("data-suspended", suspended);
|
|
880
|
+
element.toggleAttribute("data-desktop-constrained", constrained);
|
|
881
|
+
element.dataset.state = sidebar.state;
|
|
882
|
+
element.dataset.presentation = mobile
|
|
883
|
+
? "mobile"
|
|
884
|
+
: constrained
|
|
885
|
+
? previewed
|
|
886
|
+
? "overlay"
|
|
887
|
+
: presentation === "replace-main"
|
|
888
|
+
? "replace-main"
|
|
889
|
+
: "inline"
|
|
890
|
+
: "inline";
|
|
891
|
+
element.hidden =
|
|
892
|
+
element.hasAttribute("data-consumer-hidden") ||
|
|
893
|
+
suspended ||
|
|
894
|
+
(!mobile &&
|
|
895
|
+
(sidebar.closed ||
|
|
896
|
+
(constrained && presentation === "preview" && !previewed)));
|
|
897
|
+
element.inert = element.hasAttribute("data-consumer-inert") || covered;
|
|
898
|
+
if (covered || consumerAriaHidden === "true") {
|
|
899
|
+
element.setAttribute("aria-hidden", "true");
|
|
900
|
+
} else if (consumerAriaHidden === null) {
|
|
901
|
+
element.removeAttribute("aria-hidden");
|
|
902
|
+
} else {
|
|
903
|
+
element.setAttribute("aria-hidden", consumerAriaHidden);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
648
906
|
}
|
|
649
907
|
|
|
650
908
|
function normalizeAppShellLayout(
|