@lapismd/design-core 0.1.2 → 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 (65) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE.md +3 -3
  3. package/README.md +20 -0
  4. package/package.json +24 -9
  5. package/spec/src/styling-and-themes.md +2 -0
  6. package/src/shared/ai/chat.css +23 -0
  7. package/src/shared/ai/chat.ts +12 -0
  8. package/src/shared/ai/context.svelte.ts +2 -0
  9. package/src/shared/ai/layout/Layout.svelte +116 -10
  10. package/src/shared/ai/message-list/MessageList.svelte +123 -43
  11. package/src/shared/ai/message-list/MessageListHistoryFixture.svelte +112 -0
  12. package/src/shared/ai/message-list/MessageListLegacyFixture.svelte +52 -0
  13. package/src/shared/ai/message-list/VirtualizedMessageRows.svelte +299 -0
  14. package/src/shared/ai/message-list/position.ts +7 -0
  15. package/src/shared/ai/new-messages.svelte.ts +24 -5
  16. package/src/shared/ai/stream-scroll.svelte.ts +135 -6
  17. package/src/shared/ai/tool-call-detail/ToolCallDetail.svelte +71 -0
  18. package/src/shared/ai/tool-call-detail/tool-call-detail.ts +209 -0
  19. package/src/shared/diff/core/merge/actions.ts +4 -3
  20. package/src/shared/diff/core/merge/render-model.ts +14 -10
  21. package/src/shared/diff/file-diff/FileDiff.css +68 -1
  22. package/src/shared/diff/file-diff/FileDiff.svelte +22 -6
  23. package/src/shared/diff/file-diff/FileDiffComposer.svelte +98 -16
  24. package/src/shared/diff/file-diff/index.ts +2 -0
  25. package/src/shared/diff/file-diff/types.ts +13 -0
  26. package/src/shared/diff/index.ts +2 -0
  27. package/src/shared/forms/confirm-button/ConfirmButton.svelte +38 -0
  28. package/src/shared/forms/confirm-button/ConfirmButtonExample.svelte +14 -0
  29. package/src/shared/forms/confirm-button/index.ts +1 -0
  30. package/src/shared/forms/confirm-dialog/ConfirmDialog.css +21 -0
  31. package/src/shared/forms/confirm-dialog/ConfirmDialog.svelte +116 -0
  32. package/src/shared/forms/confirm-dialog/ConfirmDialogExample.svelte +17 -0
  33. package/src/shared/forms/confirm-dialog/index.ts +2 -0
  34. package/src/shared/forms/dialog-frame/DialogFrame.css +130 -0
  35. package/src/shared/forms/dialog-frame/DialogFrame.svelte +108 -0
  36. package/src/shared/forms/dialog-frame/DialogFrameExample.svelte +37 -0
  37. package/src/shared/forms/dialog-frame/index.ts +6 -0
  38. package/src/shared/forms/dialog.ts +8 -0
  39. package/src/shared/forms/index.ts +11 -0
  40. package/src/shared/shadcn/button/button.svelte +4 -0
  41. package/src/shared/shadcn/code-block/highlightRanges.ts +10 -6
  42. package/src/shared/shadcn/code-block/syntax/defineSyntaxTheme.ts +4 -2
  43. package/src/shared/shadcn/code-block/tokenizer.ts +3 -3
  44. package/src/shared/shadcn/dialog/dialog-content.svelte +3 -4
  45. package/src/shared/shadcn/dialog/dialog-portal.svelte +10 -2
  46. package/src/shared/shadcn/tooltip/tooltip-content.svelte +10 -4
  47. package/src/shared/shadcn/tooltip/tooltip.tokens.ts +4 -0
  48. package/src/shared/shell/app-shell/AppShell.css +112 -32
  49. package/src/shared/shell/app-shell/AppShellMain.svelte +12 -1
  50. package/src/shared/shell/app-shell/AppShellRoot.svelte +27 -13
  51. package/src/shared/shell/app-shell/AppShellSidebar.svelte +121 -19
  52. package/src/shared/shell/app-shell/AppShellSidebarClose.svelte +17 -17
  53. package/src/shared/shell/app-shell/AppShellSidebarToggle.svelte +38 -58
  54. package/src/shared/shell/app-shell/AppShellSurfaceLayer.svelte +173 -0
  55. package/src/shared/shell/app-shell/AppShellSurfaceLayerBody.svelte +32 -0
  56. package/src/shared/shell/app-shell/AppShellSurfaceLayerHeader.svelte +26 -0
  57. package/src/shared/shell/app-shell/app-shell-context.svelte.ts +6 -3
  58. package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +246 -2
  59. package/src/shared/shell/app-shell/app-shell-sidebar-context.svelte.ts +1 -0
  60. package/src/shared/shell/app-shell/examples/AppShellExamples.css +103 -0
  61. package/src/shared/shell/app-shell/examples/AppShellMultiPanelDemo.svelte +187 -0
  62. package/src/shared/shell/app-shell/index.ts +14 -0
  63. package/src/shared/shell/shell.tokens.css +2 -0
  64. package/src/shared/shell/shell.tokens.ts +5 -0
  65. package/src/theme.css +10 -0
