@svadmin/ui 0.47.1 → 0.49.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/dist/components/AdminApp.svelte +20 -0
- package/dist/components/AdminApp.svelte.d.ts +5 -1
- package/dist/components/ApiSettings.svelte +137 -50
- package/dist/components/AutoTable.svelte +7 -3
- package/dist/components/AutoTable.svelte.d.ts +1 -0
- package/dist/components/Layout.svelte +15 -1
- package/dist/components/PasswordInput.svelte +4 -0
- package/dist/components/PasswordInput.svelte.d.ts +1 -0
- package/dist/components/SecuritySettings.svelte +156 -91
- package/dist/components/SecuritySettings.svelte.d.ts +1 -2
- package/dist/components/account/SettingsEnterprisePage.svelte +227 -23
- package/dist/components/account/SettingsSidebarPage.svelte +12 -1
- package/dist/components/content/WorkspaceActionBar.svelte +33 -0
- package/dist/components/content/WorkspaceActionBar.svelte.d.ts +12 -0
- package/dist/components/content/WorkspaceInspector.svelte +57 -0
- package/dist/components/content/WorkspaceInspector.svelte.d.ts +15 -0
- package/dist/components/content/WorkspaceLayout.svelte +12 -1
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +2 -0
- package/dist/components/content/WorkspaceSplitPane.svelte +34 -0
- package/dist/components/content/WorkspaceSplitPane.svelte.d.ts +11 -0
- package/dist/components/content/WorkspaceStageStepper.svelte +71 -0
- package/dist/components/content/WorkspaceStageStepper.svelte.d.ts +17 -0
- package/dist/components/content/index.d.ts +4 -0
- package/dist/components/content/index.js +4 -0
- package/dist/components/ui/sheet/sheet.svelte +122 -10
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
primaryAction: Snippet;
|
|
10
|
+
secondaryActions?: Snippet;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { title, description, status, primaryAction, secondaryActions, class: className = '' }: Props = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<section
|
|
18
|
+
class={cn('flex flex-col gap-3 border-y border-border bg-card px-4 py-3 sm:flex-row sm:items-center sm:justify-between', className)}
|
|
19
|
+
aria-label={title}
|
|
20
|
+
data-svadmin-workspace-action-bar
|
|
21
|
+
>
|
|
22
|
+
<div class="min-w-0">
|
|
23
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
24
|
+
{#if status}<span class="rounded-sm bg-warning/15 px-2 py-0.5 text-xs font-medium text-warning-foreground">{status}</span>{/if}
|
|
25
|
+
<h2 class="text-sm font-semibold text-foreground">{title}</h2>
|
|
26
|
+
</div>
|
|
27
|
+
{#if description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>{/if}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
|
30
|
+
{#if secondaryActions}{@render secondaryActions()}{/if}
|
|
31
|
+
{@render primaryAction()}
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
primaryAction: Snippet;
|
|
7
|
+
secondaryActions?: Snippet;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const WorkspaceActionBar: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type WorkspaceActionBar = ReturnType<typeof WorkspaceActionBar>;
|
|
12
|
+
export default WorkspaceActionBar;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { PanelRightClose, PanelRightOpen } from '@lucide/svelte';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { cn } from '../../utils.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
open?: boolean;
|
|
11
|
+
ontoggle?: (open: boolean) => void;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
footer?: Snippet;
|
|
14
|
+
openLabel?: string;
|
|
15
|
+
closeLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
open = true,
|
|
23
|
+
ontoggle,
|
|
24
|
+
children,
|
|
25
|
+
footer,
|
|
26
|
+
openLabel = 'Open inspector',
|
|
27
|
+
closeLabel = 'Close inspector',
|
|
28
|
+
class: className = '',
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
function toggle(): void {
|
|
32
|
+
ontoggle?.(!open);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<aside class={cn('overflow-hidden rounded-lg border border-border bg-card', className)} data-svadmin-workspace-inspector data-open={open}>
|
|
37
|
+
{#if open}
|
|
38
|
+
<header class="flex items-start justify-between gap-3 border-b border-border px-4 py-3">
|
|
39
|
+
<div class="min-w-0">
|
|
40
|
+
<h2 class="text-sm font-semibold text-foreground">{title}</h2>
|
|
41
|
+
{#if description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>{/if}
|
|
42
|
+
</div>
|
|
43
|
+
<Button variant="ghost" size="icon-sm" aria-label={closeLabel} title={closeLabel} onclick={toggle}>
|
|
44
|
+
<PanelRightClose class="size-4" />
|
|
45
|
+
</Button>
|
|
46
|
+
</header>
|
|
47
|
+
<div class="max-h-[calc(100vh-15rem)] overflow-y-auto p-4">{@render children()}</div>
|
|
48
|
+
{#if footer}<footer class="border-t border-border p-3">{@render footer()}</footer>{/if}
|
|
49
|
+
{:else}
|
|
50
|
+
<div class="flex min-h-64 flex-col items-center gap-3 py-3">
|
|
51
|
+
<Button variant="ghost" size="icon-sm" aria-label={openLabel} title={openLabel} onclick={toggle}>
|
|
52
|
+
<PanelRightOpen class="size-4" />
|
|
53
|
+
</Button>
|
|
54
|
+
<span class="[writing-mode:vertical-rl] text-xs font-medium text-muted-foreground">{title}</span>
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
</aside>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
ontoggle?: (open: boolean) => void;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
footer?: Snippet;
|
|
9
|
+
openLabel?: string;
|
|
10
|
+
closeLabel?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const WorkspaceInspector: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type WorkspaceInspector = ReturnType<typeof WorkspaceInspector>;
|
|
15
|
+
export default WorkspaceInspector;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
secondary?: Snippet;
|
|
8
8
|
summary?: Snippet;
|
|
9
9
|
secondaryWidth?: string;
|
|
10
|
+
secondaryCollapsed?: boolean;
|
|
11
|
+
secondaryCollapsedWidth?: string;
|
|
10
12
|
mobileOrder?: 'primary-first' | 'secondary-first';
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
@@ -16,12 +18,21 @@
|
|
|
16
18
|
secondary,
|
|
17
19
|
summary,
|
|
18
20
|
secondaryWidth = '22rem',
|
|
21
|
+
secondaryCollapsed = false,
|
|
22
|
+
secondaryCollapsedWidth = '3rem',
|
|
19
23
|
mobileOrder = 'primary-first',
|
|
20
24
|
class: className = '',
|
|
21
25
|
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const resolvedSecondaryWidth = $derived(secondaryCollapsed ? secondaryCollapsedWidth : secondaryWidth);
|
|
22
28
|
</script>
|
|
23
29
|
|
|
24
|
-
<div
|
|
30
|
+
<div
|
|
31
|
+
class={cn('flex flex-col gap-6', className)}
|
|
32
|
+
data-svadmin-workspace-layout
|
|
33
|
+
data-secondary-collapsed={secondaryCollapsed}
|
|
34
|
+
style:--workspace-secondary-width={resolvedSecondaryWidth}
|
|
35
|
+
>
|
|
25
36
|
{#if summary}<div data-svadmin-workspace-summary>{@render summary()}</div>{/if}
|
|
26
37
|
<div class={cn('grid min-w-0 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_var(--workspace-secondary-width)]', mobileOrder === 'secondary-first' && 'grid-rows-[auto_auto]')}>
|
|
27
38
|
{#if mobileOrder === 'secondary-first' && secondary}<aside class="order-1 min-w-0 xl:order-2">{@render secondary()}</aside>{/if}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
primary: Snippet;
|
|
7
|
+
secondary: Snippet;
|
|
8
|
+
secondaryWidth?: string;
|
|
9
|
+
reverse?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
primary,
|
|
15
|
+
secondary,
|
|
16
|
+
secondaryWidth = '42%',
|
|
17
|
+
reverse = false,
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
class={cn(
|
|
24
|
+
'grid min-w-0 overflow-hidden rounded-lg border border-border bg-card',
|
|
25
|
+
reverse ? 'lg:grid-cols-[var(--workspace-split-width)_minmax(0,1fr)]' : 'lg:grid-cols-[minmax(0,1fr)_var(--workspace-split-width)]',
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
style:--workspace-split-width={secondaryWidth}
|
|
29
|
+
data-svadmin-workspace-split-pane
|
|
30
|
+
data-reverse={reverse}
|
|
31
|
+
>
|
|
32
|
+
<section class={cn('min-w-0 p-4', reverse && 'lg:order-2')} data-svadmin-split-primary>{@render primary()}</section>
|
|
33
|
+
<section class={cn('min-w-0 border-t border-border bg-muted/15 p-4 lg:border-l lg:border-t-0', reverse && 'lg:order-1 lg:border-l-0 lg:border-r')} data-svadmin-split-secondary>{@render secondary()}</section>
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
primary: Snippet;
|
|
4
|
+
secondary: Snippet;
|
|
5
|
+
secondaryWidth?: string;
|
|
6
|
+
reverse?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const WorkspaceSplitPane: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type WorkspaceSplitPane = ReturnType<typeof WorkspaceSplitPane>;
|
|
11
|
+
export default WorkspaceSplitPane;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type WorkspaceStageStatus = 'complete' | 'current' | 'blocked' | 'pending';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceStage {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status: WorkspaceStageStatus;
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { AlertTriangle, Check, Circle } from '@lucide/svelte';
|
|
14
|
+
import { cn } from '../../utils.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
stages: WorkspaceStage[];
|
|
18
|
+
activeId: string;
|
|
19
|
+
onselect?: (stage: WorkspaceStage) => void;
|
|
20
|
+
ariaLabel?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
stages,
|
|
26
|
+
activeId,
|
|
27
|
+
onselect,
|
|
28
|
+
ariaLabel = 'Workflow stages',
|
|
29
|
+
class: className = '',
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
const markerClass: Record<WorkspaceStageStatus, string> = {
|
|
33
|
+
complete: 'border-success/30 bg-success/10 text-success',
|
|
34
|
+
current: 'border-primary bg-primary text-primary-foreground',
|
|
35
|
+
blocked: 'border-warning/40 bg-warning/15 text-warning-foreground',
|
|
36
|
+
pending: 'border-border bg-background text-muted-foreground',
|
|
37
|
+
};
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<nav aria-label={ariaLabel} class={cn('overflow-x-auto', className)} data-svadmin-workspace-stages>
|
|
41
|
+
<ol class="flex min-w-max items-start gap-0 px-0.5 py-1">
|
|
42
|
+
{#each stages as stage, index (stage.id)}
|
|
43
|
+
<li class="flex items-start">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="group grid min-w-28 grid-cols-[auto_minmax(0,1fr)] gap-x-2 rounded-md px-2 py-1.5 text-left outline-none transition-colors hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring sm:min-w-36"
|
|
47
|
+
aria-current={stage.id === activeId ? 'step' : undefined}
|
|
48
|
+
aria-label={`${stage.label}: ${stage.status}`}
|
|
49
|
+
onclick={() => onselect?.(stage)}
|
|
50
|
+
>
|
|
51
|
+
<span class={cn('flex size-7 items-center justify-center rounded-full border', markerClass[stage.status])}>
|
|
52
|
+
{#if stage.status === 'complete'}
|
|
53
|
+
<Check class="size-3.5" />
|
|
54
|
+
{:else if stage.status === 'blocked'}
|
|
55
|
+
<AlertTriangle class="size-3.5" />
|
|
56
|
+
{:else}
|
|
57
|
+
<Circle class={cn('size-2.5', stage.status === 'current' && 'fill-current')} />
|
|
58
|
+
{/if}
|
|
59
|
+
</span>
|
|
60
|
+
<span class="min-w-0 pt-0.5">
|
|
61
|
+
<span class={cn('block text-xs font-medium', stage.id === activeId ? 'text-foreground' : 'text-muted-foreground')}>{stage.label}</span>
|
|
62
|
+
{#if stage.description}<span class="mt-0.5 block max-w-32 truncate text-[0.6875rem] text-muted-foreground">{stage.description}</span>{/if}
|
|
63
|
+
</span>
|
|
64
|
+
</button>
|
|
65
|
+
{#if index < stages.length - 1}
|
|
66
|
+
<span class={cn('mt-5 h-px w-4 bg-border sm:w-7', stage.status === 'complete' && 'bg-success/50')} aria-hidden="true"></span>
|
|
67
|
+
{/if}
|
|
68
|
+
</li>
|
|
69
|
+
{/each}
|
|
70
|
+
</ol>
|
|
71
|
+
</nav>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type WorkspaceStageStatus = 'complete' | 'current' | 'blocked' | 'pending';
|
|
2
|
+
export interface WorkspaceStage {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
status: WorkspaceStageStatus;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
stages: WorkspaceStage[];
|
|
10
|
+
activeId: string;
|
|
11
|
+
onselect?: (stage: WorkspaceStage) => void;
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const WorkspaceStageStepper: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type WorkspaceStageStepper = ReturnType<typeof WorkspaceStageStepper>;
|
|
17
|
+
export default WorkspaceStageStepper;
|
|
@@ -3,6 +3,10 @@ export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
|
3
3
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
4
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
5
|
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
|
+
export { default as WorkspaceStageStepper } from './WorkspaceStageStepper.svelte';
|
|
7
|
+
export { default as WorkspaceActionBar } from './WorkspaceActionBar.svelte';
|
|
8
|
+
export { default as WorkspaceInspector } from './WorkspaceInspector.svelte';
|
|
9
|
+
export { default as WorkspaceSplitPane } from './WorkspaceSplitPane.svelte';
|
|
6
10
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
7
11
|
export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
8
12
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
@@ -3,6 +3,10 @@ export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
|
3
3
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
4
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
5
|
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
|
+
export { default as WorkspaceStageStepper } from './WorkspaceStageStepper.svelte';
|
|
7
|
+
export { default as WorkspaceActionBar } from './WorkspaceActionBar.svelte';
|
|
8
|
+
export { default as WorkspaceInspector } from './WorkspaceInspector.svelte';
|
|
9
|
+
export { default as WorkspaceSplitPane } from './WorkspaceSplitPane.svelte';
|
|
6
10
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
7
11
|
export { default as SettingsFieldRow } from './SettingsFieldRow.svelte';
|
|
8
12
|
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
@@ -1,8 +1,51 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
let bodyScrollLockDepth = 0;
|
|
3
|
+
let bodyOverflowBeforeLock = "";
|
|
4
|
+
const openSheets: HTMLElement[] = [];
|
|
5
|
+
|
|
6
|
+
function lockBodyScroll() {
|
|
7
|
+
if (bodyScrollLockDepth === 0) bodyOverflowBeforeLock = document.body.style.overflow;
|
|
8
|
+
bodyScrollLockDepth += 1;
|
|
9
|
+
document.body.style.overflow = "hidden";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function unlockBodyScroll() {
|
|
13
|
+
bodyScrollLockDepth = Math.max(0, bodyScrollLockDepth - 1);
|
|
14
|
+
if (bodyScrollLockDepth > 0) return;
|
|
15
|
+
document.body.style.overflow = bodyOverflowBeforeLock;
|
|
16
|
+
bodyOverflowBeforeLock = "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function registerSheet(panel: HTMLElement) {
|
|
20
|
+
openSheets.push(panel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unregisterSheet(panel: HTMLElement) {
|
|
24
|
+
const index = openSheets.lastIndexOf(panel);
|
|
25
|
+
if (index >= 0) openSheets.splice(index, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isTopmostSheet(panel: HTMLElement) {
|
|
29
|
+
return openSheets.at(-1) === panel;
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
1
33
|
<script lang="ts">
|
|
2
34
|
/* eslint-disable svelte/no-unused-svelte-ignore */
|
|
35
|
+
import { tick } from "svelte";
|
|
3
36
|
import { cn, type WithElementRef } from "../../../utils.js";
|
|
4
37
|
import type { HTMLAttributes } from "svelte/elements";
|
|
5
38
|
|
|
39
|
+
const focusableSelector = [
|
|
40
|
+
'a[href]',
|
|
41
|
+
'button',
|
|
42
|
+
'input',
|
|
43
|
+
'select',
|
|
44
|
+
'textarea',
|
|
45
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
46
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
47
|
+
].join(",");
|
|
48
|
+
|
|
6
49
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
7
50
|
open?: boolean;
|
|
8
51
|
side?: "left" | "right";
|
|
@@ -15,6 +58,7 @@
|
|
|
15
58
|
side = "right",
|
|
16
59
|
onClose,
|
|
17
60
|
class: className,
|
|
61
|
+
tabindex = -1,
|
|
18
62
|
children,
|
|
19
63
|
...restProps
|
|
20
64
|
}: Props = $props();
|
|
@@ -24,22 +68,88 @@
|
|
|
24
68
|
onClose?.();
|
|
25
69
|
}
|
|
26
70
|
|
|
27
|
-
function
|
|
28
|
-
|
|
71
|
+
function isContentEditableHost(element: HTMLElement) {
|
|
72
|
+
const contentEditable = element.getAttribute("contenteditable");
|
|
73
|
+
const isEditable = element.isContentEditable || (contentEditable !== null && contentEditable.toLowerCase() !== "false");
|
|
74
|
+
return isEditable && !element.parentElement?.isContentEditable;
|
|
29
75
|
}
|
|
30
76
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
document.removeEventListener("keydown", handleKeydown);
|
|
37
|
-
document.body.style.overflow = "";
|
|
38
|
-
};
|
|
77
|
+
function isVisible(element: HTMLElement) {
|
|
78
|
+
for (let current: HTMLElement | null = element; current; current = current.parentElement) {
|
|
79
|
+
const style = getComputedStyle(current);
|
|
80
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
81
|
+
if (current === ref) break;
|
|
39
82
|
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isTabbable(element: HTMLElement) {
|
|
87
|
+
if (element.matches(":disabled, [aria-disabled='true']")) return false;
|
|
88
|
+
if (element.closest("[hidden], [inert], [aria-hidden='true']")) return false;
|
|
89
|
+
if (element.hasAttribute("tabindex") && element.tabIndex < 0) return false;
|
|
90
|
+
if (!element.hasAttribute("tabindex") && !isContentEditableHost(element) && element.tabIndex < 0) return false;
|
|
91
|
+
return isVisible(element);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function tabbableElements() {
|
|
95
|
+
return ref ? [...ref.querySelectorAll<HTMLElement>(focusableSelector)].filter(isTabbable) : [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
99
|
+
if (event.defaultPrevented || !open || !ref || !isTopmostSheet(ref)) return;
|
|
100
|
+
if (event.key === "Escape") {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
close();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.key !== "Tab") return;
|
|
106
|
+
|
|
107
|
+
const tabbable = tabbableElements();
|
|
108
|
+
if (tabbable.length === 0) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
ref.focus();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const first = tabbable[0];
|
|
115
|
+
const last = tabbable.at(-1);
|
|
116
|
+
const activeIndex = tabbable.indexOf(document.activeElement as HTMLElement);
|
|
117
|
+
if (!ref.contains(document.activeElement) || activeIndex === -1) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
(event.shiftKey ? last : first)?.focus();
|
|
120
|
+
} else if (event.shiftKey && activeIndex === 0) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
last?.focus();
|
|
123
|
+
} else if (!event.shiftKey && activeIndex === tabbable.length - 1) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
first.focus();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$effect(() => {
|
|
130
|
+
if (!open || !ref) return;
|
|
131
|
+
const panel = ref;
|
|
132
|
+
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
133
|
+
registerSheet(panel);
|
|
134
|
+
lockBodyScroll();
|
|
135
|
+
void tick().then(() => {
|
|
136
|
+
if (open && ref === panel && isTopmostSheet(panel)) {
|
|
137
|
+
(tabbableElements()[0] ?? panel).focus();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return () => {
|
|
141
|
+
unregisterSheet(panel);
|
|
142
|
+
unlockBodyScroll();
|
|
143
|
+
const remainingSheet = openSheets.at(-1);
|
|
144
|
+
if (!remainingSheet || (previousFocus && remainingSheet.contains(previousFocus))) {
|
|
145
|
+
previousFocus?.focus();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
40
148
|
});
|
|
41
149
|
</script>
|
|
42
150
|
|
|
151
|
+
<svelte:document onkeydown={handleKeydown} />
|
|
152
|
+
|
|
43
153
|
{#if open}
|
|
44
154
|
<!-- Overlay -->
|
|
45
155
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
@@ -50,9 +160,11 @@
|
|
|
50
160
|
></div>
|
|
51
161
|
|
|
52
162
|
<!-- Sheet panel -->
|
|
163
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
53
164
|
<div
|
|
54
165
|
bind:this={ref}
|
|
55
166
|
data-slot="sheet"
|
|
167
|
+
{tabindex}
|
|
56
168
|
class={cn(
|
|
57
169
|
"fixed z-50 flex translate-x-0 flex-col gap-4 bg-background p-6 shadow-lg",
|
|
58
170
|
side === "right"
|
package/dist/index.d.ts
CHANGED
|
@@ -159,6 +159,11 @@ export { default as ContentPageHeader } from './components/content/ContentPageHe
|
|
|
159
159
|
export { default as SectionHeader } from './components/content/SectionHeader.svelte';
|
|
160
160
|
export { default as PageToolbar } from './components/content/PageToolbar.svelte';
|
|
161
161
|
export { default as WorkspaceLayout } from './components/content/WorkspaceLayout.svelte';
|
|
162
|
+
export { default as WorkspaceStageStepper } from './components/content/WorkspaceStageStepper.svelte';
|
|
163
|
+
export type { WorkspaceStage, WorkspaceStageStatus } from './components/content/WorkspaceStageStepper.svelte';
|
|
164
|
+
export { default as WorkspaceActionBar } from './components/content/WorkspaceActionBar.svelte';
|
|
165
|
+
export { default as WorkspaceInspector } from './components/content/WorkspaceInspector.svelte';
|
|
166
|
+
export { default as WorkspaceSplitPane } from './components/content/WorkspaceSplitPane.svelte';
|
|
162
167
|
export { default as SettingsGroup } from './components/content/SettingsGroup.svelte';
|
|
163
168
|
export { default as SettingsFieldRow } from './components/content/SettingsFieldRow.svelte';
|
|
164
169
|
export { default as MetricBlock } from './components/content/MetricBlock.svelte';
|
package/dist/index.js
CHANGED
|
@@ -174,6 +174,10 @@ export { default as ContentPageHeader } from './components/content/ContentPageHe
|
|
|
174
174
|
export { default as SectionHeader } from './components/content/SectionHeader.svelte';
|
|
175
175
|
export { default as PageToolbar } from './components/content/PageToolbar.svelte';
|
|
176
176
|
export { default as WorkspaceLayout } from './components/content/WorkspaceLayout.svelte';
|
|
177
|
+
export { default as WorkspaceStageStepper } from './components/content/WorkspaceStageStepper.svelte';
|
|
178
|
+
export { default as WorkspaceActionBar } from './components/content/WorkspaceActionBar.svelte';
|
|
179
|
+
export { default as WorkspaceInspector } from './components/content/WorkspaceInspector.svelte';
|
|
180
|
+
export { default as WorkspaceSplitPane } from './components/content/WorkspaceSplitPane.svelte';
|
|
177
181
|
export { default as SettingsGroup } from './components/content/SettingsGroup.svelte';
|
|
178
182
|
export { default as SettingsFieldRow } from './components/content/SettingsFieldRow.svelte';
|
|
179
183
|
export { default as MetricBlock } from './components/content/MetricBlock.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./components/*": "./dist/components/*"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@svadmin/core": "^0.
|
|
42
|
+
"@svadmin/core": "^0.39.0",
|
|
43
43
|
"@tanstack/svelte-query": "^6.1.38",
|
|
44
44
|
"highlight.js": "^11.11.1",
|
|
45
45
|
"isomorphic-dompurify": "^3.22.0",
|