@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/LICENSE.md +3 -3
  3. package/README.md +21 -1
  4. package/package.json +34 -15
  5. package/spec/src/styling-and-themes.md +11 -9
  6. package/src/shared/ai/chat.css +25 -1
  7. package/src/shared/ai/chat.ts +12 -0
  8. package/src/shared/ai/composer-input/ComposerInput.svelte +3 -2
  9. package/src/shared/ai/context.svelte.ts +2 -0
  10. package/src/shared/ai/experimental/reaction-bar/ReactionBar.svelte +1 -1
  11. package/src/shared/ai/layout/Layout.svelte +116 -10
  12. package/src/shared/ai/message-list/MessageList.svelte +123 -43
  13. package/src/shared/ai/message-list/MessageListHistoryFixture.svelte +112 -0
  14. package/src/shared/ai/message-list/MessageListLegacyFixture.svelte +52 -0
  15. package/src/shared/ai/message-list/VirtualizedMessageRows.svelte +299 -0
  16. package/src/shared/ai/message-list/position.ts +7 -0
  17. package/src/shared/ai/new-messages.svelte.ts +24 -5
  18. package/src/shared/ai/stream-scroll.svelte.ts +135 -6
  19. package/src/shared/ai/tool-call-detail/ToolCallDetail.svelte +71 -0
  20. package/src/shared/ai/tool-call-detail/tool-call-detail.ts +209 -0
  21. package/src/shared/ai/tool-calls/ToolCalls.svelte +1 -1
  22. package/src/shared/ai/trigger-menu.ts +2 -3
  23. package/src/shared/diff/core/merge/actions.ts +4 -3
  24. package/src/shared/diff/core/merge/render-model.ts +14 -10
  25. package/src/shared/diff/file-diff/FileDiff.css +68 -1
  26. package/src/shared/diff/file-diff/FileDiff.svelte +27 -14
  27. package/src/shared/diff/file-diff/FileDiffComposer.svelte +98 -16
  28. package/src/shared/diff/file-diff/index.ts +2 -0
  29. package/src/shared/diff/file-diff/types.ts +13 -0
  30. package/src/shared/diff/file-listing/FileListingViewModeToggle.css +42 -5
  31. package/src/shared/diff/index.ts +2 -0
  32. package/src/shared/diff/merge-editor/MergeEditor.svelte +2 -7
  33. package/src/shared/filter/filter-query/filters.ts +2 -2
  34. package/src/shared/filter/filter-query/parser.ts +9 -8
  35. package/src/shared/filter/power-search/PowerSearch.svelte +1 -1
  36. package/src/shared/filter/search-filter-bar/SearchFilterBar.svelte +2 -2
  37. package/src/shared/filter/search-filter-bar/SearchFilterInput.svelte +1 -1
  38. package/src/shared/filter/search-filter-bar/SearchFilterPredicateEditor.svelte +1 -1
  39. package/src/shared/filter/search-filter-bar/search-filter-syntax.ts +1 -1
  40. package/src/shared/forms/autocomplete-input/AutocompleteInput.svelte +19 -7
  41. package/src/shared/forms/confirm-button/ConfirmButton.svelte +38 -0
  42. package/src/shared/forms/confirm-button/ConfirmButtonExample.svelte +14 -0
  43. package/src/shared/forms/confirm-button/index.ts +1 -0
  44. package/src/shared/forms/confirm-dialog/ConfirmDialog.css +21 -0
  45. package/src/shared/forms/confirm-dialog/ConfirmDialog.svelte +116 -0
  46. package/src/shared/forms/confirm-dialog/ConfirmDialogExample.svelte +17 -0
  47. package/src/shared/forms/confirm-dialog/index.ts +2 -0
  48. package/src/shared/forms/dialog-frame/DialogFrame.css +130 -0
  49. package/src/shared/forms/dialog-frame/DialogFrame.svelte +108 -0
  50. package/src/shared/forms/dialog-frame/DialogFrameExample.svelte +37 -0
  51. package/src/shared/forms/dialog-frame/index.ts +6 -0
  52. package/src/shared/forms/dialog.ts +8 -0
  53. package/src/shared/forms/index.ts +11 -0
  54. package/src/shared/shadcn/button/button.svelte +4 -0
  55. package/src/shared/shadcn/code-block/highlightRanges.ts +10 -6
  56. package/src/shared/shadcn/code-block/syntax/defineSyntaxTheme.ts +4 -2
  57. package/src/shared/shadcn/code-block/tokenizer.ts +3 -3
  58. package/src/shared/shadcn/command/command-dialog.svelte +1 -1
  59. package/src/shared/shadcn/command/command-group.svelte +3 -1
  60. package/src/shared/shadcn/command-view/command-view.svelte +1 -6
  61. package/src/shared/shadcn/dialog/dialog-content.svelte +3 -4
  62. package/src/shared/shadcn/dialog/dialog-portal.svelte +10 -2
  63. package/src/shared/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -691
  64. package/src/shared/shadcn/dropdown-menu/dropdown-menu-content.svelte +691 -0
  65. package/src/shared/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +1 -1
  66. package/src/shared/shadcn/scroll-area/scroll-area.svelte +2 -0
  67. package/src/shared/shadcn/spinner/spinner.svelte +3 -3
  68. package/src/shared/shadcn/tooltip/tooltip-content.svelte +10 -4
  69. package/src/shared/shadcn/tooltip/tooltip.tokens.ts +4 -0
  70. package/src/shared/shell/app-shell/AppShell.css +112 -32
  71. package/src/shared/shell/app-shell/AppShell.demo.css +37 -0
  72. package/src/shared/shell/app-shell/AppShellBodyContent.svelte +5 -1
  73. package/src/shared/shell/app-shell/AppShellBodySidebar.svelte +5 -1
  74. package/src/shared/shell/app-shell/AppShellMain.svelte +12 -1
  75. package/src/shared/shell/app-shell/AppShellMobilePanelSelector.svelte +1 -1
  76. package/src/shared/shell/app-shell/AppShellRoot.svelte +27 -13
  77. package/src/shared/shell/app-shell/AppShellSidebar.svelte +121 -19
  78. package/src/shared/shell/app-shell/AppShellSidebarClose.svelte +17 -17
  79. package/src/shared/shell/app-shell/AppShellSidebarToggle.svelte +38 -58
  80. package/src/shared/shell/app-shell/AppShellSurfaceLayer.svelte +173 -0
  81. package/src/shared/shell/app-shell/AppShellSurfaceLayerBody.svelte +32 -0
  82. package/src/shared/shell/app-shell/AppShellSurfaceLayerHeader.svelte +26 -0
  83. package/src/shared/shell/app-shell/app-shell-context.svelte.ts +6 -3
  84. package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +269 -11
  85. package/src/shared/shell/app-shell/app-shell-sidebar-context.svelte.ts +1 -0
  86. package/src/shared/shell/app-shell/examples/AppShellBodyDemo.svelte +4 -0
  87. package/src/shared/shell/app-shell/examples/AppShellCompleteDemo.svelte +1 -0
  88. package/src/shared/shell/app-shell/examples/AppShellConversationDemo.svelte +1 -0
  89. package/src/shared/shell/app-shell/examples/AppShellExamples.css +624 -0
  90. package/src/shared/shell/app-shell/examples/AppShellFilesSidebarDemo.svelte +1 -0
  91. package/src/shared/shell/app-shell/examples/AppShellMarkdownDocumentDemo.svelte +6 -1
  92. package/src/shared/shell/app-shell/examples/AppShellMarkdownFilesDemo.svelte +1 -0
  93. package/src/shared/shell/app-shell/examples/AppShellModeSwitchDemo.svelte +1 -0
  94. package/src/shared/shell/app-shell/examples/AppShellMultiPanelDemo.svelte +187 -0
  95. package/src/shared/shell/app-shell/examples/AppShellProjectSidebarDemo.svelte +1 -0
  96. package/src/shared/shell/app-shell/examples/AppShellSidebarDemo.svelte +1 -0
  97. package/src/shared/shell/app-shell/examples/AppShellToolbarDemo.svelte +1 -0
  98. package/src/shared/shell/app-shell/index.ts +14 -0
  99. package/src/shared/shell/shell.tokens.css +2 -0
  100. package/src/shared/shell/shell.tokens.ts +5 -0
  101. package/src/shared/workspace/menu/WorkspaceContextMenuItems.svelte +2 -1
  102. package/src/shared/workspace/menu/WorkspaceMenuItems.svelte +2 -1
  103. package/src/shared/workspace/problems/ProblemsQuickFix.svelte +1 -4
  104. package/src/shared/workspace/settings/AppSettingsContent.svelte +6 -0
  105. package/src/shared/workspace/settings/AppSettingsNavigation.svelte +7 -0
  106. package/src/shared/workspace/settings/CopyableValue.svelte +81 -0
  107. package/src/shared/workspace/settings/DeclarativeSettingsSection.svelte +20 -0
  108. package/src/shared/workspace/settings/WorkspaceSettingField.svelte +110 -41
  109. package/src/shared/workspace/settings/WorkspaceSettingMultiSelect.svelte +7 -6
  110. package/src/shared/workspace/settings/WorkspaceSettingObjectTable.svelte +187 -23
  111. package/src/shared/workspace/settings/WorkspaceSettingSelect.svelte +29 -29
  112. package/src/shared/workspace/settings/WorkspaceSettings.css +190 -4
  113. package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomField.svelte +2 -2
  114. package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomPage.svelte +13 -0
  115. package/src/shared/workspace/settings/WorkspaceSettingsSurface.svelte +50 -0
  116. package/src/shared/workspace/settings/configuration.ts +5 -0
  117. package/src/shared/workspace/settings/copyable-value.ts +10 -0
  118. package/src/shared/workspace/settings/index.ts +5 -0
  119. package/src/shared/workspace/settings/settings-controller.svelte.ts +349 -15
  120. package/src/shared/workspace/settings/types.ts +90 -8
  121. package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.css +25 -0
  122. package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.svelte +48 -0
  123. package/src/storybook/app-shell-reference/index.ts +1 -0
  124. 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 = getContext<AppShellController | undefined>(
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(() => this.activePanel(side)?.element?.focus());
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) queueMicrotask(() => focusTarget?.focus());
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?: AppShellLayoutPersistence;
486
+ readonly #persistence: AppShellLayoutPersistence | undefined;
454
487
  readonly #saveDebounceMs: number;
455
- readonly #onPersistenceError?: (event: AppShellPersistenceErrorEvent) => void;
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
- minWidth: options.sidebarMinWidth,
472
- maxWidth: options.sidebarMaxWidth,
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
- width: options.leftWidth,
480
- closed: options.leftClosed,
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
- width: options.rightWidth,
489
- closed: options.rightClosed,
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(
@@ -12,6 +12,7 @@ export interface AppShellSidebarContext {
12
12
  readonly side: AppShellSide;
13
13
  readonly closeable: boolean;
14
14
  readonly controller: AppShellSidebarController;
15
+ readonly syncLayout: () => void;
15
16
  readonly dismissOverlay: () => void;
16
17
  }
17
18
 
@@ -1,3 +1,7 @@
1
+ <script lang="ts">
2
+ import "./AppShellExamples.css";
3
+ </script>
4
+
1
5
  <div class="ui-shell-story-body-demo">
2
6
  <header>
3
7
  <p>Composable layout</p>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import "./AppShellExamples.css";
2
3
  import { untrack } from "svelte";
3
4
  import type {
4
5
  AppShellController,
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import "./AppShellExamples.css";
2
3
  import SparklesIcon from "@lucide/svelte/icons/sparkles";
3
4
  import {
4
5
  Composer,