@@ -0,0 +1,108 @@
1
+ <script lang="ts" module>
2
+ import type { ComponentProps, Snippet } from "svelte";
3
+ import type * as DialogTypes from "../../shadcn/dialog/index.js";
4
+
5
+ export type DialogFrameSize = "compact" | "form" | "large" | "wide";
6
+ export type DialogFrameSurface = "plain" | "inset";
7
+ export type DialogFrameProps = Omit<
8
+ ComponentProps<typeof DialogTypes.Content>,
9
+ "children" | "title" | "showCloseButton" | "dataUiComponent" | "dataUiPart"
10
+ > & {
11
+ title: string | Snippet;
12
+ description?: string | Snippet;
13
+ descriptionHidden?: boolean;
14
+ size?: DialogFrameSize;
15
+ /** Visual treatment for the dialog shell and main content surface. */
16
+ surface?: DialogFrameSurface;
17
+ busy?: boolean;
18
+ closeLabel?: string;
19
+ sidebar?: Snippet;
20
+ headerActions?: Snippet;
21
+ footer?: Snippet;
22
+ children: Snippet;
23
+ bodyPadding?: "default" | "none";
24
+ };
25
+ </script>
26
+
27
+ <script lang="ts">
28
+ import * as Dialog from "../../shadcn/dialog/index.js";
29
+ import { Button } from "../../shadcn/button/index.js";
30
+ import { ScrollArea } from "../../shadcn/scroll-area/index.js";
31
+ import XIcon from "@lucide/svelte/icons/x";
32
+ import "./DialogFrame.css";
33
+
34
+ let {
35
+ title,
36
+ description,
37
+ descriptionHidden = false,
38
+ size = "form",
39
+ surface = "plain",
40
+ busy = false,
41
+ closeLabel = "Close",
42
+ sidebar,
43
+ headerActions,
44
+ footer,
45
+ children,
46
+ bodyPadding = "default",
47
+ ref = $bindable(null),
48
+ onInteractOutside,
49
+ onEscapeKeydown,
50
+ ...contentProps
51
+ }: DialogFrameProps = $props();
52
+ </script>
53
+
54
+ <Dialog.Content
55
+ {...contentProps}
56
+ bind:ref
57
+ showCloseButton={false}
58
+ data-ui-dialog-frame=""
59
+ data-frame-size={size}
60
+ data-frame-surface={surface}
61
+ data-frame-padding={bodyPadding}
62
+ onInteractOutside={(event) => {
63
+ if (busy) event.preventDefault();
64
+ else onInteractOutside?.(event);
65
+ }}
66
+ onEscapeKeydown={(event) => {
67
+ if (busy) event.preventDefault();
68
+ else onEscapeKeydown?.(event);
69
+ }}
70
+ >
71
+ <div data-ui-part="dialog-frame-header">
72
+ <div data-ui-part="dialog-frame-heading">
73
+ <Dialog.Title
74
+ >{#if typeof title === "string"}{title}{:else}{@render title()}{/if}</Dialog.Title
75
+ >
76
+ {#if description}<Dialog.Description
77
+ class={descriptionHidden ? "sr-only" : undefined}
78
+ >{#if typeof description === "string"}{description}{:else}{@render description()}{/if}</Dialog.Description
79
+ >{/if}
80
+ </div>
81
+ {@render headerActions?.()}
82
+ <Dialog.Close>
83
+ {#snippet child({ props })}
84
+ <Button
85
+ {...props}
86
+ variant="ghost"
87
+ size="icon-sm"
88
+ disabled={busy}
89
+ aria-label={closeLabel}
90
+ data-ui-part="dialog-frame-close"><XIcon aria-hidden="true" /></Button
91
+ >
92
+ {/snippet}
93
+ </Dialog.Close>
94
+ </div>
95
+ <div data-ui-part="dialog-frame-layout" data-has-sidebar={Boolean(sidebar)}>
96
+ {#if sidebar}<aside data-ui-part="dialog-frame-sidebar">
97
+ {@render sidebar()}
98
+ </aside>{/if}
99
+ <div data-ui-part="dialog-frame-main">
100
+ <ScrollArea class="ui-dialog-frame-scroll" type="always">
101
+ <div data-ui-part="dialog-frame-body">{@render children()}</div>
102
+ </ScrollArea>
103
+ {#if footer}<Dialog.Footer data-ui-part="dialog-frame-footer"
104
+ >{@render footer()}</Dialog.Footer
105
+ >{/if}
106
+ </div>
107
+ </div>
108
+ </Dialog.Content>
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import { DialogFrame } from "@lapismd/design-core/forms/dialog";
3
+ import * as Dialog from "@lapismd/design-core/shadcn/dialog";
4
+ import { Button } from "@lapismd/design-core/shadcn/button";
5
+ import { Input } from "@lapismd/design-core/shadcn/input";
6
+ let open = $state(false);
7
+ let name = $state("");
8
+ </script>
9
+
10
+ <Dialog.Root bind:open>
11
+ <Dialog.Trigger
12
+ >{#snippet child({ props })}<Button {...props}>Edit record</Button
13
+ >{/snippet}</Dialog.Trigger
14
+ >
15
+ <DialogFrame
16
+ title="Edit record"
17
+ description={name
18
+ ? `Editing ${name}`
19
+ : "The form scrolls while its actions remain visible."}
20
+ size="large"
21
+ surface="inset"
22
+ class={name ? "record-filled" : "record-empty"}
23
+ >
24
+ {#snippet sidebar()}<nav aria-label="Sections">
25
+ <Button variant="secondary">Details</Button>
26
+ </nav>{/snippet}
27
+ {#each Array.from({ length: 24 }) as _, index}<label
28
+ style="display: grid; gap: 0.5rem; margin-block: 1rem;"
29
+ >Field {index + 1}{#if index === 0}<Input
30
+ bind:value={name}
31
+ />{:else}<Input />{/if}</label
32
+ >{/each}
33
+ {#snippet footer()}<Button variant="outline" onclick={() => (open = false)}
34
+ >Cancel</Button
35
+ ><Button onclick={() => (open = false)}>Save</Button>{/snippet}
36
+ </DialogFrame>
37
+ </Dialog.Root>
@@ -0,0 +1,6 @@
1
+ export { default as DialogFrame } from "./DialogFrame.svelte";
2
+ export type {
3
+ DialogFrameProps,
4
+ DialogFrameSize,
5
+ DialogFrameSurface,
6
+ } from "./DialogFrame.svelte";
@@ -0,0 +1,8 @@
1
+ export { DialogFrame } from "./dialog-frame/index.js";
2
+ export type {
3
+ DialogFrameProps,
4
+ DialogFrameSize,
5
+ } from "./dialog-frame/index.js";
6
+ export { ConfirmDialog } from "./confirm-dialog/index.js";
7
+ export type { ConfirmDialogProps } from "./confirm-dialog/index.js";
8
+ export { ConfirmButton } from "./confirm-button/index.js";
@@ -129,3 +129,14 @@ export {
129
129
  export type { InlineOptionPickerOption } from "./inline-option-picker/InlineOptionPicker.svelte";
130
130
  export type { AddSectionOption } from "./add-section-chooser/AddSectionChooser.svelte";
131
131
  export * from "./core/core";
132
+
133
+ export {
134
+ DialogFrame,
135
+ type DialogFrameProps,
136
+ type DialogFrameSize,
137
+ } from "./dialog-frame/index.js";
138
+ export {
139
+ ConfirmDialog,
140
+ type ConfirmDialogProps,
141
+ } from "./confirm-dialog/index.js";
142
+ export { ConfirmButton } from "./confirm-button/index.js";
@@ -276,6 +276,7 @@
276
276
  }
277
277
  [data-ui-component="button"][data-variant="outline"]:hover,
278
278
  [data-ui-component="button"][data-variant="ghost"]:hover {
279
+ background-color: var(--accent);
279
280
  color: var(--foreground);
280
281
  }
281
282
  [data-ui-component="button"][data-variant="link"]:hover {
@@ -294,6 +295,9 @@
294
295
  );
295
296
  }
296
297
  }
298
+ [data-ui-component="button"][data-variant="ghost"]:focus-visible {
299
+ background-color: var(--accent);
300
+ }
297
301
  [data-ui-component="button"]:focus-visible {
298
302
  border-color: var(--ui-button-focus-ring-color, var(--ring));
299
303
  }
@@ -179,8 +179,9 @@ function applyRangesToContainer(
179
179
  for (let i = 0; i < lineDivs.length; i++) {
180
180
  const tokenIndex = globalLineOffset + i;
181
181
  const tokens = tokenLines[tokenIndex];
182
- if (tokens && tokens.length > 0) {
183
- applyLineRanges(lineDivs[i], tokens, results, resolve);
182
+ const lineDiv = lineDivs[i];
183
+ if (lineDiv && tokens && tokens.length > 0) {
184
+ applyLineRanges(lineDiv, tokens, results, resolve);
184
185
  }
185
186
  }
186
187
  return results;
@@ -216,6 +217,7 @@ export function applyHighlightRangesChunked(
216
217
 
217
218
  for (let i = 0; i < codeEl.children.length; i++) {
218
219
  const child = codeEl.children[i];
220
+ if (!child) continue;
219
221
  const lineDivs = child.querySelectorAll("[data-line]");
220
222
  if (
221
223
  lineDivs.length > 0 &&
@@ -235,8 +237,9 @@ export function applyHighlightRangesChunked(
235
237
  const lineDivs = codeEl.querySelectorAll("[data-line]");
236
238
  for (let i = 0; i < lineDivs.length; i++) {
237
239
  const tokens = tokenLines[i];
238
- if (tokens && tokens.length > 0) {
239
- applyLineRanges(lineDivs[i], tokens, allRanges, resolve);
240
+ const lineDiv = lineDivs[i];
241
+ if (lineDiv && tokens && tokens.length > 0) {
242
+ applyLineRanges(lineDiv, tokens, allRanges, resolve);
240
243
  }
241
244
  }
242
245
  return () => cleanupRanges(allRanges);
@@ -253,7 +256,7 @@ export function applyHighlightRangesChunked(
253
256
  const ranges = applyRangesToContainer(
254
257
  wrapper,
255
258
  tokenLines,
256
- chunkLineOffsets[index],
259
+ chunkLineOffsets[index] ?? 0,
257
260
  resolve,
258
261
  );
259
262
  chunkRanges.set(wrapper, ranges);
@@ -269,6 +272,7 @@ export function applyHighlightRangesChunked(
269
272
 
270
273
  for (let i = 0; i < chunkWrappers.length; i++) {
271
274
  const wrapper = chunkWrappers[i];
275
+ if (!wrapper) continue;
272
276
 
273
277
  const handler = (e: Event) => {
274
278
  // contentvisibilityautostatechange fires with .skipped = true when
@@ -347,7 +351,7 @@ export function applyHighlightRangesBatch(
347
351
 
348
352
  const lineDiv = lineDivs[divIndex];
349
353
  const tokens = tokenLines[i];
350
- if (tokens && tokens.length > 0) {
354
+ if (lineDiv && tokens && tokens.length > 0) {
351
355
  applyLineRanges(lineDiv, tokens, results, resolve);
352
356
  }
353
357
  }
@@ -60,11 +60,13 @@ export function resolveSyntaxTokenForMode(
60
60
  mode: "light" | "dark",
61
61
  ): string {
62
62
  if (Array.isArray(value)) {
63
- return mode === "dark" ? value[1] : value[0];
63
+ return (mode === "dark" ? value[1] : value[0]) ?? "";
64
64
  }
65
65
  const match = value.match(/^light-dark\(([^,]+),([^)]+)\)$/);
66
66
  if (match) {
67
- return mode === "dark" ? match[2].trim() : match[1].trim();
67
+ return mode === "dark"
68
+ ? (match[2]?.trim() ?? "")
69
+ : (match[1]?.trim() ?? "");
68
70
  }
69
71
  return value;
70
72
  }
@@ -517,13 +517,13 @@ export function flatTokensToLines(
517
517
  for (const token of tokens) {
518
518
  while (
519
519
  lineIdx < lineStarts.length - 1 &&
520
- token.start >= lineStarts[lineIdx + 1]
520
+ token.start >= (lineStarts[lineIdx + 1] ?? Number.POSITIVE_INFINITY)
521
521
  ) {
522
522
  lineIdx++;
523
523
  }
524
524
 
525
- const lineStart = lineStarts[lineIdx];
526
- result[lineIdx].push({
525
+ const lineStart = lineStarts[lineIdx] ?? 0;
526
+ result[lineIdx]?.push({
527
527
  type: token.type,
528
528
  start: token.start - lineStart,
529
529
  end: token.end - lineStart,
@@ -50,7 +50,6 @@
50
50
  variant="ghost"
51
51
  size="icon-sm"
52
52
  {...props}
53
- dataUiComponent="dialog"
54
53
  data-ui-part="dialog-content-anon-0"
55
54
  data-slot="dialog-content-anon-0"
56
55
  >
@@ -75,7 +74,7 @@
75
74
  }
76
75
  }
77
76
  @layer utilities {
78
- [data-ui-component="dialog"][data-ui-part="dialog-content-anon-0"] {
77
+ [data-ui-component="button"][data-ui-part="dialog-content-anon-0"] {
79
78
  position: absolute;
80
79
  }
81
80
  [data-ui-component="dialog"][data-ui-part="dialog-content"],
@@ -88,10 +87,10 @@
88
87
  [data-ui-component="dialog"][data-ui-part="dialog-content"] {
89
88
  top: 50%;
90
89
  }
91
- [data-ui-component="dialog"][data-ui-part="dialog-content-anon-0"] {
90
+ [data-ui-component="button"][data-ui-part="dialog-content-anon-0"] {
92
91
  top: calc(var(--spacing) * 4);
93
92
  }
94
- [data-ui-component="dialog"][data-ui-part="dialog-content-anon-0"] {
93
+ [data-ui-component="button"][data-ui-part="dialog-content-anon-0"] {
95
94
  right: calc(var(--spacing) * 4);
96
95
  }
97
96
  [data-ui-component="dialog"][data-ui-part="dialog-content"] {
@@ -1,7 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { Dialog as DialogPrimitive } from "bits-ui";
3
3
 
4
- let { ...restProps }: DialogPrimitive.PortalProps = $props();
4
+ let { to, disabled, children }: DialogPrimitive.PortalProps = $props();
5
+ const target = $derived(to);
6
+ const portalDisabled = $derived(disabled);
5
7
  </script>
6
8
 
7
- <DialogPrimitive.Portal {...restProps} />
9
+ <!-- Keep portal inputs independent of reactive content so updates do not remount it. -->
10
+ <DialogPrimitive.Portal
11
+ {...target === undefined ? {} : { to: target }}
12
+ {...portalDisabled === undefined ? {} : { disabled: portalDisabled }}
13
+ >
14
+ {@render children?.()}
15
+ </DialogPrimitive.Portal>
@@ -119,14 +119,20 @@
119
119
  fill: var(--foreground);
120
120
  }
121
121
  [data-ui-component="tooltip"][data-ui-part="tooltip-content"] {
122
- padding-inline: calc(var(--spacing) * 3);
122
+ padding-inline: var(
123
+ --ui-tooltip-padding-inline,
124
+ calc(var(--spacing) * 2.5)
125
+ );
123
126
  }
124
127
  [data-ui-component="tooltip"][data-ui-part="tooltip-content"] {
125
- padding-block: calc(var(--spacing) * 1.5);
128
+ padding-block: var(
129
+ --ui-tooltip-padding-block,
130
+ calc(var(--spacing) * 1)
131
+ );
126
132
  }
127
133
  [data-ui-component="tooltip"][data-ui-part="tooltip-content"] {
128
- font-size: var(--text-xs);
129
- line-height: var(--tw-leading, var(--text-xs--line-height));
134
+ font-size: var(--ui-tooltip-font-size, 0.6875rem);
135
+ line-height: var(--ui-tooltip-line-height, 1rem);
130
136
  }
131
137
  [data-ui-component="tooltip"][data-ui-part="tooltip-content"] {
132
138
  color: var(--ui-tooltip-foreground, var(--background));
@@ -4,6 +4,10 @@ export const tooltipTokenNames = {
4
4
  borderColor: "--ui-tooltip-border-color",
5
5
  radius: "--ui-tooltip-radius",
6
6
  focusRingColor: "--ui-tooltip-focus-ring-color",
7
+ fontSize: "--ui-tooltip-font-size",
8
+ lineHeight: "--ui-tooltip-line-height",
9
+ paddingInline: "--ui-tooltip-padding-inline",
10
+ paddingBlock: "--ui-tooltip-padding-block",
7
11
  } as const;
8
12
 
9
13
  export type TooltipToken =
@@ -64,6 +64,10 @@
64
64
  background: transparent;
65
65
  }
66
66
 
67
+ .ui-minimal-app-shell__sidebar[hidden] {
68
+ display: none !important;
69
+ }
70
+
67
71
  .ui-minimal-app-shell__sidebar[data-side="left"] {
68
72
  width: var(--ui-shell-sidebar-size, var(--ui-shell-left-sidebar-width));
69
73
  }
@@ -78,12 +82,19 @@
78
82
  background: var(--background);
79
83
  }
80
84
 
85
+ .ui-minimal-app-shell__sidebar[data-variant="rail"][data-presentation="inline"] {
86
+ margin-block: var(--ui-shell-main-block-inset);
87
+ border: 0;
88
+ border-radius: 0;
89
+ background: transparent;
90
+ box-shadow: none;
91
+ }
92
+
81
93
  .ui-minimal-app-shell__sidebar[data-variant="outer"][data-side="left"][data-presentation="inline"] {
82
94
  border-inline-end: 1px solid var(--border);
83
95
  }
84
96
 
85
- .ui-minimal-app-shell__sidebar[data-presentation="overlay"],
86
- .ui-minimal-app-shell__sidebar[data-desktop-overlay-preview] {
97
+ .ui-minimal-app-shell__sidebar[data-presentation="overlay"] {
87
98
  position: absolute;
88
99
  z-index: 31;
89
100
  inset-block: 0;
@@ -92,14 +103,12 @@
92
103
  color-mix(in srgb, var(--foreground) 18%, transparent);
93
104
  }
94
105
 
95
- .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="left"],
96
- .ui-minimal-app-shell__sidebar[data-desktop-overlay-preview][data-side="left"] {
106
+ .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="left"] {
97
107
  inset-inline-start: 0;
98
108
  border-inline-end: 1px solid var(--border);
99
109
  }
100
110
 
101
- .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="right"],
102
- .ui-minimal-app-shell__sidebar[data-desktop-overlay-preview][data-side="right"] {
111
+ .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="right"] {
103
112
  inset-inline-end: 0;
104
113
  margin-inline-start: 0;
105
114
  border-inline-start: 1px solid var(--border);
@@ -139,24 +148,27 @@
139
148
  display: block;
140
149
  }
141
150
 
142
- .ui-minimal-app-shell[data-desktop-overlay-right]
143
- > .ui-minimal-app-shell__composition
144
- > .ui-minimal-app-shell__sidebar[data-side="right"]:not(
145
- [data-presentation="overlay"]
146
- ):not([data-desktop-overlay-preview]),
147
- .ui-minimal-app-shell[data-desktop-overlay-left]
148
- > .ui-minimal-app-shell__composition
149
- > .ui-minimal-app-shell__sidebar[data-mobile-panel-id="left"]:not(
150
- [data-presentation="overlay"]
151
- ):not([data-desktop-overlay-preview]),
152
- .ui-minimal-app-shell[data-desktop-overlay-outer-left]
153
- > .ui-minimal-app-shell__composition
154
- > .ui-minimal-app-shell__sidebar[data-side="left"][data-variant="outer"]:not(
155
- [data-presentation="overlay"]
156
- ):not([data-desktop-overlay-preview]) {
151
+ .ui-minimal-app-shell__sidebar[data-desktop-constrained][data-constrained-presentation="preview"][data-presentation="inline"] {
157
152
  display: none;
158
153
  }
159
154
 
155
+ .ui-minimal-app-shell__sidebar[data-presentation="replace-main"] {
156
+ position: absolute;
157
+ z-index: 31;
158
+ inset-block-start: var(--ui-shell-replacement-block-start, 0px);
159
+ inset-block-end: var(--ui-shell-replacement-block-end, 0px);
160
+ inset-inline-start: var(--ui-shell-replacement-inline-start, 0px);
161
+ inset-inline-end: var(--ui-shell-replacement-inline-end, 0px);
162
+ display: flex;
163
+ width: auto;
164
+ overflow: hidden;
165
+ margin: 0;
166
+ border: var(--ui-shell-main-border);
167
+ border-radius: var(--ui-shell-main-radius);
168
+ background: var(--background);
169
+ box-shadow: none;
170
+ }
171
+
160
172
  .ui-minimal-app-shell__sidebar[data-side="right"][data-state="expanded"] {
161
173
  border-inline-start: 1px solid var(--border);
162
174
  }
@@ -171,13 +183,11 @@
171
183
  min-width: 0;
172
184
  }
173
185
 
174
- .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="left"],
175
- .ui-minimal-app-shell__sidebar[data-desktop-overlay-preview][data-side="left"] {
186
+ .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="left"] {
176
187
  width: var(--ui-shell-sidebar-size, var(--ui-shell-left-sidebar-width));
177
188
  }
178
189
 
179
- .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="right"],
180
- .ui-minimal-app-shell__sidebar[data-desktop-overlay-preview][data-side="right"] {
190
+ .ui-minimal-app-shell__sidebar[data-presentation="overlay"][data-side="right"] {
181
191
  width: var(--ui-shell-sidebar-size, var(--ui-shell-right-sidebar-width));
182
192
  }
183
193
 
@@ -271,6 +281,76 @@
271
281
  box-shadow: var(--ui-shell-main-shadow);
272
282
  }
273
283
 
284
+ .ui-minimal-app-shell__surface-layer {
285
+ position: absolute;
286
+ z-index: 32;
287
+ min-width: 0;
288
+ min-height: 0;
289
+ pointer-events: none;
290
+ }
291
+
292
+ .ui-minimal-app-shell__surface-layer-scrim {
293
+ position: absolute;
294
+ inset: 0;
295
+ padding: 0;
296
+ border: 0;
297
+ border-radius: var(--ui-shell-main-radius);
298
+ background: color-mix(in oklab, var(--background) 75%, transparent);
299
+ pointer-events: auto;
300
+ cursor: pointer;
301
+ transition: background-color 150ms ease;
302
+ }
303
+
304
+ .ui-minimal-app-shell__surface-layer-scrim:hover {
305
+ background: color-mix(in oklab, var(--background) 65%, transparent);
306
+ }
307
+
308
+ .ui-minimal-app-shell__surface-layer-surface {
309
+ position: absolute;
310
+ inset-block: 0;
311
+ inset-inline-start: var(--ui-shell-surface-layer-main-start-allowance);
312
+ inset-inline-end: var(--ui-shell-surface-layer-inline-end-inset);
313
+ display: grid;
314
+ min-width: 0;
315
+ min-height: 0;
316
+ overflow: hidden;
317
+ grid-template-rows: auto minmax(0, 1fr);
318
+ border: var(--ui-shell-main-border);
319
+ border-radius: var(--ui-shell-main-radius);
320
+ outline: none;
321
+ background: var(--background);
322
+ box-shadow: none;
323
+ pointer-events: auto;
324
+ }
325
+
326
+ .ui-minimal-app-shell__surface-layer-surface:focus-visible {
327
+ outline: 2px solid var(--ring);
328
+ outline-offset: -2px;
329
+ }
330
+
331
+ .ui-minimal-app-shell__surface-layer-header {
332
+ display: flex;
333
+ min-width: 0;
334
+ min-height: var(--ui-shell-toolbar-height);
335
+ align-items: center;
336
+ justify-content: space-between;
337
+ gap: 1rem;
338
+ padding: 0.625rem 0.75rem 0.625rem 1rem;
339
+ }
340
+
341
+ .ui-minimal-app-shell__surface-layer-body {
342
+ display: flex;
343
+ min-width: 0;
344
+ min-height: 0;
345
+ overflow: hidden;
346
+ }
347
+
348
+ .ui-minimal-app-shell__surface-layer-scroll-area {
349
+ flex: 1;
350
+ min-width: 0;
351
+ min-height: 0;
352
+ }
353
+
274
354
  .ui-minimal-app-shell[data-display-mode="desktop"] .ui-minimal-app-shell__main {
275
355
  min-width: min(100%, var(--ui-shell-desktop-min-main-width));
276
356
  }
@@ -301,12 +381,6 @@
301
381
  margin-inline-end: var(--ui-shell-main-block-inset);
302
382
  }
303
383
 
304
- .ui-minimal-app-shell[data-desktop-overlay-right]
305
- > .ui-minimal-app-shell__composition
306
- > .ui-minimal-app-shell__main {
307
- margin-inline-end: var(--ui-shell-main-block-inset);
308
- }
309
-
310
384
  .ui-minimal-app-shell__toolbar {
311
385
  display: flex;
312
386
  flex: none;
@@ -434,6 +508,11 @@
434
508
  min-height: 0;
435
509
  }
436
510
 
511
+ .ui-minimal-app-shell[data-display-mode="mobile"]
512
+ .ui-minimal-app-shell__surface-layer-surface {
513
+ inset-inline-start: 0;
514
+ }
515
+
437
516
  .ui-minimal-app-shell[data-display-mode="mobile"]
438
517
  > .ui-minimal-app-shell__composition {
439
518
  display: none;
@@ -445,7 +524,8 @@
445
524
  height: 100%;
446
525
  min-width: 0;
447
526
  min-height: 0;
448
- overflow: hidden;
527
+ /* Stage position belongs to the translated track, not focus scrolling. */
528
+ overflow: clip;
449
529
  }
450
530
 
451
531
  .ui-minimal-app-shell__mobile-track {
@@ -8,15 +8,24 @@
8
8
  let {
9
9
  ref = $bindable(null),
10
10
  tabindex: _tabindex,
11
+ inert: inertProp,
12
+ "aria-hidden": ariaHidden,
11
13
  class: className,
12
14
  children,
13
15
  ...restProps
14
16
  }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
15
17
 
16
18
  const controller = useAppShell();
19
+ let mainOccluded = $state(controller.mainOccluded);
20
+ const unsubscribeProjection = controller.onProjectionChange(() => {
21
+ mainOccluded = controller.mainOccluded;
22
+ });
17
23
 
18
24
  $effect(() => controller.mobile.setMainElement(ref));
19
- onDestroy(() => controller.mobile.setMainElement(null));
25
+ onDestroy(() => {
26
+ unsubscribeProjection();
27
+ controller.mobile.setMainElement(null);
28
+ });
20
29
  </script>
21
30
 
22
31
  <div
@@ -30,6 +39,8 @@
30
39
  class={["ui-minimal-app-shell__main", className].filter(Boolean).join(" ")}
31
40
  data-ui-component="app-shell"
32
41
  data-ui-part="main"
42
+ inert={Boolean(inertProp) || mainOccluded}
43
+ aria-hidden={Boolean(inertProp) || mainOccluded ? "true" : ariaHidden}
33
44
  >
34
45
  {@render children?.()}
35
46
  </div>