mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
♻️ refactor: centralize AppLoadingSkeleton in shared-ui
Add configurable AppLoadingSkeleton component to @manacore/shared-ui with multiple layout presets: list, tasks, sidebar, centered, minimal. Migrate 3 apps to use the shared component: - contacts: uses default 'list' layout - todo: uses 'tasks' layout - questions: uses 'sidebar' layout Apps with highly specific layouts (calendar, clock) retain their local implementations for now.
This commit is contained in:
parent
cdac341882
commit
8804ab77a2
13 changed files with 494 additions and 306 deletions
|
|
@ -1,117 +0,0 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AppLoadingSkeleton - Full page loading skeleton for initial app load
|
||||
* Shows a minimal skeleton layout while auth is being checked
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="app-loading-skeleton" role="status" aria-label="App wird geladen...">
|
||||
<!-- Header placeholder -->
|
||||
<div class="header-skeleton">
|
||||
<SkeletonBox width="120px" height="32px" borderRadius="8px" />
|
||||
<div class="header-nav">
|
||||
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
|
||||
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
|
||||
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
|
||||
</div>
|
||||
<SkeletonBox width="36px" height="36px" borderRadius="50%" />
|
||||
</div>
|
||||
|
||||
<!-- Content placeholder -->
|
||||
<div class="content-skeleton">
|
||||
<!-- Page title -->
|
||||
<div class="title-row">
|
||||
<SkeletonBox width="200px" height="32px" />
|
||||
<SkeletonBox width="120px" height="40px" borderRadius="8px" />
|
||||
</div>
|
||||
|
||||
<!-- Search bar -->
|
||||
<SkeletonBox width="100%" height="48px" borderRadius="12px" />
|
||||
|
||||
<!-- List items -->
|
||||
<div class="list-skeleton">
|
||||
{#each Array(5) as _, i}
|
||||
<div class="list-item" style="opacity: {Math.max(0.3, 1 - i * 0.15)};">
|
||||
<SkeletonBox width="48px" height="48px" borderRadius="50%" />
|
||||
<div class="item-content">
|
||||
<SkeletonBox width="60%" height="18px" />
|
||||
<SkeletonBox width="40%" height="14px" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-loading-skeleton {
|
||||
min-height: 100vh;
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
.header-skeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.content-skeleton {
|
||||
max-width: 80rem;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.list-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-skeleton {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content-skeleton {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -22,8 +22,8 @@ export { default as TagGridSkeleton } from './TagGridSkeleton.svelte';
|
|||
export { default as DuplicateGroupSkeleton } from './DuplicateGroupSkeleton.svelte';
|
||||
export { default as DuplicateListSkeleton } from './DuplicateListSkeleton.svelte';
|
||||
|
||||
// App Loading Skeleton
|
||||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
// App Loading Skeleton (from shared-ui)
|
||||
export { AppLoadingSkeleton } from '@manacore/shared-ui';
|
||||
|
||||
// Import Preview Skeleton
|
||||
export { default as ImportPreviewSkeleton } from './ImportPreviewSkeleton.svelte';
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
<div class="flex min-h-screen animate-pulse">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 border-r border-border bg-card">
|
||||
<!-- Header -->
|
||||
<div class="flex h-16 items-center border-b border-border px-4">
|
||||
<div class="h-6 w-28 rounded bg-muted"></div>
|
||||
</div>
|
||||
|
||||
<!-- New Question Button -->
|
||||
<div class="p-4">
|
||||
<div class="h-10 w-full rounded-lg bg-muted"></div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="space-y-1 px-2">
|
||||
<div class="h-10 w-full rounded-lg bg-muted"></div>
|
||||
|
||||
<div class="my-4 px-3">
|
||||
<div class="h-3 w-20 rounded bg-muted"></div>
|
||||
</div>
|
||||
|
||||
{#each Array(4) as _}
|
||||
<div class="h-10 w-full rounded-lg bg-muted"></div>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-6">
|
||||
<div class="mb-6">
|
||||
<div class="h-8 w-48 rounded bg-muted"></div>
|
||||
<div class="mt-2 h-4 w-32 rounded bg-muted"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 flex gap-4">
|
||||
<div class="h-10 flex-1 rounded-lg bg-muted"></div>
|
||||
<div class="h-10 w-32 rounded-lg bg-muted"></div>
|
||||
<div class="h-10 w-24 rounded-lg bg-muted"></div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each Array(5) as _}
|
||||
<div class="h-24 rounded-xl bg-muted"></div>
|
||||
{/each}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export { default as QuestionSkeleton } from './QuestionSkeleton.svelte';
|
||||
export { default as QuestionDetailSkeleton } from './QuestionDetailSkeleton.svelte';
|
||||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
|
||||
// App Loading Skeleton (from shared-ui with 'sidebar' layout)
|
||||
export { AppLoadingSkeleton } from '@manacore/shared-ui';
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
</script>
|
||||
|
||||
{#if !appReady}
|
||||
<AppLoadingSkeleton />
|
||||
<AppLoadingSkeleton layout="sidebar" />
|
||||
{:else}
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AppLoadingSkeleton - Full page loading skeleton for initial app load
|
||||
* Shows a minimal skeleton layout while auth is being checked
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="app-loading-skeleton" role="status" aria-label="App wird geladen...">
|
||||
<!-- Header placeholder -->
|
||||
<div class="header-skeleton">
|
||||
<SkeletonBox width="140px" height="32px" borderRadius="8px" />
|
||||
<div class="header-nav">
|
||||
<SkeletonBox width="36px" height="36px" borderRadius="50%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content placeholder -->
|
||||
<div class="content-skeleton">
|
||||
<!-- Page title -->
|
||||
<div class="title-row">
|
||||
<SkeletonBox width="180px" height="28px" />
|
||||
</div>
|
||||
<SkeletonBox width="220px" height="16px" />
|
||||
|
||||
<!-- Quick add bar -->
|
||||
<div class="quick-add-skeleton">
|
||||
<SkeletonBox width="100%" height="52px" borderRadius="12px" />
|
||||
</div>
|
||||
|
||||
<!-- Task sections -->
|
||||
<div class="sections-skeleton">
|
||||
<!-- Section header -->
|
||||
<div class="section-header">
|
||||
<SkeletonBox width="100px" height="20px" />
|
||||
<SkeletonBox width="28px" height="28px" borderRadius="50%" />
|
||||
</div>
|
||||
|
||||
<!-- Task items -->
|
||||
<div class="task-list">
|
||||
{#each Array(4) as _, i}
|
||||
<div class="task-item" style="opacity: {Math.max(0.3, 1 - i * 0.18)};">
|
||||
<SkeletonBox width="22px" height="22px" borderRadius="6px" />
|
||||
<div class="task-content">
|
||||
<SkeletonBox width="{70 - i * 8}%" height="18px" />
|
||||
<SkeletonBox width="{40 + i * 5}%" height="14px" />
|
||||
</div>
|
||||
<SkeletonBox width="24px" height="24px" borderRadius="4px" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-loading-skeleton {
|
||||
min-height: 100vh;
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
.header-skeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.content-skeleton {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-add-skeleton {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.sections-skeleton {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-skeleton {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
* Built on top of @manacore/shared-ui skeleton primitives.
|
||||
*/
|
||||
|
||||
// App Loading Skeleton
|
||||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
// App Loading Skeleton (from shared-ui with 'tasks' layout)
|
||||
export { AppLoadingSkeleton } from '@manacore/shared-ui';
|
||||
|
||||
// Task List Skeletons
|
||||
export { default as TaskItemSkeleton } from './TaskItemSkeleton.svelte';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
</script>
|
||||
|
||||
{#if !appReady}
|
||||
<AppLoadingSkeleton />
|
||||
<AppLoadingSkeleton layout="tasks" listItemCount={4} />
|
||||
{:else}
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ Nach eingehender Analyse aller Web-Apps im Monorepo wurden folgende Bereiche auf
|
|||
6. ✅ **i18n zu 6 Apps hinzugefügt** - todo, skilltree, nutriphi, planta, questions, matrix (jeweils DE + EN)
|
||||
7. ✅ **AuthGateModal zentralisiert** - `@manacore/shared-auth-ui` für 4 Apps (chat, todo, contacts, calendar)
|
||||
8. ✅ **Global Error Handler zentralisiert** - `@manacore/shared-ui` für 7 Apps (calendar, chat, clock, contacts, matrix, picture, storage)
|
||||
9. ✅ **AppLoadingSkeleton zentralisiert** - `@manacore/shared-ui` für 3 Apps (contacts, todo, questions) - Apps mit spezifischen Layouts (calendar, clock) behalten lokale Version
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -216,10 +217,15 @@ Alle Apps nutzen **Mana Core Auth** mit `@manacore/shared-auth`.
|
|||
- i18n: DE + EN eingebaut
|
||||
- Optionale Migration-Info für Session-Daten
|
||||
|
||||
#### AppLoadingSkeleton
|
||||
#### AppLoadingSkeleton ✅
|
||||
|
||||
- Jede App hat eigene Version
|
||||
- Könnte mit `@manacore/shared-ui` Skeletons vereinheitlicht werden
|
||||
> **Status: Erledigt (29.01.2026)**
|
||||
|
||||
- ✅ Zentrales `AppLoadingSkeleton` in `@manacore/shared-ui`
|
||||
- ✅ Migrierte Apps: contacts, todo, questions
|
||||
- ⏭️ Behalten lokale Version (spezifische Layouts): calendar, clock
|
||||
- Layout-Presets: `list`, `tasks`, `sidebar`, `centered`, `minimal`
|
||||
- Slot-Support für benutzerdefinierte Inhalte
|
||||
|
||||
#### Global Error Handler ✅
|
||||
|
||||
|
|
@ -275,7 +281,7 @@ _(Keine offenen Aufgaben mit mittlerer Priorität)_
|
|||
|
||||
| Aufgabe | Aufwand | Impact |
|
||||
|---------|---------|--------|
|
||||
| App-Skeletons vereinheitlichen | Niedrig | Code-Reduktion |
|
||||
| ~~App-Skeletons vereinheitlichen~~ | ~~Niedrig~~ | ✅ Erledigt |
|
||||
| Auth Store Pattern dokumentieren | Niedrig | Onboarding |
|
||||
|
||||
---
|
||||
|
|
@ -286,7 +292,7 @@ _(Keine offenen Aufgaben mit mittlerer Priorität)_
|
|||
2. ~~**i18n** zu fehlenden Apps hinzufügen~~ ✅ Erledigt (6 Apps)
|
||||
3. ~~**AuthGateModal** in Shared Package extrahieren~~ ✅ Erledigt (4 Apps)
|
||||
4. ~~**Global Error Handler** extrahieren~~ ✅ Erledigt (7 Apps)
|
||||
5. **App-Skeletons vereinheitlichen** (niedrige Priorität)
|
||||
5. ~~**App-Skeletons vereinheitlichen**~~ ✅ Erledigt (3 Apps)
|
||||
6. **Auth Store Pattern dokumentieren** (niedrige Priorität)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export {
|
|||
SkeletonList,
|
||||
SkeletonCard,
|
||||
SkeletonGrid,
|
||||
AppLoadingSkeleton,
|
||||
} from './molecules';
|
||||
|
||||
// Feedback
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export {
|
|||
SkeletonList,
|
||||
SkeletonCard,
|
||||
SkeletonGrid,
|
||||
AppLoadingSkeleton,
|
||||
} from './loaders';
|
||||
|
||||
// Feedback components
|
||||
|
|
|
|||
|
|
@ -0,0 +1,466 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AppLoadingSkeleton - Full page loading skeleton for app initialization
|
||||
*
|
||||
* A flexible loading skeleton that supports different layout presets:
|
||||
* - list: Default list view with search bar and item rows
|
||||
* - centered: Centered content (ideal for clock, calendar previews)
|
||||
* - sidebar: Sidebar + main content layout
|
||||
* - tasks: Task list with quick-add bar
|
||||
* - minimal: Just a centered spinner placeholder
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <AppLoadingSkeleton />
|
||||
* <AppLoadingSkeleton layout="centered" appLogo="/logo.svg" />
|
||||
* <AppLoadingSkeleton layout="sidebar" />
|
||||
* ```
|
||||
*/
|
||||
|
||||
import SkeletonBox from './SkeletonBox.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type LayoutPreset = 'list' | 'centered' | 'sidebar' | 'tasks' | 'minimal';
|
||||
|
||||
interface Props {
|
||||
/** Layout preset for the content area */
|
||||
layout?: LayoutPreset;
|
||||
/** Show header skeleton (default: true, false for minimal) */
|
||||
showHeader?: boolean;
|
||||
/** Number of list items to show (for list/tasks layouts) */
|
||||
listItemCount?: number;
|
||||
/** App logo URL (shown in centered layout) */
|
||||
appLogo?: string;
|
||||
/** Loading text for screen readers */
|
||||
loadingLabel?: string;
|
||||
/** Custom content slot (overrides preset content) */
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
layout = 'list',
|
||||
showHeader = true,
|
||||
listItemCount = 5,
|
||||
appLogo,
|
||||
loadingLabel = 'App wird geladen...',
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
// Hide header in minimal layout by default
|
||||
const displayHeader = $derived(layout === 'minimal' ? false : showHeader);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="app-loading-skeleton"
|
||||
class:minimal-layout={layout === 'minimal'}
|
||||
class:sidebar-layout={layout === 'sidebar'}
|
||||
role="status"
|
||||
aria-label={loadingLabel}
|
||||
>
|
||||
{#if displayHeader}
|
||||
<!-- Header Skeleton -->
|
||||
<div class="header-skeleton">
|
||||
<SkeletonBox width="120px" height="32px" borderRadius="8px" />
|
||||
<div class="header-nav">
|
||||
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
|
||||
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
|
||||
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
|
||||
</div>
|
||||
<SkeletonBox width="36px" height="36px" borderRadius="50%" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Content Area -->
|
||||
{#if children}
|
||||
<!-- Custom content via slot -->
|
||||
<div class="content-skeleton custom-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else if layout === 'minimal'}
|
||||
<!-- Minimal: Centered spinner placeholder -->
|
||||
<div class="minimal-content">
|
||||
<SkeletonBox width="64px" height="64px" borderRadius="50%" />
|
||||
<SkeletonBox width="120px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
{:else if layout === 'centered'}
|
||||
<!-- Centered: Logo + preview card -->
|
||||
<div class="centered-content">
|
||||
{#if appLogo}
|
||||
<img src={appLogo} alt="" class="app-logo" />
|
||||
{:else}
|
||||
<SkeletonBox width="64px" height="64px" borderRadius="16px" />
|
||||
{/if}
|
||||
<SkeletonBox width="180px" height="24px" borderRadius="8px" />
|
||||
|
||||
<div class="centered-card">
|
||||
<div class="card-header">
|
||||
<SkeletonBox width="120px" height="20px" />
|
||||
<div class="card-actions">
|
||||
<SkeletonBox width="32px" height="32px" borderRadius="8px" />
|
||||
<SkeletonBox width="32px" height="32px" borderRadius="8px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
{#each Array(4) as _, i}
|
||||
<SkeletonBox
|
||||
width="100%"
|
||||
height="32px"
|
||||
borderRadius="8px"
|
||||
class="opacity-{Math.max(30, 100 - i * 20)}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SkeletonBox width="140px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
{:else if layout === 'sidebar'}
|
||||
<!-- Sidebar: Left sidebar + main content -->
|
||||
<div class="sidebar-wrapper">
|
||||
<aside class="sidebar-skeleton">
|
||||
<div class="sidebar-header">
|
||||
<SkeletonBox width="100%" height="40px" borderRadius="8px" />
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<SkeletonBox width="100%" height="40px" borderRadius="8px" />
|
||||
<div class="sidebar-divider">
|
||||
<SkeletonBox width="80px" height="12px" />
|
||||
</div>
|
||||
{#each Array(4) as _}
|
||||
<SkeletonBox width="100%" height="40px" borderRadius="8px" />
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="main-skeleton">
|
||||
<div class="main-header">
|
||||
<SkeletonBox width="200px" height="32px" />
|
||||
<SkeletonBox width="120px" height="16px" />
|
||||
</div>
|
||||
<div class="main-toolbar">
|
||||
<SkeletonBox width="100%" height="40px" borderRadius="8px" class="flex-1" />
|
||||
<SkeletonBox width="120px" height="40px" borderRadius="8px" />
|
||||
<SkeletonBox width="100px" height="40px" borderRadius="8px" />
|
||||
</div>
|
||||
<div class="main-content">
|
||||
{#each Array(listItemCount) as _, i}
|
||||
<div style="opacity: {Math.max(0.3, 1 - i * 0.15)}">
|
||||
<SkeletonBox width="100%" height="80px" borderRadius="12px" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{:else if layout === 'tasks'}
|
||||
<!-- Tasks: Quick-add + task sections -->
|
||||
<div class="content-skeleton tasks-content">
|
||||
<div class="tasks-header">
|
||||
<SkeletonBox width="180px" height="28px" />
|
||||
<SkeletonBox width="220px" height="16px" />
|
||||
</div>
|
||||
|
||||
<div class="quick-add">
|
||||
<SkeletonBox width="100%" height="52px" borderRadius="12px" />
|
||||
</div>
|
||||
|
||||
<div class="task-section">
|
||||
<div class="section-header">
|
||||
<SkeletonBox width="100px" height="20px" />
|
||||
<SkeletonBox width="28px" height="28px" borderRadius="50%" />
|
||||
</div>
|
||||
<div class="task-list">
|
||||
{#each Array(listItemCount) as _, i}
|
||||
<div class="task-item" style="opacity: {Math.max(0.3, 1 - i * 0.18)}">
|
||||
<SkeletonBox width="22px" height="22px" borderRadius="6px" />
|
||||
<div class="task-content">
|
||||
<SkeletonBox width="{70 - i * 8}%" height="18px" />
|
||||
<SkeletonBox width="{40 + i * 5}%" height="14px" />
|
||||
</div>
|
||||
<SkeletonBox width="24px" height="24px" borderRadius="4px" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- List (default): Search + item rows -->
|
||||
<div class="content-skeleton list-content">
|
||||
<div class="title-row">
|
||||
<SkeletonBox width="200px" height="32px" />
|
||||
<SkeletonBox width="120px" height="40px" borderRadius="8px" />
|
||||
</div>
|
||||
|
||||
<SkeletonBox width="100%" height="48px" borderRadius="12px" />
|
||||
|
||||
<div class="list-skeleton">
|
||||
{#each Array(listItemCount) as _, i}
|
||||
<div class="list-item" style="opacity: {Math.max(0.3, 1 - i * 0.15)}">
|
||||
<SkeletonBox width="48px" height="48px" borderRadius="50%" />
|
||||
<div class="item-content">
|
||||
<SkeletonBox width="60%" height="18px" />
|
||||
<SkeletonBox width="40%" height="14px" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-loading-skeleton {
|
||||
min-height: 100vh;
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
.app-loading-skeleton.sidebar-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header-skeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Content Areas */
|
||||
.content-skeleton {
|
||||
max-width: 80rem;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.custom-content {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Minimal Layout */
|
||||
.minimal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Centered Layout */
|
||||
.centered-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 2rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.centered-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Sidebar Layout */
|
||||
.sidebar-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
.sidebar-skeleton {
|
||||
width: 16rem;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--card));
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
padding: 1rem 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.main-skeleton {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.main-toolbar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Tasks Layout */
|
||||
.tasks-content {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.tasks-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-add {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.task-section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* List Layout (Default) */
|
||||
.list-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.list-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-skeleton {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content-skeleton {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-skeleton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
min-height: calc(100vh - 57px);
|
||||
}
|
||||
|
||||
.centered-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -11,6 +11,9 @@
|
|||
* - SkeletonList: Multiple rows with fade effect
|
||||
* - SkeletonCard: Card with avatar, title, body, footer
|
||||
* - SkeletonGrid: Grid of cards with fade effect
|
||||
*
|
||||
* Full Page:
|
||||
* - AppLoadingSkeleton: Full page loading skeleton with layout presets
|
||||
*/
|
||||
|
||||
// Primitives
|
||||
|
|
@ -23,3 +26,6 @@ export { default as SkeletonRow } from './SkeletonRow.svelte';
|
|||
export { default as SkeletonList } from './SkeletonList.svelte';
|
||||
export { default as SkeletonCard } from './SkeletonCard.svelte';
|
||||
export { default as SkeletonGrid } from './SkeletonGrid.svelte';
|
||||
|
||||
// Full Page
|
||||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue