mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:41:08 +02:00
✨ feat(shared-auth-ui): add GuestWelcomeModal for guest onboarding
Add a unified welcome modal for guest mode that displays: - App icon, name, and description from shared-branding - Feature list of what guests can do (localized DE/EN) - Warning about local-only data storage - Login, Register, Help, and "Continue as Guest" buttons New files: - GuestWelcomeModal.svelte - The modal component - guestWelcome.ts - localStorage utilities for tracking seen state Integrated into: contacts, chat, todo, calendar, and clock apps
This commit is contained in:
parent
6402f287e8
commit
14c83cb4bd
10 changed files with 873 additions and 7 deletions
|
|
@ -70,6 +70,7 @@
|
|||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
import { heatmapStore } from '$lib/stores/heatmap.svelte';
|
||||
import { sessionEventsStore } from '$lib/stores/session-events.svelte';
|
||||
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import type { CalendarViewType } from '@calendar/shared';
|
||||
|
||||
// App switcher items
|
||||
|
|
@ -567,6 +568,9 @@
|
|||
let showAuthGateModal = $state(false);
|
||||
let authGateAction = $state<'save' | 'sync' | 'feature'>('save');
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// Show auth gate modal (can be called from child components)
|
||||
function showAuthGate(action: 'save' | 'sync' | 'feature' = 'save') {
|
||||
authGateAction = action;
|
||||
|
|
@ -587,6 +591,11 @@
|
|||
// Initialize session events for guest mode
|
||||
sessionEventsStore.initialize();
|
||||
|
||||
// Show guest welcome modal for unauthenticated users
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('calendar')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Load calendars and tags (works in both guest and authenticated mode)
|
||||
await calendarsStore.fetchCalendars();
|
||||
|
||||
|
|
@ -839,6 +848,23 @@
|
|||
action={authGateAction}
|
||||
/>
|
||||
|
||||
<!-- Guest Welcome Modal -->
|
||||
<GuestWelcomeModal
|
||||
appId="calendar"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/login');
|
||||
}}
|
||||
onRegister={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/register');
|
||||
}}
|
||||
helpHref="/help"
|
||||
locale={currentLocale === 'en' ? 'en' : 'de'}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
|
||||
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
// App switcher items
|
||||
|
|
@ -40,6 +41,9 @@
|
|||
let showAuthGateModal = $state(false);
|
||||
let authGateAction = $state<'save' | 'sync' | 'ai' | 'feature'>('ai');
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// Check if in guest mode
|
||||
let isGuestMode = $derived(!authStore.isAuthenticated);
|
||||
let sessionConversationCount = $derived(sessionConversationsStore.count);
|
||||
|
|
@ -93,6 +97,7 @@
|
|||
// Base navigation items for Chat (settings moved to user dropdown)
|
||||
const baseNavItems: PillNavItem[] = [
|
||||
{ href: '/chat', label: 'Chat', icon: 'home' },
|
||||
{ href: '/compare', label: 'Vergleichen', icon: 'scale' },
|
||||
{ href: '/templates', label: 'Templates', icon: 'document' },
|
||||
{ href: '/spaces', label: 'Spaces', icon: 'building' },
|
||||
{ href: '/documents', label: 'Dokumente', icon: 'archive' },
|
||||
|
|
@ -183,6 +188,11 @@
|
|||
|
||||
await authStore.initialize();
|
||||
|
||||
// Show guest welcome modal for unauthenticated users
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('chat')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Load user settings if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
|
|
@ -291,6 +301,23 @@
|
|||
conversationCount={sessionConversationCount}
|
||||
onClose={() => (showAuthGateModal = false)}
|
||||
/>
|
||||
|
||||
<!-- Guest Welcome Modal -->
|
||||
<GuestWelcomeModal
|
||||
appId="chat"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/login');
|
||||
}}
|
||||
onRegister={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/register');
|
||||
}}
|
||||
helpHref="/help"
|
||||
locale={currentLocale === 'en' ? 'en' : 'de'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
import { alarmsApi } from '$lib/api/alarms';
|
||||
import { timersApi } from '$lib/api/timers';
|
||||
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
|
||||
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('clock');
|
||||
|
|
@ -122,10 +123,16 @@
|
|||
let showAuthGateModal = $state(false);
|
||||
let authGateAction = $state<'save' | 'sync' | 'feature'>('save');
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// Check if in guest mode
|
||||
let isGuestMode = $derived(!authStore.isAuthenticated);
|
||||
let sessionItemCount = $derived(sessionAlarmsStore.count + sessionTimersStore.count);
|
||||
|
||||
// Language for GuestWelcomeModal
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
|
|
@ -266,6 +273,11 @@
|
|||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
// Show guest welcome modal for unauthenticated users
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('clock')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Load user settings if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
|
|
@ -367,6 +379,23 @@
|
|||
itemCount={sessionItemCount}
|
||||
onClose={() => (showAuthGateModal = false)}
|
||||
/>
|
||||
|
||||
<!-- Guest Welcome Modal -->
|
||||
<GuestWelcomeModal
|
||||
appId="clock"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/login');
|
||||
}}
|
||||
onRegister={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/register');
|
||||
}}
|
||||
helpHref="/help"
|
||||
locale={currentLocale === 'en' ? 'en' : 'de'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
import ContactsToolbar from '$lib/components/ContactsToolbar.svelte';
|
||||
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
|
||||
import { sessionContactsStore } from '$lib/stores/session-contacts.svelte';
|
||||
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
|
||||
// Tags state for Quick-Create
|
||||
let availableTags = $state<{ id: string; name: string }[]>([]);
|
||||
|
|
@ -217,6 +218,9 @@
|
|||
let showAuthGateModal = $state(false);
|
||||
let authGateAction = $state<'save' | 'sync' | 'feature'>('save');
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// Show auth gate modal (can be called from child components)
|
||||
function showAuthGate(action: 'save' | 'sync' | 'feature' = 'save') {
|
||||
authGateAction = action;
|
||||
|
|
@ -297,6 +301,11 @@
|
|||
// Initialize session contacts for guest mode
|
||||
sessionContactsStore.initialize();
|
||||
|
||||
// Show guest welcome modal for unauthenticated users
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('contacts')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Only fetch user data if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
// Load user settings and tags
|
||||
|
|
@ -486,6 +495,23 @@
|
|||
action={authGateAction}
|
||||
/>
|
||||
|
||||
<!-- Guest Welcome Modal -->
|
||||
<GuestWelcomeModal
|
||||
appId="contacts"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/login');
|
||||
}}
|
||||
onRegister={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/register');
|
||||
}}
|
||||
helpHref="/help"
|
||||
locale={currentLocale === 'en' ? 'en' : 'de'}
|
||||
/>
|
||||
|
||||
<style>
|
||||
/* Guest banner styling */
|
||||
.guest-banner {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser';
|
||||
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
|
||||
import { sessionTasksStore } from '$lib/stores/session-tasks.svelte';
|
||||
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('todo');
|
||||
|
|
@ -276,6 +277,9 @@
|
|||
let showAuthGateModal = $state(false);
|
||||
let authGateAction = $state<'save' | 'sync' | 'feature'>('save');
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// Show auth gate modal (can be called from child components)
|
||||
function showAuthGate(action: 'save' | 'sync' | 'feature' = 'save') {
|
||||
authGateAction = action;
|
||||
|
|
@ -285,6 +289,9 @@
|
|||
// Session tasks indicator
|
||||
let sessionTaskCount = $derived(sessionTasksStore.count);
|
||||
|
||||
// Language for GuestWelcomeModal
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize split-panel from URL/localStorage
|
||||
splitPanel.initialize();
|
||||
|
|
@ -295,6 +302,11 @@
|
|||
// Initialize session tasks for guest mode
|
||||
sessionTasksStore.initialize();
|
||||
|
||||
// Show guest welcome modal for unauthenticated users
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('todo')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
||||
// Load projects (works in both guest and authenticated mode)
|
||||
await projectsStore.fetchProjects();
|
||||
|
||||
|
|
@ -503,6 +515,23 @@
|
|||
action={authGateAction}
|
||||
/>
|
||||
|
||||
<!-- Guest Welcome Modal -->
|
||||
<GuestWelcomeModal
|
||||
appId="todo"
|
||||
visible={showGuestWelcome}
|
||||
onClose={() => (showGuestWelcome = false)}
|
||||
onLogin={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/login');
|
||||
}}
|
||||
onRegister={() => {
|
||||
showGuestWelcome = false;
|
||||
goto('/register');
|
||||
}}
|
||||
helpHref="/help"
|
||||
locale={currentLocale === 'en' ? 'en' : 'de'}
|
||||
/>
|
||||
|
||||
<style>
|
||||
/* Guest banner styling */
|
||||
.guest-banner {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@
|
|||
"peerDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*"
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.16.0",
|
||||
|
|
|
|||
652
packages/shared-auth-ui/src/components/GuestWelcomeModal.svelte
Normal file
652
packages/shared-auth-ui/src/components/GuestWelcomeModal.svelte
Normal file
|
|
@ -0,0 +1,652 @@
|
|||
<script lang="ts">
|
||||
import { getManaApp, type AppIconId } from '@manacore/shared-branding';
|
||||
import { X, Info, Warning, SignIn, UserPlus, Question, ArrowRight } from '@manacore/shared-icons';
|
||||
import { markGuestWelcomeSeen } from '../utils/guestWelcome';
|
||||
import type { GuestWelcomeTranslations } from '../types';
|
||||
|
||||
const defaultTranslationsDE: GuestWelcomeTranslations = {
|
||||
title: 'Willkommen',
|
||||
guestModeTitle: 'Als Gast kannst du:',
|
||||
whatYouCanDo: 'Was du als Gast tun kannst',
|
||||
dataWarningTitle: 'Hinweis',
|
||||
dataWarningText:
|
||||
'Daten werden nur in diesem Browser-Tab gespeichert und gehen beim Schließen verloren.',
|
||||
loginButton: 'Anmelden',
|
||||
registerButton: 'Kostenloses Konto erstellen',
|
||||
helpButton: 'Hilfe & Erste Schritte',
|
||||
continueAsGuest: 'Weiter als Gast',
|
||||
};
|
||||
|
||||
const defaultTranslationsEN: GuestWelcomeTranslations = {
|
||||
title: 'Welcome',
|
||||
guestModeTitle: 'As a guest you can:',
|
||||
whatYouCanDo: 'What you can do as a guest',
|
||||
dataWarningTitle: 'Note',
|
||||
dataWarningText: 'Data is only stored in this browser tab and will be lost when you close it.',
|
||||
loginButton: 'Sign In',
|
||||
registerButton: 'Create Free Account',
|
||||
helpButton: 'Help & Getting Started',
|
||||
continueAsGuest: 'Continue as Guest',
|
||||
};
|
||||
|
||||
/** Default features per app (German) */
|
||||
const defaultFeaturesDE: Record<string, string[]> = {
|
||||
contacts: [
|
||||
'Kontakte erstellen und bearbeiten',
|
||||
'Tags und Gruppen verwenden',
|
||||
'Alle Features ausprobieren',
|
||||
],
|
||||
chat: ['Mit der KI chatten', 'Verschiedene Modelle ausprobieren', 'Nachrichten formatieren'],
|
||||
todo: [
|
||||
'Aufgaben erstellen und organisieren',
|
||||
'Projekte und Labels nutzen',
|
||||
'Subtasks und Deadlines setzen',
|
||||
],
|
||||
calendar: [
|
||||
'Termine erstellen und bearbeiten',
|
||||
'Verschiedene Ansichten nutzen',
|
||||
'Erinnerungen setzen',
|
||||
],
|
||||
clock: ['Weltzeituhr anzeigen', 'Timer und Stoppuhr nutzen', 'Wecker einrichten'],
|
||||
zitare: ['Inspirierende Zitate entdecken', 'Favoriten markieren', 'Zitate teilen'],
|
||||
picture: [
|
||||
'Bilder mit KI generieren',
|
||||
'Verschiedene Stile ausprobieren',
|
||||
'Bilder herunterladen',
|
||||
],
|
||||
manadeck: ['Karteikarten erstellen', 'Decks organisieren', 'Lernmodus testen'],
|
||||
};
|
||||
|
||||
/** Default features per app (English) */
|
||||
const defaultFeaturesEN: Record<string, string[]> = {
|
||||
contacts: ['Create and edit contacts', 'Use tags and groups', 'Try all features'],
|
||||
chat: ['Chat with the AI', 'Try different models', 'Format messages'],
|
||||
todo: ['Create and organize tasks', 'Use projects and labels', 'Set subtasks and deadlines'],
|
||||
calendar: ['Create and edit events', 'Use different views', 'Set reminders'],
|
||||
clock: ['View world clocks', 'Use timer and stopwatch', 'Set alarms'],
|
||||
zitare: ['Discover inspiring quotes', 'Mark favorites', 'Share quotes'],
|
||||
picture: ['Generate images with AI', 'Try different styles', 'Download images'],
|
||||
manadeck: ['Create flashcards', 'Organize decks', 'Test learning mode'],
|
||||
};
|
||||
|
||||
interface Props {
|
||||
/** The app ID to show welcome for */
|
||||
appId: AppIconId;
|
||||
/** Whether the modal is visible */
|
||||
visible: boolean;
|
||||
/** Callback when modal is closed */
|
||||
onClose: () => void;
|
||||
/** Callback when login is clicked */
|
||||
onLogin: () => void;
|
||||
/** Callback when register is clicked */
|
||||
onRegister: () => void;
|
||||
/** Optional callback when help is clicked */
|
||||
onHelp?: () => void;
|
||||
/** Alternative: direct href for help link */
|
||||
helpHref?: string;
|
||||
/** Locale for translations (default: 'de') */
|
||||
locale?: 'de' | 'en';
|
||||
/** Custom feature list (overrides default) */
|
||||
features?: string[];
|
||||
/** Custom translations (partial) */
|
||||
translations?: Partial<GuestWelcomeTranslations>;
|
||||
}
|
||||
|
||||
let {
|
||||
appId,
|
||||
visible,
|
||||
onClose,
|
||||
onLogin,
|
||||
onRegister,
|
||||
onHelp,
|
||||
helpHref,
|
||||
locale = 'de',
|
||||
features,
|
||||
translations = {},
|
||||
}: Props = $props();
|
||||
|
||||
// Get app info from branding
|
||||
const appInfo = $derived(getManaApp(appId));
|
||||
|
||||
// Merge default translations with custom ones
|
||||
const defaultTranslations = $derived(
|
||||
locale === 'de' ? defaultTranslationsDE : defaultTranslationsEN
|
||||
);
|
||||
const t = $derived({ ...defaultTranslations, ...translations });
|
||||
|
||||
// Get features (custom > default by app > generic)
|
||||
const defaultFeatures = $derived(locale === 'de' ? defaultFeaturesDE : defaultFeaturesEN);
|
||||
const featureList = $derived(
|
||||
features ||
|
||||
t.features ||
|
||||
defaultFeatures[appId] || [
|
||||
locale === 'de' ? 'Alle Features ausprobieren' : 'Try all features',
|
||||
locale === 'de' ? 'Daten lokal speichern' : 'Store data locally',
|
||||
]
|
||||
);
|
||||
|
||||
// App description based on locale
|
||||
const appDescription = $derived(
|
||||
appInfo ? (locale === 'de' ? appInfo.longDescription.de : appInfo.longDescription.en) : ''
|
||||
);
|
||||
|
||||
function handleContinueAsGuest() {
|
||||
markGuestWelcomeSeen(appId);
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
markGuestWelcomeSeen(appId);
|
||||
onLogin();
|
||||
}
|
||||
|
||||
function handleRegister() {
|
||||
markGuestWelcomeSeen(appId);
|
||||
onRegister();
|
||||
}
|
||||
|
||||
function handleHelp() {
|
||||
if (onHelp) {
|
||||
onHelp();
|
||||
} else if (helpHref && typeof window !== 'undefined') {
|
||||
window.location.href = helpHref;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
handleContinueAsGuest();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && visible) {
|
||||
handleContinueAsGuest();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if visible && appInfo}
|
||||
<!-- Modal Backdrop -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleBackdropClick(e as unknown as MouseEvent)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="welcome-title"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="modal-content"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Close Button -->
|
||||
<button type="button" class="close-button" onclick={handleContinueAsGuest} aria-label="Close">
|
||||
<X size={20} weight="bold" />
|
||||
</button>
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="icon-container" style:--app-color={appInfo.color}>
|
||||
<img src={appInfo.icon} alt={appInfo.name} class="app-icon" />
|
||||
</div>
|
||||
|
||||
<!-- App Name & Description -->
|
||||
<h2 id="welcome-title" class="app-name">{appInfo.name}</h2>
|
||||
<p class="app-description">{appDescription}</p>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Guest Features -->
|
||||
<div class="features-section">
|
||||
<div class="section-header">
|
||||
<Info size={18} class="section-icon info-icon" />
|
||||
<span class="section-title">{t.guestModeTitle}</span>
|
||||
</div>
|
||||
<ul class="features-list">
|
||||
{#each featureList as feature}
|
||||
<li class="feature-item">
|
||||
<span class="feature-bullet">•</span>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Data Warning -->
|
||||
<div class="warning-section">
|
||||
<Warning size={18} class="section-icon warning-icon" />
|
||||
<p class="warning-text">{t.dataWarningText}</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="buttons-section">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
style:--btn-color={appInfo.color}
|
||||
onclick={handleLogin}
|
||||
>
|
||||
<SignIn size={20} />
|
||||
<span>{t.loginButton}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
style:--btn-color={appInfo.color}
|
||||
onclick={handleRegister}
|
||||
>
|
||||
<UserPlus size={20} />
|
||||
<span>{t.registerButton}</span>
|
||||
</button>
|
||||
|
||||
{#if onHelp || helpHref}
|
||||
<button type="button" class="btn btn-tertiary" onclick={handleHelp}>
|
||||
<Question size={18} />
|
||||
<span>{t.helpButton}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button type="button" class="btn btn-ghost" onclick={handleContinueAsGuest}>
|
||||
<span>{t.continueAsGuest}</span>
|
||||
<ArrowRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 1rem;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.04) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem 1.5rem;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Light mode support */
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-content {
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 255, 255, 0.95) 0%,
|
||||
rgba(255, 255, 255, 0.9) 100%
|
||||
);
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.05) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.close-button {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1rem;
|
||||
border-radius: 1.25rem;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, var(--app-color) 20%, transparent),
|
||||
color-mix(in srgb, var(--app-color) 10%, transparent)
|
||||
);
|
||||
border: 2px solid color-mix(in srgb, var(--app-color) 40%, transparent);
|
||||
box-shadow:
|
||||
0 8px 24px color-mix(in srgb, var(--app-color) 25%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, var(--app-color) 15%, transparent) inset;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.app-name {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.app-description {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.app-description {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 1rem 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.divider {
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.features-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.section-title {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.info-icon) {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
:global(.warning-icon) {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.features-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.feature-item {
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-bullet {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.feature-bullet {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-radius: 0.875rem;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.warning-text {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.buttons-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.875rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, var(--btn-color) 90%, white),
|
||||
var(--btn-color)
|
||||
);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, var(--btn-color) 35%, transparent);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px color-mix(in srgb, var(--btn-color) 45%, transparent);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: color-mix(in srgb, var(--btn-color) 15%, transparent);
|
||||
color: var(--btn-color);
|
||||
border: 1px solid color-mix(in srgb, var(--btn-color) 30%, transparent);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: color-mix(in srgb, var(--btn-color) 25%, transparent);
|
||||
}
|
||||
|
||||
.btn-tertiary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.btn-tertiary:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.btn-tertiary {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-tertiary:hover {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 0.625rem 1rem;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.btn-ghost {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal-backdrop,
|
||||
.modal-content {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.close-button {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.modal-content {
|
||||
padding: 1.5rem 1.25rem;
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,6 +6,7 @@ export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte
|
|||
// Components
|
||||
export { default as GoogleSignInButton } from './components/GoogleSignInButton.svelte';
|
||||
export { default as AppleSignInButton } from './components/AppleSignInButton.svelte';
|
||||
export { default as GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
|
|
@ -28,10 +29,17 @@ export {
|
|||
type AppleAuthorizationResponse,
|
||||
} from './utils/appleAuth';
|
||||
|
||||
// Types
|
||||
export type { AuthUIConfig, AuthServiceInterface, AuthResult } from './types';
|
||||
export {
|
||||
shouldShowGuestWelcome,
|
||||
markGuestWelcomeSeen,
|
||||
resetGuestWelcome,
|
||||
resetAllGuestWelcome,
|
||||
} from './utils/guestWelcome';
|
||||
|
||||
// Page Translation Types
|
||||
export type { LoginTranslations } from './pages/LoginPage.svelte';
|
||||
export type { RegisterTranslations } from './pages/RegisterPage.svelte';
|
||||
export type { ForgotPasswordTranslations } from './pages/ForgotPasswordPage.svelte';
|
||||
// Types
|
||||
export type {
|
||||
AuthUIConfig,
|
||||
AuthServiceInterface,
|
||||
AuthResult,
|
||||
GuestWelcomeTranslations,
|
||||
} from './types';
|
||||
|
|
|
|||
|
|
@ -60,3 +60,20 @@ export interface AuthResult {
|
|||
error?: string;
|
||||
needsVerification?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation strings for the guest welcome modal
|
||||
*/
|
||||
export interface GuestWelcomeTranslations {
|
||||
title: string;
|
||||
guestModeTitle: string;
|
||||
whatYouCanDo: string;
|
||||
dataWarningTitle: string;
|
||||
dataWarningText: string;
|
||||
loginButton: string;
|
||||
registerButton: string;
|
||||
helpButton: string;
|
||||
continueAsGuest: string;
|
||||
/** App-specific feature list (array of strings) */
|
||||
features?: string[];
|
||||
}
|
||||
|
|
|
|||
51
packages/shared-auth-ui/src/utils/guestWelcome.ts
Normal file
51
packages/shared-auth-ui/src/utils/guestWelcome.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Utility functions for managing guest welcome modal state
|
||||
*/
|
||||
|
||||
const STORAGE_PREFIX = 'guest-welcome-seen';
|
||||
|
||||
/**
|
||||
* Check if the guest welcome modal should be shown for an app
|
||||
* @param appId The app identifier (e.g., 'contacts', 'chat')
|
||||
* @returns true if the modal should be shown (not seen before)
|
||||
*/
|
||||
export function shouldShowGuestWelcome(appId: string): boolean {
|
||||
if (typeof localStorage === 'undefined') return false;
|
||||
const key = `${STORAGE_PREFIX}-${appId}`;
|
||||
return localStorage.getItem(key) !== 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the guest welcome modal as seen for an app
|
||||
* @param appId The app identifier
|
||||
*/
|
||||
export function markGuestWelcomeSeen(appId: string): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
const key = `${STORAGE_PREFIX}-${appId}`;
|
||||
localStorage.setItem(key, 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the guest welcome modal state for an app (will show again)
|
||||
* @param appId The app identifier
|
||||
*/
|
||||
export function resetGuestWelcome(appId: string): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
const key = `${STORAGE_PREFIX}-${appId}`;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the guest welcome modal state for all apps
|
||||
*/
|
||||
export function resetAllGuestWelcome(): void {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key?.startsWith(STORAGE_PREFIX)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue