mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
feat(guides): PillNavigation + public shared guide route
- Replace custom sidebar/bottom-nav with PillNavigation (matches ecosystem pattern) - Full theme/language/app-switcher/user-dropdown support via PillNavigation - GuestWelcomeModal + SessionExpiredBanner wired up - GET /shared/[token] public route: checklist, progress bar, step types, CTA - Separate layout for /shared so it bypasses AuthGate - Port corrected to 3027 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8a7efdd654
commit
71db334028
3 changed files with 374 additions and 77 deletions
|
|
@ -1,12 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import { SyncIndicator } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { guidesStore } from '$lib/stores/guides.svelte';
|
||||
import { guidesStore as dbStore } from '$lib/data/local-store.js';
|
||||
import { BookOpen, StackSimple, ClockCounterClockwise, Plus } from '@manacore/shared-icons';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_THEME_VARIANTS,
|
||||
EXTENDED_THEME_VARIANTS,
|
||||
filterHiddenNavItems,
|
||||
} from '@manacore/shared-theme';
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems, getManaApp } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { AuthGate, GuestWelcomeModal, SessionExpiredBanner } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
|
|
@ -16,102 +31,168 @@
|
|||
setContext('openCreateGuide', () => { showCreateModal = true; });
|
||||
setContext('openImportGuide', () => { showImportModal = true; });
|
||||
|
||||
// App switcher
|
||||
let appItems = $derived(getPillAppItems('guides', undefined, undefined, authStore.user?.tier));
|
||||
|
||||
// Collapsed state
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Theme
|
||||
let isDark = $derived(theme.isDark);
|
||||
let pinnedThemes = $derived<ThemeVariant[]>(
|
||||
[].filter((t): t is ThemeVariant => EXTENDED_THEME_VARIANTS.includes(t as ThemeVariant))
|
||||
);
|
||||
let visibleThemes = $derived<ThemeVariant[]>([...DEFAULT_THEME_VARIANTS, ...pinnedThemes]);
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...visibleThemes.map((variant) => ({
|
||||
id: variant,
|
||||
label: THEME_DEFINITIONS[variant]?.label || variant,
|
||||
icon: THEME_DEFINITIONS[variant]?.icon || '🎨',
|
||||
onClick: () => theme.setVariant(variant),
|
||||
active: (theme.variant || 'lume') === variant,
|
||||
})),
|
||||
{
|
||||
id: 'all-themes',
|
||||
label: 'Alle Themes',
|
||||
icon: 'palette',
|
||||
onClick: () => goto('/themes'),
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
let currentThemeVariantLabel = $derived(
|
||||
THEME_DEFINITIONS[theme.variant]?.label || THEME_DEFINITIONS.lume?.label || 'Lume'
|
||||
);
|
||||
|
||||
// Language
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as Parameters<typeof setLocale>[0]);
|
||||
}
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// User
|
||||
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
|
||||
|
||||
// Nav items
|
||||
const navItems = [
|
||||
{ href: '/', icon: BookOpen, label: 'Bibliothek' },
|
||||
{ href: '/collections', icon: StackSimple, label: 'Sammlungen' },
|
||||
{ href: '/history', icon: ClockCounterClockwise, label: 'Verlauf' },
|
||||
const baseNavItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Bibliothek', icon: 'book' },
|
||||
{ href: '/collections', label: 'Sammlungen', icon: 'stack' },
|
||||
{ href: '/history', label: 'Verlauf', icon: 'clock-counter-clockwise' },
|
||||
];
|
||||
|
||||
let currentPath = $derived($page.url.pathname);
|
||||
let isActive = (href: string) =>
|
||||
href === '/' ? currentPath === '/' : currentPath.startsWith(href);
|
||||
const navItems = $derived(filterHiddenNavItems('guides', baseNavItems, {}));
|
||||
|
||||
onMount(async () => {
|
||||
// Guest welcome
|
||||
let showGuestWelcome = $state(false);
|
||||
function initGuestWelcome() {
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('guides')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
}
|
||||
function handleToggleTheme() { theme.toggleMode(); }
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') { theme.setMode(mode); }
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
await dbStore.initialize();
|
||||
if (authStore.isLoggedIn) {
|
||||
if (authStore.isAuthenticated) {
|
||||
dbStore.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
});
|
||||
const savedCollapsed = localStorage.getItem('guides-nav-collapsed');
|
||||
if (savedCollapsed === 'true') isCollapsed = true;
|
||||
initGuestWelcome();
|
||||
}
|
||||
</script>
|
||||
|
||||
<AuthGate requiredTier="beta" allowGuest={true}>
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar (desktop) -->
|
||||
<aside class="hidden w-56 flex-shrink-0 flex-col border-r border-border bg-surface md:flex">
|
||||
<div class="flex items-center gap-2 px-4 py-5">
|
||||
<span class="text-xl">📖</span>
|
||||
<span class="text-lg font-semibold text-foreground">Guides</span>
|
||||
</div>
|
||||
<svelte:window onkeydown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
|
||||
const num = parseInt(e.key);
|
||||
if (num >= 1 && num <= baseNavItems.length) {
|
||||
e.preventDefault();
|
||||
goto(baseNavItems[num - 1].href);
|
||||
}
|
||||
}
|
||||
}} />
|
||||
|
||||
<nav class="flex flex-1 flex-col gap-1 px-2">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors
|
||||
{isActive(item.href)
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-foreground'}"
|
||||
>
|
||||
<item.icon class="h-4 w-4" />
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<AuthGate
|
||||
{authStore}
|
||||
{goto}
|
||||
allowGuest={true}
|
||||
onReady={handleAuthReady}
|
||||
requiredTier={getManaApp('guides')?.requiredTier}
|
||||
appName={getManaApp('guides')?.name}
|
||||
>
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Guides"
|
||||
homeRoute="/"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#0d9488"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<button
|
||||
onclick={() => (showCreateModal = true)}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-hover"
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
Neue Anleitung
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (showImportModal = true)}
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
↓ Importieren
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="flex-1 overflow-y-auto pb-20 md:pb-0">
|
||||
{@render children()}
|
||||
</div>
|
||||
<main class="relative z-0 pb-24" style="padding-top: 0">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<SyncIndicator />
|
||||
</div>
|
||||
|
||||
<!-- Bottom nav (mobile) -->
|
||||
<nav class="fixed bottom-0 left-0 right-0 z-40 border-t border-border bg-surface md:hidden">
|
||||
<div class="flex">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex flex-1 flex-col items-center gap-1 py-3 text-xs transition-colors
|
||||
{isActive(item.href) ? 'text-primary' : 'text-muted-foreground'}"
|
||||
>
|
||||
<item.icon class="h-5 w-5" />
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- FAB (mobile) -->
|
||||
<!-- FAB -->
|
||||
<button
|
||||
onclick={() => (showCreateModal = true)}
|
||||
class="fixed bottom-20 right-4 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-lg transition-transform hover:scale-105 active:scale-95 md:hidden"
|
||||
class="fixed bottom-20 right-4 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-lg transition-transform hover:scale-105 active:scale-95"
|
||||
aria-label="Neue Anleitung erstellen"
|
||||
>
|
||||
<Plus class="h-6 w-6" />
|
||||
<svg width="24" height="24" viewBox="0 0 256 256" fill="currentColor"><path d="M228 128a12 12 0 0 1-12 12h-76v76a12 12 0 0 1-24 0v-76H40a12 12 0 0 1 0-24h76V40a12 12 0 0 1 24 0v76h76a12 12 0 0 1 12 12Z"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Guest Welcome -->
|
||||
<GuestWelcomeModal
|
||||
appId="guides"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => goto('/login')}
|
||||
onRegister={() => goto('/register')}
|
||||
locale={currentLocale === 'de' ? 'de' : 'en'}
|
||||
/>
|
||||
|
||||
{#if authStore.isAuthenticated}
|
||||
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
||||
{#if showCreateModal}
|
||||
<!-- GuideEditModal dynamically imported to keep bundle small -->
|
||||
{#await import('$lib/components/GuideEditModal.svelte') then { default: GuideEditModal }}
|
||||
<GuideEditModal
|
||||
open={true}
|
||||
|
|
|
|||
5
apps/guides/apps/web/src/routes/shared/+layout.svelte
Normal file
5
apps/guides/apps/web/src/routes/shared/+layout.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
211
apps/guides/apps/web/src/routes/shared/[token]/+page.svelte
Normal file
211
apps/guides/apps/web/src/routes/shared/[token]/+page.svelte
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const SERVER_URL = import.meta.env.PUBLIC_GUIDES_SERVER_URL || 'http://localhost:3027';
|
||||
|
||||
interface SharedStep {
|
||||
id: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
type?: string;
|
||||
checkable?: boolean;
|
||||
}
|
||||
|
||||
interface SharedGuide {
|
||||
title: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
difficulty?: string;
|
||||
estimatedMinutes?: number;
|
||||
tags?: string[];
|
||||
coverEmoji?: string;
|
||||
coverColor?: string;
|
||||
}
|
||||
|
||||
let token = $derived($page.params.token);
|
||||
|
||||
type State = 'loading' | 'loaded' | 'error' | 'expired';
|
||||
let state = $state<State>('loading');
|
||||
let guide = $state<SharedGuide | null>(null);
|
||||
let steps = $state<SharedStep[]>([]);
|
||||
let errorMsg = $state('');
|
||||
|
||||
// Checklist state (local only — shared guides are read-only)
|
||||
let checked = $state<Record<string, boolean>>({});
|
||||
|
||||
$effect(() => {
|
||||
const t = token;
|
||||
state = 'loading';
|
||||
fetch(`${SERVER_URL}/api/v1/share/${t}`)
|
||||
.then(async (res) => {
|
||||
if (res.status === 410) { state = 'expired'; return; }
|
||||
if (!res.ok) { state = 'error'; errorMsg = 'Guide nicht gefunden'; return; }
|
||||
const data = await res.json<{ guide: SharedGuide; sections: SharedStep[] | { steps: SharedStep[] }[] }>();
|
||||
guide = data.guide as SharedGuide;
|
||||
// flatten sections → steps
|
||||
const raw = data.sections as unknown[];
|
||||
if (raw.length > 0 && 'steps' in (raw[0] as object)) {
|
||||
steps = (raw as { steps: SharedStep[] }[]).flatMap((s) => s.steps ?? []);
|
||||
} else {
|
||||
steps = raw as SharedStep[];
|
||||
}
|
||||
state = 'loaded';
|
||||
})
|
||||
.catch(() => { state = 'error'; errorMsg = 'Server nicht erreichbar'; });
|
||||
});
|
||||
|
||||
const difficultyLabel: Record<string, string> = { easy: 'Einfach', medium: 'Mittel', hard: 'Schwer' };
|
||||
const progress = $derived(
|
||||
steps.filter((s) => s.checkable).length > 0
|
||||
? Math.round(
|
||||
(steps.filter((s) => s.checkable && checked[s.id]).length /
|
||||
steps.filter((s) => s.checkable).length) *
|
||||
100
|
||||
)
|
||||
: 0
|
||||
);
|
||||
|
||||
const stepTypeConfig: Record<string, { icon: string; border: string; bg: string }> = {
|
||||
instruction: { icon: '→', border: 'border-l-teal-500', bg: '' },
|
||||
warning: { icon: '⚠', border: 'border-l-orange-400', bg: 'bg-orange-50/50 dark:bg-orange-950/20' },
|
||||
tip: { icon: '💡', border: 'border-l-violet-400', bg: 'bg-violet-50/50 dark:bg-violet-950/20' },
|
||||
checkpoint: { icon: '✓', border: 'border-l-blue-400', bg: 'bg-blue-50/50 dark:bg-blue-950/20' },
|
||||
code: { icon: '</>', border: 'border-l-slate-400', bg: 'bg-slate-50/50 dark:bg-slate-950/20 font-mono text-xs' },
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if guide}
|
||||
<title>{guide.title} — Mana Guides</title>
|
||||
<meta name="description" content={guide.description ?? ''} />
|
||||
{:else}
|
||||
<title>Geteilte Anleitung — Mana Guides</title>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
|
||||
<div class="mx-auto max-w-2xl px-4 py-8">
|
||||
|
||||
{#if state === 'loading'}
|
||||
<div class="flex items-center justify-center py-24">
|
||||
<svg class="animate-spin h-8 w-8 text-teal-600" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{:else if state === 'expired'}
|
||||
<div class="flex flex-col items-center justify-center py-24 text-center">
|
||||
<span class="mb-4 text-5xl">⏳</span>
|
||||
<h1 class="text-xl font-bold text-neutral-900 dark:text-white mb-2">Link abgelaufen</h1>
|
||||
<p class="text-sm text-neutral-500">Dieser Freigabe-Link ist nicht mehr gültig (nach 7 Tagen abgelaufen).</p>
|
||||
<a href="/" class="mt-6 text-sm text-teal-600 hover:underline">Zur Guides-App →</a>
|
||||
</div>
|
||||
|
||||
{:else if state === 'error'}
|
||||
<div class="flex flex-col items-center justify-center py-24 text-center">
|
||||
<span class="mb-4 text-5xl">❌</span>
|
||||
<h1 class="text-xl font-bold text-neutral-900 dark:text-white mb-2">{errorMsg}</h1>
|
||||
<a href="/" class="mt-6 text-sm text-teal-600 hover:underline">Zur Guides-App →</a>
|
||||
</div>
|
||||
|
||||
{:else if guide}
|
||||
<!-- Branding strip -->
|
||||
<div class="mb-6 flex items-center gap-2">
|
||||
<span class="text-xl">📖</span>
|
||||
<span class="text-sm font-semibold text-teal-600">Mana Guides</span>
|
||||
<span class="ml-auto text-xs text-neutral-400">Geteilte Anleitung</span>
|
||||
</div>
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="mb-6 rounded-2xl p-5" style="background-color: {guide.coverColor ?? '#0d9488'}18">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="text-5xl">{guide.coverEmoji ?? '📖'}</span>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-neutral-900 dark:text-white">{guide.title}</h1>
|
||||
{#if guide.description}
|
||||
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">{guide.description}</p>
|
||||
{/if}
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{#if guide.difficulty}
|
||||
<span class="text-xs bg-teal-100 dark:bg-teal-900/40 text-teal-700 dark:text-teal-300 px-2 py-0.5 rounded-full">
|
||||
{difficultyLabel[guide.difficulty] ?? guide.difficulty}
|
||||
</span>
|
||||
{/if}
|
||||
{#if guide.estimatedMinutes}
|
||||
<span class="text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 px-2 py-0.5 rounded-full">
|
||||
⏱ {guide.estimatedMinutes} Min.
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 px-2 py-0.5 rounded-full">
|
||||
{steps.length} Schritte
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress (if any checkable steps) -->
|
||||
{#if steps.some((s) => s.checkable)}
|
||||
<div class="mb-6 rounded-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Fortschritt</span>
|
||||
<span class="text-sm text-teal-600 font-semibold">{progress}%</span>
|
||||
</div>
|
||||
<div class="h-2 bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-teal-500 rounded-full transition-all" style="width: {progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Steps -->
|
||||
<div class="space-y-3">
|
||||
{#each steps as step, i}
|
||||
{@const cfg = stepTypeConfig[step.type ?? 'instruction'] ?? stepTypeConfig.instruction}
|
||||
<div
|
||||
class="rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 border-l-4 {cfg.border} {cfg.bg} overflow-hidden"
|
||||
>
|
||||
<div class="p-4 flex gap-3 items-start">
|
||||
{#if step.checkable}
|
||||
<button
|
||||
onclick={() => { checked[step.id] = !checked[step.id]; }}
|
||||
class="shrink-0 mt-0.5 w-5 h-5 rounded border-2 transition-colors flex items-center justify-center
|
||||
{checked[step.id]
|
||||
? 'bg-teal-600 border-teal-600 text-white'
|
||||
: 'border-neutral-300 dark:border-neutral-600 hover:border-teal-400'}"
|
||||
aria-label="Schritt abhaken"
|
||||
>
|
||||
{#if checked[step.id]}
|
||||
<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor"><path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z"/></svg>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="shrink-0 mt-0.5 text-sm text-neutral-400">{cfg.icon}</span>
|
||||
{/if}
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-neutral-900 dark:text-white {checked[step.id] ? 'line-through text-neutral-400' : ''}">
|
||||
{i + 1}. {step.title}
|
||||
</p>
|
||||
{#if step.content}
|
||||
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400 whitespace-pre-wrap">{step.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="mt-10 rounded-2xl bg-teal-50 dark:bg-teal-950/30 border border-teal-200 dark:border-teal-800 p-5 text-center">
|
||||
<p class="text-sm font-semibold text-teal-800 dark:text-teal-200 mb-1">Eigene Anleitungen erstellen?</p>
|
||||
<p class="text-xs text-teal-600 dark:text-teal-400 mb-4">Mit Mana Guides kannst du SOPs, Rezepte, Tutorials und Lernpfade erstellen — kostenlos.</p>
|
||||
<a
|
||||
href="/"
|
||||
class="inline-block rounded-xl bg-teal-600 text-white text-sm font-semibold px-5 py-2.5 hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Mana Guides ausprobieren →
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue