@svadmin/ui 0.55.1 → 0.57.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/app.css +28 -0
- package/dist/app.theme.css +28 -0
- package/dist/components/DetailDrawer.svelte +37 -26
- package/dist/components/DetailDrawer.svelte.d.ts +5 -4
- package/dist/components/EmptyState.svelte +1 -1
- package/dist/components/ErrorComponent.svelte +1 -1
- package/dist/components/FieldRenderer.svelte +2 -1
- package/dist/components/TaskQueueDrawer.svelte +1 -1
- package/dist/components/account/MembersStarterPage.svelte +1 -1
- package/dist/components/content/DataState.svelte +1 -1
- package/dist/components/content/FilterToolbar.svelte +6 -2
- package/dist/components/content/MediaThumbnail.svelte +109 -0
- package/dist/components/content/MediaThumbnail.svelte.d.ts +22 -0
- package/dist/components/content/ProjectCard.svelte +2 -1
- package/dist/components/content/SystemErrorState.svelte +1 -1
- package/dist/components/content/WorkspaceActionBar.svelte +27 -2
- package/dist/components/content/WorkspaceActionBar.svelte.d.ts +2 -0
- package/dist/components/content/WorkspaceTabBar.svelte +94 -0
- package/dist/components/content/WorkspaceTabBar.svelte.d.ts +23 -0
- package/dist/components/content/index.d.ts +5 -0
- package/dist/components/content/index.js +2 -0
- package/dist/components/fields/ImageField.svelte +19 -4
- package/dist/components/fields/ImageField.svelte.d.ts +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/app.css
CHANGED
|
@@ -263,6 +263,27 @@
|
|
|
263
263
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
@layer components {
|
|
267
|
+
/* Collapsed semantic containers must not retain surface chrome or spacing. */
|
|
268
|
+
details.svadmin-collapsible:not([open]),
|
|
269
|
+
[data-svadmin-collapsible]:not([data-open="true"]) {
|
|
270
|
+
margin: 0 !important;
|
|
271
|
+
padding: 0 !important;
|
|
272
|
+
border: 0 !important;
|
|
273
|
+
background: transparent !important;
|
|
274
|
+
box-shadow: none !important;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
[data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced[hidden] {
|
|
278
|
+
display: none !important;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
[data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced:not([hidden]) {
|
|
282
|
+
min-width: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
266
287
|
body {
|
|
267
288
|
background: var(--background);
|
|
268
289
|
color: var(--foreground);
|
|
@@ -605,6 +626,13 @@ main {
|
|
|
605
626
|
font-weight: 600;
|
|
606
627
|
}
|
|
607
628
|
|
|
629
|
+
/* Collapsible details normalization stays scoped to svadmin-owned hooks. */
|
|
630
|
+
details[data-svadmin-filter]:not([open]) > :not(summary),
|
|
631
|
+
details.svadmin-collapsible-filter:not([open]) > :not(summary),
|
|
632
|
+
details.svadmin-collapsible:not([open]) > :not(summary) {
|
|
633
|
+
display: none !important;
|
|
634
|
+
}
|
|
635
|
+
|
|
608
636
|
/* --- Stripe-first layout preset (clean-flat) --- */
|
|
609
637
|
/* Keep this public class for layoutPreset API compatibility. The preset only
|
|
610
638
|
* adjusts density and composition; color, focus, and elevation stay semantic. */
|
package/dist/app.theme.css
CHANGED
|
@@ -263,6 +263,27 @@
|
|
|
263
263
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
@layer components {
|
|
267
|
+
/* Collapsed semantic containers must not retain surface chrome or spacing. */
|
|
268
|
+
details.svadmin-collapsible:not([open]),
|
|
269
|
+
[data-svadmin-collapsible]:not([data-open="true"]) {
|
|
270
|
+
margin: 0 !important;
|
|
271
|
+
padding: 0 !important;
|
|
272
|
+
border: 0 !important;
|
|
273
|
+
background: transparent !important;
|
|
274
|
+
box-shadow: none !important;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
[data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced[hidden] {
|
|
278
|
+
display: none !important;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
[data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced:not([hidden]) {
|
|
282
|
+
min-width: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
266
287
|
body {
|
|
267
288
|
background: var(--background);
|
|
268
289
|
color: var(--foreground);
|
|
@@ -604,6 +625,13 @@ main {
|
|
|
604
625
|
font-weight: 600;
|
|
605
626
|
}
|
|
606
627
|
|
|
628
|
+
/* Collapsible details normalization stays scoped to svadmin-owned hooks. */
|
|
629
|
+
details[data-svadmin-filter]:not([open]) > :not(summary),
|
|
630
|
+
details.svadmin-collapsible-filter:not([open]) > :not(summary),
|
|
631
|
+
details.svadmin-collapsible:not([open]) > :not(summary) {
|
|
632
|
+
display: none !important;
|
|
633
|
+
}
|
|
634
|
+
|
|
607
635
|
/* --- Stripe-first layout preset (clean-flat) --- */
|
|
608
636
|
/* Keep this public class for layoutPreset API compatibility. The preset only
|
|
609
637
|
* adjusts density and composition; color, focus, and elevation stay semantic. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { Snippet } from
|
|
3
|
-
import type { HTMLAttributes } from
|
|
4
|
-
import { cn, type WithElementRef } from
|
|
5
|
-
import * as Sheet from
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { cn, type WithElementRef } from "../utils.js";
|
|
5
|
+
import * as Sheet from "./ui/sheet/index.js";
|
|
6
6
|
|
|
7
7
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
8
8
|
open?: boolean;
|
|
@@ -11,30 +11,32 @@
|
|
|
11
11
|
description?: string;
|
|
12
12
|
descriptionId?: string;
|
|
13
13
|
closeLabel?: string;
|
|
14
|
-
side?:
|
|
14
|
+
side?: "left" | "right";
|
|
15
15
|
width?: string;
|
|
16
16
|
onClose?: () => void;
|
|
17
17
|
children?: Snippet;
|
|
18
18
|
footer?: Snippet;
|
|
19
|
+
extra?: Snippet;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
let {
|
|
22
23
|
open = $bindable(false),
|
|
23
|
-
title =
|
|
24
|
-
description =
|
|
25
|
-
side =
|
|
26
|
-
width =
|
|
24
|
+
title = "",
|
|
25
|
+
description = "",
|
|
26
|
+
side = "right",
|
|
27
|
+
width = "max-w-md w-full sm:max-w-lg",
|
|
27
28
|
ref = $bindable(null),
|
|
28
29
|
class: className,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
"aria-label": ariaLabel,
|
|
31
|
+
"aria-labelledby": ariaLabelledby,
|
|
32
|
+
"aria-describedby": ariaDescribedby,
|
|
32
33
|
titleId,
|
|
33
34
|
descriptionId,
|
|
34
|
-
closeLabel =
|
|
35
|
+
closeLabel = "Close",
|
|
35
36
|
onClose,
|
|
36
37
|
children,
|
|
37
38
|
footer,
|
|
39
|
+
extra,
|
|
38
40
|
...restProps
|
|
39
41
|
}: Props = $props();
|
|
40
42
|
|
|
@@ -50,34 +52,43 @@
|
|
|
50
52
|
{side}
|
|
51
53
|
onClose={handleClose}
|
|
52
54
|
{closeLabel}
|
|
53
|
-
class={cn(
|
|
55
|
+
class={cn("gap-0 p-0 overflow-hidden", width, className)}
|
|
54
56
|
role="dialog"
|
|
55
57
|
aria-modal="true"
|
|
56
|
-
aria-label={(ariaLabelledby || titleId) ? ariaLabel : (ariaLabel || title ||
|
|
58
|
+
aria-label={(ariaLabelledby || titleId) ? ariaLabel : (ariaLabel || title || "Details")}
|
|
57
59
|
aria-labelledby={ariaLabelledby || (titleId ? titleId : undefined)}
|
|
58
60
|
aria-describedby={ariaDescribedby || (descriptionId ? descriptionId : undefined)}
|
|
59
61
|
{...restProps}
|
|
60
62
|
>
|
|
61
|
-
<Sheet.Content class="flex h-full flex-col">
|
|
62
|
-
{#if title || description}
|
|
63
|
-
<Sheet.Header>
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
<Sheet.Content class="flex h-full flex-col gap-0 p-0">
|
|
64
|
+
{#if title || description || extra}
|
|
65
|
+
<Sheet.Header class="flex-shrink-0 border-b border-border px-6 py-4 pr-12">
|
|
66
|
+
<div class="flex items-start justify-between gap-4">
|
|
67
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
68
|
+
{#if title}
|
|
69
|
+
<Sheet.Title id={titleId}>{title}</Sheet.Title>
|
|
70
|
+
{/if}
|
|
71
|
+
{#if description}
|
|
72
|
+
<Sheet.Description id={descriptionId}>{description}</Sheet.Description>
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
{#if extra}
|
|
76
|
+
<div class="flex flex-shrink-0 items-center gap-2">
|
|
77
|
+
{@render extra()}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
70
81
|
</Sheet.Header>
|
|
71
82
|
{/if}
|
|
72
83
|
|
|
73
|
-
<div class="flex-1 overflow-y-auto
|
|
84
|
+
<div class="flex-1 overflow-y-auto p-6">
|
|
74
85
|
{#if children}
|
|
75
86
|
{@render children()}
|
|
76
87
|
{/if}
|
|
77
88
|
</div>
|
|
78
89
|
|
|
79
90
|
{#if footer}
|
|
80
|
-
<Sheet.Footer>
|
|
91
|
+
<Sheet.Footer class="mt-auto flex-shrink-0 border-t border-border px-6 py-4">
|
|
81
92
|
{@render footer()}
|
|
82
93
|
</Sheet.Footer>
|
|
83
94
|
{/if}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Snippet } from
|
|
2
|
-
import type { HTMLAttributes } from
|
|
3
|
-
import { type WithElementRef } from
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { type WithElementRef } from "../utils.js";
|
|
4
4
|
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
5
5
|
open?: boolean;
|
|
6
6
|
title?: string;
|
|
@@ -8,11 +8,12 @@ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
|
8
8
|
description?: string;
|
|
9
9
|
descriptionId?: string;
|
|
10
10
|
closeLabel?: string;
|
|
11
|
-
side?:
|
|
11
|
+
side?: "left" | "right";
|
|
12
12
|
width?: string;
|
|
13
13
|
onClose?: () => void;
|
|
14
14
|
children?: Snippet;
|
|
15
15
|
footer?: Snippet;
|
|
16
|
+
extra?: Snippet;
|
|
16
17
|
};
|
|
17
18
|
declare const DetailDrawer: import("svelte").Component<Props, {}, "open" | "ref">;
|
|
18
19
|
type DetailDrawer = ReturnType<typeof DetailDrawer>;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
19
|
<div
|
|
20
|
-
class="flex flex-col items-center justify-center
|
|
20
|
+
class="flex flex-col items-center justify-center rounded-lg bg-card/50 px-8 py-10 text-center shadow-sm"
|
|
21
21
|
in:scale={{ start: 0.95, duration: 250, opacity: 0 }}
|
|
22
22
|
>
|
|
23
23
|
<div class="h-20 w-20 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}>();
|
|
20
20
|
</script>
|
|
21
21
|
|
|
22
|
-
<div class="flex flex-col items-center justify-center
|
|
22
|
+
<div class="flex flex-col items-center justify-center px-8 py-10 text-center">
|
|
23
23
|
<Alert.Root variant="destructive" class="max-w-md text-left">
|
|
24
24
|
<AlertTriangle class="h-5 w-5" />
|
|
25
25
|
<Alert.Title>{title}</Alert.Title>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import TooltipButton from './TooltipButton.svelte';
|
|
18
18
|
import ComboboxField from './ComboboxField.svelte';
|
|
19
19
|
import ArrayField from './fields/ArrayField.svelte';
|
|
20
|
+
import MediaThumbnail from './content/MediaThumbnail.svelte';
|
|
20
21
|
import { Plus, X } from '@lucide/svelte';
|
|
21
22
|
import type { Snippet } from 'svelte';
|
|
22
23
|
import { getRichTextEditor } from '../editor-config.svelte.js';
|
|
@@ -429,7 +430,7 @@
|
|
|
429
430
|
{disabled}
|
|
430
431
|
/>
|
|
431
432
|
{#if url}
|
|
432
|
-
<
|
|
433
|
+
<div class="size-9 shrink-0"><MediaThumbnail src={url} alt="preview" size="full" fit="cover" showOverlay={false} /></div>
|
|
433
434
|
{/if}
|
|
434
435
|
<TooltipButton tooltip={i18n.t('common.removeImage')} variant="ghost" size="icon" class="h-8 w-8 shrink-0" onclick={() => removeImage(i)}>
|
|
435
436
|
<X class="h-3.5 w-3.5" />
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
}}
|
|
363
363
|
/>
|
|
364
364
|
{:else}
|
|
365
|
-
<div class="flex h-full
|
|
365
|
+
<div class="flex h-full items-center justify-center rounded-lg border border-dashed border-border/60 bg-background/70 p-8 text-center text-sm text-muted-foreground">
|
|
366
366
|
{i18n.t('task.selectHint')}
|
|
367
367
|
</div>
|
|
368
368
|
{/if}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<ContentPageShell pageId="account-members-starter" width="wide">
|
|
26
26
|
<ContentPageHeader title={isZh ? '开始组建团队' : 'Start building your team'} description={isZh ? '这是零成员工作区的起始页。邀请第一位成员,或通过 CSV 批量导入。' : 'This is the starting state for a workspace with no members. Invite the first teammate or import a CSV roster.'} />
|
|
27
27
|
|
|
28
|
-
<section class="grid
|
|
28
|
+
<section class="grid overflow-hidden rounded-lg border border-border bg-card lg:grid-cols-[minmax(0,1fr)_21rem]">
|
|
29
29
|
<div class="flex flex-col items-center justify-center px-6 py-12 text-center">
|
|
30
30
|
<span class="flex size-14 items-center justify-center rounded-lg bg-primary/10 text-primary"><Users class="size-6" /></span>
|
|
31
31
|
<Badge variant="outline" class="mt-5">0 {isZh ? '名成员' : 'members'}</Badge>
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
{#if action}<div class="mt-3">{@render action()}</div>{/if}
|
|
54
54
|
</Alert.Root>
|
|
55
55
|
{:else}
|
|
56
|
-
<div class={'flex
|
|
56
|
+
<div class={'flex flex-col items-center justify-center rounded-lg border border-dashed border-border bg-card px-6 py-8 text-center ' + className}>
|
|
57
57
|
<Inbox class="size-8 text-muted-foreground" /><h3 class="mt-3 text-sm font-semibold text-foreground">{resolvedTitle}</h3><p class="mt-1 max-w-sm text-sm text-muted-foreground">{resolvedDescription}</p>
|
|
58
58
|
{#if action}<div class="mt-4">{@render action()}</div>{/if}
|
|
59
59
|
</div>
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { Badge } from '../ui/badge/index.js';
|
|
7
7
|
import { cn } from '../../utils.js';
|
|
8
8
|
|
|
9
|
+
const uid = $props.id();
|
|
10
|
+
|
|
9
11
|
interface Props {
|
|
10
12
|
query?: string;
|
|
11
13
|
placeholder?: string;
|
|
@@ -37,9 +39,10 @@
|
|
|
37
39
|
}: Props = $props();
|
|
38
40
|
|
|
39
41
|
const isCompact = $derived(density === 'compact');
|
|
42
|
+
const advancedPanelId = `svadmin-filter-toolbar-advanced-${uid}`;
|
|
40
43
|
</script>
|
|
41
44
|
|
|
42
|
-
<div class={cn('space-y-2', className)}>
|
|
45
|
+
<div class={cn('space-y-2', className)} data-svadmin-filter-toolbar data-advanced-open={advanced && advancedOpen ? 'true' : 'false'}>
|
|
43
46
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
44
47
|
{#if showSearch}
|
|
45
48
|
<div class="relative min-w-0 flex-1">
|
|
@@ -76,6 +79,7 @@
|
|
|
76
79
|
class={cn('gap-1.5 shrink-0', isCompact ? 'h-8 text-xs px-2.5' : '')}
|
|
77
80
|
onclick={() => advancedOpen = !advancedOpen}
|
|
78
81
|
aria-expanded={advancedOpen}
|
|
82
|
+
aria-controls={advancedPanelId}
|
|
79
83
|
>
|
|
80
84
|
<SlidersHorizontal class="size-3.5" />
|
|
81
85
|
<span>{advancedLabel}</span>
|
|
@@ -98,7 +102,7 @@
|
|
|
98
102
|
{/if}
|
|
99
103
|
</div>
|
|
100
104
|
{#if advanced && advancedOpen}
|
|
101
|
-
<div class="rounded-lg border border-border/60 bg-muted/20 p-3">
|
|
105
|
+
<div id={advancedPanelId} class="svadmin-filter-toolbar-advanced rounded-lg border border-border/60 bg-muted/20 p-3" aria-hidden="false">
|
|
102
106
|
{@render advanced()}
|
|
103
107
|
</div>
|
|
104
108
|
{/if}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Eye, FileText, ImageOff, Loader2 } from '@lucide/svelte';
|
|
3
|
+
import * as Dialog from '../ui/dialog/index.js';
|
|
4
|
+
import { cn } from '../../utils.js';
|
|
5
|
+
|
|
6
|
+
export type MediaThumbnailSize = 'sm' | 'md' | 'lg' | 'full';
|
|
7
|
+
export type MediaThumbnailFit = 'contain' | 'cover';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
src?: string | null;
|
|
11
|
+
alt?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
fileName?: string;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
size?: MediaThumbnailSize;
|
|
16
|
+
fit?: MediaThumbnailFit;
|
|
17
|
+
aspectRatio?: string;
|
|
18
|
+
showOverlay?: boolean;
|
|
19
|
+
overlayText?: string;
|
|
20
|
+
emptyLabel?: string;
|
|
21
|
+
loadingLabel?: string;
|
|
22
|
+
errorLabel?: string;
|
|
23
|
+
onopen?: () => void;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
src = null,
|
|
29
|
+
alt = '',
|
|
30
|
+
title = '',
|
|
31
|
+
fileName = '',
|
|
32
|
+
mimeType = '',
|
|
33
|
+
size = 'md',
|
|
34
|
+
fit = 'contain',
|
|
35
|
+
aspectRatio,
|
|
36
|
+
showOverlay = true,
|
|
37
|
+
overlayText = 'View',
|
|
38
|
+
emptyLabel = 'No media',
|
|
39
|
+
loadingLabel = 'Loading media',
|
|
40
|
+
errorLabel = 'Image unavailable',
|
|
41
|
+
onopen,
|
|
42
|
+
class: className = '',
|
|
43
|
+
}: Props = $props();
|
|
44
|
+
|
|
45
|
+
const isImage = $derived(Boolean(
|
|
46
|
+
(mimeType && mimeType.startsWith('image/'))
|
|
47
|
+
|| (src && /\.(png|jpe?g|webp|gif|svg|bmp|avif)(\?.*)?$/i.test(src))
|
|
48
|
+
|| (fileName && /\.(png|jpe?g|webp|gif|svg|bmp|avif)$/i.test(fileName))
|
|
49
|
+
|| (src && !mimeType && !fileName && !/\.(pdf|docx?|xlsx?|zip|tar|gz|txt|csv|json)$/i.test(src))
|
|
50
|
+
));
|
|
51
|
+
const sizeClasses: Record<MediaThumbnailSize, string> = {
|
|
52
|
+
sm: 'h-16 min-w-16 max-w-24',
|
|
53
|
+
md: 'min-h-20 max-h-56 w-full',
|
|
54
|
+
lg: 'min-h-32 max-h-80 w-full',
|
|
55
|
+
full: 'h-full w-full',
|
|
56
|
+
};
|
|
57
|
+
const fitClasses: Record<MediaThumbnailFit, string> = {
|
|
58
|
+
contain: 'object-contain',
|
|
59
|
+
cover: 'object-cover',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let imageState = $state<'loading' | 'loaded' | 'error'>('loading');
|
|
63
|
+
let previewOpen = $state(false);
|
|
64
|
+
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (src && isImage) imageState = 'loading';
|
|
67
|
+
previewOpen = false;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function openMedia() {
|
|
71
|
+
if (onopen) onopen();
|
|
72
|
+
else previewOpen = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function openDocument() {
|
|
76
|
+
if (onopen) onopen();
|
|
77
|
+
else if (src) window.open(src, '_blank', 'noopener,noreferrer');
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
{#if isImage && src}
|
|
82
|
+
<div class={cn('relative flex items-center justify-center overflow-hidden rounded-lg border border-border bg-muted/30 transition-colors', sizeClasses[size], className)} style:aspect-ratio={aspectRatio} data-slot="media-thumbnail" data-size={size} data-media-state={imageState} aria-busy={imageState === 'loading' || undefined}>
|
|
83
|
+
<img {src} alt={alt || title || fileName || 'Thumbnail'} class={cn('block max-h-full max-w-full transition-transform duration-200', showOverlay && 'group-hover:scale-[1.02]', fitClasses[fit], imageState !== 'loaded' && 'invisible')} loading="lazy" onload={() => imageState = 'loaded'} onerror={() => imageState = 'error'} />
|
|
84
|
+
|
|
85
|
+
{#if imageState === 'loading'}
|
|
86
|
+
<div class="absolute inset-0 flex items-center justify-center bg-muted/40" role="status" aria-label={loadingLabel}><Loader2 class="size-4 animate-spin text-muted-foreground" aria-hidden="true" /></div>
|
|
87
|
+
{:else if imageState === 'error'}
|
|
88
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center gap-1 p-3 text-center text-xs text-muted-foreground" role="img" aria-label={errorLabel} data-slot="media-thumbnail-error"><ImageOff class="size-5" aria-hidden="true" /><span class="max-w-[12rem] truncate text-[11px]">{errorLabel}</span></div>
|
|
89
|
+
{:else if showOverlay}
|
|
90
|
+
<Dialog.Root bind:open={previewOpen}>
|
|
91
|
+
<button type="button" class="group absolute inset-0 cursor-zoom-in outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset" onclick={openMedia} aria-label={alt ? `Preview ${alt}` : overlayText}>
|
|
92
|
+
<span class="pointer-events-none absolute inset-0 flex items-center justify-center gap-1.5 bg-background/60 text-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" aria-hidden="true"><Eye class="size-4" /><span class="text-xs font-semibold">{overlayText}</span></span>
|
|
93
|
+
</button>
|
|
94
|
+
{#if !onopen}
|
|
95
|
+
<Dialog.Content class="max-w-[min(92vw,72rem)] border-none bg-transparent p-0 shadow-none">
|
|
96
|
+
<Dialog.Header class="sr-only"><Dialog.Title>{alt || title || fileName || 'Media preview'}</Dialog.Title><Dialog.Description>{alt || title || fileName || 'Expanded media preview'}</Dialog.Description></Dialog.Header>
|
|
97
|
+
<img {src} alt={alt || title || fileName || 'Media preview'} class="max-h-[82vh] w-full rounded-md object-contain" />
|
|
98
|
+
</Dialog.Content>
|
|
99
|
+
{/if}
|
|
100
|
+
</Dialog.Root>
|
|
101
|
+
{/if}
|
|
102
|
+
</div>
|
|
103
|
+
{:else if src || fileName}
|
|
104
|
+
<button type="button" class={cn('inline-flex items-center gap-2 rounded-lg border border-border bg-muted/20 px-3 py-2 text-left text-xs text-foreground transition-colors hover:bg-muted/40', className)} onclick={openDocument} data-slot="media-thumbnail-document">
|
|
105
|
+
<FileText class="size-4 shrink-0 text-muted-foreground" /><span class="truncate font-medium" title={fileName || alt || title}>{fileName || alt || title || 'Document'}</span>
|
|
106
|
+
</button>
|
|
107
|
+
{:else}
|
|
108
|
+
<span class={cn('inline-flex items-center gap-1 text-xs text-muted-foreground', className)} data-media-state="empty"><ImageOff class="size-3.5" aria-hidden="true" /><span>{emptyLabel}</span></span>
|
|
109
|
+
{/if}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type MediaThumbnailSize = 'sm' | 'md' | 'lg' | 'full';
|
|
2
|
+
export type MediaThumbnailFit = 'contain' | 'cover';
|
|
3
|
+
interface Props {
|
|
4
|
+
src?: string | null;
|
|
5
|
+
alt?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
fileName?: string;
|
|
8
|
+
mimeType?: string;
|
|
9
|
+
size?: MediaThumbnailSize;
|
|
10
|
+
fit?: MediaThumbnailFit;
|
|
11
|
+
aspectRatio?: string;
|
|
12
|
+
showOverlay?: boolean;
|
|
13
|
+
overlayText?: string;
|
|
14
|
+
emptyLabel?: string;
|
|
15
|
+
loadingLabel?: string;
|
|
16
|
+
errorLabel?: string;
|
|
17
|
+
onopen?: () => void;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const MediaThumbnail: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type MediaThumbnail = ReturnType<typeof MediaThumbnail>;
|
|
22
|
+
export default MediaThumbnail;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Badge } from '../ui/badge/index.js';
|
|
6
6
|
import Progress from '../ui/progress/progress.svelte';
|
|
7
7
|
import StatusBadge from './StatusBadge.svelte';
|
|
8
|
+
import MediaThumbnail from './MediaThumbnail.svelte';
|
|
8
9
|
export type ProjectStatus = 'active' | 'completed' | 'on-hold' | 'draft';
|
|
9
10
|
export interface ProjectSummary {
|
|
10
11
|
id: string;
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
const progress = $derived(project.progress ?? (project.status === 'completed' ? 100 : project.status === 'active' ? 72 : 0));
|
|
23
24
|
</script>
|
|
24
25
|
<Card.Card data-interactive={onclick ? 'true' : undefined} class={'h-full ' + className}>
|
|
25
|
-
{#if project.image}<
|
|
26
|
+
{#if project.image}<div class="h-32 w-full"><MediaThumbnail src={project.image} alt={project.name} size="full" fit="cover" showOverlay={false} /></div>{/if}
|
|
26
27
|
<Card.CardContent class="flex h-full flex-col gap-4 p-4">
|
|
27
28
|
<div class="flex items-start justify-between gap-3">
|
|
28
29
|
<div class="flex min-w-0 items-center gap-3"><span class="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-muted/50 text-muted-foreground"><FolderKanban class="size-4" /></span><h3 class="truncate text-sm font-semibold text-foreground">{project.name}</h3></div>
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
const resolvedTitle = $derived(title ?? (isNotFound ? i18n.t('common.pageNotFound') : i18n.t('common.error')));
|
|
14
14
|
const resolvedDescription = $derived(description ?? (isNotFound ? i18n.t('error.pageNotFoundDescription') : i18n.t('error.internalServerErrorDescription')));
|
|
15
15
|
</script>
|
|
16
|
-
<div class="flex
|
|
16
|
+
<div class="flex items-center justify-center p-4" data-svadmin-system-error>
|
|
17
17
|
<Card.Card class="w-full max-w-md"><Card.CardContent class="space-y-5 p-8 text-center"><div class="mx-auto flex size-14 items-center justify-center rounded-lg bg-muted text-muted-foreground">{#if isNotFound}<FileQuestion class="size-7" />{:else}<AlertTriangle class="size-7 text-destructive" />{/if}</div><div class="space-y-2"><p class="text-xs font-medium text-muted-foreground">{status}</p><h2 class="text-xl font-semibold text-foreground">{resolvedTitle}</h2><p class="text-sm leading-6 text-muted-foreground">{resolvedDescription}</p></div>{#if action}<div>{@render action()}</div>{/if}<div class="flex flex-wrap justify-center gap-2"><Button variant="outline" onclick={() => adminContext.back()}><ArrowLeft class="size-4" />{i18n.t('common.back')}</Button><Button onclick={() => adminContext.navigate('/')}><Home class="size-4" />{i18n.t('common.returnHome')}</Button></div></Card.CardContent></Card.Card>
|
|
18
18
|
</div>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type WorkspaceActionBarTone = 'default' | 'warning' | 'success' | 'destructive' | 'info';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
6
|
import type { Snippet } from 'svelte';
|
|
3
7
|
import { cn } from '../../utils.js';
|
|
@@ -6,12 +10,29 @@
|
|
|
6
10
|
title: string;
|
|
7
11
|
description?: string;
|
|
8
12
|
status?: string;
|
|
13
|
+
statusTone?: WorkspaceActionBarTone;
|
|
9
14
|
primaryAction: Snippet;
|
|
10
15
|
secondaryActions?: Snippet;
|
|
11
16
|
class?: string;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
let {
|
|
19
|
+
let {
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
status,
|
|
23
|
+
statusTone = 'warning',
|
|
24
|
+
primaryAction,
|
|
25
|
+
secondaryActions,
|
|
26
|
+
class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const toneClassMap: Record<WorkspaceActionBarTone, string> = {
|
|
30
|
+
default: 'bg-muted text-muted-foreground',
|
|
31
|
+
warning: 'bg-warning/15 text-warning-foreground',
|
|
32
|
+
success: 'bg-success/15 text-success',
|
|
33
|
+
destructive: 'bg-destructive/15 text-destructive',
|
|
34
|
+
info: 'bg-info/15 text-info',
|
|
35
|
+
};
|
|
15
36
|
</script>
|
|
16
37
|
|
|
17
38
|
<section
|
|
@@ -21,7 +42,11 @@
|
|
|
21
42
|
>
|
|
22
43
|
<div class="min-w-0">
|
|
23
44
|
<div class="flex flex-wrap items-center gap-2">
|
|
24
|
-
{#if status}
|
|
45
|
+
{#if status}
|
|
46
|
+
<span class={cn('rounded-sm px-2 py-0.5 text-xs font-medium', toneClassMap[statusTone] || toneClassMap.warning)}>
|
|
47
|
+
{status}
|
|
48
|
+
</span>
|
|
49
|
+
{/if}
|
|
25
50
|
<h2 class="text-sm font-semibold text-foreground">{title}</h2>
|
|
26
51
|
</div>
|
|
27
52
|
{#if description}<p class="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>{/if}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
export type WorkspaceActionBarTone = 'default' | 'warning' | 'success' | 'destructive' | 'info';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
interface Props {
|
|
3
4
|
title: string;
|
|
4
5
|
description?: string;
|
|
5
6
|
status?: string;
|
|
7
|
+
statusTone?: WorkspaceActionBarTone;
|
|
6
8
|
primaryAction: Snippet;
|
|
7
9
|
secondaryActions?: Snippet;
|
|
8
10
|
class?: string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceTabItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: Component<{ class?: string; 'aria-hidden'?: string | boolean }>;
|
|
8
|
+
badge?: string | number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { cn } from '../../utils.js';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
items: WorkspaceTabItem[];
|
|
19
|
+
activeId: string;
|
|
20
|
+
onselect?: (item: WorkspaceTabItem) => void;
|
|
21
|
+
variant?: 'segmented' | 'pill' | 'underline';
|
|
22
|
+
ariaLabel?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
items,
|
|
28
|
+
activeId,
|
|
29
|
+
onselect,
|
|
30
|
+
variant = 'segmented',
|
|
31
|
+
ariaLabel = 'Workspace tabs',
|
|
32
|
+
class: className = '',
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<nav
|
|
37
|
+
aria-label={ariaLabel}
|
|
38
|
+
class={cn(
|
|
39
|
+
'flex items-center gap-1 overflow-x-auto',
|
|
40
|
+
variant === 'segmented' && 'rounded-lg border border-border bg-muted/40 p-1',
|
|
41
|
+
variant === 'pill' && 'gap-1.5 p-0.5',
|
|
42
|
+
variant === 'underline' && 'gap-2 border-b border-border',
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
data-svadmin-workspace-tab-bar
|
|
46
|
+
data-variant={variant}
|
|
47
|
+
>
|
|
48
|
+
{#each items as item (item.id)}
|
|
49
|
+
{@const Icon = item.icon}
|
|
50
|
+
{@const isActive = item.id === activeId}
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
disabled={item.disabled}
|
|
54
|
+
class={cn(
|
|
55
|
+
'inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
56
|
+
variant === 'segmented' && [
|
|
57
|
+
isActive
|
|
58
|
+
? 'bg-background text-foreground shadow-sm font-semibold'
|
|
59
|
+
: 'text-muted-foreground hover:bg-background/50 hover:text-foreground',
|
|
60
|
+
],
|
|
61
|
+
variant === 'pill' && [
|
|
62
|
+
isActive
|
|
63
|
+
? 'bg-primary text-primary-foreground font-semibold'
|
|
64
|
+
: 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
65
|
+
],
|
|
66
|
+
variant === 'underline' && [
|
|
67
|
+
'rounded-none border-b-2 px-3 py-2',
|
|
68
|
+
isActive
|
|
69
|
+
? 'border-primary text-primary font-semibold'
|
|
70
|
+
: 'border-transparent text-muted-foreground hover:border-border hover:text-foreground',
|
|
71
|
+
],
|
|
72
|
+
)}
|
|
73
|
+
aria-current={isActive ? 'page' : undefined}
|
|
74
|
+
onclick={() => onselect?.(item)}
|
|
75
|
+
>
|
|
76
|
+
{#if Icon}
|
|
77
|
+
<Icon class="size-4 shrink-0" aria-hidden="true" />
|
|
78
|
+
{/if}
|
|
79
|
+
<span>{item.label}</span>
|
|
80
|
+
{#if item.badge !== undefined && item.badge !== ''}
|
|
81
|
+
<span
|
|
82
|
+
class={cn(
|
|
83
|
+
'inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-[0.6875rem] font-bold leading-none tabular-nums',
|
|
84
|
+
isActive
|
|
85
|
+
? (variant === 'pill' ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-primary/10 text-primary')
|
|
86
|
+
: 'bg-muted text-muted-foreground',
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{item.badge}
|
|
90
|
+
</span>
|
|
91
|
+
{/if}
|
|
92
|
+
</button>
|
|
93
|
+
{/each}
|
|
94
|
+
</nav>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
export interface WorkspaceTabItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: Component<{
|
|
6
|
+
class?: string;
|
|
7
|
+
'aria-hidden'?: string | boolean;
|
|
8
|
+
}>;
|
|
9
|
+
badge?: string | number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
interface Props {
|
|
14
|
+
items: WorkspaceTabItem[];
|
|
15
|
+
activeId: string;
|
|
16
|
+
onselect?: (item: WorkspaceTabItem) => void;
|
|
17
|
+
variant?: 'segmented' | 'pill' | 'underline';
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const WorkspaceTabBar: Component<Props, {}, "">;
|
|
22
|
+
type WorkspaceTabBar = ReturnType<typeof WorkspaceTabBar>;
|
|
23
|
+
export default WorkspaceTabBar;
|
|
@@ -5,6 +5,9 @@ export { default as PageToolbar } from './PageToolbar.svelte';
|
|
|
5
5
|
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
6
|
export { default as WorkspaceStageStepper } from './WorkspaceStageStepper.svelte';
|
|
7
7
|
export { default as WorkspaceActionBar } from './WorkspaceActionBar.svelte';
|
|
8
|
+
export type { WorkspaceActionBarTone } from './WorkspaceActionBar.svelte';
|
|
9
|
+
export { default as WorkspaceTabBar } from './WorkspaceTabBar.svelte';
|
|
10
|
+
export type { WorkspaceTabItem } from './WorkspaceTabBar.svelte';
|
|
8
11
|
export { default as WorkspaceInspector } from './WorkspaceInspector.svelte';
|
|
9
12
|
export { default as WorkspaceSplitPane } from './WorkspaceSplitPane.svelte';
|
|
10
13
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
@@ -43,3 +46,5 @@ export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
|
43
46
|
export type { FeedbackNoticeTone, FeedbackNoticePriority } from './FeedbackNotice.svelte';
|
|
44
47
|
export { default as AuditTimeline } from './AuditTimeline.svelte';
|
|
45
48
|
export type { TimelineItem } from './AuditTimeline.svelte';
|
|
49
|
+
export { default as MediaThumbnail } from './MediaThumbnail.svelte';
|
|
50
|
+
export type { MediaThumbnailSize, MediaThumbnailFit } from './MediaThumbnail.svelte';
|
|
@@ -5,6 +5,7 @@ export { default as PageToolbar } from './PageToolbar.svelte';
|
|
|
5
5
|
export { default as WorkspaceLayout } from './WorkspaceLayout.svelte';
|
|
6
6
|
export { default as WorkspaceStageStepper } from './WorkspaceStageStepper.svelte';
|
|
7
7
|
export { default as WorkspaceActionBar } from './WorkspaceActionBar.svelte';
|
|
8
|
+
export { default as WorkspaceTabBar } from './WorkspaceTabBar.svelte';
|
|
8
9
|
export { default as WorkspaceInspector } from './WorkspaceInspector.svelte';
|
|
9
10
|
export { default as WorkspaceSplitPane } from './WorkspaceSplitPane.svelte';
|
|
10
11
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
|
@@ -30,3 +31,4 @@ export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
|
30
31
|
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
31
32
|
export { default as FeedbackNotice } from './FeedbackNotice.svelte';
|
|
32
33
|
export { default as AuditTimeline } from './AuditTimeline.svelte';
|
|
34
|
+
export { default as MediaThumbnail } from './MediaThumbnail.svelte';
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import MediaThumbnail from '../content/MediaThumbnail.svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value: string | null | undefined;
|
|
6
|
+
alt?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
previewable?: boolean;
|
|
11
|
+
emptyLabel?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { value, alt = '', title, width = 80, height = 80, previewable = true, emptyLabel = 'No image', class: className = '' }: Props = $props();
|
|
4
16
|
</script>
|
|
17
|
+
|
|
5
18
|
{#if value}
|
|
6
|
-
<
|
|
19
|
+
<div style={`width: ${width}px; height: ${height}px;`} class="inline-block shrink-0">
|
|
20
|
+
<MediaThumbnail src={value} {alt} {title} size="full" fit="cover" showOverlay={previewable} class={className} />
|
|
21
|
+
</div>
|
|
7
22
|
{:else}
|
|
8
|
-
<
|
|
23
|
+
<MediaThumbnail src={null} {emptyLabel} class={className} />
|
|
9
24
|
{/if}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
value: string | null | undefined;
|
|
3
3
|
alt?: string;
|
|
4
|
+
title?: string;
|
|
4
5
|
width?: number;
|
|
5
6
|
height?: number;
|
|
7
|
+
previewable?: boolean;
|
|
8
|
+
emptyLabel?: string;
|
|
9
|
+
class?: string;
|
|
6
10
|
}
|
|
7
11
|
declare const ImageField: import("svelte").Component<Props, {}, "">;
|
|
8
12
|
type ImageField = ReturnType<typeof ImageField>;
|
package/dist/index.d.ts
CHANGED
|
@@ -170,6 +170,9 @@ export { default as WorkspaceLayout } from './components/content/WorkspaceLayout
|
|
|
170
170
|
export { default as WorkspaceStageStepper } from './components/content/WorkspaceStageStepper.svelte';
|
|
171
171
|
export type { WorkspaceStage, WorkspaceStageStatus } from './components/content/WorkspaceStageStepper.svelte';
|
|
172
172
|
export { default as WorkspaceActionBar } from './components/content/WorkspaceActionBar.svelte';
|
|
173
|
+
export type { WorkspaceActionBarTone } from './components/content/WorkspaceActionBar.svelte';
|
|
174
|
+
export { default as WorkspaceTabBar } from './components/content/WorkspaceTabBar.svelte';
|
|
175
|
+
export type { WorkspaceTabItem } from './components/content/WorkspaceTabBar.svelte';
|
|
173
176
|
export { default as WorkspaceInspector } from './components/content/WorkspaceInspector.svelte';
|
|
174
177
|
export { default as WorkspaceSplitPane } from './components/content/WorkspaceSplitPane.svelte';
|
|
175
178
|
export { default as SettingsGroup } from './components/content/SettingsGroup.svelte';
|
|
@@ -224,3 +227,5 @@ export type { DateRangeFormat, DateRangeValue } from './components/fields/DateRa
|
|
|
224
227
|
export type { PercentTone } from './components/fields/PercentField.svelte';
|
|
225
228
|
export type { RatingSize } from './components/fields/RatingField.svelte';
|
|
226
229
|
export type { CurrencyTone } from './components/fields/CurrencyField.svelte';
|
|
230
|
+
export { default as MediaThumbnail } from './components/content/MediaThumbnail.svelte';
|
|
231
|
+
export type { MediaThumbnailSize, MediaThumbnailFit } from './components/content/MediaThumbnail.svelte';
|
package/dist/index.js
CHANGED
|
@@ -184,6 +184,7 @@ export { default as PageToolbar } from './components/content/PageToolbar.svelte'
|
|
|
184
184
|
export { default as WorkspaceLayout } from './components/content/WorkspaceLayout.svelte';
|
|
185
185
|
export { default as WorkspaceStageStepper } from './components/content/WorkspaceStageStepper.svelte';
|
|
186
186
|
export { default as WorkspaceActionBar } from './components/content/WorkspaceActionBar.svelte';
|
|
187
|
+
export { default as WorkspaceTabBar } from './components/content/WorkspaceTabBar.svelte';
|
|
187
188
|
export { default as WorkspaceInspector } from './components/content/WorkspaceInspector.svelte';
|
|
188
189
|
export { default as WorkspaceSplitPane } from './components/content/WorkspaceSplitPane.svelte';
|
|
189
190
|
export { default as SettingsGroup } from './components/content/SettingsGroup.svelte';
|
|
@@ -213,3 +214,4 @@ export { default as DetailDrawer } from './components/DetailDrawer.svelte';
|
|
|
213
214
|
export { default as AuditTimeline } from './components/content/AuditTimeline.svelte';
|
|
214
215
|
export { default as FilterDropdown } from './components/FilterDropdown.svelte';
|
|
215
216
|
export { default as BatchActionBar } from './components/BatchActionBar.svelte';
|
|
217
|
+
export { default as MediaThumbnail } from './components/content/MediaThumbnail.svelte';
|