@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.
- package/CHANGELOG.md +22 -0
- package/LICENSE.md +3 -3
- package/README.md +20 -0
- package/package.json +24 -9
- package/spec/src/styling-and-themes.md +2 -0
- package/src/shared/ai/chat.css +23 -0
- package/src/shared/ai/chat.ts +12 -0
- package/src/shared/ai/context.svelte.ts +2 -0
- 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/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 +22 -6
- 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/index.ts +2 -0
- 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/dialog/dialog-content.svelte +3 -4
- package/src/shared/shadcn/dialog/dialog-portal.svelte +10 -2
- 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/AppShellMain.svelte +12 -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 +246 -2
- package/src/shared/shell/app-shell/app-shell-sidebar-context.svelte.ts +1 -0
- package/src/shared/shell/app-shell/examples/AppShellExamples.css +103 -0
- package/src/shared/shell/app-shell/examples/AppShellMultiPanelDemo.svelte +187 -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/theme.css +10 -0
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
language,
|
|
41
41
|
scrollTo,
|
|
42
42
|
lineAccessory,
|
|
43
|
+
lineAnnotation,
|
|
43
44
|
}: {
|
|
44
45
|
path: string;
|
|
45
46
|
oldText?: string | null;
|
|
@@ -51,6 +52,8 @@
|
|
|
51
52
|
language?: string;
|
|
52
53
|
scrollTo?: FileDiffScrollTarget | null;
|
|
53
54
|
lineAccessory?: Snippet<[FileDiffLineContext]>;
|
|
55
|
+
/** Host-owned content inserted after each matching unified diff row. */
|
|
56
|
+
lineAnnotation?: Snippet<[FileDiffLineContext]>;
|
|
54
57
|
} = $props();
|
|
55
58
|
|
|
56
59
|
let expandedBlocks = $state<ExpandedBlockState>({});
|
|
@@ -197,6 +200,14 @@
|
|
|
197
200
|
data-diff-line-variant={row.variant}
|
|
198
201
|
data-variant={row.variant}
|
|
199
202
|
>
|
|
203
|
+
{#if lineAccessory}
|
|
204
|
+
<div
|
|
205
|
+
class="ui-diff-file-diff__accessory"
|
|
206
|
+
data-ui-part="diff-row-accessory"
|
|
207
|
+
>
|
|
208
|
+
{@render lineAccessory(lineContext(row))}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
200
211
|
<span class="ui-diff-file-diff__gutter" aria-hidden="true">
|
|
201
212
|
{row.lineNumber ?? ""}
|
|
202
213
|
</span>
|
|
@@ -204,10 +215,10 @@
|
|
|
204
215
|
{row.variant === "added" ? "+" : row.variant === "removed" ? "-" : " "}
|
|
205
216
|
</span>
|
|
206
217
|
{@render highlightedText(row)}
|
|
207
|
-
{#if lineAccessory}
|
|
208
|
-
{@render lineAccessory(lineContext(row))}
|
|
209
|
-
{/if}
|
|
210
218
|
</div>
|
|
219
|
+
{#if lineAnnotation}
|
|
220
|
+
{@render lineAnnotation(lineContext(row))}
|
|
221
|
+
{/if}
|
|
211
222
|
{/snippet}
|
|
212
223
|
|
|
213
224
|
{#snippet splitCell(
|
|
@@ -226,13 +237,18 @@
|
|
|
226
237
|
data-side={side}
|
|
227
238
|
data-pair-key={pairKey}
|
|
228
239
|
>
|
|
240
|
+
{#if lineAccessory}
|
|
241
|
+
<div
|
|
242
|
+
class="ui-diff-file-diff__accessory"
|
|
243
|
+
data-ui-part="diff-row-accessory"
|
|
244
|
+
>
|
|
245
|
+
{@render lineAccessory(lineContext(row))}
|
|
246
|
+
</div>
|
|
247
|
+
{/if}
|
|
229
248
|
<span class="ui-diff-file-diff__gutter" aria-hidden="true">
|
|
230
249
|
{lineNumberForSplitSide(row, side) ?? ""}
|
|
231
250
|
</span>
|
|
232
251
|
{@render highlightedText(row)}
|
|
233
|
-
{#if lineAccessory}
|
|
234
|
-
{@render lineAccessory(lineContext(row))}
|
|
235
|
-
{/if}
|
|
236
252
|
</div>
|
|
237
253
|
{:else}
|
|
238
254
|
<div
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
+
import { tick } from "svelte";
|
|
4
|
+
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
|
5
|
+
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
|
6
|
+
import { Button } from "../../shadcn/button/index.js";
|
|
3
7
|
import { truncatePathMiddle } from "../core/unified-diff.js";
|
|
4
8
|
import FileDiff from "./FileDiff.svelte";
|
|
5
9
|
import type {
|
|
10
|
+
FileDiffComposerFileContext,
|
|
6
11
|
FileDiffFile,
|
|
12
|
+
FileDiffFileScrollTarget,
|
|
7
13
|
FileDiffLineContext,
|
|
8
14
|
FileDiffScrollTarget,
|
|
9
15
|
FileDiffViewMode,
|
|
@@ -13,48 +19,124 @@
|
|
|
13
19
|
let {
|
|
14
20
|
files,
|
|
15
21
|
viewMode = "unified",
|
|
22
|
+
selectedPath,
|
|
23
|
+
collapsedFilePaths = [],
|
|
24
|
+
scrollToFile,
|
|
25
|
+
stickyHeaders = false,
|
|
16
26
|
scrollTo,
|
|
27
|
+
onFileToggle,
|
|
28
|
+
fileHeaderLeading,
|
|
29
|
+
fileHeaderTrailing,
|
|
17
30
|
lineAccessory,
|
|
31
|
+
lineAnnotation,
|
|
18
32
|
}: {
|
|
19
33
|
files: FileDiffFile[];
|
|
20
34
|
viewMode?: FileDiffViewMode;
|
|
35
|
+
selectedPath?: string;
|
|
36
|
+
collapsedFilePaths?: readonly string[];
|
|
37
|
+
scrollToFile?: FileDiffFileScrollTarget | null;
|
|
38
|
+
stickyHeaders?: boolean;
|
|
21
39
|
scrollTo?: FileDiffScrollTarget | null;
|
|
40
|
+
onFileToggle?: (path: string, collapsed: boolean) => void;
|
|
41
|
+
fileHeaderLeading?: Snippet<[FileDiffComposerFileContext]>;
|
|
42
|
+
fileHeaderTrailing?: Snippet<[FileDiffComposerFileContext]>;
|
|
22
43
|
lineAccessory?: Snippet<[FileDiffLineContext]>;
|
|
44
|
+
/** Host-owned content inserted after each matching unified diff row. */
|
|
45
|
+
lineAnnotation?: Snippet<[FileDiffLineContext]>;
|
|
23
46
|
} = $props();
|
|
47
|
+
|
|
48
|
+
let rootEl: HTMLElement | null = $state(null);
|
|
49
|
+
|
|
50
|
+
function fileContext(file: FileDiffFile): FileDiffComposerFileContext {
|
|
51
|
+
return {
|
|
52
|
+
file,
|
|
53
|
+
path: file.path,
|
|
54
|
+
collapsed: collapsedFilePaths.includes(file.path),
|
|
55
|
+
selected: selectedPath === file.path,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$effect(() => {
|
|
60
|
+
const target = scrollToFile;
|
|
61
|
+
if (!target || !rootEl) return;
|
|
62
|
+
target.requestId;
|
|
63
|
+
void tick().then(() => {
|
|
64
|
+
const section = [
|
|
65
|
+
...(rootEl?.querySelectorAll<HTMLElement>("[data-diff-file-path]") ??
|
|
66
|
+
[]),
|
|
67
|
+
].find((candidate) => candidate.dataset.diffFilePath === target.path);
|
|
68
|
+
section?.scrollIntoView({ block: "start", inline: "nearest" });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
24
71
|
</script>
|
|
25
72
|
|
|
26
73
|
<div
|
|
74
|
+
bind:this={rootEl}
|
|
27
75
|
class="ui-diff-file-diff-composer"
|
|
28
76
|
data-ui-component="file-diff-composer"
|
|
29
77
|
data-ui-part="file-diff-composer"
|
|
78
|
+
data-sticky-headers={stickyHeaders}
|
|
30
79
|
>
|
|
31
80
|
{#if files.length === 0}
|
|
32
81
|
<p class="ui-diff-file-diff__empty">No file diffs</p>
|
|
33
82
|
{:else}
|
|
34
83
|
{#each files as file (file.path)}
|
|
84
|
+
{@const context = fileContext(file)}
|
|
35
85
|
<section
|
|
36
86
|
class="ui-diff-file-diff-composer__file"
|
|
37
87
|
data-ui-part="file-section"
|
|
38
88
|
data-diff-file-path={file.path}
|
|
89
|
+
data-selected={context.selected}
|
|
90
|
+
data-collapsed={context.collapsed}
|
|
39
91
|
>
|
|
40
92
|
<header class="ui-diff-file-diff-composer__header">
|
|
41
|
-
<
|
|
93
|
+
<div class="ui-diff-file-diff-composer__header-start">
|
|
94
|
+
{#if fileHeaderLeading}
|
|
95
|
+
<div class="ui-diff-file-diff-composer__header-leading">
|
|
96
|
+
{@render fileHeaderLeading(context)}
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
99
|
+
{#if onFileToggle}
|
|
100
|
+
<Button
|
|
101
|
+
type="button"
|
|
102
|
+
variant="ghost"
|
|
103
|
+
size="icon-xs"
|
|
104
|
+
aria-label={`${context.collapsed ? "Expand" : "Collapse"} ${file.path}`}
|
|
105
|
+
onclick={() => onFileToggle?.(file.path, !context.collapsed)}
|
|
106
|
+
>
|
|
107
|
+
{#if context.collapsed}
|
|
108
|
+
<ChevronRightIcon aria-hidden="true" />
|
|
109
|
+
{:else}
|
|
110
|
+
<ChevronDownIcon aria-hidden="true" />
|
|
111
|
+
{/if}
|
|
112
|
+
</Button>
|
|
113
|
+
{/if}
|
|
114
|
+
<span title={file.path}>{truncatePathMiddle(file.path)}</span>
|
|
115
|
+
</div>
|
|
116
|
+
{#if fileHeaderTrailing}
|
|
117
|
+
<div class="ui-diff-file-diff-composer__header-trailing">
|
|
118
|
+
{@render fileHeaderTrailing(context)}
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
42
121
|
</header>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
scrollTo
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
122
|
+
{#if !context.collapsed}
|
|
123
|
+
<FileDiff
|
|
124
|
+
path={file.path}
|
|
125
|
+
oldText={file.oldText}
|
|
126
|
+
newText={file.newText}
|
|
127
|
+
patch={file.patch}
|
|
128
|
+
language={file.language}
|
|
129
|
+
{viewMode}
|
|
130
|
+
scrollTo={scrollTo?.path === file.path ||
|
|
131
|
+
(scrollTo != null &&
|
|
132
|
+
scrollTo.path == null &&
|
|
133
|
+
files[0]?.path === file.path)
|
|
134
|
+
? scrollTo
|
|
135
|
+
: null}
|
|
136
|
+
{lineAccessory}
|
|
137
|
+
{lineAnnotation}
|
|
138
|
+
/>
|
|
139
|
+
{/if}
|
|
58
140
|
</section>
|
|
59
141
|
{/each}
|
|
60
142
|
{/if}
|
|
@@ -2,7 +2,9 @@ export { default as FileDiff } from "./FileDiff.svelte";
|
|
|
2
2
|
export { default as FileDiffComposer } from "./FileDiffComposer.svelte";
|
|
3
3
|
export { resolveDiffLanguage } from "./language.js";
|
|
4
4
|
export type {
|
|
5
|
+
FileDiffComposerFileContext,
|
|
5
6
|
FileDiffFile,
|
|
7
|
+
FileDiffFileScrollTarget,
|
|
6
8
|
FileDiffLineContext,
|
|
7
9
|
FileDiffScrollTarget,
|
|
8
10
|
FileDiffViewMode,
|
|
@@ -17,6 +17,19 @@ export interface FileDiffFile {
|
|
|
17
17
|
language?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface FileDiffComposerFileContext {
|
|
21
|
+
file: FileDiffFile;
|
|
22
|
+
path: string;
|
|
23
|
+
collapsed: boolean;
|
|
24
|
+
selected: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FileDiffFileScrollTarget {
|
|
28
|
+
path: string;
|
|
29
|
+
/** Changes for every scroll request, including repeated requests for one path. */
|
|
30
|
+
requestId: string | number;
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export interface FileDiffScrollTarget {
|
|
21
34
|
path?: string;
|
|
22
35
|
lineNumber: number;
|
package/src/shared/diff/index.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, type ButtonProps } from "../../shadcn/button/index.js";
|
|
3
|
+
import ConfirmDialog, {
|
|
4
|
+
type ConfirmDialogProps,
|
|
5
|
+
} from "../confirm-dialog/ConfirmDialog.svelte";
|
|
6
|
+
import type { Snippet } from "svelte";
|
|
7
|
+
let {
|
|
8
|
+
children,
|
|
9
|
+
disabled = false,
|
|
10
|
+
variant,
|
|
11
|
+
size = "default",
|
|
12
|
+
...confirmation
|
|
13
|
+
}: Omit<ConfirmDialogProps, "open"> & {
|
|
14
|
+
children: Snippet;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
variant?: ButtonProps["variant"];
|
|
17
|
+
size?: ButtonProps["size"];
|
|
18
|
+
} = $props();
|
|
19
|
+
let open = $state(false);
|
|
20
|
+
let trigger = $state<HTMLButtonElement | null>(null);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<Button
|
|
24
|
+
bind:ref={trigger}
|
|
25
|
+
{disabled}
|
|
26
|
+
{size}
|
|
27
|
+
variant={variant ?? (confirmation.destructive ? "destructive" : "outline")}
|
|
28
|
+
onclick={() => (open = true)}>{@render children()}</Button
|
|
29
|
+
>
|
|
30
|
+
<ConfirmDialog
|
|
31
|
+
{...confirmation}
|
|
32
|
+
bind:open
|
|
33
|
+
onCloseAutoFocus={(event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
trigger?.focus();
|
|
36
|
+
confirmation.onCloseAutoFocus?.(event);
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ConfirmButton } from "@lapismd/design-core/forms/dialog";
|
|
3
|
+
let count = $state(0);
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<ConfirmButton
|
|
7
|
+
title="Clear history?"
|
|
8
|
+
description="Remove saved history from this device."
|
|
9
|
+
destructive
|
|
10
|
+
onConfirm={() => {
|
|
11
|
+
count += 1;
|
|
12
|
+
}}>Clear history</ConfirmButton
|
|
13
|
+
>
|
|
14
|
+
<p>Cleared {count} time</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ConfirmButton } from "./ConfirmButton.svelte";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[data-ui-confirm-dialog][data-ui-part="alert-dialog-content"] {
|
|
2
|
+
width: min(28rem, calc(100vw - 2rem));
|
|
3
|
+
max-width: 28rem;
|
|
4
|
+
max-height: calc(100dvh - 2rem);
|
|
5
|
+
overflow: auto;
|
|
6
|
+
font-family: var(--font-sans, sans-serif);
|
|
7
|
+
font-size: var(--ui-dialog-frame-font-size, 0.875rem);
|
|
8
|
+
}
|
|
9
|
+
[data-ui-confirm-dialog] [data-slot="alert-dialog-title"] {
|
|
10
|
+
font-size: var(--ui-dialog-frame-title-size, 1.125rem);
|
|
11
|
+
}
|
|
12
|
+
[data-ui-confirm-dialog] [data-slot="alert-dialog-footer"] {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
justify-content: flex-end;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
}
|
|
18
|
+
[data-ui-confirm-dialog] [role="alert"] {
|
|
19
|
+
color: var(--destructive);
|
|
20
|
+
margin: 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { ComponentProps, Snippet } from "svelte";
|
|
3
|
+
import type * as AlertDialogTypes from "../../shadcn/alert-dialog/index.js";
|
|
4
|
+
export interface ConfirmDialogProps {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
title: string | Snippet;
|
|
7
|
+
description: string | Snippet;
|
|
8
|
+
confirmLabel?: string | Snippet;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
confirmDisabled?: boolean;
|
|
11
|
+
cancelLabel?: string;
|
|
12
|
+
destructive?: boolean;
|
|
13
|
+
pending?: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
/** Resolve false to retain an externally reported validation error. Reject to show a retryable error. */
|
|
16
|
+
onConfirm: () => void | boolean | Promise<void | boolean>;
|
|
17
|
+
onCancel?: () => void;
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
19
|
+
onCloseAutoFocus?: ComponentProps<
|
|
20
|
+
typeof AlertDialogTypes.Content
|
|
21
|
+
>["onCloseAutoFocus"];
|
|
22
|
+
portalProps?: ComponentProps<
|
|
23
|
+
typeof AlertDialogTypes.Content
|
|
24
|
+
>["portalProps"];
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
import * as AlertDialog from "../../shadcn/alert-dialog/index.js";
|
|
30
|
+
import { Button } from "../../shadcn/button/index.js";
|
|
31
|
+
import "./ConfirmDialog.css";
|
|
32
|
+
let {
|
|
33
|
+
open = $bindable(false),
|
|
34
|
+
title,
|
|
35
|
+
description,
|
|
36
|
+
confirmLabel = "Confirm",
|
|
37
|
+
cancelLabel = "Cancel",
|
|
38
|
+
children,
|
|
39
|
+
confirmDisabled = false,
|
|
40
|
+
destructive = false,
|
|
41
|
+
pending = false,
|
|
42
|
+
error = "",
|
|
43
|
+
onConfirm,
|
|
44
|
+
onCancel,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
onCloseAutoFocus,
|
|
47
|
+
portalProps,
|
|
48
|
+
}: ConfirmDialogProps = $props();
|
|
49
|
+
let submitting = $state(false);
|
|
50
|
+
let failure = $state("");
|
|
51
|
+
const busy = $derived(pending || submitting);
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!open) failure = "";
|
|
54
|
+
});
|
|
55
|
+
function changeOpen(next: boolean) {
|
|
56
|
+
if (busy) return;
|
|
57
|
+
open = next;
|
|
58
|
+
onOpenChange?.(next);
|
|
59
|
+
if (!next) onCancel?.();
|
|
60
|
+
}
|
|
61
|
+
async function confirm() {
|
|
62
|
+
if (busy) return;
|
|
63
|
+
submitting = true;
|
|
64
|
+
failure = "";
|
|
65
|
+
try {
|
|
66
|
+
if ((await onConfirm()) !== false) {
|
|
67
|
+
open = false;
|
|
68
|
+
onOpenChange?.(false);
|
|
69
|
+
}
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
failure =
|
|
72
|
+
cause instanceof Error
|
|
73
|
+
? cause.message
|
|
74
|
+
: "The action failed. Please try again.";
|
|
75
|
+
} finally {
|
|
76
|
+
submitting = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<AlertDialog.Root {open} onOpenChange={changeOpen}>
|
|
82
|
+
<AlertDialog.Content
|
|
83
|
+
data-ui-confirm-dialog=""
|
|
84
|
+
{...portalProps === undefined ? {} : { portalProps }}
|
|
85
|
+
{...onCloseAutoFocus === undefined ? {} : { onCloseAutoFocus }}
|
|
86
|
+
onEscapeKeydown={(event) => {
|
|
87
|
+
if (busy) event.preventDefault();
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<AlertDialog.Header>
|
|
91
|
+
<AlertDialog.Title
|
|
92
|
+
>{#if typeof title === "string"}{title}{:else}{@render title()}{/if}</AlertDialog.Title
|
|
93
|
+
>
|
|
94
|
+
<AlertDialog.Description
|
|
95
|
+
>{#if typeof description === "string"}{description}{:else}{@render description()}{/if}</AlertDialog.Description
|
|
96
|
+
>
|
|
97
|
+
</AlertDialog.Header>
|
|
98
|
+
{@render children?.()}
|
|
99
|
+
{#if failure || error}<p role="alert">{failure || error}</p>{/if}
|
|
100
|
+
<AlertDialog.Footer>
|
|
101
|
+
<Button
|
|
102
|
+
variant="outline"
|
|
103
|
+
disabled={busy}
|
|
104
|
+
onclick={() => changeOpen(false)}
|
|
105
|
+
autofocus>{cancelLabel}</Button
|
|
106
|
+
>
|
|
107
|
+
<Button
|
|
108
|
+
variant={destructive ? "destructive" : "default"}
|
|
109
|
+
disabled={busy || confirmDisabled}
|
|
110
|
+
aria-busy={busy}
|
|
111
|
+
onclick={confirm}
|
|
112
|
+
>{#if busy}Working…{:else if typeof confirmLabel === "string"}{confirmLabel}{:else}{@render confirmLabel()}{/if}</Button
|
|
113
|
+
>
|
|
114
|
+
</AlertDialog.Footer>
|
|
115
|
+
</AlertDialog.Content>
|
|
116
|
+
</AlertDialog.Root>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ConfirmDialog } from "@lapismd/design-core/forms/dialog";
|
|
3
|
+
let open = $state(true);
|
|
4
|
+
let removed = $state(false);
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<ConfirmDialog
|
|
8
|
+
bind:open
|
|
9
|
+
title="Remove record?"
|
|
10
|
+
description="Remove the selected record."
|
|
11
|
+
confirmLabel="Remove"
|
|
12
|
+
destructive
|
|
13
|
+
onConfirm={async () => {
|
|
14
|
+
removed = true;
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
<p>{removed ? "Removed" : "Available"}</p>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
[data-ui-dialog-frame][data-ui-part="dialog-content"] {
|
|
2
|
+
--ui-dialog-frame-width: 40rem;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 0;
|
|
6
|
+
width: min(var(--ui-dialog-frame-width), calc(100vw - 2rem));
|
|
7
|
+
max-width: calc(100vw - 2rem);
|
|
8
|
+
max-height: calc(100dvh - 2rem);
|
|
9
|
+
padding: 0;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
font-family: var(--font-sans, sans-serif);
|
|
12
|
+
font-size: var(--ui-dialog-frame-font-size, 0.875rem);
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
}
|
|
15
|
+
[data-ui-dialog-frame][data-frame-size="compact"] {
|
|
16
|
+
--ui-dialog-frame-width: 28rem;
|
|
17
|
+
}
|
|
18
|
+
[data-ui-dialog-frame][data-frame-size="wide"] {
|
|
19
|
+
--ui-dialog-frame-width: 68rem;
|
|
20
|
+
}
|
|
21
|
+
[data-ui-dialog-frame][data-frame-size="large"] {
|
|
22
|
+
--ui-dialog-frame-width: 56rem;
|
|
23
|
+
max-height: min(42rem, calc(100dvh - 2rem));
|
|
24
|
+
}
|
|
25
|
+
[data-ui-dialog-frame][data-frame-surface="inset"] {
|
|
26
|
+
background: var(--muted);
|
|
27
|
+
}
|
|
28
|
+
[data-ui-dialog-frame] > [data-ui-part="dialog-frame-header"] {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: flex-start;
|
|
31
|
+
gap: 1rem;
|
|
32
|
+
flex: none;
|
|
33
|
+
padding: 1.25rem 1.5rem;
|
|
34
|
+
}
|
|
35
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-heading"] {
|
|
36
|
+
flex: 1;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
display: grid;
|
|
39
|
+
gap: 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
[data-ui-dialog-frame] [data-slot="dialog-title"] {
|
|
42
|
+
font-size: var(--ui-dialog-frame-title-size, 1.125rem);
|
|
43
|
+
line-height: 1.4;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
}
|
|
46
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-close"] {
|
|
47
|
+
flex: none;
|
|
48
|
+
}
|
|
49
|
+
[data-ui-dialog-frame][data-frame-surface="inset"]
|
|
50
|
+
> [data-ui-part="dialog-frame-header"] {
|
|
51
|
+
background: var(--muted);
|
|
52
|
+
}
|
|
53
|
+
[data-ui-dialog-frame][data-frame-surface="inset"]
|
|
54
|
+
[data-ui-part="dialog-frame-close"]:is(:hover, :focus-visible) {
|
|
55
|
+
background: var(--background);
|
|
56
|
+
color: var(--foreground);
|
|
57
|
+
}
|
|
58
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-layout"] {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex: 1;
|
|
61
|
+
min-height: 0;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
}
|
|
64
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-sidebar"] {
|
|
65
|
+
flex: 0 0 10rem;
|
|
66
|
+
padding: 0.5rem 1rem 1rem;
|
|
67
|
+
background: var(--muted);
|
|
68
|
+
}
|
|
69
|
+
[data-ui-dialog-frame][data-frame-surface="inset"]
|
|
70
|
+
[data-ui-part="dialog-frame-sidebar"] {
|
|
71
|
+
flex-basis: 13rem;
|
|
72
|
+
}
|
|
73
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-main"] {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
flex: 1;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
min-height: 0;
|
|
79
|
+
}
|
|
80
|
+
[data-ui-dialog-frame][data-frame-surface="inset"]
|
|
81
|
+
[data-ui-part="dialog-frame-main"] {
|
|
82
|
+
margin: 0 1rem 1rem 0;
|
|
83
|
+
border-radius: 1rem;
|
|
84
|
+
background: var(--background);
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
}
|
|
87
|
+
[data-ui-dialog-frame] .ui-dialog-frame-scroll {
|
|
88
|
+
flex: 1;
|
|
89
|
+
min-height: 0;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
}
|
|
95
|
+
[data-ui-dialog-frame]
|
|
96
|
+
.ui-dialog-frame-scroll
|
|
97
|
+
> [data-ui-part="scroll-area-viewport"] {
|
|
98
|
+
flex: 1;
|
|
99
|
+
height: auto;
|
|
100
|
+
min-height: 0;
|
|
101
|
+
}
|
|
102
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-body"] {
|
|
103
|
+
padding: 0.5rem 1.5rem 1.5rem;
|
|
104
|
+
}
|
|
105
|
+
[data-ui-dialog-frame][data-frame-padding="none"]
|
|
106
|
+
[data-ui-part="dialog-frame-body"] {
|
|
107
|
+
padding: 0;
|
|
108
|
+
}
|
|
109
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-footer"] {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex: none;
|
|
112
|
+
flex-direction: row;
|
|
113
|
+
justify-content: flex-end;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
gap: 0.5rem;
|
|
116
|
+
padding: 1rem 1.5rem;
|
|
117
|
+
background: var(--popover);
|
|
118
|
+
}
|
|
119
|
+
@media (max-width: 40rem) {
|
|
120
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-layout"] {
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
}
|
|
123
|
+
[data-ui-dialog-frame] [data-ui-part="dialog-frame-sidebar"] {
|
|
124
|
+
flex: none;
|
|
125
|
+
}
|
|
126
|
+
[data-ui-dialog-frame][data-frame-surface="inset"]
|
|
127
|
+
[data-ui-part="dialog-frame-main"] {
|
|
128
|
+
margin-left: 1rem;
|
|
129
|
+
}
|
|
130
|
+
}
|