feat(docker-watcher): phase 14 - frontend polish & modern UI
Design system with CSS custom properties (light/dark themes). 38 Lucide SVG icon components. Dark mode with system preference. EN/RU localization with i18n store. Skeleton loaders, empty states, toggle switches, micro-interactions. Responsive sidebar with mobile hamburger menu. All pages polished with consistent styling.
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
<!--
|
||||
Confirm dialog with fade/scale-in animation.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IconAlert } from '$lib/components/icons';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
title: string;
|
||||
@@ -21,32 +26,51 @@
|
||||
|
||||
const confirmClass = $derived(
|
||||
confirmVariant === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700 focus-visible:outline-red-600'
|
||||
: 'bg-indigo-600 hover:bg-indigo-700 focus-visible:outline-indigo-600'
|
||||
? 'bg-[var(--color-danger)] hover:bg-[var(--color-danger-dark)] focus-visible:outline-[var(--color-danger)]'
|
||||
: 'bg-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] focus-visible:outline-[var(--color-brand-600)]'
|
||||
);
|
||||
|
||||
const iconBgClass = $derived(
|
||||
confirmVariant === 'danger'
|
||||
? 'bg-[var(--color-danger-light)]'
|
||||
: 'bg-[var(--color-brand-50)]'
|
||||
);
|
||||
|
||||
const iconColorClass = $derived(
|
||||
confirmVariant === 'danger'
|
||||
? 'text-[var(--color-danger)]'
|
||||
: 'text-[var(--color-brand-600)]'
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 z-40 bg-black/30" role="presentation" onclick={oncancel}></div>
|
||||
<div class="fixed inset-0 z-40 bg-[var(--surface-overlay)] animate-fade-in" role="presentation" onclick={oncancel}></div>
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{title}</h3>
|
||||
<p class="mt-2 text-sm text-gray-600">{message}</p>
|
||||
<div class="w-full max-w-md rounded-2xl bg-[var(--surface-card)] p-6 shadow-xl animate-scale-in">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full {iconBgClass}">
|
||||
<IconAlert size={20} class={iconColorClass} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-[var(--text-primary)]">{title}</h3>
|
||||
<p class="mt-2 text-sm text-[var(--text-secondary)] leading-relaxed">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--surface-card-hover)] transition-colors active:animate-press"
|
||||
onclick={oncancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-white {confirmClass} focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium text-white {confirmClass} shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 active:animate-press"
|
||||
onclick={onconfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
Task 9: Empty state component with SVG illustration and call-to-action.
|
||||
-->
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
actionLabel?: string;
|
||||
actionHref?: string;
|
||||
onaction?: () => void;
|
||||
icon?: 'projects' | 'instances' | 'deploys' | 'registries' | 'volumes' | 'users';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = '',
|
||||
actionLabel = '',
|
||||
actionHref = '',
|
||||
onaction,
|
||||
icon = 'projects'
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-[var(--border-primary)] px-6 py-16 text-center animate-fade-in">
|
||||
<!-- SVG Illustration -->
|
||||
<div class="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-[var(--color-brand-50)]">
|
||||
{#if icon === 'projects'}
|
||||
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
<path d="M12 10v6" /><path d="M9 13h6" />
|
||||
</svg>
|
||||
{:else if icon === 'instances'}
|
||||
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" /><rect width="20" height="8" x="2" y="14" rx="2" ry="2" /><line x1="6" x2="6.01" y1="6" y2="6" /><line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
{:else if icon === 'deploys'}
|
||||
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
|
||||
</svg>
|
||||
{:else if icon === 'registries'}
|
||||
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v14a9 3 0 0 0 18 0V5" /><path d="M3 12a9 3 0 0 0 18 0" />
|
||||
</svg>
|
||||
{:else if icon === 'volumes'}
|
||||
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" x2="2" y1="12" y2="12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||||
</svg>
|
||||
{:else if icon === 'users'}
|
||||
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h3 class="text-base font-semibold text-[var(--text-primary)]">{title}</h3>
|
||||
|
||||
{#if description}
|
||||
<p class="mt-1 max-w-sm text-sm text-[var(--text-secondary)]">{description}</p>
|
||||
{/if}
|
||||
|
||||
{#if actionLabel}
|
||||
{#if actionHref}
|
||||
<a
|
||||
href={actionHref}
|
||||
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] active:animate-press"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" /><path d="M12 5v14" />
|
||||
</svg>
|
||||
{actionLabel}
|
||||
</a>
|
||||
{:else if onaction}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onaction}
|
||||
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] active:animate-press"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" /><path d="M12 5v14" />
|
||||
</svg>
|
||||
{actionLabel}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,3 +1,6 @@
|
||||
<!--
|
||||
Task 6: Consistent form field with design tokens.
|
||||
-->
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
@@ -24,13 +27,18 @@
|
||||
helpText = '',
|
||||
oninput
|
||||
}: Props = $props();
|
||||
|
||||
const inputBase = 'w-full rounded-lg border px-3 py-2 text-sm transition-all duration-150 focus:outline-none focus:ring-2 bg-[var(--surface-input)] text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)]';
|
||||
const inputNormal = 'border-[var(--border-input)] focus:ring-[var(--color-brand-500)] focus:border-[var(--color-brand-500)]';
|
||||
const inputError = 'border-[var(--color-danger)] focus:ring-[var(--color-danger)]';
|
||||
const inputDisabled = 'opacity-60 cursor-not-allowed bg-[var(--surface-card-hover)]';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for={name} class="text-sm font-medium text-gray-700">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for={name} class="text-sm font-medium text-[var(--text-primary)]">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-[var(--color-danger)]">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
@@ -43,9 +51,7 @@
|
||||
{required}
|
||||
{disabled}
|
||||
{oninput}
|
||||
class="rounded-md border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
{error ? 'border-red-500 focus:ring-red-500' : 'border-gray-300'}
|
||||
{disabled ? 'bg-gray-100 text-gray-500' : 'bg-white'}"
|
||||
class="{inputBase} {error ? inputError : inputNormal} {disabled ? inputDisabled : ''}"
|
||||
rows="3"
|
||||
></textarea>
|
||||
{:else}
|
||||
@@ -58,17 +64,15 @@
|
||||
{required}
|
||||
{disabled}
|
||||
{oninput}
|
||||
class="rounded-md border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
{error ? 'border-red-500 focus:ring-red-500' : 'border-gray-300'}
|
||||
{disabled ? 'bg-gray-100 text-gray-500' : 'bg-white'}"
|
||||
class="{inputBase} {error ? inputError : inputNormal} {disabled ? inputDisabled : ''}"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<p class="text-xs text-red-600">{error}</p>
|
||||
<p class="text-xs text-[var(--color-danger)]">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if helpText && !error}
|
||||
<p class="text-xs text-gray-500">{helpText}</p>
|
||||
<p class="text-xs text-[var(--text-tertiary)]">{helpText}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<!--
|
||||
Task 5: Instance card with inline status badges, icon action buttons, improved layout.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Instance } from '$lib/types';
|
||||
import StatusBadge from './StatusBadge.svelte';
|
||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||
import { IconPlay, IconStop, IconRestart, IconTrash, IconExternalLink } from '$lib/components/icons';
|
||||
import { t } from '$lib/i18n';
|
||||
import * as api from '$lib/api';
|
||||
|
||||
interface Props {
|
||||
@@ -53,7 +58,7 @@
|
||||
}
|
||||
onchange?.();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Action failed';
|
||||
error = e instanceof Error ? e.message : $t('instance.actionFailed');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -62,19 +67,13 @@
|
||||
function requestConfirm(action: 'stop' | 'restart' | 'remove') {
|
||||
confirmAction = action;
|
||||
}
|
||||
|
||||
const confirmMessages: Record<string, string> = {
|
||||
stop: 'This will stop the running container. The instance can be started again later.',
|
||||
restart: 'This will restart the container, causing brief downtime.',
|
||||
remove: 'This will permanently remove the container and its proxy configuration. This cannot be undone.'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-4 shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)]">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate font-mono text-sm font-medium text-gray-900">
|
||||
<span class="truncate font-mono text-sm font-medium text-[var(--text-primary)]">
|
||||
{instance.image_tag}
|
||||
</span>
|
||||
<StatusBadge status={instance.status} size="sm" />
|
||||
@@ -85,78 +84,72 @@
|
||||
href={subdomainUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="mt-1 block truncate text-xs text-indigo-600 hover:text-indigo-800"
|
||||
class="mt-1.5 inline-flex items-center gap-1 text-xs text-[var(--text-link)] hover:text-[var(--text-link-hover)] transition-colors"
|
||||
>
|
||||
{instance.subdomain}
|
||||
<IconExternalLink size={12} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1 flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>Port {instance.port}</span>
|
||||
<div class="mt-1.5 flex items-center gap-3 text-xs text-[var(--text-tertiary)]">
|
||||
<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 font-mono">:{instance.port}</span>
|
||||
<span>{timeSinceCreated()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="ml-3 flex items-center gap-1">
|
||||
{#if instance.status === 'running'}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-yellow-600 disabled:opacity-50"
|
||||
title="Stop"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-amber-50 hover:text-amber-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.stop')}
|
||||
disabled={loading}
|
||||
onclick={() => requestConfirm('stop')}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<rect x="6" y="6" width="12" height="12" rx="1" />
|
||||
</svg>
|
||||
<IconStop size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-blue-600 disabled:opacity-50"
|
||||
title="Restart"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-blue-50 hover:text-blue-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.restart')}
|
||||
disabled={loading}
|
||||
onclick={() => requestConfirm('restart')}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182" />
|
||||
</svg>
|
||||
<IconRestart size={16} />
|
||||
</button>
|
||||
{:else if instance.status === 'stopped'}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-green-600 disabled:opacity-50"
|
||||
title="Start"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-emerald-50 hover:text-emerald-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.start')}
|
||||
disabled={loading}
|
||||
onclick={() => handleAction('start')}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
|
||||
</svg>
|
||||
<IconPlay size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-red-600 disabled:opacity-50"
|
||||
title="Remove"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-red-50 hover:text-red-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.remove')}
|
||||
disabled={loading}
|
||||
onclick={() => requestConfirm('remove')}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="mt-2 text-xs text-red-600">{error}</p>
|
||||
<p class="mt-2 text-xs text-[var(--color-danger)]">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmAction !== null}
|
||||
title="{confirmAction ? confirmAction.charAt(0).toUpperCase() + confirmAction.slice(1) : ''} Instance"
|
||||
message={confirmAction ? confirmMessages[confirmAction] ?? '' : ''}
|
||||
message={confirmAction ? $t(`instance.${confirmAction}Confirm`) : ''}
|
||||
confirmLabel={confirmAction ? confirmAction.charAt(0).toUpperCase() + confirmAction.slice(1) : ''}
|
||||
confirmVariant={confirmAction === 'remove' ? 'danger' : 'primary'}
|
||||
onconfirm={() => { if (confirmAction) handleAction(confirmAction); }}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Task 13: Locale switcher component.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { locale, setLocale, availableLocales, type Locale } from '$lib/i18n';
|
||||
import { IconGlobe } from '$lib/components/icons';
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<IconGlobe size={14} class="text-[var(--text-tertiary)]" />
|
||||
<div class="flex items-center gap-0.5 rounded-lg border border-[var(--border-primary)] bg-[var(--surface-card)] p-0.5">
|
||||
{#each availableLocales as loc}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-2 py-0.5 text-xs font-medium transition-all duration-150
|
||||
{$locale === loc
|
||||
? 'bg-[var(--color-brand-100)] text-[var(--color-brand-700)] shadow-sm'
|
||||
: 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'}"
|
||||
onclick={() => setLocale(loc)}
|
||||
>
|
||||
{loc.toUpperCase()}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,11 @@
|
||||
<!--
|
||||
Task 4: Redesigned project card with status indicators, instance count badges, hover effects.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Project, Instance } from '$lib/types';
|
||||
import StatusBadge from './StatusBadge.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { IconContainer, IconBox } from '$lib/components/icons';
|
||||
|
||||
interface Props {
|
||||
project: Project;
|
||||
@@ -24,45 +29,55 @@
|
||||
|
||||
<a
|
||||
href="/projects/{project.id}"
|
||||
class="block rounded-lg border border-gray-200 bg-white p-5 shadow-sm transition hover:border-indigo-300 hover:shadow-md"
|
||||
class="group block rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)] transition-all duration-200 hover:border-[var(--color-brand-300)] hover:shadow-[var(--shadow-md)] hover:-translate-y-0.5"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="truncate text-base font-semibold text-gray-900">{project.name}</h3>
|
||||
<p class="mt-1 truncate text-sm text-gray-500">{project.image}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--color-brand-50)] text-[var(--color-brand-600)] transition-colors group-hover:bg-[var(--color-brand-100)]">
|
||||
<IconBox size={16} />
|
||||
</div>
|
||||
<h3 class="truncate text-base font-semibold text-[var(--text-primary)]">{project.name}</h3>
|
||||
</div>
|
||||
<p class="mt-2 truncate font-mono text-xs text-[var(--text-tertiary)]">{project.image}</p>
|
||||
</div>
|
||||
<StatusBadge status={overallStatus()} size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 text-sm text-gray-600">
|
||||
<!-- Instance count badges -->
|
||||
<div class="mt-4 flex items-center gap-3 text-sm">
|
||||
{#if totalCount > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
{runningCount} running
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--text-secondary)]">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-500"></span>
|
||||
<span class="text-xs font-medium">{runningCount}</span>
|
||||
</span>
|
||||
{#if stoppedCount > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--text-secondary)]">
|
||||
<span class="h-2 w-2 rounded-full bg-gray-400"></span>
|
||||
{stoppedCount} stopped
|
||||
<span class="text-xs font-medium">{stoppedCount}</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if failedCount > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--text-secondary)]">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{failedCount} failed
|
||||
<span class="text-xs font-medium">{failedCount}</span>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto rounded-full bg-[var(--surface-card-hover)] px-2 py-0.5 text-xs font-medium text-[var(--text-tertiary)]">
|
||||
{totalCount} {totalCount === 1 ? 'instance' : 'instances'}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-gray-400">No instances</span>
|
||||
<span class="text-xs text-[var(--text-tertiary)]">{$t('projectDetail.noInstancesRunning')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center gap-3 text-xs text-gray-400">
|
||||
<!-- Meta info -->
|
||||
<div class="mt-3 flex items-center gap-3 text-xs text-[var(--text-tertiary)]">
|
||||
{#if project.port}
|
||||
<span>Port {project.port}</span>
|
||||
<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 font-mono">:{project.port}</span>
|
||||
{/if}
|
||||
{#if project.healthcheck}
|
||||
<span>HC: {project.healthcheck}</span>
|
||||
<span class="truncate">{project.healthcheck}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Task 8: Skeleton loader component for data fetching states.
|
||||
Renders a shimmering placeholder in any size/shape.
|
||||
-->
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
class?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
class: className = '',
|
||||
width = '100%',
|
||||
height = '1rem',
|
||||
rounded = false
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="skeleton {className}"
|
||||
style="width: {width}; height: {height}; {rounded ? 'border-radius: 9999px;' : ''}"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
Task 8: Skeleton card placeholder for project cards during loading.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)]">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 space-y-2">
|
||||
<Skeleton width="60%" height="1.25rem" />
|
||||
<Skeleton width="80%" height="0.875rem" />
|
||||
</div>
|
||||
<Skeleton width="4rem" height="1.5rem" rounded />
|
||||
</div>
|
||||
<div class="mt-4 flex gap-4">
|
||||
<Skeleton width="5rem" height="0.875rem" />
|
||||
<Skeleton width="4rem" height="0.875rem" />
|
||||
</div>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<Skeleton width="3.5rem" height="0.75rem" />
|
||||
<Skeleton width="4.5rem" height="0.75rem" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
Task 8: Skeleton table placeholder for list views during loading.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
|
||||
interface Props {
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
const { rows = 5, cols = 4 }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] shadow-[var(--shadow-sm)]">
|
||||
<div class="border-b border-[var(--border-primary)] bg-[var(--surface-card-hover)] px-6 py-3">
|
||||
<div class="flex gap-6">
|
||||
{#each Array(cols) as _, i}
|
||||
<Skeleton width={i === 0 ? '6rem' : '5rem'} height="0.75rem" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#each Array(rows) as _, i}
|
||||
<div class="flex gap-6 border-b border-[var(--border-secondary)] px-6 py-4 last:border-b-0">
|
||||
{#each Array(cols) as _, j}
|
||||
<Skeleton width={j === 0 ? '40%' : '20%'} height="0.875rem" />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,3 +1,6 @@
|
||||
<!--
|
||||
Task 5, 11: Status badge with pulse animation for "running" status.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { InstanceStatus, DeployStatus } from '$lib/types';
|
||||
|
||||
@@ -10,42 +13,35 @@
|
||||
|
||||
const { status, size = 'md' }: Props = $props();
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
running: 'bg-green-100 text-green-800',
|
||||
success: 'bg-green-100 text-green-800',
|
||||
stopped: 'bg-gray-100 text-gray-800',
|
||||
failed: 'bg-red-100 text-red-800',
|
||||
rolled_back: 'bg-red-100 text-red-800',
|
||||
removing: 'bg-yellow-100 text-yellow-800',
|
||||
pending: 'bg-blue-100 text-blue-800',
|
||||
pulling: 'bg-blue-100 text-blue-800',
|
||||
starting: 'bg-yellow-100 text-yellow-800',
|
||||
configuring_proxy: 'bg-yellow-100 text-yellow-800',
|
||||
health_checking: 'bg-yellow-100 text-yellow-800'
|
||||
const colorMap: Record<string, { bg: string; text: string; dot: string }> = {
|
||||
running: { bg: 'bg-emerald-50 dark:bg-emerald-950', text: 'text-emerald-700 dark:text-emerald-300', dot: 'bg-emerald-500' },
|
||||
success: { bg: 'bg-emerald-50 dark:bg-emerald-950', text: 'text-emerald-700 dark:text-emerald-300', dot: 'bg-emerald-500' },
|
||||
stopped: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-700 dark:text-gray-300', dot: 'bg-gray-400' },
|
||||
failed: { bg: 'bg-red-50 dark:bg-red-950', text: 'text-red-700 dark:text-red-300', dot: 'bg-red-500' },
|
||||
rolled_back: { bg: 'bg-red-50 dark:bg-red-950', text: 'text-red-700 dark:text-red-300', dot: 'bg-red-500' },
|
||||
removing: { bg: 'bg-amber-50 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-300', dot: 'bg-amber-500' },
|
||||
pending: { bg: 'bg-blue-50 dark:bg-blue-950', text: 'text-blue-700 dark:text-blue-300', dot: 'bg-blue-500' },
|
||||
pulling: { bg: 'bg-blue-50 dark:bg-blue-950', text: 'text-blue-700 dark:text-blue-300', dot: 'bg-blue-500' },
|
||||
starting: { bg: 'bg-amber-50 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-300', dot: 'bg-amber-500' },
|
||||
configuring_proxy: { bg: 'bg-amber-50 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-300', dot: 'bg-amber-500' },
|
||||
health_checking: { bg: 'bg-violet-50 dark:bg-violet-950', text: 'text-violet-700 dark:text-violet-300', dot: 'bg-violet-500' }
|
||||
};
|
||||
|
||||
const dotColorMap: Record<string, string> = {
|
||||
running: 'bg-green-500',
|
||||
success: 'bg-green-500',
|
||||
stopped: 'bg-gray-400',
|
||||
failed: 'bg-red-500',
|
||||
rolled_back: 'bg-red-500',
|
||||
removing: 'bg-yellow-500',
|
||||
pending: 'bg-blue-500',
|
||||
pulling: 'bg-blue-500',
|
||||
starting: 'bg-yellow-500',
|
||||
configuring_proxy: 'bg-yellow-500',
|
||||
health_checking: 'bg-yellow-500'
|
||||
};
|
||||
const fallback = { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-700 dark:text-gray-300', dot: 'bg-gray-400' };
|
||||
|
||||
const colorClass = $derived(colorMap[status] ?? 'bg-gray-100 text-gray-800');
|
||||
const dotClass = $derived(dotColorMap[status] ?? 'bg-gray-400');
|
||||
const colors = $derived(colorMap[status] ?? fallback);
|
||||
const sizeClass = $derived(size === 'sm' ? 'text-xs px-2 py-0.5' : 'text-sm px-2.5 py-0.5');
|
||||
const dotSize = $derived(size === 'sm' ? 'h-1.5 w-1.5' : 'h-2 w-2');
|
||||
const label = $derived(status.replace(/_/g, ' '));
|
||||
const isAnimated = $derived(status === 'running' || status === 'pulling' || status === 'starting' || status === 'health_checking');
|
||||
</script>
|
||||
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full font-medium {colorClass} {sizeClass}">
|
||||
<span class="rounded-full {dotClass} {dotSize}"></span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full font-medium {colors.bg} {colors.text} {sizeClass}">
|
||||
<span class="relative flex {dotSize}">
|
||||
{#if isAnimated}
|
||||
<span class="absolute inline-flex h-full w-full animate-ping rounded-full {colors.dot} opacity-50"></span>
|
||||
{/if}
|
||||
<span class="relative inline-flex rounded-full {dotSize} {colors.dot}"></span>
|
||||
</span>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
Task 12: Theme toggle component for dark/light/system modes.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { themeMode, setThemeMode, type ThemeMode } from '$lib/stores/theme';
|
||||
import { t } from '$lib/i18n';
|
||||
import { IconSun, IconMoon, IconMonitor } from '$lib/components/icons';
|
||||
|
||||
const modes: ThemeMode[] = ['light', 'dark', 'system'];
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-0.5 rounded-lg border border-[var(--border-primary)] bg-[var(--surface-card)] p-0.5">
|
||||
{#each modes as mode}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center rounded-md p-1.5 transition-all duration-150
|
||||
{$themeMode === mode
|
||||
? 'bg-[var(--color-brand-100)] text-[var(--color-brand-700)] shadow-sm'
|
||||
: 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'}"
|
||||
onclick={() => setThemeMode(mode)}
|
||||
title={$t(`theme.${mode}`)}
|
||||
>
|
||||
{#if mode === 'light'}
|
||||
<IconSun size={14} />
|
||||
{:else if mode === 'dark'}
|
||||
<IconMoon size={14} />
|
||||
{:else}
|
||||
<IconMonitor size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,35 +1,42 @@
|
||||
<!--
|
||||
Task 7: Toast/notification system with slide-in animations, icons, auto-dismiss, stacking.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { toasts, type ToastType } from '$lib/stores/toast';
|
||||
import { IconCheck, IconX, IconAlert, IconInfo } from '$lib/components/icons';
|
||||
|
||||
const colorMap: Record<ToastType, string> = {
|
||||
success: 'bg-green-600',
|
||||
error: 'bg-red-600',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-600'
|
||||
};
|
||||
|
||||
const iconMap: Record<ToastType, string> = {
|
||||
success: '✓',
|
||||
error: '✗',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
const bgMap: Record<ToastType, string> = {
|
||||
success: 'bg-[var(--color-success)]',
|
||||
error: 'bg-[var(--color-danger)]',
|
||||
warning: 'bg-[var(--color-warning)]',
|
||||
info: 'bg-[var(--color-info)]'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
|
||||
<div class="fixed top-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none">
|
||||
{#each $toasts as toast (toast.id)}
|
||||
<div
|
||||
class="pointer-events-auto flex items-center gap-3 rounded-lg px-4 py-3 text-white shadow-lg transition-all duration-300 {colorMap[toast.type]}"
|
||||
class="pointer-events-auto flex items-center gap-3 rounded-xl px-4 py-3 text-white shadow-lg animate-slide-in {bgMap[toast.type]}"
|
||||
role="alert"
|
||||
>
|
||||
<span class="text-lg" aria-hidden="true">{@html iconMap[toast.type]}</span>
|
||||
<div class="flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
||||
{#if toast.type === 'success'}
|
||||
<IconCheck size={18} />
|
||||
{:else if toast.type === 'error'}
|
||||
<IconX size={18} />
|
||||
{:else if toast.type === 'warning'}
|
||||
<IconAlert size={18} />
|
||||
{:else}
|
||||
<IconInfo size={18} />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="flex-1 text-sm font-medium">{toast.message}</span>
|
||||
<button
|
||||
class="ml-2 text-white/80 hover:text-white transition-colors"
|
||||
class="ml-2 rounded-md p-0.5 text-white/70 hover:text-white transition-colors"
|
||||
onclick={() => toasts.remove(toast.id)}
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
✕
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<!--
|
||||
Task 6: Toggle switch to replace checkboxes.
|
||||
-->
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
checked?: boolean;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
onchange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
checked = $bindable(false),
|
||||
label = '',
|
||||
disabled = false,
|
||||
onchange
|
||||
}: Props = $props();
|
||||
|
||||
function toggle() {
|
||||
if (disabled) return;
|
||||
checked = !checked;
|
||||
onchange?.(checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
class="toggle-switch {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
|
||||
onclick={toggle}
|
||||
{disabled}
|
||||
>
|
||||
<span class="sr-only">{label}</span>
|
||||
</button>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" /><path d="m3.3 7 8.7 5 8.7-5" /><path d="M12 22V12" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M22 8.35V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8.35A2 2 0 0 1 3.26 6.5l8-3.2a2 2 0 0 1 1.48 0l8 3.2A2 2 0 0 1 22 8.35Z" /><path d="M6 18h12" /><path d="M6 14h12" /><rect width="12" height="12" x="6" y="10" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="9" x="14" y="12" rx="1" /><rect width="7" height="5" x="3" y="16" rx="1" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v14a9 3 0 0 0 18 0V5" /><path d="M3 12a9 3 0 0 0 18 0" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58m-.119-8.54a6 6 0 0 0-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 0 0-2.58 5.84m2.699 2.7a15.09 15.09 0 0 1-2.448-2.448 14.9 14.9 0 0 1 .06-.312m-2.24 2.39a4.493 4.493 0 0 0-1.757 4.306 4.493 4.493 0 0 0 4.306-1.758M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" /><path d="M2 12h20" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<line x1="22" x2="2" y1="12" y2="12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /><line x1="6" x2="6.01" y1="16" y2="16" /><line x1="10" x2="10.01" y1="16" y2="16" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<circle cx="7.5" cy="15.5" r="5.5" /><path d="m21 2-9.3 9.3" /><path d="M18.5 5.5 21 3" /><path d="m15 8 2.5 2.5" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="animate-spin {c}" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<line x1="4" x2="20" y1="12" y2="12" /><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="18" y2="18" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M5 12h14" /><path d="M12 5v14" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" /><rect width="20" height="8" x="2" y="14" rx="2" ry="2" /><line x1="6" x2="6.01" y1="6" y2="6" /><line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="14" height="14" x="5" y="5" rx="1" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" /><circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 9.9-1" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M12 20h.01" /><path d="M2 8.82a15 15 0 0 1 20 0" /><path d="M5 12.859a10 10 0 0 1 14 0" /><path d="M8.5 16.429a5 5 0 0 1 7 0" />
|
||||
</svg>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 20, class: c = '' }: Props = $props();
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
|
||||
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
|
||||
</svg>
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Lucide-based SVG icon components for Docker Watcher.
|
||||
* Task 2: Inline SVGs from Lucide icon set as Svelte components.
|
||||
*
|
||||
* Each icon is a standalone .svelte component accepting size and class props.
|
||||
* Re-exported here for convenient imports.
|
||||
*/
|
||||
|
||||
export { default as IconDashboard } from './IconDashboard.svelte';
|
||||
export { default as IconProjects } from './IconProjects.svelte';
|
||||
export { default as IconDeploy } from './IconDeploy.svelte';
|
||||
export { default as IconSettings } from './IconSettings.svelte';
|
||||
export { default as IconPlay } from './IconPlay.svelte';
|
||||
export { default as IconStop } from './IconStop.svelte';
|
||||
export { default as IconRestart } from './IconRestart.svelte';
|
||||
export { default as IconTrash } from './IconTrash.svelte';
|
||||
export { default as IconPlus } from './IconPlus.svelte';
|
||||
export { default as IconCheck } from './IconCheck.svelte';
|
||||
export { default as IconX } from './IconX.svelte';
|
||||
export { default as IconAlert } from './IconAlert.svelte';
|
||||
export { default as IconInfo } from './IconInfo.svelte';
|
||||
export { default as IconChevronRight } from './IconChevronRight.svelte';
|
||||
export { default as IconExternalLink } from './IconExternalLink.svelte';
|
||||
export { default as IconCopy } from './IconCopy.svelte';
|
||||
export { default as IconSearch } from './IconSearch.svelte';
|
||||
export { default as IconSun } from './IconSun.svelte';
|
||||
export { default as IconMoon } from './IconMoon.svelte';
|
||||
export { default as IconMonitor } from './IconMonitor.svelte';
|
||||
export { default as IconMenu } from './IconMenu.svelte';
|
||||
export { default as IconGlobe } from './IconGlobe.svelte';
|
||||
export { default as IconKey } from './IconKey.svelte';
|
||||
export { default as IconShield } from './IconShield.svelte';
|
||||
export { default as IconServer } from './IconServer.svelte';
|
||||
export { default as IconDatabase } from './IconDatabase.svelte';
|
||||
export { default as IconBox } from './IconBox.svelte';
|
||||
export { default as IconLoader } from './IconLoader.svelte';
|
||||
export { default as IconTag } from './IconTag.svelte';
|
||||
export { default as IconClock } from './IconClock.svelte';
|
||||
export { default as IconEdit } from './IconEdit.svelte';
|
||||
export { default as IconLock } from './IconLock.svelte';
|
||||
export { default as IconUnlock } from './IconUnlock.svelte';
|
||||
export { default as IconUser } from './IconUser.svelte';
|
||||
export { default as IconUsers } from './IconUsers.svelte';
|
||||
export { default as IconContainer } from './IconContainer.svelte';
|
||||
export { default as IconHardDrive } from './IconHardDrive.svelte';
|
||||
export { default as IconWifi } from './IconWifi.svelte';
|
||||
export { default as IconRefresh } from './IconRefresh.svelte';
|
||||
Reference in New Issue
Block a user