svelte refactor
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
|
||||
type Variant = 'primary' | 'secondary' | 'danger';
|
||||
|
||||
interface Props {
|
||||
/** Visual style → `.btn-{variant}` (default `secondary`). */
|
||||
variant?: Variant;
|
||||
/** Optional leading icon-registry name. */
|
||||
icon?: string;
|
||||
/** Compact size → adds `.btn-sm`. */
|
||||
small?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
/** Extra classes appended after the base `.btn` classes. */
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'secondary',
|
||||
icon,
|
||||
small = false,
|
||||
type = 'button',
|
||||
disabled = false,
|
||||
title,
|
||||
onclick,
|
||||
class: cls = '',
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
const className = $derived(
|
||||
['btn', `btn-${variant}`, small ? 'btn-sm' : '', cls].filter(Boolean).join(' ')
|
||||
);
|
||||
</script>
|
||||
|
||||
<button class={className} {type} {disabled} {title} {onclick}>
|
||||
{#if icon}<Icon name={icon} />{/if}
|
||||
{@render children?.()}
|
||||
</button>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Icon-registry name shown above the title (omit for a text-only state). */
|
||||
icon?: string;
|
||||
/** Primary line. */
|
||||
title?: string;
|
||||
/** Secondary explanatory line. */
|
||||
hint?: string;
|
||||
/** Error styling (danger-coloured icon) + assertive `role="alert"`. */
|
||||
error?: boolean;
|
||||
/** Extra content (e.g. a call-to-action button) rendered below the hint. */
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { icon, title, hint, error = false, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="empty-state" class:empty-state--error={error} role={error ? 'alert' : undefined}>
|
||||
{#if icon}<Icon name={icon} class="empty-state__icon" />{/if}
|
||||
{#if title}<p class="empty-state__title">{title}</p>{/if}
|
||||
{#if hint}<p class="empty-state__hint">{hint}</p>{/if}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Layout comes from the global `.empty-state` (styles/ported/content.css);
|
||||
these refine the icon/title/hint elements consistently across views. */
|
||||
.empty-state :global(.empty-state__icon) {
|
||||
font-size: var(--text-5xl);
|
||||
color: var(--color-text-faint);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.empty-state--error :global(.empty-state__icon) {
|
||||
color: var(--color-danger-text);
|
||||
}
|
||||
|
||||
.empty-state__title {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--weight-semibold);
|
||||
color: var(--color-text-heading);
|
||||
}
|
||||
|
||||
.empty-state__hint {
|
||||
margin: 0;
|
||||
max-width: 28rem;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { errorToast } from '$lib/utils/errors';
|
||||
import { listFolder, moveFolder } from '$lib/api/endpoints/folders';
|
||||
import { moveFile } from '$lib/api/endpoints/files';
|
||||
import { copyFiles, copyFolders } from '$lib/api/endpoints/batch';
|
||||
@@ -42,7 +43,7 @@
|
||||
currentId = id;
|
||||
folders = (await listFolder(id)).folders;
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -98,7 +99,7 @@
|
||||
open = false;
|
||||
onmoved?.();
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
} finally {
|
||||
working = false;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import SkeletonList from '$lib/components/SkeletonList.svelte';
|
||||
import ListToolbar from '$lib/components/ListToolbar.svelte';
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||
@@ -396,37 +398,15 @@
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="empty-state">
|
||||
<Icon name="exclamation-circle" class="empty-state-icon empty-state-icon--error" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
<EmptyState icon="exclamation-circle" title={error} error />
|
||||
{:else if loading && isEmpty}
|
||||
<div class="files-container">
|
||||
<div
|
||||
class={filesStore.viewMode === 'grid' ? 'files-grid-view files-skeleton' : 'files-skeleton'}
|
||||
>
|
||||
{#each SKELETON as i (i)}
|
||||
{#if filesStore.viewMode === 'grid'}
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-thumb"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--medium"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="skeleton-row">
|
||||
<div class="skeleton skeleton-icon"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--medium"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonList count={SKELETON.length} />
|
||||
{:else if isEmpty}
|
||||
<div class="empty-state">
|
||||
{#if emptyIcon}<Icon name={emptyIcon} class="empty-state-icon" />{/if}
|
||||
<p>{emptyText ?? t('common.empty', 'Nothing here yet.')}</p>
|
||||
{#if emptyHint}<p class="empty-state__hint">{emptyHint}</p>{/if}
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={emptyIcon}
|
||||
title={emptyText ?? t('common.empty', 'Nothing here yet.')}
|
||||
hint={emptyHint}
|
||||
/>
|
||||
{:else}
|
||||
<div class="files-container">
|
||||
<div class={viewClass} style="--files-list-columns: {columns}">
|
||||
@@ -690,20 +670,4 @@
|
||||
.rl-ctx-item--danger {
|
||||
color: var(--color-danger-text);
|
||||
}
|
||||
|
||||
.empty-state__hint {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* Empty/error icon lives inside the <Icon> child component's <svg>. */
|
||||
.empty-state :global(.empty-state-icon) {
|
||||
font-size: var(--text-5xl);
|
||||
color: var(--color-text-faint);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.empty-state :global(.empty-state-icon--error) {
|
||||
color: var(--color-danger-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { errorToast } from '$lib/utils/errors';
|
||||
import {
|
||||
copyShareLink,
|
||||
createShare,
|
||||
@@ -118,7 +119,7 @@
|
||||
directoryAvailable = isDirectoryAvailable();
|
||||
members = groupGrants(await fetchGrantsForResource(item.kind, item.id));
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
} finally {
|
||||
grantsLoading = false;
|
||||
}
|
||||
@@ -153,7 +154,7 @@
|
||||
summarizeNotifications(res.notification.outcomes);
|
||||
await loadGrants();
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +169,7 @@
|
||||
);
|
||||
await loadGrants();
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@
|
||||
);
|
||||
await loadGrants();
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@
|
||||
for (const id of m.grantIds) await revokeGrant(id);
|
||||
await loadGrants();
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@
|
||||
const set = await notifyGrantRecipient(m.notifyGrantId);
|
||||
summarizeNotifications(set.outcomes);
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +253,7 @@
|
||||
try {
|
||||
shares = await listSharesForItem(item.id, item.kind);
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
} finally {
|
||||
linkLoading = false;
|
||||
}
|
||||
@@ -275,7 +276,7 @@
|
||||
await loadShares();
|
||||
ui.notify(t('share.created', 'Public link created'), 'success');
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
@@ -286,7 +287,7 @@
|
||||
await updateShare(share.id, { expiresAt: expiry });
|
||||
await loadShares();
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +302,7 @@
|
||||
'success'
|
||||
);
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +311,7 @@
|
||||
await deleteShare(share.id);
|
||||
shares = shares.filter((s) => s.id !== share.id);
|
||||
} catch (e) {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Number of placeholder cards/rows to render (default 6). */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
let { count = 6 }: Props = $props();
|
||||
|
||||
const placeholders = $derived(Array.from({ length: count }, (_, i) => i));
|
||||
</script>
|
||||
|
||||
<div class="files-container">
|
||||
<div class={filesStore.viewMode === 'grid' ? 'files-grid-view files-skeleton' : 'files-skeleton'}>
|
||||
{#each placeholders as i (i)}
|
||||
{#if filesStore.viewMode === 'grid'}
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton skeleton-thumb"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--medium"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="skeleton-row">
|
||||
<div class="skeleton skeleton-icon"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--medium"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { errorToast } from '$lib/utils/errors';
|
||||
import { getEditorUrlWithFallback } from '$lib/api/endpoints/wopi';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { ui } from '$lib/stores/ui.svelte';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -68,7 +68,7 @@
|
||||
queueMicrotask(() => form?.submit());
|
||||
})
|
||||
.catch((e) => {
|
||||
ui.notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
errorToast(e);
|
||||
close();
|
||||
})
|
||||
.finally(() => (loading = false));
|
||||
|
||||
Reference in New Issue
Block a user