@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
|
@@ -22,11 +22,16 @@
|
|
|
22
22
|
resizable = true,
|
|
23
23
|
closeable = false,
|
|
24
24
|
variant = "default",
|
|
25
|
+
constraintPriority,
|
|
26
|
+
constrainedPresentation = "preview",
|
|
25
27
|
revealOnEdgeHover = false,
|
|
26
28
|
edgeRevealLabel,
|
|
27
29
|
resizeLabel,
|
|
28
30
|
mobileLabel,
|
|
29
31
|
tabindex: _tabindex,
|
|
32
|
+
hidden: hiddenProp,
|
|
33
|
+
inert: inertProp,
|
|
34
|
+
"aria-hidden": ariaHidden,
|
|
30
35
|
style,
|
|
31
36
|
class: className,
|
|
32
37
|
children,
|
|
@@ -43,7 +48,11 @@
|
|
|
43
48
|
/** Whether nested `AppShell.Sidebar.Close` actions may close this sidebar. */
|
|
44
49
|
closeable?: boolean;
|
|
45
50
|
/** Full-height outer chrome or the default transparent shell sidebar. */
|
|
46
|
-
variant?: "default" | "outer";
|
|
51
|
+
variant?: "default" | "outer" | "rail";
|
|
52
|
+
/** Lower values leave inline desktop flow before higher values. */
|
|
53
|
+
constraintPriority?: number;
|
|
54
|
+
/** Presentation used when protected main width displaces this panel. */
|
|
55
|
+
constrainedPresentation?: "preview" | "replace-main";
|
|
47
56
|
/** Preview a collapsed or closed sidebar from the corresponding page edge. */
|
|
48
57
|
revealOnEdgeHover?: boolean;
|
|
49
58
|
/** Accessible name for the collapsed/closed edge-preview control. */
|
|
@@ -79,6 +88,7 @@
|
|
|
79
88
|
get controller() {
|
|
80
89
|
return sidebar;
|
|
81
90
|
},
|
|
91
|
+
syncLayout,
|
|
82
92
|
dismissOverlay,
|
|
83
93
|
});
|
|
84
94
|
let renderedWidth = $state(APP_SHELL_DEFAULT_SIDEBAR_WIDTH);
|
|
@@ -99,6 +109,18 @@
|
|
|
99
109
|
(side === "left" ? "Preview left sidebar" : "Preview right sidebar"),
|
|
100
110
|
);
|
|
101
111
|
let mobilePanelId = $derived(controller.getPanelId(sidebar));
|
|
112
|
+
let desktopConstrained = $state(false);
|
|
113
|
+
let desktopPreviewed = $state(false);
|
|
114
|
+
let suspended = $state(false);
|
|
115
|
+
let covered = $state(false);
|
|
116
|
+
function syncProjection(): void {
|
|
117
|
+
desktopConstrained = controller.isPanelConstrained(mobilePanelId);
|
|
118
|
+
desktopPreviewed = controller.isPanelDesktopPreviewed(mobilePanelId);
|
|
119
|
+
suspended = controller.isPanelSuspended(mobilePanelId);
|
|
120
|
+
covered = controller.isPanelCovered(mobilePanelId);
|
|
121
|
+
}
|
|
122
|
+
syncProjection();
|
|
123
|
+
const unsubscribeProjection = controller.onProjectionChange(syncProjection);
|
|
102
124
|
let mobileMode = $derived(controller.mobile.resolvedMode === "mobile");
|
|
103
125
|
let mobileActive = $derived(
|
|
104
126
|
!mobileMode ||
|
|
@@ -106,13 +128,15 @@
|
|
|
106
128
|
controller.mobile.activePanelId(side) === mobilePanelId),
|
|
107
129
|
);
|
|
108
130
|
let presentation = $derived(
|
|
109
|
-
mobileMode ? "mobile" : sidebar.previewed ? "overlay" : "inline",
|
|
110
|
-
);
|
|
111
|
-
let renderSidebar = $derived(
|
|
112
131
|
mobileMode
|
|
113
|
-
?
|
|
114
|
-
:
|
|
132
|
+
? "mobile"
|
|
133
|
+
: sidebar.previewed || desktopPreviewed
|
|
134
|
+
? "overlay"
|
|
135
|
+
: desktopConstrained && constrainedPresentation === "replace-main"
|
|
136
|
+
? "replace-main"
|
|
137
|
+
: "inline",
|
|
115
138
|
);
|
|
139
|
+
let renderSidebar = $derived(mobileMode ? mobilePanelId !== undefined : true);
|
|
116
140
|
let preserveCollapsedRail = $derived(
|
|
117
141
|
!mobileMode && !sidebarClosed && sidebarCollapsed && sidebar.previewed,
|
|
118
142
|
);
|
|
@@ -147,10 +171,78 @@
|
|
|
147
171
|
get element() {
|
|
148
172
|
return ref;
|
|
149
173
|
},
|
|
174
|
+
...(constraintPriority === undefined ? {} : { constraintPriority }),
|
|
175
|
+
constrainedPresentation,
|
|
150
176
|
};
|
|
151
177
|
return untrack(() => controller.mobile.registerPanel(registration));
|
|
152
178
|
});
|
|
153
179
|
|
|
180
|
+
$effect(() => {
|
|
181
|
+
const panelId = mobilePanelId;
|
|
182
|
+
const element = ref;
|
|
183
|
+
untrack(() => controller.setPanelElement(panelId, element));
|
|
184
|
+
return () => untrack(() => controller.setPanelElement(panelId, null));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
$effect(() => {
|
|
188
|
+
const element = ref;
|
|
189
|
+
const root = controller.mobile.getRootElement();
|
|
190
|
+
const main = controller.mobile.getMainElement();
|
|
191
|
+
if (
|
|
192
|
+
presentation !== "replace-main" ||
|
|
193
|
+
!element ||
|
|
194
|
+
!root ||
|
|
195
|
+
!main ||
|
|
196
|
+
typeof ResizeObserver === "undefined"
|
|
197
|
+
) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const updateInsets = () => {
|
|
202
|
+
const rootBounds = root.getBoundingClientRect();
|
|
203
|
+
const mainBounds = main.getBoundingClientRect();
|
|
204
|
+
const paddingLeft = rootBounds.left + root.clientLeft;
|
|
205
|
+
const paddingRight = paddingLeft + root.clientWidth;
|
|
206
|
+
const paddingTop = rootBounds.top + root.clientTop;
|
|
207
|
+
const paddingBottom = paddingTop + root.clientHeight;
|
|
208
|
+
const rtl = getComputedStyle(root).direction === "rtl";
|
|
209
|
+
const inlineStart = rtl
|
|
210
|
+
? paddingRight - mainBounds.right
|
|
211
|
+
: mainBounds.left - paddingLeft;
|
|
212
|
+
const inlineEnd = rtl
|
|
213
|
+
? mainBounds.left - paddingLeft
|
|
214
|
+
: paddingRight - mainBounds.right;
|
|
215
|
+
element.style.setProperty(
|
|
216
|
+
"--ui-shell-replacement-inline-start",
|
|
217
|
+
`${Math.max(0, inlineStart)}px`,
|
|
218
|
+
);
|
|
219
|
+
element.style.setProperty(
|
|
220
|
+
"--ui-shell-replacement-inline-end",
|
|
221
|
+
`${Math.max(0, inlineEnd)}px`,
|
|
222
|
+
);
|
|
223
|
+
element.style.setProperty(
|
|
224
|
+
"--ui-shell-replacement-block-start",
|
|
225
|
+
`${Math.max(0, mainBounds.top - paddingTop)}px`,
|
|
226
|
+
);
|
|
227
|
+
element.style.setProperty(
|
|
228
|
+
"--ui-shell-replacement-block-end",
|
|
229
|
+
`${Math.max(0, paddingBottom - mainBounds.bottom)}px`,
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
let frame = requestAnimationFrame(updateInsets);
|
|
233
|
+
const scheduleUpdate = () => {
|
|
234
|
+
cancelAnimationFrame(frame);
|
|
235
|
+
frame = requestAnimationFrame(updateInsets);
|
|
236
|
+
};
|
|
237
|
+
const observer = new ResizeObserver(scheduleUpdate);
|
|
238
|
+
observer.observe(root);
|
|
239
|
+
observer.observe(main);
|
|
240
|
+
return () => {
|
|
241
|
+
cancelAnimationFrame(frame);
|
|
242
|
+
observer.disconnect();
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
|
|
154
246
|
$effect(() => {
|
|
155
247
|
const sidebarElement = ref;
|
|
156
248
|
if (!sidebarElement) return;
|
|
@@ -227,8 +319,12 @@
|
|
|
227
319
|
|
|
228
320
|
onDestroy(() => {
|
|
229
321
|
unsubscribeLayout();
|
|
322
|
+
unsubscribeProjection();
|
|
230
323
|
stopResize();
|
|
231
324
|
sidebar.dismissPreview();
|
|
325
|
+
controller.setPanelElement(mobilePanelId, null);
|
|
326
|
+
if (mobilePanelId)
|
|
327
|
+
controller.setPanelDesktopPreviewed(mobilePanelId, false);
|
|
232
328
|
});
|
|
233
329
|
|
|
234
330
|
function revealFromEdge(element?: Element | null): void {
|
|
@@ -239,7 +335,9 @@
|
|
|
239
335
|
return;
|
|
240
336
|
}
|
|
241
337
|
if (isDesktopOverlayOnly(element)) {
|
|
242
|
-
|
|
338
|
+
if (mobilePanelId) {
|
|
339
|
+
controller.setPanelDesktopPreviewed(mobilePanelId, true);
|
|
340
|
+
}
|
|
243
341
|
queueMicrotask(() => ref?.focus({ preventScroll: true }));
|
|
244
342
|
return;
|
|
245
343
|
}
|
|
@@ -247,18 +345,14 @@
|
|
|
247
345
|
}
|
|
248
346
|
|
|
249
347
|
function isDesktopOverlayOnly(element?: Element | null): boolean {
|
|
250
|
-
|
|
251
|
-
return
|
|
252
|
-
(element ?? ref)
|
|
253
|
-
?.closest("[data-shell-root]")
|
|
254
|
-
?.getAttribute("data-desktop-overlay-panels")
|
|
255
|
-
?.split(/\s+/)
|
|
256
|
-
.includes(mobilePanelId) ?? false
|
|
257
|
-
);
|
|
348
|
+
void element;
|
|
349
|
+
return desktopConstrained && constrainedPresentation === "preview";
|
|
258
350
|
}
|
|
259
351
|
|
|
260
352
|
function dismissOverlay(): void {
|
|
261
353
|
sidebar.dismissPreview();
|
|
354
|
+
if (mobilePanelId)
|
|
355
|
+
controller.setPanelDesktopPreviewed(mobilePanelId, false);
|
|
262
356
|
}
|
|
263
357
|
|
|
264
358
|
function keepOverlay(): void {
|
|
@@ -412,7 +506,9 @@
|
|
|
412
506
|
onblur={handleOverlayBlur}
|
|
413
507
|
onclick={(event) => {
|
|
414
508
|
if (isDesktopOverlayOnly(event.currentTarget)) {
|
|
415
|
-
|
|
509
|
+
if (mobilePanelId) {
|
|
510
|
+
controller.setPanelDesktopPreviewed(mobilePanelId, true);
|
|
511
|
+
}
|
|
416
512
|
queueMicrotask(() => ref?.focus({ preventScroll: true }));
|
|
417
513
|
} else sidebar.toggle();
|
|
418
514
|
}}
|
|
@@ -448,15 +544,21 @@
|
|
|
448
544
|
data-state={sidebarState}
|
|
449
545
|
data-variant={variant}
|
|
450
546
|
data-presentation={presentation}
|
|
547
|
+
data-desktop-constrained={desktopConstrained || undefined}
|
|
548
|
+
data-constrained-presentation={constrainedPresentation}
|
|
451
549
|
data-collapsed={sidebarCollapsed || undefined}
|
|
452
550
|
data-closeable={closeable || undefined}
|
|
453
551
|
data-previewed={sidebar.previewed || undefined}
|
|
454
552
|
data-mobile-panel-id={mobilePanelId}
|
|
455
553
|
data-mobile-panel-active={mobileActive || undefined}
|
|
456
554
|
data-resizing={resizing || undefined}
|
|
555
|
+
data-consumer-hidden={Boolean(hiddenProp) || undefined}
|
|
556
|
+
data-consumer-inert={Boolean(inertProp) || undefined}
|
|
557
|
+
data-consumer-aria-hidden={ariaHidden}
|
|
457
558
|
aria-label={accessibleLabel}
|
|
458
|
-
|
|
459
|
-
|
|
559
|
+
hidden={Boolean(hiddenProp) || suspended}
|
|
560
|
+
aria-hidden={(mobileMode && !mobileActive) || covered ? "true" : ariaHidden}
|
|
561
|
+
inert={Boolean(inertProp) || (mobileMode && !mobileActive) || covered}
|
|
460
562
|
onmouseenter={keepOverlay}
|
|
461
563
|
onmouseleave={presentation === "overlay"
|
|
462
564
|
? handleOverlayPointerLeave
|
|
@@ -465,7 +567,7 @@
|
|
|
465
567
|
onfocusout={presentation === "overlay" ? handleOverlayBlur : undefined}
|
|
466
568
|
>
|
|
467
569
|
{@render children?.()}
|
|
468
|
-
{#if !mobileMode && resizable && (!sidebarCollapsed || presentation === "overlay")}
|
|
570
|
+
{#if !mobileMode && resizable && presentation !== "replace-main" && (!sidebarCollapsed || presentation === "overlay")}
|
|
469
571
|
<div
|
|
470
572
|
role="slider"
|
|
471
573
|
tabindex="0"
|
|
@@ -28,13 +28,10 @@
|
|
|
28
28
|
|
|
29
29
|
function isDesktopOverlayOnly(element?: Element | null): boolean {
|
|
30
30
|
const panelId = controller.getPanelId(sidebar);
|
|
31
|
-
|
|
31
|
+
void element;
|
|
32
32
|
return (
|
|
33
|
-
(
|
|
34
|
-
|
|
35
|
-
?.getAttribute("data-desktop-overlay-panels")
|
|
36
|
-
?.split(/\s+/)
|
|
37
|
-
.includes(panelId) ?? false
|
|
33
|
+
controller.isPanelConstrained(panelId) &&
|
|
34
|
+
controller.getConstrainedPresentation(panelId) === "preview"
|
|
38
35
|
);
|
|
39
36
|
}
|
|
40
37
|
|
|
@@ -43,19 +40,16 @@
|
|
|
43
40
|
'[data-ui-part="sidebar"][data-mobile-panel-id]',
|
|
44
41
|
);
|
|
45
42
|
const panelId = sidebarElement?.dataset.mobilePanelId;
|
|
46
|
-
const shellRoot =
|
|
47
|
-
const toggle =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: null;
|
|
43
|
+
const shellRoot = controller.mobile.getRootElement();
|
|
44
|
+
const toggle = panelId
|
|
45
|
+
? shellRoot?.querySelector<HTMLButtonElement>(
|
|
46
|
+
`[data-ui-part="sidebar-toggle"][data-target-panel-id="${CSS.escape(panelId)}"]`,
|
|
47
|
+
)
|
|
48
|
+
: null;
|
|
53
49
|
if (toggle) {
|
|
54
50
|
toggle.click();
|
|
55
51
|
toggle.focus({ preventScroll: true });
|
|
56
|
-
} else
|
|
57
|
-
sidebarElement?.removeAttribute("data-desktop-overlay-preview");
|
|
58
|
-
}
|
|
52
|
+
} else if (panelId) controller.setPanelDesktopPreviewed(panelId, false);
|
|
59
53
|
}
|
|
60
54
|
</script>
|
|
61
55
|
|
|
@@ -85,11 +79,17 @@
|
|
|
85
79
|
const targetSidebar = panelId ? controller.getPanel(panelId) : sidebar;
|
|
86
80
|
flushSync(() => {
|
|
87
81
|
targetSidebar?.close();
|
|
82
|
+
context.syncLayout();
|
|
88
83
|
sidebarElement?.dispatchEvent(
|
|
89
84
|
new Event(APP_SHELL_SIDEBAR_LAYOUT_SYNC_EVENT),
|
|
90
85
|
);
|
|
91
86
|
});
|
|
92
|
-
|
|
87
|
+
controller.syncPanelProjections();
|
|
88
|
+
requestAnimationFrame(() => {
|
|
89
|
+
if (!targetSidebar?.closed || !sidebarElement?.isConnected) return;
|
|
90
|
+
sidebarElement.hidden = true;
|
|
91
|
+
sidebarElement.dataset.state = "closed";
|
|
92
|
+
});
|
|
93
93
|
context.dismissOverlay();
|
|
94
94
|
}
|
|
95
95
|
onclick?.(event);
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
import { onDestroy, untrack } from "svelte";
|
|
3
3
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
4
4
|
import { Button } from "../../shadcn/button/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { useOptionalAppShell } from "./app-shell-context.svelte.js";
|
|
6
6
|
import type {
|
|
7
|
+
AppShellController,
|
|
7
8
|
AppShellSide,
|
|
8
9
|
AppShellSidebarController,
|
|
9
10
|
} from "./app-shell-controller.svelte.js";
|
|
10
11
|
|
|
11
12
|
let {
|
|
12
13
|
ref = $bindable(null),
|
|
14
|
+
shellController,
|
|
13
15
|
side,
|
|
14
16
|
sidebarController,
|
|
15
17
|
sidebarName,
|
|
@@ -25,6 +27,8 @@
|
|
|
25
27
|
...restProps
|
|
26
28
|
}: HTMLButtonAttributes & {
|
|
27
29
|
ref?: HTMLButtonElement | null;
|
|
30
|
+
/** Explicit root controller for persistent chrome rendered outside Root. */
|
|
31
|
+
shellController?: AppShellController;
|
|
28
32
|
/** Sidebar controlled by this toolbar action. */
|
|
29
33
|
side: AppShellSide;
|
|
30
34
|
/** Optional independent sidebar controller for repeated same-side layouts. */
|
|
@@ -41,7 +45,16 @@
|
|
|
41
45
|
previewDismissDelay?: number;
|
|
42
46
|
} = $props();
|
|
43
47
|
|
|
44
|
-
const
|
|
48
|
+
const contextualController = useOptionalAppShell();
|
|
49
|
+
const resolvedController = untrack(
|
|
50
|
+
() => shellController ?? contextualController,
|
|
51
|
+
);
|
|
52
|
+
if (!resolvedController) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"AppShell.Sidebar.Toggle requires AppShell.Root context or shellController.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const controller: AppShellController = resolvedController;
|
|
45
58
|
const sidebar = untrack(
|
|
46
59
|
() => sidebarController ?? controller.getSidebar(side),
|
|
47
60
|
);
|
|
@@ -56,8 +69,13 @@
|
|
|
56
69
|
const unsubscribeLayout = sidebar.onLayoutChange(syncLayout);
|
|
57
70
|
let mobileMode = $derived(controller.mobile.resolvedMode === "mobile");
|
|
58
71
|
let mobilePanelId = $derived(controller.getPanelId(sidebar));
|
|
59
|
-
let desktopDomPreview = $
|
|
60
|
-
|
|
72
|
+
let desktopDomPreview = $derived(
|
|
73
|
+
controller.isPanelDesktopPreviewed(mobilePanelId),
|
|
74
|
+
);
|
|
75
|
+
let desktopOverlayKnown = $derived(
|
|
76
|
+
controller.isPanelConstrained(mobilePanelId) &&
|
|
77
|
+
controller.getConstrainedPresentation(mobilePanelId) === "preview",
|
|
78
|
+
);
|
|
61
79
|
let suppressHoverPreview = false;
|
|
62
80
|
let desktopOverlayOnly = $derived(!mobileMode && desktopOverlayKnown);
|
|
63
81
|
let mobileExpanded = $derived(
|
|
@@ -82,71 +100,28 @@
|
|
|
82
100
|
onDestroy(() => {
|
|
83
101
|
unsubscribeLayout();
|
|
84
102
|
sidebar.dismissPreview();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
$effect(() => {
|
|
91
|
-
const button = ref;
|
|
92
|
-
const panelId = mobilePanelId;
|
|
93
|
-
const shellRoot = button?.closest<HTMLElement>("[data-shell-root]");
|
|
94
|
-
if (!shellRoot || !panelId) {
|
|
95
|
-
desktopOverlayKnown = false;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const update = () => {
|
|
100
|
-
desktopOverlayKnown =
|
|
101
|
-
shellRoot
|
|
102
|
-
.getAttribute("data-desktop-overlay-panels")
|
|
103
|
-
?.split(/\s+/)
|
|
104
|
-
.includes(panelId) ?? false;
|
|
105
|
-
};
|
|
106
|
-
update();
|
|
107
|
-
const observer = new MutationObserver(update);
|
|
108
|
-
observer.observe(shellRoot, {
|
|
109
|
-
attributes: true,
|
|
110
|
-
attributeFilter: ["data-desktop-overlay-panels"],
|
|
111
|
-
});
|
|
112
|
-
return () => observer.disconnect();
|
|
103
|
+
if (mobilePanelId)
|
|
104
|
+
controller.setPanelDesktopPreviewed(mobilePanelId, false);
|
|
113
105
|
});
|
|
114
106
|
|
|
115
107
|
function isDesktopOverlayOnly(element?: Element | null): boolean {
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
(element ?? ref)
|
|
119
|
-
?.closest("[data-shell-root]")
|
|
120
|
-
?.getAttribute("data-desktop-overlay-panels")
|
|
121
|
-
?.split(/\s+/)
|
|
122
|
-
.includes(mobilePanelId) ?? false
|
|
123
|
-
);
|
|
108
|
+
void element;
|
|
109
|
+
return desktopOverlayOnly;
|
|
124
110
|
}
|
|
125
111
|
|
|
126
112
|
function getDesktopOverlayTarget(
|
|
127
113
|
element?: Element | null,
|
|
128
114
|
): HTMLElement | null {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
!shellRoot
|
|
133
|
-
?.getAttribute("data-desktop-overlay-panels")
|
|
134
|
-
?.split(/\s+/)
|
|
135
|
-
.includes(mobilePanelId)
|
|
136
|
-
) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
return shellRoot.querySelector<HTMLElement>(
|
|
140
|
-
`[data-ui-part="sidebar"][data-mobile-panel-id="${CSS.escape(mobilePanelId)}"]`,
|
|
141
|
-
);
|
|
115
|
+
void element;
|
|
116
|
+
return mobilePanelId ? controller.getPanelElement(mobilePanelId) : null;
|
|
142
117
|
}
|
|
143
118
|
|
|
144
119
|
function toggleDesktopDomPreview(element: Element): boolean {
|
|
145
120
|
const target = getDesktopOverlayTarget(element);
|
|
146
|
-
if (!target) return false;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
121
|
+
if (!target || !mobilePanelId || !desktopOverlayOnly) return false;
|
|
122
|
+
const nextPreviewed = !desktopDomPreview;
|
|
123
|
+
controller.setPanelDesktopPreviewed(mobilePanelId, nextPreviewed);
|
|
124
|
+
if (nextPreviewed) {
|
|
150
125
|
queueMicrotask(() => target.focus({ preventScroll: true }));
|
|
151
126
|
}
|
|
152
127
|
return true;
|
|
@@ -168,7 +143,11 @@
|
|
|
168
143
|
? mobileExpanded
|
|
169
144
|
? "expanded"
|
|
170
145
|
: "collapsed"
|
|
171
|
-
:
|
|
146
|
+
: desktopOverlayOnly
|
|
147
|
+
? desktopDomPreview
|
|
148
|
+
? "expanded"
|
|
149
|
+
: "closed"
|
|
150
|
+
: sidebarState}
|
|
172
151
|
aria-label={resolveAccessibleLabel()}
|
|
173
152
|
aria-expanded={mobileMode
|
|
174
153
|
? mobileExpanded
|
|
@@ -209,6 +188,7 @@
|
|
|
209
188
|
sidebar.dismissPreview();
|
|
210
189
|
sidebar.toggle();
|
|
211
190
|
syncLayout();
|
|
191
|
+
controller.syncPanelProjections();
|
|
212
192
|
}
|
|
213
193
|
onclick?.(event);
|
|
214
194
|
}}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { useAppShell } from "./app-shell-context.svelte.js";
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
ref = $bindable(null),
|
|
8
|
+
open,
|
|
9
|
+
label,
|
|
10
|
+
coverPanelIds = [],
|
|
11
|
+
suspendPanelIds = [],
|
|
12
|
+
returnFocus,
|
|
13
|
+
dismissLabel = `Close ${label}`,
|
|
14
|
+
dismissOnEscape = true,
|
|
15
|
+
onDismiss,
|
|
16
|
+
class: className,
|
|
17
|
+
children,
|
|
18
|
+
...restProps
|
|
19
|
+
}: HTMLAttributes<HTMLDivElement> & {
|
|
20
|
+
ref?: HTMLElement | null;
|
|
21
|
+
/** Whether the structural replacement layer is mounted. */
|
|
22
|
+
open: boolean;
|
|
23
|
+
/** Accessible name for the non-modal replacement surface. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** Mounted sidebar ids included in the covered surface bounds. */
|
|
26
|
+
coverPanelIds?: readonly string[];
|
|
27
|
+
/** Sidebar ids hidden transiently while the layer is active. */
|
|
28
|
+
suspendPanelIds?: readonly string[];
|
|
29
|
+
/** Focus target restored after dismissal. Defaults to the opener. */
|
|
30
|
+
returnFocus?: HTMLElement | null;
|
|
31
|
+
/** Accessible name for the dismissible scrim. */
|
|
32
|
+
dismissLabel?: string;
|
|
33
|
+
/** Whether unhandled Escape dismisses the layer. */
|
|
34
|
+
dismissOnEscape?: boolean;
|
|
35
|
+
/** Consumer-owned route or state transition that closes the layer. */
|
|
36
|
+
onDismiss?: () => void;
|
|
37
|
+
children?: Snippet;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
const controller = useAppShell();
|
|
41
|
+
const token = Symbol("app-shell-surface-layer");
|
|
42
|
+
let surfaceElement = $state<HTMLElement | null>(null);
|
|
43
|
+
let inlineStart = $state(0);
|
|
44
|
+
let inlineEnd = $state(0);
|
|
45
|
+
let blockStart = $state(0);
|
|
46
|
+
let blockEnd = $state(0);
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
if (!open || typeof document === "undefined") return;
|
|
50
|
+
const { focusTarget, registration } = untrack(() => ({
|
|
51
|
+
focusTarget:
|
|
52
|
+
returnFocus ??
|
|
53
|
+
(document.activeElement instanceof HTMLElement
|
|
54
|
+
? document.activeElement
|
|
55
|
+
: null),
|
|
56
|
+
registration: {
|
|
57
|
+
token,
|
|
58
|
+
coverPanelIds: [...coverPanelIds],
|
|
59
|
+
suspendPanelIds: [...suspendPanelIds],
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
untrack(() => controller.activateSurfaceLayer(registration));
|
|
63
|
+
queueMicrotask(() => surfaceElement?.focus({ preventScroll: true }));
|
|
64
|
+
|
|
65
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
66
|
+
if (
|
|
67
|
+
!dismissOnEscape ||
|
|
68
|
+
event.defaultPrevented ||
|
|
69
|
+
event.key !== "Escape"
|
|
70
|
+
) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
onDismiss?.();
|
|
75
|
+
};
|
|
76
|
+
document.addEventListener("keydown", handleKeydown);
|
|
77
|
+
return () => {
|
|
78
|
+
document.removeEventListener("keydown", handleKeydown);
|
|
79
|
+
untrack(() => controller.deactivateSurfaceLayer(token));
|
|
80
|
+
queueMicrotask(() => {
|
|
81
|
+
const target = returnFocus ?? focusTarget;
|
|
82
|
+
if (target?.isConnected) target.focus({ preventScroll: true });
|
|
83
|
+
else controller.mobile.getMainElement()?.focus({ preventScroll: true });
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
$effect(() => {
|
|
89
|
+
void controller.panelElementsVersion;
|
|
90
|
+
if (!open || !ref || typeof ResizeObserver === "undefined") return;
|
|
91
|
+
const root = controller.mobile.getRootElement();
|
|
92
|
+
const main = controller.mobile.getMainElement();
|
|
93
|
+
if (!root || !main) return;
|
|
94
|
+
const coveredElements = coverPanelIds.flatMap((id) => {
|
|
95
|
+
const element = controller.getPanelElement(id);
|
|
96
|
+
return element ? [element] : [];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const updateInsets = () => {
|
|
100
|
+
const rootBounds = root.getBoundingClientRect();
|
|
101
|
+
const paddingLeft = rootBounds.left + root.clientLeft;
|
|
102
|
+
const paddingRight = paddingLeft + root.clientWidth;
|
|
103
|
+
const paddingTop = rootBounds.top + root.clientTop;
|
|
104
|
+
const paddingBottom = paddingTop + root.clientHeight;
|
|
105
|
+
const visibleBounds = [main, ...coveredElements]
|
|
106
|
+
.map((element) => element.getBoundingClientRect())
|
|
107
|
+
.filter((bounds) => bounds.width > 0 && bounds.height > 0);
|
|
108
|
+
if (visibleBounds.length === 0) return;
|
|
109
|
+
const left = Math.min(...visibleBounds.map((bounds) => bounds.left));
|
|
110
|
+
const right = Math.max(...visibleBounds.map((bounds) => bounds.right));
|
|
111
|
+
const top = main.getBoundingClientRect().top;
|
|
112
|
+
const bottom = main.getBoundingClientRect().bottom;
|
|
113
|
+
const rtl = getComputedStyle(root).direction === "rtl";
|
|
114
|
+
inlineStart = Math.max(
|
|
115
|
+
0,
|
|
116
|
+
rtl ? paddingRight - right : left - paddingLeft,
|
|
117
|
+
);
|
|
118
|
+
inlineEnd = Math.max(0, rtl ? left - paddingLeft : paddingRight - right);
|
|
119
|
+
blockStart = Math.max(0, top - paddingTop);
|
|
120
|
+
blockEnd = Math.max(0, paddingBottom - bottom);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let frame = requestAnimationFrame(updateInsets);
|
|
124
|
+
const scheduleUpdate = () => {
|
|
125
|
+
cancelAnimationFrame(frame);
|
|
126
|
+
frame = requestAnimationFrame(updateInsets);
|
|
127
|
+
};
|
|
128
|
+
const observer = new ResizeObserver(scheduleUpdate);
|
|
129
|
+
observer.observe(root);
|
|
130
|
+
observer.observe(main);
|
|
131
|
+
for (const element of coveredElements) observer.observe(element);
|
|
132
|
+
return () => {
|
|
133
|
+
cancelAnimationFrame(frame);
|
|
134
|
+
observer.disconnect();
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
{#if open}
|
|
140
|
+
<div
|
|
141
|
+
bind:this={ref}
|
|
142
|
+
{...restProps}
|
|
143
|
+
class={["ui-minimal-app-shell__surface-layer", className]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join(" ")}
|
|
146
|
+
data-ui-component="app-shell"
|
|
147
|
+
data-ui-part="surface-layer"
|
|
148
|
+
style:inset-inline-start={`${inlineStart}px`}
|
|
149
|
+
style:inset-inline-end={`${inlineEnd}px`}
|
|
150
|
+
style:inset-block-start={`${blockStart}px`}
|
|
151
|
+
style:inset-block-end={`${blockEnd}px`}
|
|
152
|
+
>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
tabindex="-1"
|
|
156
|
+
class="ui-minimal-app-shell__surface-layer-scrim"
|
|
157
|
+
data-ui-component="app-shell"
|
|
158
|
+
data-ui-part="surface-layer-scrim"
|
|
159
|
+
aria-label={dismissLabel}
|
|
160
|
+
onclick={() => onDismiss?.()}
|
|
161
|
+
></button>
|
|
162
|
+
<section
|
|
163
|
+
bind:this={surfaceElement}
|
|
164
|
+
class="ui-minimal-app-shell__surface-layer-surface"
|
|
165
|
+
data-ui-component="app-shell"
|
|
166
|
+
data-ui-part="surface-layer-surface"
|
|
167
|
+
aria-label={label}
|
|
168
|
+
tabindex="-1"
|
|
169
|
+
>
|
|
170
|
+
{@render children?.()}
|
|
171
|
+
</section>
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { WithElementRef } from "../../../lib/utils.js";
|
|
4
|
+
import { ScrollArea } from "../../shadcn/scroll-area/index.js";
|
|
5
|
+
import { useAppShell } from "./app-shell-context.svelte.js";
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
ref = $bindable(null),
|
|
9
|
+
class: className,
|
|
10
|
+
children,
|
|
11
|
+
...restProps
|
|
12
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
13
|
+
|
|
14
|
+
useAppShell();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
bind:this={ref}
|
|
19
|
+
{...restProps}
|
|
20
|
+
class={["ui-minimal-app-shell__surface-layer-body", className]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(" ")}
|
|
23
|
+
data-ui-component="app-shell"
|
|
24
|
+
data-ui-part="surface-layer-body"
|
|
25
|
+
>
|
|
26
|
+
<ScrollArea
|
|
27
|
+
type="auto"
|
|
28
|
+
class="ui-minimal-app-shell__surface-layer-scroll-area"
|
|
29
|
+
>
|
|
30
|
+
{@render children?.()}
|
|
31
|
+
</ScrollArea>
|
|
32
|
+
</div>
|