mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
feat: unified guest mode with AuthGate + createGuestMode composable
Major refactor of ManaCore's (app) layout for guest mode support: - New createGuestMode() composable in shared-stores — encapsulates welcome modal state, nudge timer, and notifications in one call - Replace monolith onMount with AuthGate + handleAuthReady callback: Phase A (auth-independent): DB init, migration, uload, dashboard Phase B (auth-dependent): sync, user settings, onboarding Phase C (guest-only): welcome modal + registration nudge - Root route / always redirects to /home (no auth check) - PillNav shows login button for guests, user email for auth users - GuestWelcomeModal with manacore-specific features - SessionWarning only renders for authenticated users - Proper cleanup via onDestroy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ead4e71af5
commit
4667d5df33
5 changed files with 264 additions and 101 deletions
|
|
@ -2,17 +2,27 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { onDestroy, setContext } from 'svelte';
|
||||
import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte';
|
||||
import SessionWarning from '$lib/components/SessionWarning.svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, TagStrip, DragPreview, ActionZone } from '@manacore/shared-ui';
|
||||
import {
|
||||
PillNavigation,
|
||||
TagStrip,
|
||||
DragPreview,
|
||||
ActionZone,
|
||||
QuickInputBar,
|
||||
} from '@manacore/shared-ui';
|
||||
import type {
|
||||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
SpotlightAction,
|
||||
ContentSearcher,
|
||||
} from '@manacore/shared-ui';
|
||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||
import { getAdapterLoader } from '$lib/quick-input/registry';
|
||||
import { createFallbackAdapter } from '$lib/quick-input/fallback-adapter';
|
||||
import { AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { tagLocalStore, tagMutations, useAllTags } from '$lib/stores/tags.svelte';
|
||||
import { linkLocalStore, linkMutations } from '@manacore/shared-links';
|
||||
import { manacoreStore } from '$lib/data/local-store';
|
||||
|
|
@ -39,30 +49,26 @@
|
|||
import { SearchRegistry } from '$lib/search/registry';
|
||||
import { registerAllProviders } from '$lib/search/providers';
|
||||
import { initSharedUload } from '@manacore/shared-uload';
|
||||
import type { DragPayload } from '@manacore/shared-ui/dnd';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
// App switcher items (filtered by user's access tier)
|
||||
// ── App switcher ────────────────────────────────────────
|
||||
let appItems = $derived(getPillAppItems('manacore', undefined, undefined, authStore.user?.tier));
|
||||
|
||||
let loading = $state(true);
|
||||
// ── UI State ────────────────────────────────────────────
|
||||
let isCollapsed = $state(false);
|
||||
let showShortcuts = $state(false);
|
||||
let showOnboarding = $state(false);
|
||||
|
||||
// Get theme state
|
||||
// ── Theme ───────────────────────────────────────────────
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
// Get pinned themes from user settings (extended themes only)
|
||||
let pinnedThemes = $derived<ThemeVariant[]>(
|
||||
(userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant =>
|
||||
EXTENDED_THEME_VARIANTS.includes(t as ThemeVariant)
|
||||
)
|
||||
);
|
||||
|
||||
// Visible themes in PillNav: default + pinned extended
|
||||
let visibleThemes = $derived<ThemeVariant[]>([...DEFAULT_THEME_VARIANTS, ...pinnedThemes]);
|
||||
|
||||
// Theme variant dropdown items
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...visibleThemes.map((variant) => ({
|
||||
id: variant,
|
||||
|
|
@ -79,11 +85,9 @@
|
|||
active: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Current theme variant label
|
||||
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
|
||||
|
||||
// Language selector items
|
||||
// ── i18n ────────────────────────────────────────────────
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
|
|
@ -94,21 +98,17 @@
|
|||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// User email for user dropdown
|
||||
let userEmail = $derived(authStore.user?.email);
|
||||
// ── User / Guest awareness ──────────────────────────────
|
||||
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
|
||||
|
||||
// Tags (local-first reactive query)
|
||||
// ── Tags ────────────────────────────────────────────────
|
||||
const allTags = useAllTags();
|
||||
|
||||
// TagStrip visibility
|
||||
let isTagStripVisible = $state(false);
|
||||
function handleTagStripToggle() {
|
||||
isTagStripVisible = !isTagStripVisible;
|
||||
}
|
||||
|
||||
// DnD: tag drop handler — set by child pages via context
|
||||
import type { DragPayload } from '@manacore/shared-ui/dnd';
|
||||
|
||||
// ── DnD context ─────────────────────────────────────────
|
||||
let tagDropHandler = $state<((tagId: string, payload: DragPayload) => void) | null>(null);
|
||||
setContext('tagDropHandler', {
|
||||
set(handler: (tagId: string, payload: DragPayload) => void) {
|
||||
|
|
@ -119,7 +119,7 @@
|
|||
},
|
||||
});
|
||||
|
||||
// Navigation items for ManaCore
|
||||
// ── Navigation ──────────────────────────────────────────
|
||||
const baseNavItems: PillNavItem[] = [
|
||||
{ href: '/home', label: 'Home', icon: 'home' },
|
||||
{ href: '/dashboard', label: 'Dashboard', icon: 'grid' },
|
||||
|
|
@ -139,13 +139,10 @@
|
|||
},
|
||||
];
|
||||
|
||||
// Only show admin link if user has admin role
|
||||
let isAdmin = $derived(authStore.user?.role === 'admin');
|
||||
let navItems = $derived<PillNavItem[]>(
|
||||
isAdmin ? [...baseNavItems, { href: '/admin', label: 'Admin', icon: 'shield' }] : baseNavItems
|
||||
);
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = navItems.map((item) => item.href);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
|
|
@ -153,22 +150,17 @@
|
|||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ? key opens keyboard shortcuts
|
||||
if (event.key === '?' && !event.ctrlKey && !event.metaKey) {
|
||||
event.preventDefault();
|
||||
showShortcuts = !showShortcuts;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= navRoutes.length) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
if (route) goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -191,7 +183,7 @@
|
|||
AppEvents.themeChanged(mode);
|
||||
}
|
||||
|
||||
// Unified sync manager — one sync engine for all apps
|
||||
// ── Sync ────────────────────────────────────────────────
|
||||
const SYNC_SERVER_URL =
|
||||
(typeof window !== 'undefined' &&
|
||||
(window as Record<string, unknown>).__PUBLIC_SYNC_SERVER_URL__) ||
|
||||
|
|
@ -201,14 +193,15 @@
|
|||
|
||||
async function handleSignOut() {
|
||||
unifiedSync?.stopAll();
|
||||
guestMode?.destroy();
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
// Track initialization state
|
||||
let isInitializing = $state(true);
|
||||
let showOnboarding = $state(false);
|
||||
// ── Guest Mode ──────────────────────────────────────────
|
||||
let guestMode: { destroy: () => void } | null = null;
|
||||
|
||||
// ── Onboarding ──────────────────────────────────────────
|
||||
function handleOnboardingComplete() {
|
||||
onboardingStore.complete();
|
||||
ManaCoreEvents.onboardingCompleted();
|
||||
|
|
@ -221,62 +214,58 @@
|
|||
showOnboarding = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth store first
|
||||
await authStore.initialize();
|
||||
|
||||
// Only after initialization is complete, check auth status
|
||||
isInitializing = false;
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize local-first databases (opens IndexedDB, seeds guest data)
|
||||
// ── Auth Ready (replaces monolith onMount) ──────────────
|
||||
async function handleAuthReady() {
|
||||
// Phase A: Auth-independent — guests + authenticated
|
||||
await Promise.all([
|
||||
manacoreStore.initialize(),
|
||||
tagLocalStore.initialize(),
|
||||
linkLocalStore.initialize(),
|
||||
]);
|
||||
|
||||
// Migrate data from legacy per-app databases (one-time, idempotent)
|
||||
await migrateFromLegacyDbs();
|
||||
|
||||
// Initialize shared-uload (opens uLoad IndexedDB for cross-app link creation)
|
||||
initSharedUload();
|
||||
|
||||
// Start unified sync — one engine for all apps via Dexie hooks
|
||||
const getToken = () => authStore.getValidToken();
|
||||
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken);
|
||||
unifiedSync.startAll();
|
||||
|
||||
// Initialize dashboard from IndexedDB
|
||||
await dashboardStore.initialize();
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem(STORAGE_KEYS.NAV_COLLAPSED);
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
// Restore nav collapsed state
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const savedCollapsed = localStorage.getItem(STORAGE_KEYS.NAV_COLLAPSED);
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Load user settings from server (still needed for shared-theme sync)
|
||||
userSettings.load().catch(() => {});
|
||||
// Phase B: Auth-dependent — sync, settings, onboarding
|
||||
if (authStore.isAuthenticated) {
|
||||
const getToken = () => authStore.getValidToken();
|
||||
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken);
|
||||
unifiedSync.startAll();
|
||||
|
||||
// Load onboarding state and show wizard if needed
|
||||
onboardingStore.load();
|
||||
if (onboardingStore.shouldShow) {
|
||||
onboardingStore.start();
|
||||
ManaCoreEvents.onboardingStarted();
|
||||
showOnboarding = true;
|
||||
userSettings.load().catch(() => {});
|
||||
|
||||
onboardingStore.load();
|
||||
if (onboardingStore.shouldShow) {
|
||||
onboardingStore.start();
|
||||
ManaCoreEvents.onboardingStarted();
|
||||
showOnboarding = true;
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
// Phase C: Guest mode — welcome modal + nudge
|
||||
if (!authStore.isAuthenticated) {
|
||||
guestMode = createGuestMode('manacore', {
|
||||
nudgeDelayMinutes: 3,
|
||||
onRegister: () => goto('/register'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
unifiedSync?.stopAll();
|
||||
guestMode?.destroy();
|
||||
});
|
||||
|
||||
// Cross-app search
|
||||
// ── Search / Spotlight ───────────────────────────────────
|
||||
const searchRegistry = new SearchRegistry();
|
||||
registerAllProviders(searchRegistry);
|
||||
|
||||
|
|
@ -284,6 +273,32 @@
|
|||
return searchRegistry.search(query, { signal });
|
||||
};
|
||||
|
||||
// ── QuickInputBar — context-aware adapter per module ─────
|
||||
let inputBarAdapter = $state<InputBarAdapter>(createFallbackAdapter(searchRegistry));
|
||||
let activeModulePrefix = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const pathname = $page.url.pathname;
|
||||
const moduleSlug = '/' + pathname.split('/')[1];
|
||||
|
||||
if (moduleSlug === activeModulePrefix) return;
|
||||
|
||||
const loader = getAdapterLoader(pathname);
|
||||
if (!loader) {
|
||||
inputBarAdapter = createFallbackAdapter(searchRegistry);
|
||||
activeModulePrefix = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const target = moduleSlug;
|
||||
loader().then(({ createAdapter }) => {
|
||||
if (target === '/' + $page.url.pathname.split('/')[1]) {
|
||||
inputBarAdapter = createAdapter() as InputBarAdapter;
|
||||
activeModulePrefix = target;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const spotlightActions: SpotlightAction[] = [
|
||||
{ id: 'home', label: 'Home', category: 'Navigation', onExecute: () => goto('/home') },
|
||||
{
|
||||
|
|
@ -311,18 +326,16 @@
|
|||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isInitializing || loading || authStore.loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary-500 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if authStore.isAuthenticated}
|
||||
<!-- Onboarding Wizard Modal -->
|
||||
{#if showOnboarding}
|
||||
<AuthGate
|
||||
{authStore}
|
||||
{goto}
|
||||
allowGuest={true}
|
||||
onReady={handleAuthReady}
|
||||
appName="ManaCore"
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
>
|
||||
<!-- Onboarding Wizard (auth only) -->
|
||||
{#if showOnboarding && authStore.isAuthenticated}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-background/95 backdrop-blur-sm"
|
||||
>
|
||||
|
|
@ -351,7 +364,8 @@
|
|||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
loginHref="/login"
|
||||
primaryColor="#6366f1"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
|
|
@ -366,6 +380,26 @@
|
|||
{contentSearcher}
|
||||
/>
|
||||
|
||||
<!-- QuickInputBar (context-aware per module) -->
|
||||
<QuickInputBar
|
||||
onSearch={inputBarAdapter.onSearch}
|
||||
onSelect={inputBarAdapter.onSelect}
|
||||
onParseCreate={inputBarAdapter.onParseCreate}
|
||||
onCreate={inputBarAdapter.onCreate}
|
||||
onSearchChange={inputBarAdapter.onSearchChange}
|
||||
placeholder={inputBarAdapter.placeholder}
|
||||
appIcon={inputBarAdapter.appIcon}
|
||||
emptyText={inputBarAdapter.emptyText}
|
||||
createText={inputBarAdapter.createText}
|
||||
deferSearch={inputBarAdapter.deferSearch}
|
||||
locale={$locale || 'de'}
|
||||
defaultOptions={inputBarAdapter.defaultOptions}
|
||||
selectedDefaultId={inputBarAdapter.selectedDefaultId}
|
||||
defaultOptionLabel={inputBarAdapter.defaultOptionLabel}
|
||||
onDefaultChange={inputBarAdapter.onDefaultChange}
|
||||
highlightPatterns={inputBarAdapter.highlightPatterns}
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
|
|
@ -383,7 +417,7 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
<!-- DnD: floating preview + action zones -->
|
||||
<!-- DnD: floating preview -->
|
||||
<DragPreview />
|
||||
|
||||
<!-- Main content -->
|
||||
|
|
@ -393,10 +427,12 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Session expiry warning -->
|
||||
<SessionWarning />
|
||||
<!-- Session expiry warning (auth only) -->
|
||||
{#if authStore.isAuthenticated}
|
||||
<SessionWarning />
|
||||
{/if}
|
||||
|
||||
<!-- Keyboard shortcuts modal -->
|
||||
<KeyboardShortcutsModal open={showShortcuts} onclose={() => (showShortcuts = false)} />
|
||||
</div>
|
||||
{/if}
|
||||
</AuthGate>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
goto('/home');
|
||||
} else {
|
||||
goto('/login');
|
||||
}
|
||||
onMount(() => {
|
||||
goto('/home', { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
/** Default features per app (German) */
|
||||
const defaultFeaturesDE: Record<string, string[]> = {
|
||||
manacore: ['Alle deine Apps an einem Ort', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
contacts: ['Alle Kontakte an einem Ort', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
chat: ['Dein persönlicher KI-Assistent', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
todo: ['Organisiere deinen Alltag', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
|
|
@ -72,6 +73,7 @@
|
|||
|
||||
/** Default features per app (English) */
|
||||
const defaultFeaturesEN: Record<string, string[]> = {
|
||||
manacore: ['All your apps in one place', 'Open-source & independent', 'Private by design'],
|
||||
contacts: ['All your contacts in one place', 'Open-source & independent', 'Private by design'],
|
||||
chat: ['Your personal AI assistant', 'Open-source & independent', 'Private by design'],
|
||||
todo: ['Organize your day', 'Open-source & independent', 'Private by design'],
|
||||
|
|
|
|||
130
packages/shared-stores/src/guest-mode.svelte.ts
Normal file
130
packages/shared-stores/src/guest-mode.svelte.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Guest Mode Composable
|
||||
*
|
||||
* Encapsulates welcome modal visibility, registration nudge timer,
|
||||
* and bottom notification state for guest users.
|
||||
*
|
||||
* Usage:
|
||||
* const guestMode = createGuestMode('manacore', { nudgeDelayMinutes: 3 });
|
||||
*/
|
||||
|
||||
// Inline localStorage utilities (no external dependency needed)
|
||||
function _shouldShowWelcome(appId: string): boolean {
|
||||
if (typeof localStorage === 'undefined') return false;
|
||||
return localStorage.getItem(`guest-welcome-seen-${appId}`) !== 'true';
|
||||
}
|
||||
|
||||
function _markWelcomeSeen(appId: string): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
localStorage.setItem(`guest-welcome-seen-${appId}`, 'true');
|
||||
}
|
||||
|
||||
function _startSession(appId: string): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
const key = `guest-nudge-session-${appId}`;
|
||||
if (!localStorage.getItem(key)) {
|
||||
localStorage.setItem(key, Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
function _shouldShowNudge(appId: string, delayMinutes: number): boolean {
|
||||
if (typeof localStorage === 'undefined') return false;
|
||||
if (localStorage.getItem(`guest-nudge-dismissed-${appId}`) === 'true') return false;
|
||||
const sessionStart = localStorage.getItem(`guest-nudge-session-${appId}`);
|
||||
if (!sessionStart) return false;
|
||||
return Date.now() - parseInt(sessionStart, 10) >= delayMinutes * 60 * 1000;
|
||||
}
|
||||
|
||||
function _dismissNudge(appId: string): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
localStorage.setItem(`guest-nudge-dismissed-${appId}`, 'true');
|
||||
}
|
||||
|
||||
export interface GuestModeNotification {
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'info' | 'warning' | 'error';
|
||||
action?: { label: string; icon?: unknown; onClick: () => void };
|
||||
dismissible?: boolean;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export interface GuestModeOptions {
|
||||
nudgeDelayMinutes?: number;
|
||||
nudgeCheckIntervalMs?: number;
|
||||
nudgeMessage?: string;
|
||||
nudgeActionLabel?: string;
|
||||
onRegister?: () => void;
|
||||
}
|
||||
|
||||
export interface GuestMode {
|
||||
readonly showWelcome: boolean;
|
||||
readonly notifications: GuestModeNotification[];
|
||||
dismissWelcome(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export function createGuestMode(appId: string, opts?: GuestModeOptions): GuestMode {
|
||||
const nudgeDelay = opts?.nudgeDelayMinutes ?? 5;
|
||||
const checkInterval = opts?.nudgeCheckIntervalMs ?? 30_000;
|
||||
const nudgeMessage =
|
||||
opts?.nudgeMessage ?? 'Gefällt es dir? Sichere deine Daten geräteübergreifend.';
|
||||
const nudgeActionLabel = opts?.nudgeActionLabel ?? 'Konto erstellen';
|
||||
|
||||
let showWelcome = $state(_shouldShowWelcome(appId));
|
||||
let notifications = $state<GuestModeNotification[]>([]);
|
||||
let nudgeInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
_startSession(appId);
|
||||
|
||||
function checkNudge() {
|
||||
if (_shouldShowNudge(appId, nudgeDelay)) {
|
||||
notifications = [
|
||||
{
|
||||
id: 'guest-nudge',
|
||||
message: nudgeMessage,
|
||||
type: 'info' as const,
|
||||
action: {
|
||||
label: nudgeActionLabel,
|
||||
onClick: () => {
|
||||
_dismissNudge(appId);
|
||||
notifications = [];
|
||||
opts?.onRegister?.();
|
||||
},
|
||||
},
|
||||
dismissible: true,
|
||||
onDismiss: () => {
|
||||
_dismissNudge(appId);
|
||||
notifications = [];
|
||||
},
|
||||
},
|
||||
];
|
||||
if (nudgeInterval) {
|
||||
clearInterval(nudgeInterval);
|
||||
nudgeInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkNudge();
|
||||
nudgeInterval = setInterval(checkNudge, checkInterval);
|
||||
|
||||
return {
|
||||
get showWelcome() {
|
||||
return showWelcome;
|
||||
},
|
||||
get notifications() {
|
||||
return notifications;
|
||||
},
|
||||
dismissWelcome() {
|
||||
showWelcome = false;
|
||||
_markWelcomeSeen(appId);
|
||||
},
|
||||
destroy() {
|
||||
if (nudgeInterval) {
|
||||
clearInterval(nudgeInterval);
|
||||
nudgeInterval = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ export {
|
|||
} from './tags-local.svelte';
|
||||
export { createTagLinkOps, type TagLinkOps, type TagLinkOpsConfig } from './tag-links';
|
||||
export { toggleField } from './toggle-field';
|
||||
|
||||
export {
|
||||
createGuestMode,
|
||||
type GuestMode,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue