Convert the remaining dynamic and hardcoded inline styles to Tailwind utilities

Second styling pass over the components and pages: move conditional theme colors into className ternaries (bg-accent / bg-surface-hover etc.), turn reused CSSProperties constants into className constants, and express static hardcoded hex/rgba colors as Tailwind arbitrary values so the exact rendered colour is preserved. Truly dynamic styling (computed geometry, gradients, multi-part shadows, data-driven colours, the undefined --sidebar/--nav layout vars) stays inline as it cannot be expressed as a static class. Updated three component tests that asserted the old inline active-state styles to assert the equivalent utility class instead.

Verified: client typecheck 0, full client suite green, and a live light/dark render check in the dev server confirms the semantic theme tokens resolve correctly (the earlier 'transparent popups' were a stale dev server that pre-dated the tailwind.config token addition, not a code issue).
This commit is contained in:
Maurice
2026-05-31 19:19:35 +02:00
parent 404981505c
commit cf21c718fa
57 changed files with 799 additions and 912 deletions
+8 -8
View File
@@ -186,13 +186,13 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
<AddonRow addon={addon} onToggle={handleToggle} t={t} /> <AddonRow addon={addon} onToggle={handleToggle} t={t} />
{addon.id === 'packing' && addon.enabled && onToggleBagTracking && ( {addon.id === 'packing' && addon.enabled && onToggleBagTracking && (
<div className="flex items-center gap-4 px-6 py-3 border-b border-edge-secondary bg-surface-secondary" style={{ paddingLeft: 70 }}> <div className="flex items-center gap-4 px-6 py-3 border-b border-edge-secondary bg-surface-secondary" style={{ paddingLeft: 70 }}>
<Luggage size={14} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <Luggage size={14} className="text-content-faint" style={{ flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div className="text-sm font-medium text-content-secondary">{t('admin.bagTracking.title')}</div> <div className="text-sm font-medium text-content-secondary">{t('admin.bagTracking.title')}</div>
<div className="text-xs mt-0.5 text-content-faint">{t('admin.bagTracking.subtitle')}</div> <div className="text-xs mt-0.5 text-content-faint">{t('admin.bagTracking.subtitle')}</div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<span className="hidden sm:inline text-xs font-medium" style={{ color: bagTrackingEnabled ? 'var(--text-primary)' : 'var(--text-faint)' }}> <span className={`hidden sm:inline text-xs font-medium ${bagTrackingEnabled ? 'text-content' : 'text-content-faint'}`}>
{bagTrackingEnabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} {bagTrackingEnabled ? t('admin.addons.enabled') : t('admin.addons.disabled')}
</span> </span>
<button onClick={onToggleBagTracking} <button onClick={onToggleBagTracking}
@@ -212,13 +212,13 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
const Icon = feat.icon const Icon = feat.icon
return ( return (
<div key={feat.key} className="flex items-center gap-4" style={{ minHeight: 32 }}> <div key={feat.key} className="flex items-center gap-4" style={{ minHeight: 32 }}>
<Icon size={14} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <Icon size={14} className="text-content-faint" style={{ flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div className="text-sm font-medium text-content-secondary">{t(feat.titleKey)}</div> <div className="text-sm font-medium text-content-secondary">{t(feat.titleKey)}</div>
<div className="text-xs mt-0.5 text-content-faint">{t(feat.subtitleKey)}</div> <div className="text-xs mt-0.5 text-content-faint">{t(feat.subtitleKey)}</div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<span className="hidden sm:inline text-xs font-medium" style={{ color: enabled ? 'var(--text-primary)' : 'var(--text-faint)' }}> <span className={`hidden sm:inline text-xs font-medium ${enabled ? 'text-content' : 'text-content-faint'}`}>
{enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} {enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')}
</span> </span>
<button onClick={() => onToggleCollabFeature(feat.key)} <button onClick={() => onToggleCollabFeature(feat.key)}
@@ -259,13 +259,13 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
const ProviderIcon = PROVIDER_ICONS[provider.key] const ProviderIcon = PROVIDER_ICONS[provider.key]
return ( return (
<div key={provider.key} className="flex items-center gap-4" style={{ minHeight: 32 }}> <div key={provider.key} className="flex items-center gap-4" style={{ minHeight: 32 }}>
{ProviderIcon && <span style={{ color: 'var(--text-faint)' }}><ProviderIcon size={14} /></span>} {ProviderIcon && <span className="text-content-faint"><ProviderIcon size={14} /></span>}
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div className="text-sm font-medium text-content-secondary">{provider.label}</div> <div className="text-sm font-medium text-content-secondary">{provider.label}</div>
<div className="text-xs mt-0.5 text-content-faint">{provider.description}</div> <div className="text-xs mt-0.5 text-content-faint">{provider.description}</div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<span className="hidden sm:inline text-xs font-medium" style={{ color: provider.enabled ? 'var(--text-primary)' : 'var(--text-faint)' }}> <span className={`hidden sm:inline text-xs font-medium ${provider.enabled ? 'text-content' : 'text-content-faint'}`}>
{provider.enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} {provider.enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')}
</span> </span>
<button <button
@@ -347,7 +347,7 @@ function AddonRow({ addon, onToggle, t, nameOverride, descriptionOverride, statu
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-semibold text-content">{displayName}</span> <span className="text-sm font-semibold text-content">{displayName}</span>
{isComingSoon && ( {isComingSoon && (
<span className="text-[9px] font-semibold px-2 py-0.5 rounded-full text-content-faint" style={{ background: 'var(--bg-tertiary)' }}> <span className="text-[9px] font-semibold px-2 py-0.5 rounded-full text-content-faint bg-surface-tertiary">
Coming Soon Coming Soon
</span> </span>
)} )}
@@ -360,7 +360,7 @@ function AddonRow({ addon, onToggle, t, nameOverride, descriptionOverride, statu
{/* Toggle */} {/* Toggle */}
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<span className="hidden sm:inline text-xs font-medium" style={{ color: (enabledState && !isComingSoon) ? 'var(--text-primary)' : 'var(--text-faint)' }}> <span className={`hidden sm:inline text-xs font-medium ${(enabledState && !isComingSoon) ? 'text-content' : 'text-content-faint'}`}>
{isComingSoon ? t('admin.addons.disabled') : enabledState ? t('admin.addons.enabled') : t('admin.addons.disabled')} {isComingSoon ? t('admin.addons.disabled') : enabledState ? t('admin.addons.enabled') : t('admin.addons.disabled')}
</span> </span>
{!hideToggle && ( {!hideToggle && (
@@ -83,14 +83,14 @@ export default function AdminMcpTokensPanel() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.mcpTokens.title')}</h2> <h2 className="text-lg font-semibold text-content">{t('admin.mcpTokens.title')}</h2>
<p className="text-sm mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.subtitle')}</p> <p className="text-sm mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{t('admin.mcpTokens.subtitle')}</p>
</div> </div>
{/* OAuth Sessions */} {/* OAuth Sessions */}
<div> <div>
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{t('admin.oauthSessions.sectionTitle')}</h3> <h3 className="text-sm font-semibold mb-2 text-content-secondary">{t('admin.oauthSessions.sectionTitle')}</h3>
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}> <div className="rounded-xl border overflow-hidden border-edge bg-surface-card">
{sessionsLoading ? ( {sessionsLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} /> <Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
@@ -102,8 +102,8 @@ export default function AdminMcpTokensPanel() {
</div> </div>
) : ( ) : (
<> <>
<div className="grid grid-cols-[1fr_auto_auto_auto] gap-x-6 px-4 py-2.5 text-xs font-medium border-b" <div className="grid grid-cols-[1fr_auto_auto_auto] gap-x-6 px-4 py-2.5 text-xs font-medium border-b border-edge bg-surface-secondary"
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}> style={{ color: 'var(--text-tertiary)' }}>
<span>{t('admin.oauthSessions.clientName')}</span> <span>{t('admin.oauthSessions.clientName')}</span>
<span>{t('admin.oauthSessions.owner')}</span> <span>{t('admin.oauthSessions.owner')}</span>
<span className="text-right">{t('admin.oauthSessions.created')}</span> <span className="text-right">{t('admin.oauthSessions.created')}</span>
@@ -115,34 +115,31 @@ export default function AdminMcpTokensPanel() {
const hidden = session.scopes.length - SCOPES_PREVIEW const hidden = session.scopes.length - SCOPES_PREVIEW
return ( return (
<div key={session.id} <div key={session.id}
className="grid grid-cols-[1fr_auto_auto_auto] items-start gap-x-6 px-4 py-3" className={`grid grid-cols-[1fr_auto_auto_auto] items-start gap-x-6 px-4 py-3 ${i < sessions.length - 1 ? 'border-b border-edge' : ''}`}>
style={{ borderBottom: i < sessions.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{session.client_name}</p> <p className="text-sm font-medium truncate text-content">{session.client_name}</p>
<div className="flex flex-wrap gap-1 mt-1.5"> <div className="flex flex-wrap gap-1 mt-1.5">
{visible.map(scope => ( {visible.map(scope => (
<span key={scope} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono" <span key={scope} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono bg-surface-secondary border border-edge"
style={{ background: 'var(--bg-secondary)', color: 'var(--text-tertiary)', border: '1px solid var(--border-primary)' }}> style={{ color: 'var(--text-tertiary)' }}>
{scope} {scope}
</span> </span>
))} ))}
{!expanded && hidden > 0 && ( {!expanded && hidden > 0 && (
<button onClick={() => toggleScopes(session.id)} <button onClick={() => toggleScopes(session.id)}
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium transition-colors hover:opacity-80" className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium transition-colors hover:opacity-80 bg-surface-secondary text-content-secondary border border-edge">
style={{ background: 'var(--bg-secondary)', color: 'var(--text-secondary)', border: '1px solid var(--border-primary)' }}>
+{hidden} more +{hidden} more
</button> </button>
)} )}
{expanded && hidden > 0 && ( {expanded && hidden > 0 && (
<button onClick={() => toggleScopes(session.id)} <button onClick={() => toggleScopes(session.id)}
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium transition-colors hover:opacity-80" className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium transition-colors hover:opacity-80 bg-surface-secondary text-content-secondary border border-edge">
style={{ background: 'var(--bg-secondary)', color: 'var(--text-secondary)', border: '1px solid var(--border-primary)' }}>
show less show less
</button> </button>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-1.5 text-sm pt-0.5" style={{ color: 'var(--text-secondary)' }}> <div className="flex items-center gap-1.5 text-sm pt-0.5 text-content-secondary">
<User className="w-3.5 h-3.5 flex-shrink-0" /> <User className="w-3.5 h-3.5 flex-shrink-0" />
<span className="whitespace-nowrap">{session.username}</span> <span className="whitespace-nowrap">{session.username}</span>
</div> </div>
@@ -164,8 +161,8 @@ export default function AdminMcpTokensPanel() {
{/* MCP Tokens */} {/* MCP Tokens */}
<div> <div>
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>{t('admin.mcpTokens.sectionTitle')}</h3> <h3 className="text-sm font-semibold mb-2 text-content-secondary">{t('admin.mcpTokens.sectionTitle')}</h3>
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}> <div className="rounded-xl border overflow-hidden border-edge bg-surface-card">
{tokensLoading ? ( {tokensLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} /> <Loader2 className="w-5 h-5 animate-spin" style={{ color: 'var(--text-tertiary)' }} />
@@ -177,8 +174,8 @@ export default function AdminMcpTokensPanel() {
</div> </div>
) : ( ) : (
<> <>
<div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b" <div className="grid grid-cols-[1fr_auto_auto_auto_auto] gap-x-4 px-4 py-2.5 text-xs font-medium border-b border-edge bg-surface-secondary"
style={{ color: 'var(--text-tertiary)', borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)' }}> style={{ color: 'var(--text-tertiary)' }}>
<span>{t('admin.mcpTokens.tokenName')}</span> <span>{t('admin.mcpTokens.tokenName')}</span>
<span>{t('admin.mcpTokens.owner')}</span> <span>{t('admin.mcpTokens.owner')}</span>
<span className="text-right">{t('admin.mcpTokens.created')}</span> <span className="text-right">{t('admin.mcpTokens.created')}</span>
@@ -187,13 +184,12 @@ export default function AdminMcpTokensPanel() {
</div> </div>
{tokens.map((token, i) => ( {tokens.map((token, i) => (
<div key={token.id} <div key={token.id}
className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3" className={`grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3 ${i < tokens.length - 1 ? 'border-b border-edge' : ''}`}>
style={{ borderBottom: i < tokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{token.name}</p> <p className="text-sm font-medium truncate text-content">{token.name}</p>
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{token.token_prefix}...</p> <p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>{token.token_prefix}...</p>
</div> </div>
<div className="flex items-center gap-1.5 text-sm" style={{ color: 'var(--text-secondary)' }}> <div className="flex items-center gap-1.5 text-sm text-content-secondary">
<User className="w-3.5 h-3.5 flex-shrink-0" /> <User className="w-3.5 h-3.5 flex-shrink-0" />
<span className="whitespace-nowrap">{token.username}</span> <span className="whitespace-nowrap">{token.username}</span>
</div> </div>
@@ -217,14 +213,14 @@ export default function AdminMcpTokensPanel() {
{/* Revoke OAuth session modal */} {/* Revoke OAuth session modal */}
{revokeConfirmId !== null && ( {revokeConfirmId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget) setRevokeConfirmId(null) }}> onClick={e => { if (e.target === e.currentTarget) setRevokeConfirmId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}> <div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.oauthSessions.revokeTitle')}</h3> <h3 className="text-base font-semibold text-content">{t('admin.oauthSessions.revokeTitle')}</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('admin.oauthSessions.revokeMessage')}</p> <p className="text-sm text-content-secondary">{t('admin.oauthSessions.revokeMessage')}</p>
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<button onClick={() => setRevokeConfirmId(null)} <button onClick={() => setRevokeConfirmId(null)}
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}> className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button onClick={() => handleRevoke(revokeConfirmId)} <button onClick={() => handleRevoke(revokeConfirmId)}
@@ -238,14 +234,14 @@ export default function AdminMcpTokensPanel() {
{/* Delete MCP token modal */} {/* Delete MCP token modal */}
{deleteConfirmId !== null && ( {deleteConfirmId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget) setDeleteConfirmId(null) }}> onClick={e => { if (e.target === e.currentTarget) setDeleteConfirmId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4" style={{ background: 'var(--bg-card)' }}> <div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
<h3 className="text-base font-semibold" style={{ color: 'var(--text-primary)' }}>{t('admin.mcpTokens.deleteTitle')}</h3> <h3 className="text-base font-semibold text-content">{t('admin.mcpTokens.deleteTitle')}</h3>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{t('admin.mcpTokens.deleteMessage')}</p> <p className="text-sm text-content-secondary">{t('admin.mcpTokens.deleteMessage')}</p>
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<button onClick={() => setDeleteConfirmId(null)} <button onClick={() => setDeleteConfirmId(null)}
className="px-4 py-2 rounded-lg text-sm border" style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}> className="px-4 py-2 rounded-lg text-sm border border-edge text-content-secondary">
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button onClick={() => handleDelete(deleteConfirmId)} <button onClick={() => handleDelete(deleteConfirmId)}
+22 -24
View File
@@ -100,54 +100,53 @@ export default function AuditLogPanel({ serverTimezone }: AuditLogPanelProps): R
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<h2 className="font-semibold text-lg m-0 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> <h2 className="font-semibold text-lg m-0 flex items-center gap-2 text-content">
<ClipboardList size={20} /> <ClipboardList size={20} />
{t('admin.tabs.audit')} {t('admin.tabs.audit')}
</h2> </h2>
<p className="text-sm m-0 mt-1" style={{ color: 'var(--text-muted)' }}>{t('admin.audit.subtitle')}</p> <p className="text-sm m-0 mt-1 text-content-muted">{t('admin.audit.subtitle')}</p>
</div> </div>
<button <button
type="button" type="button"
disabled={loading} disabled={loading}
onClick={() => loadFirstPage()} onClick={() => loadFirstPage()}
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium border transition-opacity disabled:opacity-50" className="inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium border transition-opacity disabled:opacity-50 border-edge text-content bg-surface-card"
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-primary)', background: 'var(--bg-card)' }}
> >
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} /> <RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
{t('admin.audit.refresh')} {t('admin.audit.refresh')}
</button> </button>
</div> </div>
<p className="text-xs m-0" style={{ color: 'var(--text-faint)' }}> <p className="text-xs m-0 text-content-faint">
{t('admin.audit.showing', { count: entries.length, total })} {t('admin.audit.showing', { count: entries.length, total })}
</p> </p>
{loading && entries.length === 0 ? ( {loading && entries.length === 0 ? (
<div className="py-12 text-center text-sm" style={{ color: 'var(--text-muted)' }}>{t('common.loading')}</div> <div className="py-12 text-center text-sm text-content-muted">{t('common.loading')}</div>
) : entries.length === 0 ? ( ) : entries.length === 0 ? (
<div className="py-12 text-center text-sm" style={{ color: 'var(--text-muted)' }}>{t('admin.audit.empty')}</div> <div className="py-12 text-center text-sm text-content-muted">{t('admin.audit.empty')}</div>
) : ( ) : (
<div className="rounded-xl border overflow-x-auto" style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}> <div className="rounded-xl border overflow-x-auto border-edge bg-surface-card">
<table className="w-full text-sm border-collapse min-w-[720px]"> <table className="w-full text-sm border-collapse min-w-[720px]">
<thead> <thead>
<tr className="border-b text-left" style={{ borderColor: 'var(--border-secondary)' }}> <tr className="border-b text-left border-edge-secondary">
<th className="p-3 font-semibold whitespace-nowrap" style={{ color: 'var(--text-secondary)' }}>{t('admin.audit.col.time')}</th> <th className="p-3 font-semibold whitespace-nowrap text-content-secondary">{t('admin.audit.col.time')}</th>
<th className="p-3 font-semibold whitespace-nowrap" style={{ color: 'var(--text-secondary)' }}>{t('admin.audit.col.user')}</th> <th className="p-3 font-semibold whitespace-nowrap text-content-secondary">{t('admin.audit.col.user')}</th>
<th className="p-3 font-semibold whitespace-nowrap" style={{ color: 'var(--text-secondary)' }}>{t('admin.audit.col.action')}</th> <th className="p-3 font-semibold whitespace-nowrap text-content-secondary">{t('admin.audit.col.action')}</th>
<th className="p-3 font-semibold whitespace-nowrap" style={{ color: 'var(--text-secondary)' }}>{t('admin.audit.col.resource')}</th> <th className="p-3 font-semibold whitespace-nowrap text-content-secondary">{t('admin.audit.col.resource')}</th>
<th className="p-3 font-semibold whitespace-nowrap" style={{ color: 'var(--text-secondary)' }}>{t('admin.audit.col.ip')}</th> <th className="p-3 font-semibold whitespace-nowrap text-content-secondary">{t('admin.audit.col.ip')}</th>
<th className="p-3 font-semibold" style={{ color: 'var(--text-secondary)' }}>{t('admin.audit.col.details')}</th> <th className="p-3 font-semibold text-content-secondary">{t('admin.audit.col.details')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{entries.map((e) => ( {entries.map((e) => (
<tr key={e.id} className="border-b align-top" style={{ borderColor: 'var(--border-secondary)' }}> <tr key={e.id} className="border-b align-top border-edge-secondary">
<td className="p-3 whitespace-nowrap font-mono text-xs" style={{ color: 'var(--text-primary)' }}>{fmtTime(e.created_at)}</td> <td className="p-3 whitespace-nowrap font-mono text-xs text-content">{fmtTime(e.created_at)}</td>
<td className="p-3" style={{ color: 'var(--text-primary)' }}>{userLabel(e)}</td> <td className="p-3 text-content">{userLabel(e)}</td>
<td className="p-3 font-mono text-xs" style={{ color: 'var(--text-primary)' }}>{e.action}</td> <td className="p-3 font-mono text-xs text-content">{e.action}</td>
<td className="p-3 font-mono text-xs break-all max-w-[140px]" style={{ color: 'var(--text-muted)' }}>{e.resource || '—'}</td> <td className="p-3 font-mono text-xs break-all max-w-[140px] text-content-muted">{e.resource || '—'}</td>
<td className="p-3 font-mono text-xs whitespace-nowrap" style={{ color: 'var(--text-muted)' }}>{e.ip || '—'}</td> <td className="p-3 font-mono text-xs whitespace-nowrap text-content-muted">{e.ip || '—'}</td>
<td className="p-3 font-mono text-xs break-all max-w-[280px]" style={{ color: 'var(--text-faint)' }}>{fmtDetails(e.details)}</td> <td className="p-3 font-mono text-xs break-all max-w-[280px] text-content-faint">{fmtDetails(e.details)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -160,8 +159,7 @@ export default function AuditLogPanel({ serverTimezone }: AuditLogPanelProps): R
type="button" type="button"
disabled={loading} disabled={loading}
onClick={() => loadMore()} onClick={() => loadMore()}
className="text-sm font-medium underline-offset-2 hover:underline disabled:opacity-50" className="text-sm font-medium underline-offset-2 hover:underline disabled:opacity-50 text-content-secondary"
style={{ color: 'var(--text-secondary)' }}
> >
{t('admin.audit.loadMore')} {t('admin.audit.loadMore')}
</button> </button>
+12 -10
View File
@@ -186,8 +186,8 @@ export default function BackupPanel() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<HardDrive className="w-5 h-5 text-gray-400" /> <HardDrive className="w-5 h-5 text-gray-400" />
<div> <div>
<h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('backup.title')}</h2> <h2 className="font-semibold text-content">{t('backup.title')}</h2>
<p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{t('backup.subtitle')}</p> <p className="text-xs mt-1 text-content-muted">{t('backup.subtitle')}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -310,8 +310,8 @@ export default function BackupPanel() {
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<Clock className="w-5 h-5 text-gray-400" /> <Clock className="w-5 h-5 text-gray-400" />
<div> <div>
<h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('backup.auto.title')}</h2> <h2 className="font-semibold text-content">{t('backup.auto.title')}</h2>
<p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{t('backup.auto.subtitle')}</p> <p className="text-xs mt-1 text-content-muted">{t('backup.auto.subtitle')}</p>
</div> </div>
</div> </div>
@@ -458,7 +458,8 @@ export default function BackupPanel() {
{/* Restore Warning Modal */} {/* Restore Warning Modal */}
{restoreConfirm && ( {restoreConfirm && (
<div <div
style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }} className="bg-[rgba(0,0,0,0.5)]"
style={{ position: 'fixed', inset: 0, zIndex: 9999, backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
onClick={() => setRestoreConfirm(null)} onClick={() => setRestoreConfirm(null)}
> >
<div <div
@@ -468,14 +469,14 @@ export default function BackupPanel() {
> >
{/* Red header */} {/* Red header */}
<div style={{ background: 'linear-gradient(135deg, #dc2626, #b91c1c)', padding: '20px 24px', display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ background: 'linear-gradient(135deg, #dc2626, #b91c1c)', padding: '20px 24px', display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'rgba(255,255,255,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[rgba(255,255,255,0.2)]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<AlertTriangle size={20} style={{ color: 'white' }} /> <AlertTriangle size={20} className="text-white" />
</div> </div>
<div> <div>
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, color: 'white' }}> <h3 className="text-white" style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>
{t('backup.restoreConfirmTitle')} {t('backup.restoreConfirmTitle')}
</h3> </h3>
<p style={{ margin: '2px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }}> <p className="text-[rgba(255,255,255,0.8)]" style={{ margin: '2px 0 0', fontSize: 12 }}>
{restoreConfirm.filename} {restoreConfirm.filename}
</p> </p>
</div> </div>
@@ -505,7 +506,8 @@ export default function BackupPanel() {
</button> </button>
<button <button
onClick={executeRestore} onClick={executeRestore}
style={{ padding: '9px 20px', borderRadius: 10, fontSize: 13, fontWeight: 600, border: 'none', cursor: 'pointer', fontFamily: 'inherit', background: '#dc2626', color: 'white' }} className="bg-[#dc2626] text-white"
style={{ padding: '9px 20px', borderRadius: 10, fontSize: 13, fontWeight: 600, border: 'none', cursor: 'pointer', fontFamily: 'inherit' }}
onMouseEnter={e => e.currentTarget.style.background = '#b91c1c'} onMouseEnter={e => e.currentTarget.style.background = '#b91c1c'}
onMouseLeave={e => e.currentTarget.style.background = '#dc2626'} onMouseLeave={e => e.currentTarget.style.background = '#dc2626'}
> >
@@ -191,8 +191,8 @@ export default function CategoryManager() {
<div className="bg-white rounded-2xl border border-gray-200 p-6"> <div className="bg-white rounded-2xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <div>
<h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('categories.title')}</h2> <h2 className="font-semibold text-content">{t('categories.title')}</h2>
<p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{t('categories.subtitle')}</p> <p className="text-xs mt-1 text-content-muted">{t('categories.subtitle')}</p>
</div> </div>
<button onClick={handleStartCreate} <button onClick={handleStartCreate}
className="flex items-center gap-2 bg-slate-900 text-white px-3 sm:px-4 py-2 rounded-lg hover:bg-slate-700 text-sm font-medium"> className="flex items-center gap-2 bg-slate-900 text-white px-3 sm:px-4 py-2 rounded-lg hover:bg-slate-700 text-sm font-medium">
@@ -35,10 +35,10 @@ function OptionRow({
}) { }) {
return ( return (
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}> <label className="block text-sm font-medium mb-2 text-content-secondary">
{label} {label}
</label> </label>
{hint && <p className="text-xs mb-2" style={{ color: 'var(--text-faint)' }}>{hint}</p>} {hint && <p className="text-xs mb-2 text-content-faint">{hint}</p>}
<div className="flex gap-3 flex-wrap">{children}</div> <div className="flex gap-3 flex-wrap">{children}</div>
</div> </div>
) )
@@ -113,8 +113,8 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
isSet(field) ? ( isSet(field) ? (
<button <button
onClick={() => reset(field)} onClick={() => reset(field)}
className="text-xs ml-2" className="text-xs ml-2 text-content-faint underline"
style={{ color: 'var(--text-faint)', textDecoration: 'underline', background: 'none', border: 'none', cursor: 'pointer' }} style={{ background: 'none', border: 'none', cursor: 'pointer' }}
> >
{t('admin.defaultSettings.resetToBuiltIn')} {t('admin.defaultSettings.resetToBuiltIn')}
</button> </button>
@@ -146,14 +146,14 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
}], []) }], [])
if (!loaded) { if (!loaded) {
return <p style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic', padding: 16 }}>Loading</p> return <p className="text-content-faint" style={{ fontSize: 12, fontStyle: 'italic', padding: 16 }}>Loading</p>
} }
const darkMode = defaults.dark_mode const darkMode = defaults.dark_mode
return ( return (
<Section title={t('admin.defaultSettings.title')} icon={Settings2}> <Section title={t('admin.defaultSettings.title')} icon={Settings2}>
<p className="text-sm" style={{ color: 'var(--text-faint)', marginTop: -8 }}> <p className="text-sm text-content-faint" style={{ marginTop: -8 }}>
{t('admin.defaultSettings.description')} {t('admin.defaultSettings.description')}
</p> </p>
@@ -224,7 +224,7 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
{/* Map Tile URL */} {/* Map Tile URL */}
<div> <div>
<label className="block text-sm font-medium mb-1.5" style={{ color: 'var(--text-secondary)' }}> <label className="block text-sm font-medium mb-1.5 text-content-secondary">
{t('settings.mapTemplate')} {t('settings.mapTemplate')}
<ResetButton field="map_tile_url" /> <ResetButton field="map_tile_url" />
</label> </label>
@@ -244,7 +244,7 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/> />
<p className="text-xs mt-1" style={{ color: 'var(--text-faint)' }}>{t('settings.mapDefaultHint')}</p> <p className="text-xs mt-1 text-content-faint">{t('settings.mapDefaultHint')}</p>
<div style={{ position: 'relative', height: '200px', width: '100%', marginTop: 12 }}> <div style={{ position: 'relative', height: '200px', width: '100%', marginTop: 12 }}>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{React.createElement(MapView as any, { {React.createElement(MapView as any, {
@@ -68,8 +68,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
<button <button
onClick={onClick} onClick={onClick}
disabled={sending !== null} disabled={sending !== null}
className="flex items-center gap-3 px-4 py-3 rounded-lg border transition-colors text-left w-full" className="flex items-center gap-3 px-4 py-3 rounded-lg border transition-colors text-left w-full border-edge bg-surface-card"
style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)' }}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)' }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)' }}
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)' }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)' }}
> >
@@ -78,8 +77,8 @@ export default function DevNotificationsPanel(): React.ReactElement {
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{label}</p> <p className="text-sm font-medium text-content">{label}</p>
<p className="text-xs truncate" style={{ color: 'var(--text-faint)' }}>{sub}</p> <p className="text-xs truncate text-content-faint">{sub}</p>
</div> </div>
{sending === id && ( {sending === id && (
<div className="w-4 h-4 border-2 border-slate-200 border-t-indigo-500 rounded-full animate-spin flex-shrink-0" /> <div className="w-4 h-4 border-2 border-slate-200 border-t-indigo-500 rounded-full animate-spin flex-shrink-0" />
@@ -88,15 +87,14 @@ export default function DevNotificationsPanel(): React.ReactElement {
) )
const SectionTitle = ({ children }: { children: React.ReactNode }) => ( const SectionTitle = ({ children }: { children: React.ReactNode }) => (
<h3 className="text-sm font-semibold mb-3" style={{ color: 'var(--text-secondary)' }}>{children}</h3> <h3 className="text-sm font-semibold mb-3 text-content-secondary">{children}</h3>
) )
const TripSelector = () => ( const TripSelector = () => (
<select <select
value={selectedTripId ?? ''} value={selectedTripId ?? ''}
onChange={e => setSelectedTripId(Number(e.target.value))} onChange={e => setSelectedTripId(Number(e.target.value))}
className="w-full px-3 py-2 rounded-lg border text-sm mb-3" className="w-full px-3 py-2 rounded-lg border text-sm mb-3 border-edge bg-surface-card text-content"
style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)', color: 'var(--text-primary)' }}
> >
{trips.map(trip => <option key={trip.id} value={trip.id}>{trip.title}</option>)} {trips.map(trip => <option key={trip.id} value={trip.id}>{trip.title}</option>)}
</select> </select>
@@ -106,8 +104,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
<select <select
value={selectedUserId ?? ''} value={selectedUserId ?? ''}
onChange={e => setSelectedUserId(Number(e.target.value))} onChange={e => setSelectedUserId(Number(e.target.value))}
className="w-full px-3 py-2 rounded-lg border text-sm mb-3" className="w-full px-3 py-2 rounded-lg border text-sm mb-3 border-edge bg-surface-card text-content"
style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-card)', color: 'var(--text-primary)' }}
> >
{users.map(u => <option key={u.id} value={u.id}>{u.username} ({u.email})</option>)} {users.map(u => <option key={u.id} value={u.id}>{u.username} ({u.email})</option>)}
</select> </select>
@@ -116,10 +113,10 @@ export default function DevNotificationsPanel(): React.ReactElement {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="px-2 py-0.5 rounded text-xs font-mono font-bold" style={{ background: '#fbbf24', color: '#000' }}> <div className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-[#fbbf24] text-[#000]">
DEV ONLY DEV ONLY
</div> </div>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <span className="text-sm font-medium text-content">
Notification Testing Notification Testing
</span> </span>
</div> </div>
@@ -127,7 +124,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
{/* ── Type Testing ─────────────────────────────────────────────────── */} {/* ── Type Testing ─────────────────────────────────────────────────── */}
<div> <div>
<SectionTitle>Type Testing</SectionTitle> <SectionTitle>Type Testing</SectionTitle>
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)' }}> <p className="text-xs mb-3 text-content-muted">
Test how each in-app notification type renders, sent to yourself. Test how each in-app notification type renders, sent to yourself.
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
@@ -175,7 +172,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
{trips.length > 0 && ( {trips.length > 0 && (
<div> <div>
<SectionTitle>Trip-Scoped Events</SectionTitle> <SectionTitle>Trip-Scoped Events</SectionTitle>
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)' }}> <p className="text-xs mb-3 text-content-muted">
Fires each trip event to all members of the selected trip (excluding yourself). Fires each trip event to all members of the selected trip (excluding yourself).
</p> </p>
<TripSelector /> <TripSelector />
@@ -228,7 +225,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
{users.length > 0 && ( {users.length > 0 && (
<div> <div>
<SectionTitle>User-Scoped Events</SectionTitle> <SectionTitle>User-Scoped Events</SectionTitle>
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)' }}> <p className="text-xs mb-3 text-content-muted">
Fires each user event to the selected recipient. Fires each user event to the selected recipient.
</p> </p>
<UserSelector /> <UserSelector />
@@ -266,7 +263,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
{/* ── Admin-Scoped Events ──────────────────────────────────────────── */} {/* ── Admin-Scoped Events ──────────────────────────────────────────── */}
<div> <div>
<SectionTitle>Admin-Scoped Events</SectionTitle> <SectionTitle>Admin-Scoped Events</SectionTitle>
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)' }}> <p className="text-xs mb-3 text-content-muted">
Fires to all admin users. Fires to all admin users.
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
+19 -27
View File
@@ -136,13 +136,12 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
href="https://ko-fi.com/mauriceboe" href="https://ko-fi.com/mauriceboe"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ff5e5b'; e.currentTarget.style.boxShadow = '0 0 0 1px #ff5e5b22' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ff5e5b'; e.currentTarget.style.boxShadow = '0 0 0 1px #ff5e5b22' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#ff5e5b15', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#ff5e5b15]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Coffee size={20} style={{ color: '#ff5e5b' }} /> <Coffee size={20} className="text-[#ff5e5b]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">Ko-fi</div> <div className="text-sm font-semibold text-content">Ko-fi</div>
@@ -154,13 +153,12 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
href="https://buymeacoffee.com/mauriceboe" href="https://buymeacoffee.com/mauriceboe"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ffdd00'; e.currentTarget.style.boxShadow = '0 0 0 1px #ffdd0022' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ffdd00'; e.currentTarget.style.boxShadow = '0 0 0 1px #ffdd0022' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#ffdd0015', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#ffdd0015]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Heart size={20} style={{ color: '#ffdd00' }} /> <Heart size={20} className="text-[#ffdd00]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">Buy Me a Coffee</div> <div className="text-sm font-semibold text-content">Buy Me a Coffee</div>
@@ -172,12 +170,11 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
href="https://discord.gg/NhZBDSd4qW" href="https://discord.gg/NhZBDSd4qW"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#5865F215', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#5865F215]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</div> </div>
<div> <div>
@@ -193,13 +190,12 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
href="https://github.com/mauriceboe/TREK/issues/new?template=bug_report.yml" href="https://github.com/mauriceboe/TREK/issues/new?template=bug_report.yml"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.boxShadow = '0 0 0 1px #ef444422' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.boxShadow = '0 0 0 1px #ef444422' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#ef444415', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#ef444415]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Bug size={20} style={{ color: '#ef4444' }} /> <Bug size={20} className="text-[#ef4444]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">{t('settings.about.reportBug')}</div> <div className="text-sm font-semibold text-content">{t('settings.about.reportBug')}</div>
@@ -211,13 +207,12 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
href="https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests" href="https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#f59e0b'; e.currentTarget.style.boxShadow = '0 0 0 1px #f59e0b22' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#f59e0b'; e.currentTarget.style.boxShadow = '0 0 0 1px #f59e0b22' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#f59e0b15', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#f59e0b15]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Lightbulb size={20} style={{ color: '#f59e0b' }} /> <Lightbulb size={20} className="text-[#f59e0b]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">{t('settings.about.featureRequest')}</div> <div className="text-sm font-semibold text-content">{t('settings.about.featureRequest')}</div>
@@ -229,13 +224,12 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
href="https://github.com/mauriceboe/TREK/wiki" href="https://github.com/mauriceboe/TREK/wiki"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#6366f1'; e.currentTarget.style.boxShadow = '0 0 0 1px #6366f122' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#6366f1'; e.currentTarget.style.boxShadow = '0 0 0 1px #6366f122' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#6366f115', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#6366f115]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<BookOpen size={20} style={{ color: '#6366f1' }} /> <BookOpen size={20} className="text-[#6366f1]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">Wiki</div> <div className="text-sm font-semibold text-content">Wiki</div>
@@ -308,14 +302,12 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
{release.tag_name} {release.tag_name}
</span> </span>
{isLatest && ( {isLatest && (
<span className="text-[10px] font-semibold px-2 py-0.5 rounded-full" <span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-[rgba(34,197,94,0.12)] text-[#16a34a]">
style={{ background: 'rgba(34,197,94,0.12)', color: '#16a34a' }}>
{t('admin.github.latest')} {t('admin.github.latest')}
</span> </span>
)} )}
{release.prerelease && ( {release.prerelease && (
<span className="text-[10px] font-semibold px-2 py-0.5 rounded-full" <span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-[rgba(245,158,11,0.12)] text-[#d97706]">
style={{ background: 'rgba(245,158,11,0.12)', color: '#d97706' }}>
{t('admin.github.prerelease')} {t('admin.github.prerelease')}
</span> </span>
)} )}
+1 -1
View File
@@ -125,7 +125,7 @@ function FilePreviewPortal({ file, onClose }: FilePreviewPortalProps) {
<div style={{ position: 'relative', maxWidth: '90vw', maxHeight: '90vh' }} onClick={e => e.stopPropagation()}> <div style={{ position: 'relative', maxWidth: '90vw', maxHeight: '90vh' }} onClick={e => e.stopPropagation()}>
{authUrl {authUrl
? <img src={authUrl} alt={file.original_name} style={{ maxWidth: '90vw', maxHeight: '90vh', objectFit: 'contain', borderRadius: 8, display: 'block' }} /> ? <img src={authUrl} alt={file.original_name} style={{ maxWidth: '90vw', maxHeight: '90vh', objectFit: 'contain', borderRadius: 8, display: 'block' }} />
: <Loader2 size={32} className="animate-spin" style={{ color: 'rgba(255,255,255,0.5)' }} /> : <Loader2 size={32} className="animate-spin text-[rgba(255,255,255,0.5)]" />
} }
<div style={{ position: 'absolute', top: -36, left: 0, right: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 4px' }}> <div style={{ position: 'absolute', top: -36, left: 0, right: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 4px' }}>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.7)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '70%' }}>{file.original_name}</span> <span style={{ fontSize: 11, color: 'rgba(255,255,255,0.7)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '70%' }}>{file.original_name}</span>
+11 -15
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, type CSSProperties } from 'react' import { useState, useEffect, useMemo } from 'react'
import { useAuthStore } from '../../store/authStore' import { useAuthStore } from '../../store/authStore'
import { useTranslation } from '../../i18n' import { useTranslation } from '../../i18n'
import { MessageCircle, StickyNote, BarChart3, Sparkles } from 'lucide-react' import { MessageCircle, StickyNote, BarChart3, Sparkles } from 'lucide-react'
@@ -17,11 +17,7 @@ function useIsDesktop(breakpoint = 1024) {
return isDesktop return isDesktop
} }
const card: CSSProperties = { const cardClass = 'flex flex-col bg-surface-card rounded-2xl border border-edge-faint overflow-hidden min-h-0'
display: 'flex', flexDirection: 'column',
background: 'var(--bg-card)', borderRadius: 16, border: '1px solid var(--border-faint)',
overflow: 'hidden', minHeight: 0,
}
interface TripMember { interface TripMember {
id: number id: number
@@ -88,7 +84,7 @@ export default function CollabPanel({ tripId, tripMembers = [], collabFeatures }
// Only chat // Only chat
return ( return (
<div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}> <div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}>
<div style={{ ...card, flex: 1 }}> <div className={cardClass} style={{ flex: 1 }}>
<CollabChat tripId={tripId} currentUser={user} /> <CollabChat tripId={tripId} currentUser={user} />
</div> </div>
</div> </div>
@@ -99,19 +95,19 @@ export default function CollabPanel({ tripId, tripMembers = [], collabFeatures }
// Chat left (380px) + right panels // Chat left (380px) + right panels
return ( return (
<div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}> <div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}>
<div style={{ ...card, flex: '0 0 380px' }}> <div className={cardClass} style={{ flex: '0 0 380px' }}>
<CollabChat tripId={tripId} currentUser={user} /> <CollabChat tripId={tripId} currentUser={user} />
</div> </div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, overflow: 'hidden', minHeight: 0 }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, overflow: 'hidden', minHeight: 0 }}>
{rightPanels.length === 1 && ( {rightPanels.length === 1 && (
<div style={{ ...card, flex: 1 }}> <div className={cardClass} style={{ flex: 1 }}>
{rightPanels[0] === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />} {rightPanels[0] === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />}
{rightPanels[0] === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />} {rightPanels[0] === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />}
{rightPanels[0] === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />} {rightPanels[0] === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />}
</div> </div>
)} )}
{rightPanels.length === 2 && rightPanels.map(p => ( {rightPanels.length === 2 && rightPanels.map(p => (
<div key={p} style={{ ...card, flex: 1 }}> <div key={p} className={cardClass} style={{ flex: 1 }}>
{p === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />} {p === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />}
{p === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />} {p === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />}
{p === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />} {p === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />}
@@ -119,14 +115,14 @@ export default function CollabPanel({ tripId, tripMembers = [], collabFeatures }
))} ))}
{rightPanels.length === 3 && ( {rightPanels.length === 3 && (
<> <>
<div style={{ ...card, flex: 1 }}> <div className={cardClass} style={{ flex: 1 }}>
<CollabNotes tripId={tripId} currentUser={user} /> <CollabNotes tripId={tripId} currentUser={user} />
</div> </div>
<div style={{ flex: 1, display: 'flex', gap: 12, overflow: 'hidden', minHeight: 0 }}> <div style={{ flex: 1, display: 'flex', gap: 12, overflow: 'hidden', minHeight: 0 }}>
<div style={{ ...card, flex: 1 }}> <div className={cardClass} style={{ flex: 1 }}>
<CollabPolls tripId={tripId} currentUser={user} /> <CollabPolls tripId={tripId} currentUser={user} />
</div> </div>
<div style={{ ...card, flex: 1 }}> <div className={cardClass} style={{ flex: 1 }}>
<WhatsNextWidget tripMembers={tripMembers} /> <WhatsNextWidget tripMembers={tripMembers} />
</div> </div>
</div> </div>
@@ -142,7 +138,7 @@ export default function CollabPanel({ tripId, tripMembers = [], collabFeatures }
if (panels.length === 1) { if (panels.length === 1) {
return ( return (
<div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}> <div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}>
<div style={{ ...card, flex: 1 }}> <div className={cardClass} style={{ flex: 1 }}>
{panels[0] === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />} {panels[0] === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />}
{panels[0] === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />} {panels[0] === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />}
{panels[0] === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />} {panels[0] === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />}
@@ -154,7 +150,7 @@ export default function CollabPanel({ tripId, tripMembers = [], collabFeatures }
return ( return (
<div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}> <div style={{ height: '100%', display: 'flex', gap: 12, padding: 12, overflow: 'hidden', minHeight: 0 }}>
{panels.map(p => ( {panels.map(p => (
<div key={p} style={{ ...card, flex: 1 }}> <div key={p} className={cardClass} style={{ flex: 1 }}>
{p === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />} {p === 'notes' && <CollabNotes tripId={tripId} currentUser={user} />}
{p === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />} {p === 'polls' && <CollabPolls tripId={tripId} currentUser={user} />}
{p === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />} {p === 'whatsnext' && <WhatsNextWidget tripMembers={tripMembers} />}
@@ -44,7 +44,7 @@ export default function ContributorInviteDialog({ journeyId, existingUserIds, on
} }
return ( return (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-5" style={{ background: 'rgba(9,9,11,0.75)' }}> <div className="fixed inset-0 z-[200] flex items-center justify-center p-5 bg-[rgba(9,9,11,0.75)]">
<div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[420px] w-full flex flex-col overflow-hidden"> <div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[420px] w-full flex flex-col overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700"> <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
@@ -91,8 +91,7 @@ export default function InAppNotificationBell(): React.ReactElement {
<span className="text-sm font-semibold text-content"> <span className="text-sm font-semibold text-content">
{t('notifications.title')} {t('notifications.title')}
{unreadCount > 0 && ( {unreadCount > 0 && (
<span className="ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium" <span className="ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-content text-surface">
style={{ background: 'var(--text-primary)', color: 'var(--bg-primary)' }}>
{unreadCount} {unreadCount}
</span> </span>
)} )}
@@ -127,7 +126,7 @@ export default function InAppNotificationBell(): React.ReactElement {
<div className="overflow-y-auto flex-1"> <div className="overflow-y-auto flex-1">
{isLoading && notifications.length === 0 ? ( {isLoading && notifications.length === 0 ? (
<div className="flex items-center justify-center py-10"> <div className="flex items-center justify-center py-10">
<div className="w-5 h-5 border-2 rounded-full animate-spin" style={{ borderColor: 'var(--border-primary)', borderTopColor: 'var(--text-primary)' }} /> <div className="w-5 h-5 border-2 rounded-full animate-spin border-edge border-t-content" />
</div> </div>
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 px-4 text-center gap-2"> <div className="flex flex-col items-center justify-center py-10 px-4 text-center gap-2">
+3 -5
View File
@@ -175,8 +175,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
{/* Share button */} {/* Share button */}
{onShare && ( {onShare && (
<button onClick={onShare} <button onClick={onShare}
className="flex items-center gap-1.5 py-1.5 px-3 rounded-lg border transition-colors text-sm font-medium flex-shrink-0 border-edge text-content-secondary" className="flex items-center gap-1.5 py-1.5 px-3 rounded-lg border transition-colors text-sm font-medium flex-shrink-0 border-edge text-content-secondary bg-surface-card"
style={{ background: 'var(--bg-card)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-card)'}> onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-card)'}>
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
@@ -187,10 +186,9 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
{/* Prerelease badge */} {/* Prerelease badge */}
{isPrerelease && appVersion && ( {isPrerelease && appVersion && (
<span <span
className="hidden sm:flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-semibold flex-shrink-0" className="hidden sm:flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-semibold flex-shrink-0 bg-[rgba(245,158,11,0.15)] text-[#d97706] border border-[rgba(245,158,11,0.3)]"
style={{ background: 'rgba(245,158,11,0.15)', color: '#d97706', border: '1px solid rgba(245,158,11,0.3)' }}
> >
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: '#f59e0b' }} /> <span className="w-1.5 h-1.5 rounded-full flex-shrink-0 bg-[#f59e0b]" />
{appVersion} {appVersion}
</span> </span>
)} )}
+2 -5
View File
@@ -90,8 +90,7 @@ export default function PageSidebar({
{mobileOpen && ( {mobileOpen && (
<> <>
<div <div
className="lg:hidden fixed inset-0 z-40" className="lg:hidden fixed inset-0 z-40 bg-[rgba(0,0,0,0.35)]"
style={{ background: 'rgba(0,0,0,0.35)' }}
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
/> />
<aside <aside
@@ -168,11 +167,9 @@ function SidebarInner({
<button <button
key={tab.id} key={tab.id}
onClick={() => onTabChange(tab.id)} onClick={() => onTabChange(tab.id)}
className="flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors" className={`flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors ${active ? 'text-content font-semibold' : 'text-content-secondary font-medium'}`}
style={{ style={{
background: active ? 'var(--bg-hover)' : 'transparent', background: active ? 'var(--bg-hover)' : 'transparent',
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
fontWeight: active ? 600 : 500,
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!active) e.currentTarget.style.background = 'var(--bg-hover)' if (!active) e.currentTarget.style.background = 'var(--bg-hover)'
+1 -2
View File
@@ -515,8 +515,7 @@ export const MapView = memo(function MapView({
center={center} center={center}
zoom={zoom} zoom={zoom}
zoomControl={false} zoomControl={false}
className="w-full h-full" className="w-full h-full bg-[#e5e7eb]"
style={{ background: '#e5e7eb' }}
> >
<TileLayer <TileLayer
url={tileUrl} url={tileUrl}
@@ -62,7 +62,7 @@ export default function ScopeGroupPicker({ selected, onChange }: Props): React.R
/> />
</div> </div>
{open[group] && ( {open[group] && (
<div className="divide-y" style={{ borderColor: 'var(--border-primary)' }}> <div className="divide-y border-edge">
{groupScopes.map(({ scope, label, description }) => ( {groupScopes.map(({ scope, label, description }) => (
<label <label
key={scope} key={scope}
+11 -10
View File
@@ -105,8 +105,8 @@ export default function AirportSelect({ value, onChange, placeholder, style }: P
return ( return (
<div ref={wrapRef} style={{ position: 'relative', ...style }}> <div ref={wrapRef} style={{ position: 'relative', ...style }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 10, border: '1px solid var(--border-primary)' }}> <div className="bg-surface-tertiary" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px', borderRadius: 10, border: '1px solid var(--border-primary)' }}>
<Plane size={14} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <Plane size={14} className="text-content-faint" style={{ flexShrink: 0 }} />
<input <input
type="text" type="text"
value={query} value={query}
@@ -114,19 +114,20 @@ export default function AirportSelect({ value, onChange, placeholder, style }: P
onChange={(e) => { setQuery(e.target.value); setOpen(true); if (value) onChange(null) }} onChange={(e) => { setQuery(e.target.value); setOpen(true); if (value) onChange(null) }}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
onKeyDown={onKey} onKeyDown={onKey}
style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', color: 'var(--text-primary)', fontSize: 13 }} className="bg-transparent text-content"
style={{ flex: 1, minWidth: 0, border: 'none', outline: 'none', fontSize: 13 }}
/> />
{value && ( {value && (
<button type="button" onClick={clear} style={{ background: 'transparent', border: 'none', padding: 2, cursor: 'pointer', color: 'var(--text-faint)', display: 'flex' }} aria-label="Clear"> <button type="button" onClick={clear} className="bg-transparent text-content-faint" style={{ border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }} aria-label="Clear">
<X size={14} /> <X size={14} />
</button> </button>
)} )}
</div> </div>
{open && (loading || results.length > 0) && ( {open && (loading || results.length > 0) && (
<div style={{ position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,0.18)', maxHeight: 260, overflowY: 'auto', zIndex: 1000 }}> <div className="bg-surface-card" style={{ position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, border: '1px solid var(--border-primary)', borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,0.18)', maxHeight: 260, overflowY: 'auto', zIndex: 1000 }}>
{loading && results.length === 0 && ( {loading && results.length === 0 && (
<div style={{ padding: 10, fontSize: 12, color: 'var(--text-faint)' }}>{t('common.loading')}</div> <div className="text-content-faint" style={{ padding: 10, fontSize: 12 }}>{t('common.loading')}</div>
)} )}
{results.map((a, i) => ( {results.map((a, i) => (
<button <button
@@ -134,17 +135,17 @@ export default function AirportSelect({ value, onChange, placeholder, style }: P
type="button" type="button"
onClick={() => pick(a)} onClick={() => pick(a)}
onMouseEnter={() => setHighlight(i)} onMouseEnter={() => setHighlight(i)}
className={`text-content ${i === highlight ? 'bg-surface-hover' : 'bg-transparent'}`}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%', display: 'flex', alignItems: 'center', gap: 8, width: '100%',
padding: '8px 12px', border: 'none', cursor: 'pointer', textAlign: 'left', padding: '8px 12px', border: 'none', cursor: 'pointer', textAlign: 'left',
background: i === highlight ? 'var(--bg-hover)' : 'transparent', fontFamily: 'inherit',
color: 'var(--text-primary)', fontFamily: 'inherit',
}} }}
> >
<span style={{ fontFamily: 'ui-monospace, SFMono-Regular, monospace', fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', minWidth: 32 }}>{a.iata}</span> <span className="text-content-muted" style={{ fontFamily: 'ui-monospace, SFMono-Regular, monospace', fontSize: 11, fontWeight: 700, minWidth: 32 }}>{a.iata}</span>
<span style={{ flex: 1, minWidth: 0 }}> <span style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{a.city || a.name}</div> <div style={{ fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{a.city || a.name}</div>
<div style={{ fontSize: 11, color: 'var(--text-faint)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{a.name}{a.country ? ` · ${displayCountry(a.country)}` : ''}</div> <div className="text-content-faint" style={{ fontSize: 11, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{a.name}{a.country ? ` · ${displayCountry(a.country)}` : ''}</div>
</span> </span>
</button> </button>
))} ))}
@@ -98,8 +98,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
return ( return (
<div className="fixed z-50" style={{ bottom: 'calc(var(--bottom-nav-h) + 20px)', left: `calc(${leftWidth}px + (100vw - ${leftWidth}px - ${rightWidth}px) / 2)`, transform: 'translateX(-50%)', width: `min(800px, calc(100vw - ${leftWidth}px - ${rightWidth}px - 32px))`, ...(mobile ? { zIndex: 10000 } : null), ...font }}> <div className="fixed z-50" style={{ bottom: 'calc(var(--bottom-nav-h) + 20px)', left: `calc(${leftWidth}px + (100vw - ${leftWidth}px - ${rightWidth}px) / 2)`, transform: 'translateX(-50%)', width: `min(800px, calc(100vw - ${leftWidth}px - ${rightWidth}px - 32px))`, ...(mobile ? { zIndex: 10000 } : null), ...font }}>
<div style={{ <div className="bg-surface-elevated" style={{
background: 'var(--bg-elevated)',
backdropFilter: 'blur(40px) saturate(180%)', backdropFilter: 'blur(40px) saturate(180%)',
WebkitBackdropFilter: 'blur(40px) saturate(180%)', WebkitBackdropFilter: 'blur(40px) saturate(180%)',
borderRadius: 20, borderRadius: 20,
@@ -109,26 +108,27 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
{/* Header */} {/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: collapsed ? '12px 16px 12px 20px' : '18px 16px 14px 20px', borderBottom: collapsed ? 'none' : '1px solid var(--border-faint)', cursor: 'pointer' }} <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: collapsed ? '12px 16px 12px 20px' : '18px 16px 14px 20px', borderBottom: collapsed ? 'none' : '1px solid var(--border-faint)', cursor: 'pointer' }}
onClick={() => toggleCollapse()}> onClick={() => toggleCollapse()}>
<div style={{ width: collapsed ? 36 : 44, height: collapsed ? 36 : 44, borderRadius: 12, background: 'var(--bg-secondary)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'all 0.15s ease' }}> <div className="bg-surface-secondary" style={{ width: collapsed ? 36 : 44, height: collapsed ? 36 : 44, borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'all 0.15s ease' }}>
<Calendar size={collapsed ? 16 : 20} style={{ color: 'var(--text-primary)' }} /> <Calendar size={collapsed ? 16 : 20} className="text-content" />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: collapsed ? 13 : 15, fontWeight: 700, color: 'var(--text-primary)', transition: 'font-size 0.15s ease' }}> <div className="text-content" style={{ fontSize: collapsed ? 13 : 15, fontWeight: 700, transition: 'font-size 0.15s ease' }}>
{day.title || t('planner.dayN', { n: (days.indexOf(day) + 1) || '?' })} {day.title || t('planner.dayN', { n: (days.indexOf(day) + 1) || '?' })}
{collapsed && formattedDate && <span style={{ fontWeight: 500, color: 'var(--text-muted)', marginLeft: 8 }}>{formattedDate}</span>} {collapsed && formattedDate && <span className="text-content-muted" style={{ fontWeight: 500, marginLeft: 8 }}>{formattedDate}</span>}
</div> </div>
{!collapsed && formattedDate && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 1 }}>{formattedDate}</div>} {!collapsed && formattedDate && <div className="text-content-muted" style={{ fontSize: 12, marginTop: 1 }}>{formattedDate}</div>}
</div> </div>
<button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? t('common.expand') : t('common.collapse')} <button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? t('common.expand') : t('common.collapse')}
style={{ background: 'var(--bg-secondary)', border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s ease' }} className="bg-surface-secondary"
style={{ border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s ease' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}> onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}>
{collapsed ? <ChevronsUp size={14} style={{ color: 'var(--text-muted)' }} /> : <ChevronsDown size={14} style={{ color: 'var(--text-muted)' }} />} {collapsed ? <ChevronsUp size={14} className="text-content-muted" /> : <ChevronsDown size={14} className="text-content-muted" />}
</button> </button>
<button onClick={(e) => { e.stopPropagation(); onClose() }} style={{ background: 'var(--bg-secondary)', border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0 }} <button onClick={(e) => { e.stopPropagation(); onClose() }} className="bg-surface-secondary" style={{ border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0 }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}> onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}>
<X size={14} style={{ color: 'var(--text-muted)' }} /> <X size={14} className="text-content-muted" />
</button> </button>
</div> </div>
@@ -1504,9 +1504,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
const isCheckOut = acc.end_day_id === day.id const isCheckOut = acc.end_day_id === day.id
const iconColor = isCheckOut && !isCheckIn ? '#ef4444' : isCheckIn ? '#22c55e' : 'var(--text-faint)' const iconColor = isCheckOut && !isCheckIn ? '#ef4444' : isCheckIn ? '#22c55e' : 'var(--text-faint)'
return ( return (
<span key={acc.id} onClick={e => { e.stopPropagation(); if ((acc as any).place_id) onPlaceClick((acc as any).place_id) }} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, flexShrink: 1, minWidth: 0, cursor: (acc as any).place_id ? 'pointer' : 'default', background: 'var(--bg-hover)', borderRadius: 7, padding: '2px 7px 2px 6px' }}> <span key={acc.id} onClick={e => { e.stopPropagation(); if ((acc as any).place_id) onPlaceClick((acc as any).place_id) }} className="bg-surface-hover" style={{ display: 'inline-flex', alignItems: 'center', gap: 4, flexShrink: 1, minWidth: 0, cursor: (acc as any).place_id ? 'pointer' : 'default', borderRadius: 7, padding: '2px 7px 2px 6px' }}>
<Hotel size={11} strokeWidth={1.8} style={{ color: iconColor, flexShrink: 0 }} /> <Hotel size={11} strokeWidth={1.8} style={{ color: iconColor, flexShrink: 0 }} />
<span style={{ fontSize: 10.5, color: 'var(--text-muted)', fontWeight: 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{(acc as any).place_name || (acc as any).reservation_title}</span> <span className="text-content-muted" style={{ fontSize: 10.5, fontWeight: 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{(acc as any).place_name || (acc as any).reservation_title}</span>
</span> </span>
) )
}) })
@@ -1516,9 +1516,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
const activeRentals = getActiveRentalsForDay(day.id) const activeRentals = getActiveRentalsForDay(day.id)
if (activeRentals.length === 0) return null if (activeRentals.length === 0) return null
return activeRentals.map(r => ( return activeRentals.map(r => (
<span key={`rental-${r.id}`} onClick={e => { e.stopPropagation(); setTransportDetail(r) }} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, flexShrink: 1, minWidth: 0, cursor: 'pointer', background: 'var(--bg-hover)', borderRadius: 7, padding: '2px 7px 2px 6px' }}> <span key={`rental-${r.id}`} onClick={e => { e.stopPropagation(); setTransportDetail(r) }} className="bg-surface-hover" style={{ display: 'inline-flex', alignItems: 'center', gap: 4, flexShrink: 1, minWidth: 0, cursor: 'pointer', borderRadius: 7, padding: '2px 7px 2px 6px' }}>
<Car size={11} strokeWidth={1.8} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <Car size={11} strokeWidth={1.8} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ fontSize: 10.5, color: 'var(--text-muted)', fontWeight: 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title}</span> <span className="text-content-muted" style={{ fontSize: 10.5, fontWeight: 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title}</span>
</span> </span>
)) ))
})()} })()}
@@ -1527,7 +1527,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
)} )}
{cost && ( {cost && (
<div style={{ marginTop: 2 }}> <div style={{ marginTop: 2 }}>
<span style={{ fontSize: 11, color: '#059669' }}>{cost}</span> <span className="text-[#059669]" style={{ fontSize: 11 }}>{cost}</span>
</div> </div>
)} )}
</div> </div>
@@ -1556,7 +1556,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
) )
})() })()
) : ( ) : (
<button onClick={e => toggleDay(day.id, e)} style={{ alignSelf: 'flex-start', flexShrink: 0, background: 'none', border: 'none', padding: 6, cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'var(--text-faint)' }}> <button onClick={e => toggleDay(day.id, e)} className="text-content-faint" style={{ alignSelf: 'flex-start', flexShrink: 0, background: 'none', border: 'none', padding: 6, cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
{isExpanded ? <ChevronDown size={16} strokeWidth={1.8} /> : <ChevronRight size={16} strokeWidth={1.8} />} {isExpanded ? <ChevronDown size={16} strokeWidth={1.8} /> : <ChevronRight size={16} strokeWidth={1.8} />}
</button> </button>
)} )}
@@ -1628,8 +1628,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
<div <div
onDragOver={e => { e.preventDefault(); if (dragOverDayId !== day.id) setDragOverDayId(day.id) }} onDragOver={e => { e.preventDefault(); if (dragOverDayId !== day.id) setDragOverDayId(day.id) }}
onDrop={e => handleDropOnDay(e, day.id)} onDrop={e => handleDropOnDay(e, day.id)}
className={dragOverDayId === day.id ? 'bg-[rgba(17,24,39,0.05)]' : 'bg-transparent'}
style={{ padding: '16px', textAlign: 'center', borderRadius: 8, style={{ padding: '16px', textAlign: 'center', borderRadius: 8,
background: dragOverDayId === day.id ? 'rgba(17,24,39,0.05)' : 'transparent',
border: dragOverDayId === day.id ? '2px dashed rgba(17,24,39,0.2)' : '2px dashed transparent', border: dragOverDayId === day.id ? '2px dashed rgba(17,24,39,0.2)' : '2px dashed transparent',
}} }}
> >
@@ -1845,9 +1845,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
const active = hasEndpoints ? visibleConnectionIds.includes(res.id) : false const active = hasEndpoints ? visibleConnectionIds.includes(res.id) : false
return ( return (
<div style={{ marginTop: 3, display: 'inline-flex', alignItems: 'center', gap: 4 }}> <div style={{ marginTop: 3, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 3, padding: '1px 6px', borderRadius: 5, fontSize: 9, fontWeight: 600, <div className={confirmed ? 'bg-[rgba(22,163,74,0.1)] text-[#16a34a]' : 'bg-[rgba(217,119,6,0.1)] text-[#d97706]'} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, padding: '1px 6px', borderRadius: 5, fontSize: 9, fontWeight: 600,
background: confirmed ? 'rgba(22,163,74,0.1)' : 'rgba(217,119,6,0.1)',
color: confirmed ? '#16a34a' : '#d97706',
}}> }}>
{(() => { const RI = RES_ICONS[res.type] || Ticket; return <RI size={8} /> })()} {(() => { const RI = RES_ICONS[res.type] || Ticket; return <RI size={8} /> })()}
<span className="hidden sm:inline">{confirmed ? t('planner.resConfirmed') : t('planner.resPending')}</span> <span className="hidden sm:inline">{confirmed ? t('planner.resConfirmed') : t('planner.resPending')}</span>
@@ -1876,13 +1874,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
type="button" type="button"
onClick={e => { e.stopPropagation(); onToggleConnection!(res.id) }} onClick={e => { e.stopPropagation(); onToggleConnection!(res.id) }}
title={t(active ? 'map.hideConnections' : 'map.showConnections')} title={t(active ? 'map.hideConnections' : 'map.showConnections')}
className={active ? 'bg-[#3b82f6] text-[#fff]' : 'bg-transparent text-content-faint'}
style={{ style={{
flexShrink: 0, appearance: 'none', flexShrink: 0, appearance: 'none',
width: 20, height: 20, borderRadius: 4, width: 20, height: 20, borderRadius: 4,
display: 'grid', placeItems: 'center', cursor: 'pointer', display: 'grid', placeItems: 'center', cursor: 'pointer',
border: 'none', border: 'none',
background: active ? '#3b82f6' : 'transparent',
color: active ? '#fff' : 'var(--text-faint)',
transition: 'color 120ms cubic-bezier(0.23,1,0.32,1), background 120ms cubic-bezier(0.23,1,0.32,1)', transition: 'color 120ms cubic-bezier(0.23,1,0.32,1), background 120ms cubic-bezier(0.23,1,0.32,1)',
}} }}
onMouseEnter={e => { if (!active) e.currentTarget.style.color = 'var(--text-primary)' }} onMouseEnter={e => { if (!active) e.currentTarget.style.color = 'var(--text-primary)' }}
@@ -1900,12 +1897,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
type="button" type="button"
onClick={e => { e.stopPropagation(); handler(res) }} onClick={e => { e.stopPropagation(); handler(res) }}
title={t('common.edit')} title={t('common.edit')}
className="bg-transparent text-content-faint"
style={{ style={{
flexShrink: 0, appearance: 'none', flexShrink: 0, appearance: 'none',
width: 20, height: 20, borderRadius: 4, width: 20, height: 20, borderRadius: 4,
display: 'grid', placeItems: 'center', cursor: 'pointer', display: 'grid', placeItems: 'center', cursor: 'pointer',
border: 'none', background: 'transparent', border: 'none',
color: 'var(--text-faint)',
transition: 'color 120ms cubic-bezier(0.23,1,0.32,1), background 120ms cubic-bezier(0.23,1,0.32,1)', transition: 'color 120ms cubic-bezier(0.23,1,0.32,1), background 120ms cubic-bezier(0.23,1,0.32,1)',
}} }}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--text-primary)' }} onMouseEnter={e => { e.currentTarget.style.color = 'var(--text-primary)' }}
@@ -1921,9 +1918,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{assignment.participants?.length > 0 && ( {assignment.participants?.length > 0 && (
<div style={{ marginTop: 3, display: 'flex', alignItems: 'center', gap: -4 }}> <div style={{ marginTop: 3, display: 'flex', alignItems: 'center', gap: -4 }}>
{assignment.participants.slice(0, 5).map((p, pi) => ( {assignment.participants.slice(0, 5).map((p, pi) => (
<div key={p.user_id} style={{ <div key={p.user_id} className="bg-surface-tertiary text-content-muted" style={{
width: 16, height: 16, borderRadius: '50%', background: 'var(--bg-tertiary)', border: '1.5px solid var(--bg-card)', width: 16, height: 16, borderRadius: '50%', border: '1.5px solid var(--bg-card)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 7, fontWeight: 700, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 7, fontWeight: 700,
marginLeft: pi > 0 ? -4 : 0, flexShrink: 0, marginLeft: pi > 0 ? -4 : 0, flexShrink: 0,
overflow: 'hidden', overflow: 'hidden',
}}> }}>
@@ -1931,16 +1928,16 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
</div> </div>
))} ))}
{assignment.participants.length > 5 && ( {assignment.participants.length > 5 && (
<span style={{ fontSize: 8, color: 'var(--text-faint)', marginLeft: 2 }}>+{assignment.participants.length - 5}</span> <span className="text-content-faint" style={{ fontSize: 8, marginLeft: 2 }}>+{assignment.participants.length - 5}</span>
)} )}
</div> </div>
)} )}
</div> </div>
{canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}> {canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}>
<button onClick={moveUp} disabled={idx === 0} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === 0 ? 'default' : 'pointer', color: idx === 0 ? 'var(--border-primary)' : 'var(--text-faint)', display: 'flex', lineHeight: 1 }}> <button onClick={moveUp} disabled={idx === 0} className={idx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}>
<ChevronUp size={12} strokeWidth={2} /> <ChevronUp size={12} strokeWidth={2} />
</button> </button>
<button onClick={moveDown} disabled={idx === merged.length - 1} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === merged.length - 1 ? 'default' : 'pointer', color: idx === merged.length - 1 ? 'var(--border-primary)' : 'var(--text-faint)', display: 'flex', lineHeight: 1 }}> <button onClick={moveDown} disabled={idx === merged.length - 1} className={idx === merged.length - 1 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === merged.length - 1 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}>
<ChevronDown size={12} strokeWidth={2} /> <ChevronDown size={12} strokeWidth={2} />
</button> </button>
</div>} </div>}
@@ -2233,12 +2230,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
)} )}
</div> </div>
{canEditDays && <div className="note-edit-buttons" style={{ display: 'flex', gap: 1, flexShrink: 0, opacity: 0, transition: 'opacity 0.15s' }}> {canEditDays && <div className="note-edit-buttons" style={{ display: 'flex', gap: 1, flexShrink: 0, opacity: 0, transition: 'opacity 0.15s' }}>
<button onClick={e => openEditNote(day.id, note, e)} style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', color: 'var(--text-faint)', display: 'flex' }}><Pencil size={10} /></button> <button onClick={e => openEditNote(day.id, note, e)} className="text-content-faint" style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }}><Pencil size={10} /></button>
<button onClick={e => deleteNote(day.id, note.id, e)} style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', color: 'var(--text-faint)', display: 'flex' }}><Trash2 size={10} /></button> <button onClick={e => deleteNote(day.id, note.id, e)} className="text-content-faint" style={{ background: 'none', border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }}><Trash2 size={10} /></button>
</div>} </div>}
{canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}> {canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}>
<button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'up') }} disabled={noteIdx === 0} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === 0 ? 'default' : 'pointer', color: noteIdx === 0 ? 'var(--border-primary)' : 'var(--text-faint)', display: 'flex', lineHeight: 1 }}><ChevronUp size={12} strokeWidth={2} /></button> <button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'up') }} disabled={noteIdx === 0} className={noteIdx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronUp size={12} strokeWidth={2} /></button>
<button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'down') }} disabled={noteIdx === merged.length - 1} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === merged.length - 1 ? 'default' : 'pointer', color: noteIdx === merged.length - 1 ? 'var(--border-primary)' : 'var(--text-faint)', display: 'flex', lineHeight: 1 }}><ChevronDown size={12} strokeWidth={2} /></button> <button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'down') }} disabled={noteIdx === merged.length - 1} className={noteIdx === merged.length - 1 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === merged.length - 1 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronDown size={12} strokeWidth={2} /></button>
</div>} </div>}
</div> </div>
</React.Fragment> </React.Fragment>
@@ -2294,22 +2291,21 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
<div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}> <div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
<button <button
onClick={() => onToggleRoute?.()} onClick={() => onToggleRoute?.()}
className={routeShown ? 'bg-accent text-accent-text' : 'bg-transparent text-content-secondary'}
style={{ style={{
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
padding: '6px 0', fontSize: 11, fontWeight: 600, borderRadius: 8, padding: '6px 0', fontSize: 11, fontWeight: 600, borderRadius: 8,
border: routeShown ? 'none' : '1px solid var(--border-faint)', border: routeShown ? 'none' : '1px solid var(--border-faint)',
background: routeShown ? 'var(--accent)' : 'transparent',
color: routeShown ? 'var(--accent-text)' : 'var(--text-secondary)',
cursor: 'pointer', fontFamily: 'inherit', cursor: 'pointer', fontFamily: 'inherit',
}} }}
> >
<RouteIcon size={12} strokeWidth={2} /> <RouteIcon size={12} strokeWidth={2} />
{t('dayplan.route')} {t('dayplan.route')}
</button> </button>
<button onClick={handleOptimize} style={{ <button onClick={handleOptimize} className="bg-surface-hover text-content-secondary" style={{
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
padding: '6px 0', fontSize: 11, fontWeight: 500, borderRadius: 8, border: 'none', padding: '6px 0', fontSize: 11, fontWeight: 500, borderRadius: 8, border: 'none',
background: 'var(--bg-hover)', color: 'var(--text-secondary)', cursor: 'pointer', fontFamily: 'inherit', cursor: 'pointer', fontFamily: 'inherit',
}}> }}>
<RotateCcw size={12} strokeWidth={2} /> <RotateCcw size={12} strokeWidth={2} />
{t('dayplan.optimize')} {t('dayplan.optimize')}
@@ -2323,11 +2319,10 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
key={p} key={p}
onClick={() => onSetRouteProfile?.(p)} onClick={() => onSetRouteProfile?.(p)}
aria-label={p === 'driving' ? 'Driving' : 'Walking'} aria-label={p === 'driving' ? 'Driving' : 'Walking'}
className={active ? 'bg-accent text-accent-text' : 'bg-transparent text-content-secondary'}
style={{ style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '6px 10px', border: 'none', cursor: 'pointer', padding: '6px 10px', border: 'none', cursor: 'pointer',
background: active ? 'var(--accent)' : 'transparent',
color: active ? 'var(--accent-text)' : 'var(--text-secondary)',
}} }}
> >
<ModeIcon size={13} strokeWidth={2} /> <ModeIcon size={13} strokeWidth={2} />
@@ -2337,9 +2332,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
</div> </div>
</div> </div>
{routeInfo && ( {routeInfo && (
<div style={{ display: 'flex', justifyContent: 'center', gap: 12, fontSize: 12, color: 'var(--text-secondary)', background: 'var(--bg-hover)', borderRadius: 8, padding: '5px 10px' }}> <div className="text-content-secondary bg-surface-hover" style={{ display: 'flex', justifyContent: 'center', gap: 12, fontSize: 12, borderRadius: 8, padding: '5px 10px' }}>
<span>{routeInfo.distance}</span> <span>{routeInfo.distance}</span>
<span style={{ color: 'var(--text-faint)' }}>·</span> <span className="text-content-faint">·</span>
<span>{routeInfo.duration}</span> <span>{routeInfo.duration}</span>
</div> </div>
)} )}
@@ -2363,17 +2358,17 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{/* Notiz-Popup-Modal — über Portal gerendert, um den backdropFilter-Stapelkontext zu umgehen */} {/* Notiz-Popup-Modal — über Portal gerendert, um den backdropFilter-Stapelkontext zu umgehen */}
{Object.entries(noteUi).map(([dayId, ui]) => ui && ReactDOM.createPortal( {Object.entries(noteUi).map(([dayId, ui]) => ui && ReactDOM.createPortal(
<div key={dayId} style={{ <div key={dayId} className="bg-[rgba(0,0,0,0.3)]" style={{
position: 'fixed', inset: 0, zIndex: 10000, position: 'fixed', inset: 0, zIndex: 10000,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(3px)', backdropFilter: 'blur(3px)',
}} onClick={() => cancelNote(Number(dayId))}> }} onClick={() => cancelNote(Number(dayId))}>
<div style={{ <div className="bg-surface-card" style={{
width: 340, background: 'var(--bg-card)', borderRadius: 16, width: 340, borderRadius: 16,
boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px', boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px',
display: 'flex', flexDirection: 'column', gap: 12, display: 'flex', flexDirection: 'column', gap: 12,
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}> <div className="text-content" style={{ fontSize: 14, fontWeight: 600 }}>
{ui.mode === 'add' ? t('dayplan.noteAdd') : t('dayplan.noteEdit')} {ui.mode === 'add' ? t('dayplan.noteAdd') : t('dayplan.noteEdit')}
</div> </div>
{/* Icon-Auswahl */} {/* Icon-Auswahl */}
@@ -2381,7 +2376,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{NOTE_ICONS.map(({ id, Icon }) => ( {NOTE_ICONS.map(({ id, Icon }) => (
<button key={id} onClick={() => setNoteUi(prev => ({ ...prev, [dayId]: { ...prev[dayId], icon: id } }))} <button key={id} onClick={() => setNoteUi(prev => ({ ...prev, [dayId]: { ...prev[dayId], icon: id } }))}
title={id} title={id}
style={{ width: 45, height: 45, borderRadius: 8, border: ui.icon === id ? '2px solid var(--text-primary)' : '2px solid var(--border-faint)', background: ui.icon === id ? 'var(--bg-hover)' : 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 }}> className={ui.icon === id ? 'bg-surface-hover' : 'bg-transparent'}
style={{ width: 45, height: 45, borderRadius: 8, border: ui.icon === id ? '2px solid var(--text-primary)' : '2px solid var(--border-faint)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 }}>
<Icon size={18} strokeWidth={1.8} color={ui.icon === id ? 'var(--text-primary)' : 'var(--text-muted)'} /> <Icon size={18} strokeWidth={1.8} color={ui.icon === id ? 'var(--text-primary)' : 'var(--text-muted)'} />
</button> </button>
))} ))}
@@ -2394,7 +2390,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); saveNote(Number(dayId)) } if (e.key === 'Escape') cancelNote(Number(dayId)) }} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); saveNote(Number(dayId)) } if (e.key === 'Escape') cancelNote(Number(dayId)) }}
placeholder={t('dayplan.noteTitle') + ' *'} placeholder={t('dayplan.noteTitle') + ' *'}
required required
style={{ fontSize: 13, fontWeight: 500, border: `1px solid ${!ui.text?.trim() ? 'var(--border-primary)' : 'var(--border-primary)'}`, borderRadius: 8, padding: '8px 10px', fontFamily: 'inherit', outline: 'none', width: '100%', boxSizing: 'border-box', color: 'var(--text-primary)' }} className="text-content"
style={{ fontSize: 13, fontWeight: 500, border: `1px solid ${!ui.text?.trim() ? 'var(--border-primary)' : 'var(--border-primary)'}`, borderRadius: 8, padding: '8px 10px', fontFamily: 'inherit', outline: 'none', width: '100%', boxSizing: 'border-box' }}
/> />
<textarea <textarea
value={ui.time} value={ui.time}
@@ -2403,12 +2400,13 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
onChange={e => setNoteUi(prev => ({ ...prev, [dayId]: { ...prev[dayId], time: e.target.value } }))} onChange={e => setNoteUi(prev => ({ ...prev, [dayId]: { ...prev[dayId], time: e.target.value } }))}
onKeyDown={e => { if (e.key === 'Escape') cancelNote(Number(dayId)) }} onKeyDown={e => { if (e.key === 'Escape') cancelNote(Number(dayId)) }}
placeholder={t('dayplan.noteSubtitle')} placeholder={t('dayplan.noteSubtitle')}
style={{ fontSize: 12, border: '1px solid var(--border-primary)', borderRadius: 8, padding: '7px 10px', fontFamily: 'inherit', outline: 'none', width: '100%', boxSizing: 'border-box', color: 'var(--text-primary)', resize: 'none', lineHeight: 1.4 }} className="text-content"
style={{ fontSize: 12, border: '1px solid var(--border-primary)', borderRadius: 8, padding: '7px 10px', fontFamily: 'inherit', outline: 'none', width: '100%', boxSizing: 'border-box', resize: 'none', lineHeight: 1.4 }}
/> />
<div style={{ textAlign: 'right', fontSize: 11, color: (ui.time?.length || 0) >= 140 ? '#d97706' : 'var(--text-faint)', marginTop: -2 }}>{ui.time?.length || 0}/150</div> <div className={(ui.time?.length || 0) >= 140 ? 'text-[#d97706]' : 'text-content-faint'} style={{ textAlign: 'right', fontSize: 11, marginTop: -2 }}>{ui.time?.length || 0}/150</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}> <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<button onClick={() => cancelNote(Number(dayId))} style={{ fontSize: 12, background: 'none', border: '1px solid var(--border-primary)', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', color: 'var(--text-muted)', fontFamily: 'inherit' }}>{t('common.cancel')}</button> <button onClick={() => cancelNote(Number(dayId))} className="text-content-muted" style={{ fontSize: 12, background: 'none', border: '1px solid var(--border-primary)', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontFamily: 'inherit' }}>{t('common.cancel')}</button>
<button onClick={() => saveNote(Number(dayId))} disabled={!ui.text?.trim()} style={{ fontSize: 12, background: !ui.text?.trim() ? 'var(--border-primary)' : 'var(--accent)', color: !ui.text?.trim() ? 'var(--text-faint)' : 'var(--accent-text)', border: 'none', borderRadius: 8, padding: '6px 16px', cursor: !ui.text?.trim() ? 'not-allowed' : 'pointer', fontWeight: 600, fontFamily: 'inherit', transition: 'background 0.15s, color 0.15s' }}> <button onClick={() => saveNote(Number(dayId))} disabled={!ui.text?.trim()} className={!ui.text?.trim() ? 'bg-[var(--border-primary)] text-content-faint' : 'bg-accent text-accent-text'} style={{ fontSize: 12, border: 'none', borderRadius: 8, padding: '6px 16px', cursor: !ui.text?.trim() ? 'not-allowed' : 'pointer', fontWeight: 600, fontFamily: 'inherit', transition: 'background 0.15s, color 0.15s' }}>
{ui.mode === 'add' ? t('common.add') : t('common.save')} {ui.mode === 'add' ? t('common.add') : t('common.save')}
</button> </button>
</div> </div>
@@ -2419,37 +2417,37 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{/* Confirm: remove time when reordering a timed place */} {/* Confirm: remove time when reordering a timed place */}
{timeConfirm && ReactDOM.createPortal( {timeConfirm && ReactDOM.createPortal(
<div style={{ <div className="bg-[rgba(0,0,0,0.3)]" style={{
position: 'fixed', inset: 0, zIndex: 1000, position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(3px)', backdropFilter: 'blur(3px)',
}} onClick={() => setTimeConfirm(null)}> }} onClick={() => setTimeConfirm(null)}>
<div style={{ <div className="bg-surface-card" style={{
width: 340, background: 'var(--bg-card)', borderRadius: 16, width: 340, borderRadius: 16,
boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px', boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px',
display: 'flex', flexDirection: 'column', gap: 12, display: 'flex', flexDirection: 'column', gap: 12,
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ <div className="bg-[rgba(239,68,68,0.12)]" style={{
width: 36, height: 36, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderRadius: '50%', background: 'rgba(239,68,68,0.12)', borderRadius: '50%',
}}> }}>
<Clock size={18} strokeWidth={1.8} color="#ef4444" /> <Clock size={18} strokeWidth={1.8} color="#ef4444" />
</div> </div>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}> <div className="text-content" style={{ fontSize: 14, fontWeight: 600 }}>
{t('dayplan.confirmRemoveTimeTitle')} {t('dayplan.confirmRemoveTimeTitle')}
</div> </div>
</div> </div>
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.5 }}> <div className="text-content-secondary" style={{ fontSize: 12.5, lineHeight: 1.5 }}>
{t('dayplan.confirmRemoveTimeBody', { time: timeConfirm.time })} {t('dayplan.confirmRemoveTimeBody', { time: timeConfirm.time })}
</div> </div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}> <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
<button onClick={() => setTimeConfirm(null)} style={{ <button onClick={() => setTimeConfirm(null)} className="text-content-muted" style={{
fontSize: 12, background: 'none', border: '1px solid var(--border-primary)', fontSize: 12, background: 'none', border: '1px solid var(--border-primary)',
borderRadius: 8, padding: '6px 14px', cursor: 'pointer', color: 'var(--text-muted)', fontFamily: 'inherit', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontFamily: 'inherit',
}}>{t('common.cancel')}</button> }}>{t('common.cancel')}</button>
<button onClick={confirmTimeRemoval} style={{ <button onClick={confirmTimeRemoval} className="bg-[#ef4444] text-white" style={{
fontSize: 12, background: '#ef4444', color: 'white', fontSize: 12,
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit', border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
}}>{t('common.confirm')}</button> }}>{t('common.confirm')}</button>
</div> </div>
@@ -2460,14 +2458,14 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{/* Transport-Detail-Modal */} {/* Transport-Detail-Modal */}
{transportDetail && ReactDOM.createPortal( {transportDetail && ReactDOM.createPortal(
<div style={{ <div className="bg-[rgba(0,0,0,0.3)]" style={{
position: 'fixed', inset: 0, zIndex: 1000, position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(3px)', backdropFilter: 'blur(3px)',
}} onClick={() => setTransportDetail(null)}> }} onClick={() => setTransportDetail(null)}>
<div style={{ <div className="bg-surface-card" style={{
width: 380, maxHeight: '80vh', overflowY: 'auto', width: 380, maxHeight: '80vh', overflowY: 'auto',
background: 'var(--bg-card)', borderRadius: 16, borderRadius: 16,
boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px', boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px',
display: 'flex', flexDirection: 'column', gap: 14, display: 'flex', flexDirection: 'column', gap: 14,
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
@@ -2504,8 +2502,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
<TransportIcon size={18} strokeWidth={1.8} color={color} /> <TransportIcon size={18} strokeWidth={1.8} color={color} />
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>{res.title}</div> <div className="text-content" style={{ fontSize: 15, fontWeight: 600 }}>{res.title}</div>
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 2 }}> <div className="text-content-faint" style={{ fontSize: 11, marginTop: 2 }}>
{(() => { {(() => {
const { date, time } = splitReservationDateTime(res.reservation_time) const { date, time } = splitReservationDateTime(res.reservation_time)
const { time: endTime } = splitReservationDateTime(res.reservation_end_time) const { time: endTime } = splitReservationDateTime(res.reservation_end_time)
@@ -2521,10 +2519,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
})()} })()}
</div> </div>
</div> </div>
<div style={{ <div className={res.status === 'confirmed' ? 'bg-[rgba(22,163,74,0.1)] text-[#16a34a]' : 'bg-[rgba(217,119,6,0.1)] text-[#d97706]'} style={{
padding: '3px 8px', borderRadius: 6, fontSize: 10, fontWeight: 600, padding: '3px 8px', borderRadius: 6, fontSize: 10, fontWeight: 600,
background: res.status === 'confirmed' ? 'rgba(22,163,74,0.1)' : 'rgba(217,119,6,0.1)',
color: res.status === 'confirmed' ? '#16a34a' : '#d97706',
}}> }}>
{(res.status === 'confirmed' ? t('planner.resConfirmed') : t('planner.resPending')).replace(/\s*·\s*$/, '')} {(res.status === 'confirmed' ? t('planner.resConfirmed') : t('planner.resPending')).replace(/\s*·\s*$/, '')}
</div> </div>
@@ -2536,14 +2532,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{detailFields.map((f, i) => { {detailFields.map((f, i) => {
const shouldBlur = f.sensitive && useSettingsStore.getState().settings.blur_booking_codes const shouldBlur = f.sensitive && useSettingsStore.getState().settings.blur_booking_codes
return ( return (
<div key={i} style={{ padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 8 }}> <div key={i} className="bg-surface-tertiary" style={{ padding: '8px 10px', borderRadius: 8 }}>
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{f.label}</div> <div className="text-content-faint" style={{ fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{f.label}</div>
<div <div
onMouseEnter={e => { if (shouldBlur) e.currentTarget.style.filter = 'none' }} onMouseEnter={e => { if (shouldBlur) e.currentTarget.style.filter = 'none' }}
onMouseLeave={e => { if (shouldBlur) e.currentTarget.style.filter = 'blur(5px)' }} onMouseLeave={e => { if (shouldBlur) e.currentTarget.style.filter = 'blur(5px)' }}
onClick={e => { if (shouldBlur) { const el = e.currentTarget; el.style.filter = el.style.filter === 'none' ? 'blur(5px)' : 'none' } }} onClick={e => { if (shouldBlur) { const el = e.currentTarget; el.style.filter = el.style.filter === 'none' ? 'blur(5px)' : 'none' } }}
className="text-content"
style={{ style={{
fontSize: 12, fontWeight: 500, color: 'var(--text-primary)', wordBreak: 'break-word', fontSize: 12, fontWeight: 500, wordBreak: 'break-word',
filter: shouldBlur ? 'blur(5px)' : 'none', transition: 'filter 0.2s', filter: shouldBlur ? 'blur(5px)' : 'none', transition: 'filter 0.2s',
cursor: shouldBlur ? 'pointer' : 'default', cursor: shouldBlur ? 'pointer' : 'default',
userSelect: shouldBlur ? 'none' : 'auto', userSelect: shouldBlur ? 'none' : 'auto',
@@ -2557,9 +2554,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{/* Notizen */} {/* Notizen */}
{res.notes && ( {res.notes && (
<div style={{ padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 8 }}> <div className="bg-surface-tertiary" style={{ padding: '8px 10px', borderRadius: 8 }}>
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{t('reservations.notes')}</div> <div className="text-content-faint" style={{ fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{t('reservations.notes')}</div>
<div className="collab-note-md" style={{ fontSize: 12, color: 'var(--text-primary)', wordBreak: 'break-word', overflowWrap: 'anywhere' }}><Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{res.notes}</Markdown></div> <div className="collab-note-md text-content" style={{ fontSize: 12, wordBreak: 'break-word', overflowWrap: 'anywhere' }}><Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{res.notes}</Markdown></div>
</div> </div>
)} )}
@@ -2574,24 +2571,25 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
if (resFiles.length === 0) return null if (resFiles.length === 0) return null
return ( return (
<div> <div>
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 6 }}>{t('files.title')}</div> <div className="text-content-faint" style={{ fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 6 }}>{t('files.title')}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{resFiles.map(f => ( {resFiles.map(f => (
<div key={f.id} <div key={f.id}
onClick={() => { setTransportDetail(null); onNavigateToFiles?.() }} onClick={() => { setTransportDetail(null); onNavigateToFiles?.() }}
className="bg-surface-tertiary"
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px',
background: 'var(--bg-tertiary)', borderRadius: 8, cursor: 'pointer', borderRadius: 8, cursor: 'pointer',
transition: 'background 0.1s', transition: 'background 0.1s',
}} }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
> >
<FileText size={14} style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> <FileText size={14} className="text-content-muted" style={{ flexShrink: 0 }} />
<span style={{ flex: 1, fontSize: 12, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span className="text-content" style={{ flex: 1, fontSize: 12, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{f.original_name} {f.original_name}
</span> </span>
<ExternalLink size={11} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <ExternalLink size={11} className="text-content-faint" style={{ flexShrink: 0 }} />
</div> </div>
))} ))}
</div> </div>
@@ -187,11 +187,13 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
onClick={handleClose} onClick={handleClose}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }} className="bg-[rgba(0,0,0,0.4)]"
style={{ position: 'fixed', inset: 0, zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
> >
<div <div
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
style={{ background: 'var(--bg-card)', borderRadius: 16, width: '100%', maxWidth: 520, padding: 24, boxShadow: '0 8px 32px rgba(0,0,0,0.2)', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }} className="bg-surface-card"
style={{ borderRadius: 16, width: '100%', maxWidth: 520, padding: 24, boxShadow: '0 8px 32px rgba(0,0,0,0.2)', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}
> >
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 6 }}> <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 6 }}>
{t('places.importFile')} {t('places.importFile')}
@@ -214,12 +216,12 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
onDragEnter={handleDragOver} onDragEnter={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
className={isDragOver ? 'bg-surface-tertiary' : 'bg-transparent'}
style={{ style={{
width: '100%', width: '100%',
minHeight: 88, minHeight: 88,
borderRadius: 12, borderRadius: 12,
border: `2px dashed ${isDragOver ? 'var(--accent)' : 'var(--border-primary)'}`, border: `2px dashed ${isDragOver ? 'var(--accent)' : 'var(--border-primary)'}`,
background: isDragOver ? 'var(--bg-tertiary)' : 'transparent',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
@@ -237,7 +239,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
> >
<Upload size={18} strokeWidth={1.8} color={isDragOver ? 'var(--accent)' : 'var(--text-faint)'} style={{ pointerEvents: 'none' }} /> <Upload size={18} strokeWidth={1.8} color={isDragOver ? 'var(--accent)' : 'var(--text-faint)'} style={{ pointerEvents: 'none' }} />
{isDragOver ? ( {isDragOver ? (
<span style={{ color: 'var(--accent)', pointerEvents: 'none' }}>{t('places.importFileDropActive')}</span> <span className="text-accent" style={{ pointerEvents: 'none' }}>{t('places.importFileDropActive')}</span>
) : file ? ( ) : file ? (
<span style={{ color: 'var(--text-primary)', textAlign: 'center', wordBreak: 'break-all', pointerEvents: 'none' }}>{file.name}</span> <span style={{ color: 'var(--text-primary)', textAlign: 'center', wordBreak: 'break-all', pointerEvents: 'none' }}>{file.name}</span>
) : ( ) : (
@@ -252,10 +254,9 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
</div> </div>
{(['waypoints', 'routes', 'tracks'] as const).map(key => ( {(['waypoints', 'routes', 'tracks'] as const).map(key => (
<label key={key} onClick={() => setGpxOpts(prev => ({ ...prev, [key]: !prev[key] }))} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer' }}> <label key={key} onClick={() => setGpxOpts(prev => ({ ...prev, [key]: !prev[key] }))} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer' }}>
<div style={{ <div className={gpxOpts[key] ? 'bg-accent' : 'bg-transparent'} style={{
width: 16, height: 16, borderRadius: 4, flexShrink: 0, width: 16, height: 16, borderRadius: 4, flexShrink: 0,
border: gpxOpts[key] ? 'none' : '1.5px solid var(--border-primary)', border: gpxOpts[key] ? 'none' : '1.5px solid var(--border-primary)',
background: gpxOpts[key] ? 'var(--accent)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
{gpxOpts[key] && <svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2" stroke="white" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>} {gpxOpts[key] && <svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2" stroke="white" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}
@@ -266,7 +267,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
</label> </label>
))} ))}
{gpxNoneSelected && ( {gpxNoneSelected && (
<div style={{ fontSize: 11, color: '#b45309', marginTop: 4 }}>{t('places.gpxImportNoneSelected')}</div> <div className="text-[#b45309]" style={{ fontSize: 11, marginTop: 4 }}>{t('places.gpxImportNoneSelected')}</div>
)} )}
</div> </div>
)} )}
@@ -278,10 +279,9 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
</div> </div>
{(['points', 'paths'] as const).map(key => ( {(['points', 'paths'] as const).map(key => (
<label key={key} onClick={() => setKmlOpts(prev => ({ ...prev, [key]: !prev[key] }))} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer' }}> <label key={key} onClick={() => setKmlOpts(prev => ({ ...prev, [key]: !prev[key] }))} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer' }}>
<div style={{ <div className={kmlOpts[key] ? 'bg-accent' : 'bg-transparent'} style={{
width: 16, height: 16, borderRadius: 4, flexShrink: 0, width: 16, height: 16, borderRadius: 4, flexShrink: 0,
border: kmlOpts[key] ? 'none' : '1.5px solid var(--border-primary)', border: kmlOpts[key] ? 'none' : '1.5px solid var(--border-primary)',
background: kmlOpts[key] ? 'var(--accent)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
{kmlOpts[key] && <svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2" stroke="white" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>} {kmlOpts[key] && <svg width="10" height="10" viewBox="0 0 10 10"><polyline points="1.5,5 4,7.5 8.5,2" stroke="white" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}
@@ -292,7 +292,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
</label> </label>
))} ))}
{kmlNoneSelected && ( {kmlNoneSelected && (
<div style={{ fontSize: 11, color: '#b45309', marginTop: 4 }}>{t('places.kmlImportNoneSelected')}</div> <div className="text-[#b45309]" style={{ fontSize: 11, marginTop: 4 }}>{t('places.kmlImportNoneSelected')}</div>
)} )}
</div> </div>
)} )}
@@ -310,7 +310,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
})} })}
</div> </div>
{summary.warnings?.length > 0 && ( {summary.warnings?.length > 0 && (
<div style={{ marginTop: 8, fontSize: 12, color: '#b45309', whiteSpace: 'pre-wrap' }}> <div className="text-[#b45309]" style={{ marginTop: 8, fontSize: 12, whiteSpace: 'pre-wrap' }}>
{summary.warnings.join('\n')} {summary.warnings.join('\n')}
</div> </div>
)} )}
@@ -318,10 +318,10 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
)} )}
{error && ( {error && (
<div style={{ <div className="bg-[rgba(239,68,68,0.08)] text-[#b91c1c]" style={{
border: '1px solid rgba(239,68,68,0.35)', borderRadius: 10, border: '1px solid rgba(239,68,68,0.35)', borderRadius: 10,
background: 'rgba(239,68,68,0.08)', padding: '8px 10px', padding: '8px 10px',
fontSize: 12, color: '#b91c1c', whiteSpace: 'pre-wrap', marginBottom: 10, fontSize: 12, whiteSpace: 'pre-wrap', marginBottom: 10,
}}> }}>
{error} {error}
</div> </div>
@@ -341,10 +341,9 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
<button <button
onClick={handleImport} onClick={handleImport}
disabled={!canImport} disabled={!canImport}
className={canImport ? 'bg-accent text-accent-text' : 'bg-surface-tertiary text-content-faint'}
style={{ style={{
padding: '8px 16px', borderRadius: 10, border: 'none', padding: '8px 16px', borderRadius: 10, border: 'none',
background: canImport ? 'var(--accent)' : 'var(--bg-tertiary)',
color: canImport ? 'var(--accent-text)' : 'var(--text-faint)',
fontSize: 13, fontWeight: 500, cursor: canImport ? 'pointer' : 'default', fontSize: 13, fontWeight: 500, cursor: canImport ? 'pointer' : 'default',
fontFamily: 'inherit', fontFamily: 'inherit',
}} }}
@@ -88,8 +88,8 @@ export default function LocationSelect({ value, onChange, placeholder, style }:
return ( return (
<div ref={wrapRef} style={{ position: 'relative', ...style }}> <div ref={wrapRef} style={{ position: 'relative', ...style }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 10, border: '1px solid var(--border-primary)' }}> <div className="bg-surface-tertiary" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px', borderRadius: 10, border: '1px solid var(--border-primary)' }}>
<MapPin size={14} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <MapPin size={14} className="text-content-faint" style={{ flexShrink: 0 }} />
<input <input
type="text" type="text"
value={query} value={query}
@@ -97,19 +97,20 @@ export default function LocationSelect({ value, onChange, placeholder, style }:
onChange={(e) => { setQuery(e.target.value); setOpen(true); if (value) onChange(null) }} onChange={(e) => { setQuery(e.target.value); setOpen(true); if (value) onChange(null) }}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
onKeyDown={onKey} onKeyDown={onKey}
style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', color: 'var(--text-primary)', fontSize: 13 }} className="bg-transparent text-content"
style={{ flex: 1, minWidth: 0, border: 'none', outline: 'none', fontSize: 13 }}
/> />
{value && ( {value && (
<button type="button" onClick={clear} style={{ background: 'transparent', border: 'none', padding: 2, cursor: 'pointer', color: 'var(--text-faint)', display: 'flex' }} aria-label="Clear"> <button type="button" onClick={clear} className="bg-transparent text-content-faint" style={{ border: 'none', padding: 2, cursor: 'pointer', display: 'flex' }} aria-label="Clear">
<X size={14} /> <X size={14} />
</button> </button>
)} )}
</div> </div>
{open && (loading || results.length > 0) && ( {open && (loading || results.length > 0) && (
<div style={{ position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,0.18)', maxHeight: 260, overflowY: 'auto', zIndex: 1000 }}> <div className="bg-surface-card" style={{ position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, border: '1px solid var(--border-primary)', borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,0.18)', maxHeight: 260, overflowY: 'auto', zIndex: 1000 }}>
{loading && results.length === 0 && ( {loading && results.length === 0 && (
<div style={{ padding: 10, fontSize: 12, color: 'var(--text-faint)' }}>{t('common.loading')}</div> <div className="text-content-faint" style={{ padding: 10, fontSize: 12 }}>{t('common.loading')}</div>
)} )}
{results.map((r, i) => ( {results.map((r, i) => (
<button <button
@@ -117,18 +118,18 @@ export default function LocationSelect({ value, onChange, placeholder, style }:
type="button" type="button"
onClick={() => pick(r)} onClick={() => pick(r)}
onMouseEnter={() => setHighlight(i)} onMouseEnter={() => setHighlight(i)}
className={`text-content ${i === highlight ? 'bg-surface-hover' : 'bg-transparent'}`}
style={{ style={{
display: 'flex', alignItems: 'flex-start', gap: 8, width: '100%', display: 'flex', alignItems: 'flex-start', gap: 8, width: '100%',
padding: '8px 12px', border: 'none', cursor: 'pointer', textAlign: 'left', padding: '8px 12px', border: 'none', cursor: 'pointer', textAlign: 'left',
background: i === highlight ? 'var(--bg-hover)' : 'transparent', fontFamily: 'inherit',
color: 'var(--text-primary)', fontFamily: 'inherit',
}} }}
> >
<MapPin size={12} style={{ color: 'var(--text-faint)', marginTop: 2, flexShrink: 0 }} /> <MapPin size={12} className="text-content-faint" style={{ marginTop: 2, flexShrink: 0 }} />
<span style={{ flex: 1, minWidth: 0 }}> <span style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.name || r.address}</div> <div style={{ fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.name || r.address}</div>
{r.address && r.name !== r.address && ( {r.address && r.name !== r.address && (
<div style={{ fontSize: 11, color: 'var(--text-faint)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.address}</div> <div className="text-content-faint" style={{ fontSize: 11, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.address}</div>
)} )}
</span> </span>
</button> </button>
@@ -518,7 +518,7 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
{/* Place Search */} {/* Place Search */}
<div className="bg-slate-50 rounded-xl p-3 border border-slate-200"> <div className="bg-slate-50 rounded-xl p-3 border border-slate-200">
{!hasMapsKey && ( {!hasMapsKey && (
<p className="mb-2 text-xs" style={{ color: 'var(--text-faint)' }}> <p className="mb-2 text-xs text-content-faint">
{t('places.osmActive')} {t('places.osmActive')}
</p> </p>
)} )}
@@ -200,8 +200,7 @@ export default function PlaceInspector({
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif", fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
}} }}
> >
<div style={{ <div className="bg-surface-elevated" style={{
background: 'var(--bg-elevated)',
backdropFilter: 'blur(40px) saturate(180%)', backdropFilter: 'blur(40px) saturate(180%)',
WebkitBackdropFilter: 'blur(40px) saturate(180%)', WebkitBackdropFilter: 'blur(40px) saturate(180%)',
borderRadius: 20, borderRadius: 20,
@@ -245,7 +244,8 @@ export default function PlaceInspector({
{(place.phone || googleDetails?.phone) && ( {(place.phone || googleDetails?.phone) && (
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
<a href={`tel:${place.phone || googleDetails.phone}`} <a href={`tel:${place.phone || googleDetails.phone}`}
style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', textDecoration: 'none' }}> className="text-content"
style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, textDecoration: 'none' }}>
<Phone size={12} /> {place.phone || googleDetails.phone} <Phone size={12} /> {place.phone || googleDetails.phone}
</a> </a>
</div> </div>
@@ -414,7 +414,7 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
return ( return (
<div style={{ borderRadius: 12, border: '1px solid var(--border-faint)', padding: '8px 10px' }}> <div style={{ borderRadius: 12, border: '1px solid var(--border-faint)', padding: '8px 10px' }}>
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 }}> <div className="text-content-faint" style={{ fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 }}>
<Users size={10} /> {t('inspector.participants')} <Users size={10} /> {t('inspector.participants')}
</div> </div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
@@ -426,19 +426,18 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
onMouseEnter={() => setHoveredId(member.id)} onMouseEnter={() => setHoveredId(member.id)}
onMouseLeave={() => setHoveredId(null)} onMouseLeave={() => setHoveredId(null)}
onClick={() => { if (canRemove) handleRemove(member.id) }} onClick={() => { if (canRemove) handleRemove(member.id) }}
className={isHovered && canRemove ? 'bg-[rgba(239,68,68,0.06)] text-[#ef4444]' : 'bg-surface-hover text-content'}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 4, padding: '2px 7px 2px 3px', borderRadius: 99, display: 'flex', alignItems: 'center', gap: 4, padding: '2px 7px 2px 3px', borderRadius: 99,
border: `1.5px solid ${isHovered && canRemove ? 'rgba(239,68,68,0.4)' : 'var(--accent)'}`, border: `1.5px solid ${isHovered && canRemove ? 'rgba(239,68,68,0.4)' : 'var(--accent)'}`,
background: isHovered && canRemove ? 'rgba(239,68,68,0.06)' : 'var(--bg-hover)',
fontSize: 10, fontWeight: 500, fontSize: 10, fontWeight: 500,
color: isHovered && canRemove ? '#ef4444' : 'var(--text-primary)',
cursor: canRemove ? 'pointer' : 'default', cursor: canRemove ? 'pointer' : 'default',
transition: 'all 0.15s', transition: 'all 0.15s',
}}> }}>
<div style={{ <div className="bg-surface-tertiary text-content-muted" style={{
width: 16, height: 16, borderRadius: '50%', background: 'var(--bg-tertiary)', width: 16, height: 16, borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 7, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 7, fontWeight: 700,
color: 'var(--text-muted)', overflow: 'hidden', flexShrink: 0, overflow: 'hidden', flexShrink: 0,
}}> }}>
{(member.avatar_url || member.avatar) ? <img src={member.avatar_url || `/uploads/avatars/${member.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()} {(member.avatar_url || member.avatar) ? <img src={member.avatar_url || `/uploads/avatars/${member.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()}
</div> </div>
@@ -450,35 +449,35 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
{/* Add button */} {/* Add button */}
{availableToAdd.length > 0 && ( {availableToAdd.length > 0 && (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button onClick={() => setShowAdd(!showAdd)} style={{ <button onClick={() => setShowAdd(!showAdd)} className="text-content-faint" style={{
width: 22, height: 22, borderRadius: '50%', border: '1.5px dashed var(--border-primary)', width: 22, height: 22, borderRadius: '50%', border: '1.5px dashed var(--border-primary)',
background: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-faint)', fontSize: 12, transition: 'all 0.12s', fontSize: 12, transition: 'all 0.12s',
}} }}
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--text-muted)'; e.currentTarget.style.color = 'var(--text-primary)' }} onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--text-muted)'; e.currentTarget.style.color = 'var(--text-primary)' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.color = 'var(--text-faint)' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.color = 'var(--text-faint)' }}
>+</button> >+</button>
{showAdd && ( {showAdd && (
<div style={{ <div className="bg-surface-card" style={{
position: 'absolute', top: 26, left: 0, zIndex: 100, position: 'absolute', top: 26, left: 0, zIndex: 100,
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10, border: '1px solid var(--border-primary)', borderRadius: 10,
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 140, boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 140,
}}> }}>
{availableToAdd.map(member => ( {availableToAdd.map(member => (
<button key={member.id} onClick={() => handleAdd(member.id)} style={{ <button key={member.id} onClick={() => handleAdd(member.id)} className="text-content" style={{
display: 'flex', alignItems: 'center', gap: 6, width: '100%', padding: '5px 8px', display: 'flex', alignItems: 'center', gap: 6, width: '100%', padding: '5px 8px',
borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer', borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer',
fontFamily: 'inherit', fontSize: 11, color: 'var(--text-primary)', textAlign: 'left', fontFamily: 'inherit', fontSize: 11, textAlign: 'left',
transition: 'background 0.1s', transition: 'background 0.1s',
}} }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'} onMouseLeave={e => e.currentTarget.style.background = 'none'}
> >
<div style={{ <div className="bg-surface-tertiary text-content-muted" style={{
width: 18, height: 18, borderRadius: '50%', background: 'var(--bg-tertiary)', width: 18, height: 18, borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 8, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 8, fontWeight: 700,
color: 'var(--text-muted)', overflow: 'hidden', flexShrink: 0, overflow: 'hidden', flexShrink: 0,
}}> }}>
{(member.avatar_url || member.avatar) ? <img src={member.avatar_url || `/uploads/avatars/${member.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()} {(member.avatar_url || member.avatar) ? <img src={member.avatar_url || `/uploads/avatars/${member.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()}
</div> </div>
@@ -530,12 +529,14 @@ function PlaceInspectorHeader({ openNow, place, category, t, editingName, nameIn
onChange={e => setNameValue(e.target.value)} onChange={e => setNameValue(e.target.value)}
onBlur={commitNameEdit} onBlur={commitNameEdit}
onKeyDown={handleNameKeyDown} onKeyDown={handleNameKeyDown}
style={{ fontWeight: 600, fontSize: 15, color: 'var(--text-primary)', lineHeight: '1.3', background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)', borderRadius: 6, padding: '1px 6px', fontFamily: 'inherit', outline: 'none', width: '100%' }} className="text-content bg-surface-secondary"
style={{ fontWeight: 600, fontSize: 15, lineHeight: '1.3', border: '1px solid var(--border-primary)', borderRadius: 6, padding: '1px 6px', fontFamily: 'inherit', outline: 'none', width: '100%' }}
/> />
) : ( ) : (
<span <span
onDoubleClick={startNameEdit} onDoubleClick={startNameEdit}
style={{ fontWeight: 600, fontSize: 15, color: 'var(--text-primary)', lineHeight: '1.3', cursor: onUpdatePlace ? 'text' : 'default' }} className="text-content"
style={{ fontWeight: 600, fontSize: 15, lineHeight: '1.3', cursor: onUpdatePlace ? 'text' : 'default' }}
>{place.name}</span> >{place.name}</span>
)} )}
{category && (() => { {category && (() => {
@@ -558,24 +559,25 @@ function PlaceInspectorHeader({ openNow, place, category, t, editingName, nameIn
{place.address && ( {place.address && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 4, marginTop: 6 }}> <div style={{ display: 'flex', alignItems: 'flex-start', gap: 4, marginTop: 6 }}>
<MapPin size={11} color="var(--text-faint)" style={{ flexShrink: 0, marginTop: 2 }} /> <MapPin size={11} color="var(--text-faint)" style={{ flexShrink: 0, marginTop: 2 }} />
<span style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: '1.4', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{place.address}</span> <span className="text-content-muted" style={{ fontSize: 12, lineHeight: '1.4', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{place.address}</span>
</div> </div>
)} )}
{place.place_time && ( {place.place_time && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 3 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 3 }}>
<Clock size={10} color="var(--text-faint)" style={{ flexShrink: 0 }} /> <Clock size={10} color="var(--text-faint)" style={{ flexShrink: 0 }} />
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>{formatTime(place.place_time, locale, timeFormat)}{place.end_time ? ` ${formatTime(place.end_time, locale, timeFormat)}` : ''}</span> <span className="text-content-muted" style={{ fontSize: 12 }}>{formatTime(place.place_time, locale, timeFormat)}{place.end_time ? ` ${formatTime(place.end_time, locale, timeFormat)}` : ''}</span>
</div> </div>
)} )}
{place.lat && place.lng && ( {place.lat && place.lng && (
<div className="hidden sm:block" style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 4, fontVariantNumeric: 'tabular-nums' }}> <div className="hidden sm:block text-content-faint" style={{ fontSize: 11, marginTop: 4, fontVariantNumeric: 'tabular-nums' }}>
{Number(place.lat).toFixed(6)}, {Number(place.lng).toFixed(6)} {Number(place.lat).toFixed(6)}, {Number(place.lng).toFixed(6)}
</div> </div>
)} )}
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--bg-hover)', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, alignSelf: 'flex-start', transition: 'background 0.15s' }} className="bg-surface-hover"
style={{ width: 28, height: 28, borderRadius: '50%', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, alignSelf: 'flex-start', transition: 'background 0.15s' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-hover)'}
> >
@@ -604,11 +606,11 @@ function PlaceReservationParticipants({ selectedAssignmentId, reservations, assi
const confirmed = res.status === 'confirmed' const confirmed = res.status === 'confirmed'
return ( return (
<div style={{ borderRadius: 12, overflow: 'hidden', border: `1px solid ${confirmed ? 'rgba(22,163,74,0.2)' : 'rgba(217,119,6,0.2)'}` }}> <div style={{ borderRadius: 12, overflow: 'hidden', border: `1px solid ${confirmed ? 'rgba(22,163,74,0.2)' : 'rgba(217,119,6,0.2)'}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: confirmed ? 'rgba(22,163,74,0.08)' : 'rgba(217,119,6,0.08)' }}> <div className={confirmed ? 'bg-[rgba(22,163,74,0.08)]' : 'bg-[rgba(217,119,6,0.08)]'} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px' }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', flexShrink: 0, background: confirmed ? '#16a34a' : '#d97706' }} /> <div className={confirmed ? 'bg-[#16a34a]' : 'bg-[#d97706]'} style={{ width: 6, height: 6, borderRadius: '50%', flexShrink: 0 }} />
<span style={{ fontSize: 10, fontWeight: 700, color: confirmed ? '#16a34a' : '#d97706' }}>{confirmed ? t('reservations.confirmed') : t('reservations.pending')}</span> <span className={confirmed ? 'text-[#16a34a]' : 'text-[#d97706]'} style={{ fontSize: 10, fontWeight: 700 }}>{confirmed ? t('reservations.confirmed') : t('reservations.pending')}</span>
<span style={{ flex: 1 }} /> <span style={{ flex: 1 }} />
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{res.title}</span> <span className="text-content" style={{ fontSize: 11, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{res.title}</span>
</div> </div>
<div style={{ padding: '6px 10px', display: 'flex', gap: 12, flexWrap: 'wrap' }}> <div style={{ padding: '6px 10px', display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{(() => { {(() => {
@@ -618,14 +620,14 @@ function PlaceReservationParticipants({ selectedAssignmentId, reservations, assi
<> <>
{date && ( {date && (
<div> <div>
<div style={{ fontSize: 8, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase' }}>{t('reservations.date')}</div> <div className="text-content-faint" style={{ fontSize: 8, fontWeight: 600, textTransform: 'uppercase' }}>{t('reservations.date')}</div>
<div style={{ fontSize: 10, fontWeight: 500, color: 'var(--text-primary)', marginTop: 1 }}>{new Date(date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div> <div className="text-content" style={{ fontSize: 10, fontWeight: 500, marginTop: 1 }}>{new Date(date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div>
</div> </div>
)} )}
{(startTime || endTime) && ( {(startTime || endTime) && (
<div> <div>
<div style={{ fontSize: 8, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase' }}>{t('reservations.time')}</div> <div className="text-content-faint" style={{ fontSize: 8, fontWeight: 600, textTransform: 'uppercase' }}>{t('reservations.time')}</div>
<div style={{ fontSize: 10, fontWeight: 500, color: 'var(--text-primary)', marginTop: 1 }}> <div className="text-content" style={{ fontSize: 10, fontWeight: 500, marginTop: 1 }}>
{startTime ? formatTime(startTime, locale, timeFormat) : ''} {startTime ? formatTime(startTime, locale, timeFormat) : ''}
{endTime ? ` ${formatTime(endTime, locale, timeFormat)}` : ''} {endTime ? ` ${formatTime(endTime, locale, timeFormat)}` : ''}
</div> </div>
@@ -636,12 +638,12 @@ function PlaceReservationParticipants({ selectedAssignmentId, reservations, assi
})()} })()}
{res.confirmation_number && ( {res.confirmation_number && (
<div> <div>
<div style={{ fontSize: 8, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase' }}>{t('reservations.confirmationCode')}</div> <div className="text-content-faint" style={{ fontSize: 8, fontWeight: 600, textTransform: 'uppercase' }}>{t('reservations.confirmationCode')}</div>
<div style={{ fontSize: 10, fontWeight: 500, color: 'var(--text-primary)', marginTop: 1 }}>{res.confirmation_number}</div> <div className="text-content" style={{ fontSize: 10, fontWeight: 500, marginTop: 1 }}>{res.confirmation_number}</div>
</div> </div>
)} )}
</div> </div>
{res.notes && <div className="collab-note-md" style={{ padding: '0 10px 6px', fontSize: 10, color: 'var(--text-faint)', lineHeight: 1.4, wordBreak: 'break-word', overflowWrap: 'anywhere' }}><Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{res.notes}</Markdown></div>} {res.notes && <div className="collab-note-md text-content-faint" style={{ padding: '0 10px 6px', fontSize: 10, lineHeight: 1.4, wordBreak: 'break-word', overflowWrap: 'anywhere' }}><Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{res.notes}</Markdown></div>}
{(() => { {(() => {
const meta = typeof res.metadata === 'string' ? JSON.parse(res.metadata || '{}') : (res.metadata || {}) const meta = typeof res.metadata === 'string' ? JSON.parse(res.metadata || '{}') : (res.metadata || {})
if (!meta || Object.keys(meta).length === 0) return null if (!meta || Object.keys(meta).length === 0) return null
@@ -654,7 +656,7 @@ function PlaceReservationParticipants({ selectedAssignmentId, reservations, assi
if (meta.check_in_time) parts.push(`Check-in ${meta.check_in_time}`) if (meta.check_in_time) parts.push(`Check-in ${meta.check_in_time}`)
if (meta.check_out_time) parts.push(`Check-out ${meta.check_out_time}`) if (meta.check_out_time) parts.push(`Check-out ${meta.check_out_time}`)
if (parts.length === 0) return null if (parts.length === 0) return null
return <div style={{ padding: '0 10px 6px', fontSize: 10, color: 'var(--text-muted)', fontWeight: 500 }}>{parts.join(' · ')}</div> return <div className="text-content-muted" style={{ padding: '0 10px 6px', fontSize: 10, fontWeight: 500 }}>{parts.join(' · ')}</div>
})()} })()}
</div> </div>
) )
@@ -684,7 +686,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
return ( return (
<div className={`grid grid-cols-1 ${openingHours?.length > 0 ? 'sm:grid-cols-2' : ''} gap-2`}> <div className={`grid grid-cols-1 ${openingHours?.length > 0 ? 'sm:grid-cols-2' : ''} gap-2`}>
{openingHours && openingHours.length > 0 && ( {openingHours && openingHours.length > 0 && (
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, overflow: 'hidden' }}> <div className="bg-surface-hover" style={{ borderRadius: 10, overflow: 'hidden' }}>
<button <button
onClick={() => setHoursExpanded(h => !h)} onClick={() => setHoursExpanded(h => !h)}
style={{ style={{
@@ -695,7 +697,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
> >
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Clock size={13} color="#9ca3af" /> <Clock size={13} color="#9ca3af" />
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}> <span className="text-content-secondary" style={{ fontSize: 12, fontWeight: 500 }}>
{hoursExpanded ? t('inspector.openingHours') : (convertHoursLine(openingHours[weekdayIndex] || '', timeFormat) || t('inspector.showHours'))} {hoursExpanded ? t('inspector.openingHours') : (convertHoursLine(openingHours[weekdayIndex] || '', timeFormat) || t('inspector.showHours'))}
</span> </span>
</div> </div>
@@ -704,8 +706,8 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
{hoursExpanded && ( {hoursExpanded && (
<div style={{ padding: '0 12px 10px' }}> <div style={{ padding: '0 12px 10px' }}>
{openingHours.map((line, i) => ( {openingHours.map((line, i) => (
<div key={i} style={{ <div key={i} className={i === weekdayIndex ? 'text-content' : 'text-content-muted'} style={{
fontSize: 12, color: i === weekdayIndex ? 'var(--text-primary)' : 'var(--text-muted)', fontSize: 12,
fontWeight: i === weekdayIndex ? 600 : 400, fontWeight: i === weekdayIndex ? 600 : 400,
padding: '2px 0', padding: '2px 0',
}}>{convertHoursLine(line, timeFormat)}</div> }}>{convertHoursLine(line, timeFormat)}</div>
@@ -765,34 +767,34 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
} }
return ( return (
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}> <div className="bg-surface-hover" style={{ borderRadius: 10, padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<TrendingUp size={13} color="#9ca3af" /> <TrendingUp size={13} color="#9ca3af" />
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>{t('inspector.trackStats')}</span> <span className="text-content-secondary" style={{ fontSize: 12, fontWeight: 500 }}>{t('inspector.trackStats')}</span>
</div> </div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', fontWeight: 600 }}> <div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
<MapPin size={12} color="#3b82f6" /> <MapPin size={12} color="#3b82f6" />
{distKm < 1 ? `${Math.round(totalDist)} m` : `${distKm.toFixed(1)} km`} {distKm < 1 ? `${Math.round(totalDist)} m` : `${distKm.toFixed(1)} km`}
</div> </div>
{hasEle && ( {hasEle && (
<> <>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', fontWeight: 600 }}> <div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
<Mountain size={12} color="#22c55e" /> <Mountain size={12} color="#22c55e" />
{Math.round(maxEle)} m {Math.round(maxEle)} m
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--text-primary)', fontWeight: 600 }}> <div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
<Mountain size={12} color="#ef4444" /> <Mountain size={12} color="#ef4444" />
{Math.round(minEle)} m {Math.round(minEle)} m
</div> </div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}> <div className="text-content-muted" style={{ fontSize: 12 }}>
{Math.round(totalUp)} m &nbsp;{Math.round(totalDown)} m {Math.round(totalUp)} m &nbsp;{Math.round(totalDown)} m
</div> </div>
</> </>
)} )}
</div> </div>
{pathD && ( {pathD && (
<svg width="100%" viewBox={`0 0 ${chartW} ${chartH}`} preserveAspectRatio="none" style={{ display: 'block', borderRadius: 6, background: 'var(--bg-tertiary)' }}> <svg width="100%" viewBox={`0 0 ${chartW} ${chartH}`} preserveAspectRatio="none" className="bg-surface-tertiary" style={{ display: 'block', borderRadius: 6 }}>
<defs> <defs>
<linearGradient id={`ele-grad-${place.id}`} x1="0" y1="0" x2="0" y2="1"> <linearGradient id={`ele-grad-${place.id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.25" /> <stop offset="0%" stopColor="#3b82f6" stopOpacity="0.25" />
@@ -810,20 +812,20 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
{/* Files section */} {/* Files section */}
{(placeFiles.length > 0 || onFileUpload) && ( {(placeFiles.length > 0 || onFileUpload) && (
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, overflow: 'hidden' }}> <div className="bg-surface-hover" style={{ borderRadius: 10, overflow: 'hidden' }}>
<div style={{ display: 'flex', alignItems: 'center', padding: '8px 12px', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', padding: '8px 12px', gap: 6 }}>
<button <button
onClick={() => setFilesExpanded(f => !f)} onClick={() => setFilesExpanded(f => !f)}
style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontFamily: 'inherit', textAlign: 'left' }} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontFamily: 'inherit', textAlign: 'left' }}
> >
<FileText size={13} color="#9ca3af" /> <FileText size={13} color="#9ca3af" />
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}> <span className="text-content-secondary" style={{ fontSize: 12, fontWeight: 500 }}>
{placeFiles.length > 0 ? t('inspector.filesCount', { count: placeFiles.length }) : t('inspector.files')} {placeFiles.length > 0 ? t('inspector.filesCount', { count: placeFiles.length }) : t('inspector.files')}
</span> </span>
{filesExpanded ? <ChevronUp size={12} color="#9ca3af" /> : <ChevronDown size={12} color="#9ca3af" />} {filesExpanded ? <ChevronUp size={12} color="#9ca3af" /> : <ChevronDown size={12} color="#9ca3af" />}
</button> </button>
{onFileUpload && ( {onFileUpload && (
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-muted)', padding: '2px 6px', borderRadius: 6, background: 'var(--bg-tertiary)' }}> <label className="text-content-muted bg-surface-tertiary" style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, padding: '2px 6px', borderRadius: 6 }}>
<input ref={fileInputRef} type="file" multiple style={{ display: 'none' }} onChange={handleFileUpload} /> <input ref={fileInputRef} type="file" multiple style={{ display: 'none' }} onChange={handleFileUpload} />
{isUploading ? ( {isUploading ? (
<span style={{ fontSize: 11 }}></span> <span style={{ fontSize: 11 }}></span>
@@ -838,8 +840,8 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
{placeFiles.map(f => ( {placeFiles.map(f => (
<button key={f.id} onClick={() => openFile(f.url).catch(() => {})} style={{ display: 'flex', alignItems: 'center', gap: 8, textDecoration: 'none', cursor: 'pointer', background: 'none', border: 'none', width: '100%', textAlign: 'left' }}> <button key={f.id} onClick={() => openFile(f.url).catch(() => {})} style={{ display: 'flex', alignItems: 'center', gap: 8, textDecoration: 'none', cursor: 'pointer', background: 'none', border: 'none', width: '100%', textAlign: 'left' }}>
{(f.mime_type || '').startsWith('image/') ? <FileImage size={12} color="#6b7280" /> : <File size={12} color="#6b7280" />} {(f.mime_type || '').startsWith('image/') ? <FileImage size={12} color="#6b7280" /> : <File size={12} color="#6b7280" />}
<span style={{ fontSize: 12, color: 'var(--text-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span> <span className="text-content-secondary" style={{ fontSize: 12, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
{f.file_size && <span style={{ fontSize: 11, color: 'var(--text-faint)', flexShrink: 0 }}>{formatFileSize(f.file_size)}</span>} {f.file_size && <span className="text-content-faint" style={{ fontSize: 11, flexShrink: 0 }}>{formatFileSize(f.file_size)}</span>}
</button> </button>
))} ))}
</div> </div>
+54 -52
View File
@@ -96,10 +96,9 @@ const MemoPlaceRow = React.memo(function MemoPlaceRow({
onMouseLeave={e => { if (!isSelected && !isChecked) e.currentTarget.style.background = 'transparent' }} onMouseLeave={e => { if (!isSelected && !isChecked) e.currentTarget.style.background = 'transparent' }}
> >
{selectMode && ( {selectMode && (
<div style={{ <div className={isChecked ? 'bg-accent' : 'bg-transparent'} style={{
width: 16, height: 16, borderRadius: 4, flexShrink: 0, width: 16, height: 16, borderRadius: 4, flexShrink: 0,
border: isChecked ? 'none' : '1.5px solid var(--border-primary)', border: isChecked ? 'none' : '1.5px solid var(--border-primary)',
background: isChecked ? 'var(--accent)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
{isChecked && <Check size={10} strokeWidth={3} color="white" />} {isChecked && <Check size={10} strokeWidth={3} color="white" />}
@@ -113,13 +112,13 @@ const MemoPlaceRow = React.memo(function MemoPlaceRow({
const CatIcon = getCategoryIcon(cat.icon) const CatIcon = getCategoryIcon(cat.icon)
return <span title={cat.name} style={{ display: 'inline-flex', flexShrink: 0 }}><CatIcon size={11} strokeWidth={2} color={cat.color || '#6366f1'} /></span> return <span title={cat.name} style={{ display: 'inline-flex', flexShrink: 0 }}><CatIcon size={11} strokeWidth={2} color={cat.color || '#6366f1'} /></span>
})()} })()}
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: 1.2 }}> <span className="text-content" style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: 1.2 }}>
{place.name} {place.name}
</span> </span>
</div> </div>
{(place.description || place.address || cat?.name) && ( {(place.description || place.address || cat?.name) && (
<div style={{ marginTop: 2 }}> <div style={{ marginTop: 2 }}>
<span style={{ fontSize: 11, color: 'var(--text-faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block', lineHeight: 1.2 }}> <span className="text-content-faint" style={{ fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block', lineHeight: 1.2 }}>
{place.description || place.address || cat?.name} {place.description || place.address || cat?.name}
</span> </span>
</div> </div>
@@ -129,11 +128,12 @@ const MemoPlaceRow = React.memo(function MemoPlaceRow({
{!selectMode && !inDay && selectedDayId && ( {!selectMode && !inDay && selectedDayId && (
<button <button
onClick={e => { e.stopPropagation(); onAssignToDay(place.id) }} onClick={e => { e.stopPropagation(); onAssignToDay(place.id) }}
className="bg-surface-hover text-content-faint"
style={{ style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 20, height: 20, borderRadius: 6, width: 20, height: 20, borderRadius: 6,
background: 'var(--bg-hover)', border: 'none', cursor: 'pointer', border: 'none', cursor: 'pointer',
color: 'var(--text-faint)', padding: 0, transition: 'background 0.15s, color 0.15s', padding: 0, transition: 'background 0.15s, color 0.15s',
}} }}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent-text)' }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent-text)' }}
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-hover)'; e.currentTarget.style.color = 'var(--text-faint)' }} onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-hover)'; e.currentTarget.style.color = 'var(--text-faint)' }}
@@ -357,7 +357,7 @@ function PlacesDropOverlay({ t }: SidebarState) {
gap: 10, pointerEvents: 'none', gap: 10, pointerEvents: 'none',
}}> }}>
<Upload size={28} strokeWidth={1.5} color="var(--accent)" /> <Upload size={28} strokeWidth={1.5} color="var(--accent)" />
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--accent)' }}>{t('places.sidebarDrop')}</span> <span className="text-accent" style={{ fontSize: 13, fontWeight: 600 }}>{t('places.sidebarDrop')}</span>
</div> </div>
) )
} }
@@ -443,22 +443,20 @@ function PlacesHeader(S: SidebarState) {
<button <button
key={f.id} key={f.id}
onClick={() => { setFilter(f.id); onPlacesFilterChange?.(f.id); setSelectedIds(new Set()) }} onClick={() => { setFilter(f.id); onPlacesFilterChange?.(f.id); setSelectedIds(new Set()) }}
className={active ? 'bg-accent text-accent-text' : 'bg-surface-card text-content'}
style={{ style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 5, display: 'inline-flex', alignItems: 'center', gap: 5,
padding: '4px 9px', borderRadius: 99, padding: '4px 9px', borderRadius: 99,
fontSize: 11, fontWeight: 500, whiteSpace: 'nowrap', fontSize: 11, fontWeight: 500, whiteSpace: 'nowrap',
background: active ? 'var(--accent)' : 'var(--bg-card)',
color: active ? 'var(--accent-text)' : 'var(--text-primary)',
boxShadow: active ? 'none' : '0 1px 2px rgba(0,0,0,0.06)', boxShadow: active ? 'none' : '0 1px 2px rgba(0,0,0,0.06)',
transition: 'background 0.15s, color 0.15s, box-shadow 0.15s', transition: 'background 0.15s, color 0.15s, box-shadow 0.15s',
}} }}
> >
{f.label} {f.label}
<span style={{ <span className={active ? 'text-accent-text' : 'text-content-faint'} style={{
fontSize: 9, fontWeight: 600, lineHeight: 1, fontSize: 9, fontWeight: 600, lineHeight: 1,
background: active ? 'color-mix(in srgb, var(--accent-text) 22%, transparent)' : 'var(--bg-tertiary)', background: active ? 'color-mix(in srgb, var(--accent-text) 22%, transparent)' : 'var(--bg-tertiary)',
color: active ? 'var(--accent-text)' : 'var(--text-faint)',
padding: '1px 5px', borderRadius: 99, minWidth: 14, textAlign: 'center', padding: '1px 5px', borderRadius: 99, minWidth: 14, textAlign: 'center',
}}> }}>
{counts[f.id]} {counts[f.id]}
@@ -478,9 +476,10 @@ function PlacesHeader(S: SidebarState) {
value={search} value={search}
onChange={e => { setSearch(e.target.value); if (selectMode) setSelectedIds(new Set()) }} onChange={e => { setSearch(e.target.value); if (selectMode) setSelectedIds(new Set()) }}
placeholder={t('places.search')} placeholder={t('places.search')}
className="bg-surface-tertiary text-content"
style={{ style={{
width: '100%', padding: '7px 30px 7px 30px', borderRadius: 10, width: '100%', padding: '7px 30px 7px 30px', borderRadius: 10,
border: 'none', background: 'var(--bg-tertiary)', fontSize: 12, color: 'var(--text-primary)', border: 'none', fontSize: 12,
outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box', outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box',
}} }}
/> />
@@ -500,14 +499,14 @@ function PlacesHeader(S: SidebarState) {
: `${categoryFilters.size} ${t('places.categoriesSelected')}` : `${categoryFilters.size} ${t('places.categoriesSelected')}`
return ( return (
<div style={{ marginTop: 6, position: 'relative', display: 'flex', gap: 6, alignItems: 'stretch' }}> <div style={{ marginTop: 6, position: 'relative', display: 'flex', gap: 6, alignItems: 'stretch' }}>
<button onClick={() => setCatDropOpen(v => !v)} style={{ <button onClick={() => setCatDropOpen(v => !v)} className="bg-surface-card text-content" style={{
flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)', padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)',
background: 'var(--bg-card)', fontSize: 12, color: 'var(--text-primary)', fontSize: 12,
cursor: 'pointer', fontFamily: 'inherit', cursor: 'pointer', fontFamily: 'inherit',
}}> }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
<ChevronDown size={12} style={{ flexShrink: 0, color: 'var(--text-faint)', transform: catDropOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} /> <ChevronDown size={12} className="text-content-faint" style={{ flexShrink: 0, transform: catDropOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
</button> </button>
{canEditPlaces && ( {canEditPlaces && (
<Tooltip label={t('common.select')} placement="bottom"> <Tooltip label={t('common.select')} placement="bottom">
@@ -515,11 +514,11 @@ function PlacesHeader(S: SidebarState) {
onClick={() => { setSelectMode(v => !v); setSelectedIds(new Set()) }} onClick={() => { setSelectMode(v => !v); setSelectedIds(new Set()) }}
aria-label={t('common.select')} aria-label={t('common.select')}
aria-pressed={selectMode} aria-pressed={selectMode}
className={selectMode ? 'text-accent' : 'text-content-faint'}
style={{ style={{
position: 'relative', width: 30, flexShrink: 0, borderRadius: 8, position: 'relative', width: 30, flexShrink: 0, borderRadius: 8,
border: `1px solid ${selectMode ? 'var(--accent)' : 'var(--border-primary)'}`, border: `1px solid ${selectMode ? 'var(--accent)' : 'var(--border-primary)'}`,
background: selectMode ? 'color-mix(in srgb, var(--accent) 14%, transparent)' : 'var(--bg-card)', background: selectMode ? 'color-mix(in srgb, var(--accent) 14%, transparent)' : 'var(--bg-card)',
color: selectMode ? 'var(--accent)' : 'var(--text-faint)',
cursor: 'pointer', fontFamily: 'inherit', padding: 0, cursor: 'pointer', fontFamily: 'inherit', padding: 0,
transition: 'background 0.18s, color 0.18s, border-color 0.18s', transition: 'background 0.18s, color 0.18s, border-color 0.18s',
overflow: 'hidden', overflow: 'hidden',
@@ -545,20 +544,19 @@ function PlacesHeader(S: SidebarState) {
</Tooltip> </Tooltip>
)} )}
{catDropOpen && ( {catDropOpen && (
<div style={{ <div className="bg-surface-card" style={{
position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 50, marginTop: 4, position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 50, marginTop: 4,
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10, border: '1px solid var(--border-primary)', borderRadius: 10,
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, maxHeight: 200, overflowY: 'auto', boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, maxHeight: 200, overflowY: 'auto',
}}> }}>
{categories.map(c => { {categories.map(c => {
const active = categoryFilters.has(String(c.id)) const active = categoryFilters.has(String(c.id))
const CatIcon = getCategoryIcon(c.icon) const CatIcon = getCategoryIcon(c.icon)
return ( return (
<button key={c.id} onClick={() => toggleCategoryFilter(String(c.id))} style={{ <button key={c.id} onClick={() => toggleCategoryFilter(String(c.id))} className={`text-content ${active ? 'bg-surface-hover' : 'bg-transparent'}`} style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%', display: 'flex', alignItems: 'center', gap: 8, width: '100%',
padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer', padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
background: active ? 'var(--bg-hover)' : 'transparent', fontFamily: 'inherit', fontSize: 12,
fontFamily: 'inherit', fontSize: 12, color: 'var(--text-primary)',
textAlign: 'left', textAlign: 'left',
}}> }}>
<div style={{ <div style={{
@@ -577,17 +575,15 @@ function PlacesHeader(S: SidebarState) {
{places.some(p => p.category_id == null) && (() => { {places.some(p => p.category_id == null) && (() => {
const active = categoryFilters.has('uncategorized') const active = categoryFilters.has('uncategorized')
return ( return (
<button onClick={() => toggleCategoryFilter('uncategorized')} style={{ <button onClick={() => toggleCategoryFilter('uncategorized')} className={`text-content-muted ${active ? 'bg-surface-hover' : 'bg-transparent'}`} style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%', display: 'flex', alignItems: 'center', gap: 8, width: '100%',
padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer', padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
background: active ? 'var(--bg-hover)' : 'transparent', fontFamily: 'inherit', fontSize: 12,
fontFamily: 'inherit', fontSize: 12, color: 'var(--text-muted)',
textAlign: 'left', borderTop: '1px solid var(--border-faint)', marginTop: 2, textAlign: 'left', borderTop: '1px solid var(--border-faint)', marginTop: 2,
}}> }}>
<div style={{ <div className={active ? 'bg-[var(--text-faint)]' : 'bg-transparent'} style={{
width: 16, height: 16, borderRadius: 4, flexShrink: 0, width: 16, height: 16, borderRadius: 4, flexShrink: 0,
border: active ? 'none' : '1.5px solid var(--border-primary)', border: active ? 'none' : '1.5px solid var(--border-primary)',
background: active ? 'var(--text-faint)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
{active && <Check size={10} strokeWidth={3} color="white" />} {active && <Check size={10} strokeWidth={3} color="white" />}
@@ -598,10 +594,10 @@ function PlacesHeader(S: SidebarState) {
) )
})()} })()}
{categoryFilters.size > 0 && ( {categoryFilters.size > 0 && (
<button onClick={() => { setCategoryFiltersLocal(new Set()); onCategoryFilterChange?.(new Set()) }} style={{ <button onClick={() => { setCategoryFiltersLocal(new Set()); onCategoryFilterChange?.(new Set()) }} className="bg-transparent text-content-faint" style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
width: '100%', padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer', width: '100%', padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
background: 'transparent', fontFamily: 'inherit', fontSize: 11, color: 'var(--text-faint)', fontFamily: 'inherit', fontSize: 11,
marginTop: 2, borderTop: '1px solid var(--border-faint)', marginTop: 2, borderTop: '1px solid var(--border-faint)',
}}> }}>
<X size={10} /> {t('places.clearFilter')} <X size={10} /> {t('places.clearFilter')}
@@ -624,7 +620,7 @@ function PlacesSelectionBar(S: SidebarState) {
background: 'color-mix(in srgb, var(--accent) 10%, transparent)', background: 'color-mix(in srgb, var(--accent) 10%, transparent)',
display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0, fontSize: 11, display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0, fontSize: 11,
}}> }}>
<span style={{ flex: 1, color: 'var(--accent)', fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <span className="text-accent" style={{ flex: 1, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{t('places.selectionCount', { count: selectedIds.size })} {t('places.selectionCount', { count: selectedIds.size })}
</span> </span>
<Tooltip label={selectedIds.size === filtered.length && filtered.length > 0 ? t('common.deselectAll') : t('common.selectAll')} placement="bottom"> <Tooltip label={selectedIds.size === filtered.length && filtered.length > 0 ? t('common.deselectAll') : t('common.selectAll')} placement="bottom">
@@ -634,10 +630,11 @@ function PlacesSelectionBar(S: SidebarState) {
else setSelectedIds(new Set(filtered.map(p => p.id))) else setSelectedIds(new Set(filtered.map(p => p.id)))
}} }}
aria-label={selectedIds.size === filtered.length && filtered.length > 0 ? t('common.deselectAll') : t('common.selectAll')} aria-label={selectedIds.size === filtered.length && filtered.length > 0 ? t('common.deselectAll') : t('common.selectAll')}
className="bg-transparent text-content-muted"
style={{ style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 24, height: 24, borderRadius: 6, border: 'none', width: 24, height: 24, borderRadius: 6, border: 'none',
background: 'transparent', color: 'var(--text-muted)', cursor: 'pointer', padding: 0, cursor: 'pointer', padding: 0,
}} }}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)' }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)' }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
@@ -654,11 +651,10 @@ function PlacesSelectionBar(S: SidebarState) {
}} }}
disabled={selectedIds.size === 0} disabled={selectedIds.size === 0}
aria-label={t('places.deleteSelected')} aria-label={t('places.deleteSelected')}
className={selectedIds.size > 0 ? 'bg-transparent text-[#ef4444]' : 'bg-transparent text-content-faint'}
style={{ style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 24, height: 24, borderRadius: 6, border: 'none', width: 24, height: 24, borderRadius: 6, border: 'none',
background: 'transparent',
color: selectedIds.size > 0 ? '#ef4444' : 'var(--text-faint)',
cursor: selectedIds.size > 0 ? 'pointer' : 'default', padding: 0, cursor: selectedIds.size > 0 ? 'pointer' : 'default', padding: 0,
}} }}
onMouseEnter={e => { if (selectedIds.size > 0) e.currentTarget.style.background = 'color-mix(in srgb, #ef4444 14%, transparent)' }} onMouseEnter={e => { if (selectedIds.size > 0) e.currentTarget.style.background = 'color-mix(in srgb, #ef4444 14%, transparent)' }}
@@ -734,17 +730,19 @@ function MobileDayPickerSheet(S: SidebarState) {
> >
<div <div
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
style={{ background: 'var(--bg-card)', borderRadius: '20px 20px 0 0', width: '100%', maxWidth: 500, maxHeight: '70vh', display: 'flex', flexDirection: 'column', overflow: 'hidden', paddingBottom: 'var(--bottom-nav-h)' }} className="bg-surface-card"
style={{ borderRadius: '20px 20px 0 0', width: '100%', maxWidth: 500, maxHeight: '70vh', display: 'flex', flexDirection: 'column', overflow: 'hidden', paddingBottom: 'var(--bottom-nav-h)' }}
> >
<div style={{ padding: '16px 20px 12px', borderBottom: '1px solid var(--border-secondary)' }}> <div style={{ padding: '16px 20px 12px', borderBottom: '1px solid var(--border-secondary)' }}>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)' }}>{dayPickerPlace.name}</div> <div className="text-content" style={{ fontSize: 15, fontWeight: 700 }}>{dayPickerPlace.name}</div>
{dayPickerPlace.address && <div style={{ fontSize: 12, color: 'var(--text-faint)', marginTop: 2 }}>{dayPickerPlace.address}</div>} {dayPickerPlace.address && <div className="text-content-faint" style={{ fontSize: 12, marginTop: 2 }}>{dayPickerPlace.address}</div>}
</div> </div>
<div style={{ overflowY: 'auto', padding: '8px 12px' }}> <div style={{ overflowY: 'auto', padding: '8px 12px' }}>
{/* View details */} {/* View details */}
<button <button
onClick={() => { onPlaceClick(dayPickerPlace.id); setDayPickerPlace(null); setMobileShowDays(false) }} onClick={() => { onPlaceClick(dayPickerPlace.id); setDayPickerPlace(null); setMobileShowDays(false) }}
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left', fontSize: 14, color: 'var(--text-primary)' }} className="bg-transparent text-content"
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', fontSize: 14 }}
> >
<Eye size={18} color="var(--text-muted)" /> {t('places.viewDetails')} <Eye size={18} color="var(--text-muted)" /> {t('places.viewDetails')}
</button> </button>
@@ -752,7 +750,8 @@ function MobileDayPickerSheet(S: SidebarState) {
{canEditPlaces && ( {canEditPlaces && (
<button <button
onClick={() => { onEditPlace(dayPickerPlace); setDayPickerPlace(null); setMobileShowDays(false) }} onClick={() => { onEditPlace(dayPickerPlace); setDayPickerPlace(null); setMobileShowDays(false) }}
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left', fontSize: 14, color: 'var(--text-primary)' }} className="bg-transparent text-content"
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', fontSize: 14 }}
> >
<Pencil size={18} color="var(--text-muted)" /> {t('common.edit')} <Pencil size={18} color="var(--text-muted)" /> {t('common.edit')}
</button> </button>
@@ -762,7 +761,8 @@ function MobileDayPickerSheet(S: SidebarState) {
<> <>
<button <button
onClick={() => setMobileShowDays(v => !v)} onClick={() => setMobileShowDays(v => !v)}
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left', fontSize: 14, color: 'var(--text-primary)' }} className="bg-transparent text-content"
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', fontSize: 14 }}
> >
<CalendarDays size={18} color="var(--text-muted)" /> {t('places.assignToDay')} <CalendarDays size={18} color="var(--text-muted)" /> {t('places.assignToDay')}
<ChevronDown size={14} style={{ marginLeft: 'auto', color: 'var(--text-faint)', transform: mobileShowDays ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} /> <ChevronDown size={14} style={{ marginLeft: 'auto', color: 'var(--text-faint)', transform: mobileShowDays ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
@@ -775,10 +775,10 @@ function MobileDayPickerSheet(S: SidebarState) {
onClick={() => { onAssignToDay(dayPickerPlace.id, day.id); setDayPickerPlace(null); setMobileShowDays(false) }} onClick={() => { onAssignToDay(dayPickerPlace.id, day.id); setDayPickerPlace(null); setMobileShowDays(false) }}
style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '10px 14px', borderRadius: 10, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left' }} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '10px 14px', borderRadius: 10, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left' }}
> >
<div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--bg-tertiary)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', flexShrink: 0 }}>{i + 1}</div> <div className="bg-surface-tertiary text-content" style={{ width: 28, height: 28, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0 }}>{i + 1}</div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>{day.title || t('dayplan.dayN', { n: i + 1 })}</div> <div className="text-content" style={{ fontSize: 13, fontWeight: 500 }}>{day.title || t('dayplan.dayN', { n: i + 1 })}</div>
{day.date && <div style={{ fontSize: 11, color: 'var(--text-faint)' }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(undefined, { timeZone: 'UTC' })}</div>} {day.date && <div className="text-content-faint" style={{ fontSize: 11 }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(undefined, { timeZone: 'UTC' })}</div>}
</div> </div>
{(assignments[String(day.id)] || []).some(a => a.place?.id === dayPickerPlace.id) && <Check size={14} color="var(--text-faint)" />} {(assignments[String(day.id)] || []).some(a => a.place?.id === dayPickerPlace.id) && <Check size={14} color="var(--text-faint)" />}
</button> </button>
@@ -811,13 +811,15 @@ function ListImportModal(S: SidebarState) {
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
onClick={() => { setListImportOpen(false); setListImportUrl('') }} onClick={() => { setListImportOpen(false); setListImportUrl('') }}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }} className="bg-[rgba(0,0,0,0.4)]"
style={{ position: 'fixed', inset: 0, zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
> >
<div <div
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
style={{ background: 'var(--bg-card)', borderRadius: 16, width: '100%', maxWidth: 440, padding: 24, boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }} className="bg-surface-card"
style={{ borderRadius: 16, width: '100%', maxWidth: 440, padding: 24, boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}
> >
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 4 }}> <div className="text-content" style={{ fontSize: 15, fontWeight: 700, marginBottom: 4 }}>
{t('places.importList')} {t('places.importList')}
</div> </div>
{hasMultipleListImportProviders && ( {hasMultipleListImportProviders && (
@@ -826,11 +828,10 @@ function ListImportModal(S: SidebarState) {
<button <button
key={provider} key={provider}
onClick={() => setListImportProvider(provider)} onClick={() => setListImportProvider(provider)}
className={listImportProvider === provider ? 'bg-accent text-accent-text' : 'bg-surface-tertiary text-content-muted'}
style={{ style={{
padding: '6px 10px', borderRadius: 20, border: 'none', cursor: 'pointer', padding: '6px 10px', borderRadius: 20, border: 'none', cursor: 'pointer',
fontSize: 11, fontWeight: 600, fontFamily: 'inherit', fontSize: 11, fontWeight: 600, fontFamily: 'inherit',
background: listImportProvider === provider ? 'var(--accent)' : 'var(--bg-tertiary)',
color: listImportProvider === provider ? 'var(--accent-text)' : 'var(--text-muted)',
}} }}
> >
{provider === 'google' ? t('places.importGoogleList') : t('places.importNaverList')} {provider === 'google' ? t('places.importGoogleList') : t('places.importNaverList')}
@@ -838,7 +839,7 @@ function ListImportModal(S: SidebarState) {
))} ))}
</div> </div>
)} )}
<div style={{ fontSize: 12, color: 'var(--text-faint)', marginBottom: 16 }}> <div className="text-content-faint" style={{ fontSize: 12, marginBottom: 16 }}>
{t(listImportProvider === 'google' ? 'places.googleListHint' : 'places.naverListHint')} {t(listImportProvider === 'google' ? 'places.googleListHint' : 'places.naverListHint')}
</div> </div>
<input <input
@@ -848,19 +849,21 @@ function ListImportModal(S: SidebarState) {
onKeyDown={e => { if (e.key === 'Enter' && !listImportLoading) handleListImport() }} onKeyDown={e => { if (e.key === 'Enter' && !listImportLoading) handleListImport() }}
placeholder={listImportProvider === 'google' ? 'https://maps.app.goo.gl/...' : 'https://naver.me/...'} placeholder={listImportProvider === 'google' ? 'https://maps.app.goo.gl/...' : 'https://naver.me/...'}
autoFocus autoFocus
className="bg-surface-tertiary text-content"
style={{ style={{
width: '100%', padding: '10px 14px', borderRadius: 10, width: '100%', padding: '10px 14px', borderRadius: 10,
border: '1px solid var(--border-primary)', background: 'var(--bg-tertiary)', border: '1px solid var(--border-primary)',
fontSize: 13, color: 'var(--text-primary)', outline: 'none', fontSize: 13, outline: 'none',
fontFamily: 'inherit', boxSizing: 'border-box', fontFamily: 'inherit', boxSizing: 'border-box',
}} }}
/> />
<div style={{ display: 'flex', gap: 8, marginTop: 16, justifyContent: 'flex-end' }}> <div style={{ display: 'flex', gap: 8, marginTop: 16, justifyContent: 'flex-end' }}>
<button <button
onClick={() => { setListImportOpen(false); setListImportUrl('') }} onClick={() => { setListImportOpen(false); setListImportUrl('') }}
className="text-content"
style={{ style={{
padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)',
background: 'none', color: 'var(--text-primary)', fontSize: 13, fontWeight: 500, background: 'none', fontSize: 13, fontWeight: 500,
cursor: 'pointer', fontFamily: 'inherit', cursor: 'pointer', fontFamily: 'inherit',
}} }}
> >
@@ -869,10 +872,9 @@ function ListImportModal(S: SidebarState) {
<button <button
onClick={handleListImport} onClick={handleListImport}
disabled={!listImportUrl.trim() || listImportLoading} disabled={!listImportUrl.trim() || listImportLoading}
className={!listImportUrl.trim() || listImportLoading ? 'bg-surface-tertiary text-content-faint' : 'bg-accent text-accent-text'}
style={{ style={{
padding: '8px 16px', borderRadius: 10, border: 'none', padding: '8px 16px', borderRadius: 10, border: 'none',
background: !listImportUrl.trim() || listImportLoading ? 'var(--bg-tertiary)' : 'var(--accent)',
color: !listImportUrl.trim() || listImportLoading ? 'var(--text-faint)' : 'var(--accent-text)',
fontSize: 13, fontWeight: 500, cursor: !listImportUrl.trim() || listImportLoading ? 'default' : 'pointer', fontSize: 13, fontWeight: 500, cursor: !listImportUrl.trim() || listImportLoading ? 'default' : 'pointer',
fontFamily: 'inherit', fontFamily: 'inherit',
}} }}
@@ -120,7 +120,7 @@ describe('ReservationModal', () => {
render(<ReservationModal {...defaultProps} />); render(<ReservationModal {...defaultProps} />);
const eventBtn = screen.getByRole('button', { name: /Event/i }); const eventBtn = screen.getByRole('button', { name: /Event/i });
await userEvent.click(eventBtn); await userEvent.click(eventBtn);
expect(eventBtn).toHaveStyle({ background: 'var(--text-primary)' }); expect(eventBtn).toHaveClass('bg-[var(--text-primary)]');
}); });
it('FE-PLANNER-RESMODAL-008: hotel type shows check-in/check-out time fields', async () => { it('FE-PLANNER-RESMODAL-008: hotel type shows check-in/check-out time fields', async () => {
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useMemo, type CSSProperties } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import apiClient from '../../api/client' import apiClient from '../../api/client'
import { useTripStore } from '../../store/tripStore' import { useTripStore } from '../../store/tripStore'
@@ -265,12 +265,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
) )
: [] : []
const inputStyle: CSSProperties = { const inputClass = 'w-full border border-edge rounded-[10px] px-[12px] py-[8px] text-[13px] font-[inherit] outline-none box-border text-content bg-surface-input'
width: '100%', border: '1px solid var(--border-primary)', borderRadius: 10, const labelClass = 'block text-[11px] font-semibold text-content-faint mb-[5px] uppercase tracking-[0.03em]'
padding: '8px 12px', fontSize: 13, fontFamily: 'inherit',
outline: 'none', boxSizing: 'border-box', color: 'var(--text-primary)', background: 'var(--bg-input)',
}
const labelStyle: CSSProperties = { display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--text-faint)', marginBottom: 5, textTransform: 'uppercase', letterSpacing: '0.03em' }
return ( return (
<Modal <Modal
@@ -280,10 +276,10 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
size="2xl" size="2xl"
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', color: 'var(--text-muted)' }}> <button type="button" onClick={onClose} className="text-content-muted" style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit' }}>
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button type="button" onClick={handleSubmit} disabled={isSaving || !form.title.trim() || isEndBeforeStart} style={{ padding: '8px 20px', borderRadius: 10, border: 'none', background: 'var(--text-primary)', color: 'var(--bg-primary)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: isSaving || !form.title.trim() || isEndBeforeStart ? 0.5 : 1 }}> <button type="button" onClick={handleSubmit} disabled={isSaving || !form.title.trim() || isEndBeforeStart} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: isSaving || !form.title.trim() || isEndBeforeStart ? 0.5 : 1 }}>
{isSaving ? t('common.saving') : reservation ? t('common.update') : t('common.add')} {isSaving ? t('common.saving') : reservation ? t('common.update') : t('common.add')}
</button> </button>
</div> </div>
@@ -293,16 +289,14 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
{/* Type selector */} {/* Type selector */}
<div> <div>
<label style={labelStyle}>{t('reservations.bookingType')}</label> <label className={labelClass}>{t('reservations.bookingType')}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
{TYPE_OPTIONS.map(({ value, labelKey, Icon }) => ( {TYPE_OPTIONS.map(({ value, labelKey, Icon }) => (
<button key={value} type="button" onClick={() => set('type', value)} style={{ <button key={value} type="button" onClick={() => set('type', value)} className={form.type === value ? 'bg-[var(--text-primary)] text-[var(--bg-primary)]' : 'bg-surface-card text-content-muted'} style={{
display: 'flex', alignItems: 'center', gap: 4, display: 'flex', alignItems: 'center', gap: 4,
padding: '5px 10px', borderRadius: 99, border: '1px solid', padding: '5px 10px', borderRadius: 99, border: '1px solid',
fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.12s', fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.12s',
background: form.type === value ? 'var(--text-primary)' : 'var(--bg-card)',
borderColor: form.type === value ? 'var(--text-primary)' : 'var(--border-primary)', borderColor: form.type === value ? 'var(--text-primary)' : 'var(--border-primary)',
color: form.type === value ? 'var(--bg-primary)' : 'var(--text-muted)',
}}> }}>
<Icon size={11} /> {t(labelKey)} <Icon size={11} /> {t(labelKey)}
</button> </button>
@@ -312,16 +306,16 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
{/* Title */} {/* Title */}
<div> <div>
<label style={labelStyle}>{t('reservations.titleLabel')} *</label> <label className={labelClass}>{t('reservations.titleLabel')} *</label>
<input type="text" value={form.title} onChange={e => set('title', e.target.value)} required <input type="text" value={form.title} onChange={e => set('title', e.target.value)} required
placeholder={t('reservations.titlePlaceholder')} style={inputStyle} /> placeholder={t('reservations.titlePlaceholder')} className={inputClass} />
</div> </div>
{/* Assignment Picker (hidden for hotels) */} {/* Assignment Picker (hidden for hotels) */}
{form.type !== 'hotel' && assignmentOptions.length > 0 && ( {form.type !== 'hotel' && assignmentOptions.length > 0 && (
<div> <div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}> <label className={labelClass}>
<Link2 size={10} style={{ display: 'inline', verticalAlign: '-1px', marginRight: 3 }} /> <Link2 size={10} style={{ display: 'inline', verticalAlign: '-1px', marginRight: 3 }} />
{t('reservations.linkAssignment')} {t('reservations.linkAssignment')}
</label> </label>
@@ -354,7 +348,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
<> <>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.date')}</label> <label className={labelClass}>{t('reservations.date')}</label>
<CustomDatePicker <CustomDatePicker
value={(() => { const [d] = (form.reservation_time || '').split('T'); return d || '' })()} value={(() => { const [d] = (form.reservation_time || '').split('T'); return d || '' })()}
onChange={d => { onChange={d => {
@@ -364,7 +358,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
/> />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.startTime')}</label> <label className={labelClass}>{t('reservations.startTime')}</label>
<CustomTimePicker <CustomTimePicker
value={(() => { const [, tm] = (form.reservation_time || '').split('T'); return tm || '' })()} value={(() => { const [, tm] = (form.reservation_time || '').split('T'); return tm || '' })()}
onChange={tm => { onChange={tm => {
@@ -378,19 +372,19 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
</div> </div>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.endDate')}</label> <label className={labelClass}>{t('reservations.endDate')}</label>
<CustomDatePicker <CustomDatePicker
value={form.end_date} value={form.end_date}
onChange={d => set('end_date', d || '')} onChange={d => set('end_date', d || '')}
/> />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.endTime')}</label> <label className={labelClass}>{t('reservations.endTime')}</label>
<CustomTimePicker value={form.reservation_end_time} onChange={v => set('reservation_end_time', v)} /> <CustomTimePicker value={form.reservation_end_time} onChange={v => set('reservation_end_time', v)} />
</div> </div>
</div> </div>
{isEndBeforeStart && ( {isEndBeforeStart && (
<div style={{ fontSize: 11, color: '#ef4444', marginTop: -6 }}>{t('reservations.validation.endBeforeStart')}</div> <div className="text-[#ef4444]" style={{ fontSize: 11, marginTop: -6 }}>{t('reservations.validation.endBeforeStart')}</div>
)} )}
</> </>
)} )}
@@ -398,21 +392,21 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
{/* Location */} {/* Location */}
{form.type !== 'hotel' && ( {form.type !== 'hotel' && (
<div> <div>
<label style={labelStyle}>{t('reservations.locationAddress')}</label> <label className={labelClass}>{t('reservations.locationAddress')}</label>
<input type="text" value={form.location} onChange={e => set('location', e.target.value)} <input type="text" value={form.location} onChange={e => set('location', e.target.value)}
placeholder={t('reservations.locationPlaceholder')} style={inputStyle} /> placeholder={t('reservations.locationPlaceholder')} className={inputClass} />
</div> </div>
)} )}
{/* Booking Code + Status */} {/* Booking Code + Status */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.confirmationCode')}</label> <label className={labelClass}>{t('reservations.confirmationCode')}</label>
<input type="text" value={form.confirmation_number} onChange={e => set('confirmation_number', e.target.value)} <input type="text" value={form.confirmation_number} onChange={e => set('confirmation_number', e.target.value)}
placeholder={t('reservations.confirmationPlaceholder')} style={inputStyle} /> placeholder={t('reservations.confirmationPlaceholder')} className={inputClass} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.status')}</label> <label className={labelClass}>{t('reservations.status')}</label>
<CustomSelect <CustomSelect
value={form.status} value={form.status}
onChange={value => set('status', value)} onChange={value => set('status', value)}
@@ -430,7 +424,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
<> <>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.meta.hotelPlace')}</label> <label className={labelClass}>{t('reservations.meta.hotelPlace')}</label>
<CustomSelect <CustomSelect
value={form.hotel_place_id} value={form.hotel_place_id}
onChange={value => { onChange={value => {
@@ -456,7 +450,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
/> />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.fromDay')}</label> <label className={labelClass}>{t('reservations.meta.fromDay')}</label>
<CustomSelect <CustomSelect
value={form.hotel_start_day} value={form.hotel_start_day}
onChange={value => setForm(prev => ({ onChange={value => setForm(prev => ({
@@ -479,7 +473,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
/> />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.toDay')}</label> <label className={labelClass}>{t('reservations.meta.toDay')}</label>
<CustomSelect <CustomSelect
value={form.hotel_end_day} value={form.hotel_end_day}
onChange={value => setForm(prev => ({ onChange={value => setForm(prev => ({
@@ -504,15 +498,15 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.meta.checkIn')}</label> <label className={labelClass}>{t('reservations.meta.checkIn')}</label>
<CustomTimePicker value={form.meta_check_in_time} onChange={v => set('meta_check_in_time', v)} /> <CustomTimePicker value={form.meta_check_in_time} onChange={v => set('meta_check_in_time', v)} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.checkInUntil')}</label> <label className={labelClass}>{t('reservations.meta.checkInUntil')}</label>
<CustomTimePicker value={form.meta_check_in_end_time} onChange={v => set('meta_check_in_end_time', v)} /> <CustomTimePicker value={form.meta_check_in_end_time} onChange={v => set('meta_check_in_end_time', v)} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.checkOut')}</label> <label className={labelClass}>{t('reservations.meta.checkOut')}</label>
<CustomTimePicker value={form.meta_check_out_time} onChange={v => set('meta_check_out_time', v)} /> <CustomTimePicker value={form.meta_check_out_time} onChange={v => set('meta_check_out_time', v)} />
</div> </div>
</div> </div>
@@ -521,21 +515,21 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
{/* Notes */} {/* Notes */}
<div> <div>
<label style={labelStyle}>{t('reservations.notes')}</label> <label className={labelClass}>{t('reservations.notes')}</label>
<textarea value={form.notes} onChange={e => set('notes', e.target.value)} rows={2} <textarea value={form.notes} onChange={e => set('notes', e.target.value)} rows={2}
placeholder={t('reservations.notesPlaceholder')} placeholder={t('reservations.notesPlaceholder')}
style={{ ...inputStyle, resize: 'none', lineHeight: 1.5 }} /> className={inputClass} style={{ resize: 'none', lineHeight: 1.5 }} />
</div> </div>
{/* Files */} {/* Files */}
<div> <div>
<label style={labelStyle}>{t('files.title')}</label> <label className={labelClass}>{t('files.title')}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{attachedFiles.map(f => ( {attachedFiles.map(f => (
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', background: 'var(--bg-secondary)', borderRadius: 8 }}> <div key={f.id} className="bg-surface-secondary" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', borderRadius: 8 }}>
<FileText size={12} style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> <FileText size={12} className="text-content-muted" style={{ flexShrink: 0 }} />
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span> <span className="text-content-secondary" style={{ flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
<a href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} style={{ color: 'var(--text-faint)', display: 'flex', flexShrink: 0, cursor: 'pointer' }}><ExternalLink size={11} /></a> <a href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} className="text-content-faint" style={{ display: 'flex', flexShrink: 0, cursor: 'pointer' }}><ExternalLink size={11} /></a>
<button type="button" onClick={async () => { <button type="button" onClick={async () => {
if (f.reservation_id === reservation?.id) { if (f.reservation_id === reservation?.id) {
try { await apiClient.put(`/trips/${tripId}/files/${f.id}`, { reservation_id: null }) } catch { toast.error(t('reservations.toast.updateError')) } try { await apiClient.put(`/trips/${tripId}/files/${f.id}`, { reservation_id: null }) } catch { toast.error(t('reservations.toast.updateError')) }
@@ -547,44 +541,44 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
} catch { toast.error(t('reservations.toast.updateError')) } } catch { toast.error(t('reservations.toast.updateError')) }
setLinkedFileIds(prev => prev.filter(id => id !== f.id)) setLinkedFileIds(prev => prev.filter(id => id !== f.id))
if (tripId) loadFiles(tripId) if (tripId) loadFiles(tripId)
}} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 0, flexShrink: 0 }}> }} className="text-content-faint" style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', padding: 0, flexShrink: 0 }}>
<X size={11} /> <X size={11} />
</button> </button>
</div> </div>
))} ))}
{pendingFiles.map((f, i) => ( {pendingFiles.map((f, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', background: 'var(--bg-secondary)', borderRadius: 8 }}> <div key={i} className="bg-surface-secondary" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', borderRadius: 8 }}>
<FileText size={12} style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> <FileText size={12} className="text-content-muted" style={{ flexShrink: 0 }} />
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span> <span className="text-content-secondary" style={{ flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
<button type="button" onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))} <button type="button" onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 0, flexShrink: 0 }}> className="text-content-faint" style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', padding: 0, flexShrink: 0 }}>
<X size={11} /> <X size={11} />
</button> </button>
</div> </div>
))} ))}
<input ref={fileInputRef} type="file" accept=".pdf,.doc,.docx,.txt,image/*" style={{ display: 'none' }} onChange={handleFileChange} /> <input ref={fileInputRef} type="file" accept=".pdf,.doc,.docx,.txt,image/*" style={{ display: 'none' }} onChange={handleFileChange} />
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{onFileUpload && <button type="button" onClick={() => fileInputRef.current?.click()} disabled={uploadingFile} style={{ {onFileUpload && <button type="button" onClick={() => fileInputRef.current?.click()} disabled={uploadingFile} className="text-content-faint" style={{
display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px',
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none', border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
fontSize: 11, color: 'var(--text-faint)', cursor: uploadingFile ? 'default' : 'pointer', fontFamily: 'inherit', fontSize: 11, cursor: uploadingFile ? 'default' : 'pointer', fontFamily: 'inherit',
}}> }}>
<Paperclip size={11} /> <Paperclip size={11} />
{uploadingFile ? t('reservations.uploading') : t('reservations.attachFile')} {uploadingFile ? t('reservations.uploading') : t('reservations.attachFile')}
</button>} </button>}
{reservation?.id && files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).length > 0 && ( {reservation?.id && files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).length > 0 && (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button type="button" onClick={() => setShowFilePicker(v => !v)} style={{ <button type="button" onClick={() => setShowFilePicker(v => !v)} className="text-content-faint" style={{
display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px',
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none', border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
fontSize: 11, color: 'var(--text-faint)', cursor: 'pointer', fontFamily: 'inherit', fontSize: 11, cursor: 'pointer', fontFamily: 'inherit',
}}> }}>
<Link2 size={11} /> {t('reservations.linkExisting')} <Link2 size={11} /> {t('reservations.linkExisting')}
</button> </button>
{showFilePicker && ( {showFilePicker && (
<div style={{ <div className="bg-surface-card" style={{
position: 'absolute', bottom: '100%', left: 0, marginBottom: 4, zIndex: 50, position: 'absolute', bottom: '100%', left: 0, marginBottom: 4, zIndex: 50,
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10, border: '1px solid var(--border-primary)', borderRadius: 10,
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 220, maxHeight: 200, overflowY: 'auto', boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 220, maxHeight: 200, overflowY: 'auto',
}}> }}>
{files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).map(f => ( {files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).map(f => (
@@ -596,14 +590,15 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
if (tripId) loadFiles(tripId) if (tripId) loadFiles(tripId)
} catch { toast.error(t('reservations.toast.updateError')) } } catch { toast.error(t('reservations.toast.updateError')) }
}} }}
className="text-content-secondary"
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 10px',
background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, fontFamily: 'inherit', background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, fontFamily: 'inherit',
color: 'var(--text-secondary)', borderRadius: 7, textAlign: 'left', borderRadius: 7, textAlign: 'left',
}} }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}> onMouseLeave={e => e.currentTarget.style.background = 'none'}>
<FileText size={12} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <FileText size={12} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
</button> </button>
))} ))}
@@ -620,15 +615,15 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
<> <>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.price')}</label> <label className={labelClass}>{t('reservations.price')}</label>
<input type="text" inputMode="decimal" value={form.price} <input type="text" inputMode="decimal" value={form.price}
onChange={e => { const v = e.target.value; if (v === '' || /^\d*[.,]?\d{0,2}$/.test(v)) set('price', v.replace(',', '.')) }} onChange={e => { const v = e.target.value; if (v === '' || /^\d*[.,]?\d{0,2}$/.test(v)) set('price', v.replace(',', '.')) }}
onPaste={e => { e.preventDefault(); let txt = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = txt.lastIndexOf(','), ld = txt.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { txt = txt.substring(0, dp).replace(/[.,]/g, '') + '.' + txt.substring(dp + 1) } else { txt = txt.replace(/[.,]/g, '') } set('price', txt) }} onPaste={e => { e.preventDefault(); let txt = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = txt.lastIndexOf(','), ld = txt.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { txt = txt.substring(0, dp).replace(/[.,]/g, '') + '.' + txt.substring(dp + 1) } else { txt = txt.replace(/[.,]/g, '') } set('price', txt) }}
placeholder="0.00" placeholder="0.00"
style={inputStyle} /> className={inputClass} />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.budgetCategory')}</label> <label className={labelClass}>{t('reservations.budgetCategory')}</label>
<CustomSelect <CustomSelect
value={form.budget_category} value={form.budget_category}
onChange={v => set('budget_category', v)} onChange={v => set('budget_category', v)}
@@ -642,7 +637,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
</div> </div>
</div> </div>
{form.price && parseFloat(form.price) > 0 && ( {form.price && parseFloat(form.price) > 0 && (
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: -4 }}> <div className="text-content-faint" style={{ fontSize: 11, marginTop: -4 }}>
{t('reservations.budgetHint')} {t('reservations.budgetHint')}
</div> </div>
)} )}
@@ -54,15 +54,9 @@ function buildAssignmentLookup(days, assignments) {
return map return map
} }
/* ── Shared field label style ── */ /* ── Shared field label/value styles ── */
const fieldLabelStyle: React.CSSProperties = { const fieldLabelClass = 'text-[10px] font-semibold uppercase tracking-[0.08em] text-content-faint mb-[5px]'
fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', const fieldValueClass = 'text-[13px] font-medium text-content px-[10px] py-[8px] bg-surface-tertiary rounded-[10px]'
color: 'var(--text-faint)', marginBottom: 5,
}
const fieldValueStyle: React.CSSProperties = {
fontSize: 13, fontWeight: 500, color: 'var(--text-primary)',
padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 10,
}
interface ReservationCardProps { interface ReservationCardProps {
r: Reservation r: Reservation
@@ -129,9 +123,9 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}> <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
<span>{name}</span> <span>{name}</span>
{badge && ( {badge && (
<span style={{ <span className="text-content-faint bg-surface-secondary" style={{
fontSize: 10, fontWeight: 600, color: 'var(--text-faint)', fontSize: 10, fontWeight: 600,
background: 'var(--bg-secondary)', padding: '1px 6px', borderRadius: 999, padding: '1px 6px', borderRadius: 999,
}}>{badge}</span> }}>{badge}</span>
)} )}
</span> </span>
@@ -139,10 +133,9 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
} }
return ( return (
<div style={{ <div className="bg-surface-card" style={{
borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column', borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column',
border: `1px solid ${confirmed ? 'rgba(22,163,74,0.25)' : 'rgba(217,119,6,0.25)'}`, border: `1px solid ${confirmed ? 'rgba(22,163,74,0.25)' : 'rgba(217,119,6,0.25)'}`,
background: 'var(--bg-card)',
transition: 'box-shadow 0.15s ease', transition: 'box-shadow 0.15s ease',
}} }}
onMouseEnter={e => e.currentTarget.style.boxShadow = '0 2px 12px rgba(0,0,0,0.06)'} onMouseEnter={e => e.currentTarget.style.boxShadow = '0 2px 12px rgba(0,0,0,0.06)'}
@@ -150,35 +143,32 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
> >
{/* Header wraps to a second row on narrow screens so the status/category chips {/* Header wraps to a second row on narrow screens so the status/category chips
never collide with the title. */} never collide with the title. */}
<div style={{ <div className={confirmed ? 'bg-[rgba(22,163,74,0.06)]' : 'bg-[rgba(217,119,6,0.06)]'} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
flexWrap: 'wrap', flexWrap: 'wrap',
padding: '12px 14px', padding: '12px 14px',
background: confirmed ? 'rgba(22,163,74,0.06)' : 'rgba(217,119,6,0.06)',
}}> }}>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, minWidth: 0, flexWrap: 'wrap' }}> <div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, minWidth: 0, flexWrap: 'wrap' }}>
<span style={{ <span className={confirmed ? 'text-[#16a34a]' : 'text-[#d97706]'} style={{
display: 'inline-flex', alignItems: 'center', gap: 6, display: 'inline-flex', alignItems: 'center', gap: 6,
fontSize: 12, fontWeight: 600, color: confirmed ? '#16a34a' : '#d97706', fontSize: 12, fontWeight: 600,
}}> }}>
<span style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0, background: confirmed ? '#16a34a' : '#d97706' }} /> <span className={confirmed ? 'bg-[#16a34a]' : 'bg-[#d97706]'} style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0 }} />
{confirmed ? t('reservations.confirmed') : t('reservations.pending')} {confirmed ? t('reservations.confirmed') : t('reservations.pending')}
</span> </span>
<span style={{ <span className="text-content-muted bg-surface-secondary" style={{
display: 'inline-flex', alignItems: 'center', gap: 5, display: 'inline-flex', alignItems: 'center', gap: 5,
fontSize: 12, color: 'var(--text-muted)', fontSize: 12,
padding: '3px 8px', borderRadius: 6, padding: '3px 8px', borderRadius: 6,
background: 'var(--bg-secondary)',
}}> }}>
<TypeIcon size={12} style={{ color: typeInfo.color }} /> <TypeIcon size={12} style={{ color: typeInfo.color }} />
{t(typeInfo.labelKey)} {t(typeInfo.labelKey)}
</span> </span>
{r.needs_review ? ( {r.needs_review ? (
<span style={{ <span className="text-[#b45309] bg-[rgba(245,158,11,0.12)]" style={{
display: 'inline-flex', alignItems: 'center', gap: 4, display: 'inline-flex', alignItems: 'center', gap: 4,
fontSize: 11, fontWeight: 600, color: '#b45309', fontSize: 11, fontWeight: 600,
padding: '3px 8px', borderRadius: 6, padding: '3px 8px', borderRadius: 6,
background: 'rgba(245,158,11,0.12)',
}} title={t('reservations.needsReviewHint')}> }} title={t('reservations.needsReviewHint')}>
<AlertCircle size={11} /> <AlertCircle size={11} />
{t('reservations.needsReview')} {t('reservations.needsReview')}
@@ -186,15 +176,15 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
) : null} ) : null}
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 2 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<span style={{ <span className="text-content" style={{
fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', marginRight: 6, fontSize: 13, fontWeight: 600, marginRight: 6,
maxWidth: 140, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 140, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>{r.title}</span> }}>{r.title}</span>
{canEdit && ( {canEdit && (
<button onClick={() => onEdit(r)} title={t('common.edit')} style={{ <button onClick={() => onEdit(r)} title={t('common.edit')} className="bg-transparent text-content-faint" style={{
appearance: 'none', border: 'none', background: 'transparent', appearance: 'none', border: 'none',
width: 26, height: 26, borderRadius: 6, display: 'grid', placeItems: 'center', width: 26, height: 26, borderRadius: 6, display: 'grid', placeItems: 'center',
cursor: 'pointer', color: 'var(--text-faint)', flexShrink: 0, cursor: 'pointer', flexShrink: 0,
}} }}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(0,0,0,0.05)'; e.currentTarget.style.color = 'var(--text-primary)' }} onMouseEnter={e => { e.currentTarget.style.background = 'rgba(0,0,0,0.05)'; e.currentTarget.style.color = 'var(--text-primary)' }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-faint)' }}> onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-faint)' }}>
@@ -202,10 +192,10 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
</button> </button>
)} )}
{canEdit && ( {canEdit && (
<button onClick={() => setShowDeleteConfirm(true)} title={t('common.delete')} style={{ <button onClick={() => setShowDeleteConfirm(true)} title={t('common.delete')} className="bg-transparent text-content-faint" style={{
appearance: 'none', border: 'none', background: 'transparent', appearance: 'none', border: 'none',
width: 26, height: 26, borderRadius: 6, display: 'grid', placeItems: 'center', width: 26, height: 26, borderRadius: 6, display: 'grid', placeItems: 'center',
cursor: 'pointer', color: 'var(--text-faint)', flexShrink: 0, cursor: 'pointer', flexShrink: 0,
}} }}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(239,68,68,0.08)'; e.currentTarget.style.color = '#ef4444' }} onMouseEnter={e => { e.currentTarget.style.background = 'rgba(239,68,68,0.08)'; e.currentTarget.style.color = '#ef4444' }}
onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-faint)' }}> onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-faint)' }}>
@@ -220,11 +210,11 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{/* Day label for transport/hotel reservations linked to days */} {/* Day label for transport/hotel reservations linked to days */}
{(isTransportType || isHotel) && startDay && ( {(isTransportType || isHotel) && startDay && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.date')}</div> <div className={fieldLabelClass}>{t('reservations.date')}</div>
<div style={{ ...fieldValueStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, flexWrap: 'wrap' }}> <div className={fieldValueClass} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, flexWrap: 'wrap' }}>
<DayLabel day={startDay} /> <DayLabel day={startDay} />
{endDay && endDay.id !== startDay.id && ( {endDay && endDay.id !== startDay.id && (
<><span style={{ color: 'var(--text-faint)' }}></span><DayLabel day={endDay} /></> <><span className="text-content-faint"></span><DayLabel day={endDay} /></>
)} )}
</div> </div>
</div> </div>
@@ -234,8 +224,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
<div style={{ display: 'grid', gap: 10, gridTemplateColumns: hasDate && hasTime ? '1fr 1fr' : '1fr' }}> <div style={{ display: 'grid', gap: 10, gridTemplateColumns: hasDate && hasTime ? '1fr 1fr' : '1fr' }}>
{hasDate && ( {hasDate && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.date')}</div> <div className={fieldLabelClass}>{t('reservations.date')}</div>
<div style={{ ...fieldValueStyle, textAlign: 'center' }}> <div className={`${fieldValueClass} text-center`}>
{fmtDate(startDt.date!)} {fmtDate(startDt.date!)}
{endDt.date && endDt.date !== startDt.date && ( {endDt.date && endDt.date !== startDt.date && (
<> {fmtDate(endDt.date)}</> <> {fmtDate(endDt.date)}</>
@@ -245,8 +235,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
)} )}
{hasTime && ( {hasTime && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.time')}</div> <div className={fieldLabelClass}>{t('reservations.time')}</div>
<div style={{ ...fieldValueStyle, textAlign: 'center' }}> <div className={`${fieldValueClass} text-center`}>
{formatTime(startDt.time, locale, timeFormat)} {formatTime(startDt.time, locale, timeFormat)}
{endDt.time ? ` ${formatTime(endDt.time, locale, timeFormat)}` : ''} {endDt.time ? ` ${formatTime(endDt.time, locale, timeFormat)}` : ''}
</div> </div>
@@ -257,13 +247,13 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{/* Booking code */} {/* Booking code */}
{hasCode && ( {hasCode && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.confirmationCode')}</div> <div className={fieldLabelClass}>{t('reservations.confirmationCode')}</div>
<div <div
onMouseEnter={() => blurCodes && setCodeRevealed(true)} onMouseEnter={() => blurCodes && setCodeRevealed(true)}
onMouseLeave={() => blurCodes && setCodeRevealed(false)} onMouseLeave={() => blurCodes && setCodeRevealed(false)}
onClick={() => blurCodes && setCodeRevealed(v => !v)} onClick={() => blurCodes && setCodeRevealed(v => !v)}
className={`${fieldValueClass} text-center`}
style={{ style={{
...fieldValueStyle, textAlign: 'center',
fontFamily: '"SF Mono", "JetBrains Mono", Menlo, monospace', fontSize: 12.5, fontFamily: '"SF Mono", "JetBrains Mono", Menlo, monospace', fontSize: 12.5,
filter: blurCodes && !codeRevealed ? 'blur(5px)' : 'none', filter: blurCodes && !codeRevealed ? 'blur(5px)' : 'none',
cursor: blurCodes ? 'pointer' : 'default', cursor: blurCodes ? 'pointer' : 'default',
@@ -281,11 +271,10 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
const to = eps.find(e => e.role === 'to') const to = eps.find(e => e.role === 'to')
if (!from || !to) return null if (!from || !to) return null
return ( return (
<div style={{ <div className="bg-surface-tertiary text-content" style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
padding: '8px 12px', borderRadius: 10, padding: '8px 12px', borderRadius: 10,
background: 'var(--bg-tertiary)', fontSize: 12.5,
fontSize: 12.5, color: 'var(--text-primary)',
}}> }}>
<span style={{ fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{from.name}</span> <span style={{ fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{from.name}</span>
<TypeIcon size={14} style={{ color: typeInfo.color, flexShrink: 0 }} /> <TypeIcon size={14} style={{ color: typeInfo.color, flexShrink: 0 }} />
@@ -314,8 +303,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
<div style={{ display: 'grid', gap: 10, gridTemplateColumns: cells.length > 1 ? `repeat(${Math.min(cells.length, 3)}, 1fr)` : '1fr' }}> <div style={{ display: 'grid', gap: 10, gridTemplateColumns: cells.length > 1 ? `repeat(${Math.min(cells.length, 3)}, 1fr)` : '1fr' }}>
{cells.map((c, i) => ( {cells.map((c, i) => (
<div key={i}> <div key={i}>
<div style={fieldLabelStyle}>{c.label}</div> <div className={fieldLabelClass}>{c.label}</div>
<div style={{ ...fieldValueStyle, textAlign: 'center' }}>{c.value}</div> <div className={`${fieldValueClass} text-center`}>{c.value}</div>
</div> </div>
))} ))}
</div> </div>
@@ -325,27 +314,27 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{/* Location / Accommodation / Assignment */} {/* Location / Accommodation / Assignment */}
{r.location && ( {r.location && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.locationAddress')}</div> <div className={fieldLabelClass}>{t('reservations.locationAddress')}</div>
<div style={{ ...fieldValueStyle, display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400 }}> <div className={fieldValueClass} style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400 }}>
<MapPin size={13} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <MapPin size={13} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.location}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.location}</span>
</div> </div>
</div> </div>
)} )}
{r.accommodation_name && ( {r.accommodation_name && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.meta.linkAccommodation')}</div> <div className={fieldLabelClass}>{t('reservations.meta.linkAccommodation')}</div>
<div style={{ ...fieldValueStyle, display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400 }}> <div className={fieldValueClass} style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400 }}>
<Hotel size={13} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <Hotel size={13} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.accommodation_name}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.accommodation_name}</span>
</div> </div>
</div> </div>
)} )}
{linked && ( {linked && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.linkAssignment')}</div> <div className={fieldLabelClass}>{t('reservations.linkAssignment')}</div>
<div style={{ ...fieldValueStyle, display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400 }}> <div className={fieldValueClass} style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400 }}>
<Link2 size={13} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <Link2 size={13} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{linked.dayTitle || t('dayplan.dayN', { n: linked.dayNumber })} {linked.placeName} {linked.dayTitle || t('dayplan.dayN', { n: linked.dayNumber })} {linked.placeName}
{linked.startTime ? ` · ${linked.startTime}${linked.endTime ? ' ' + linked.endTime : ''}` : ''} {linked.startTime ? ` · ${linked.startTime}${linked.endTime ? ' ' + linked.endTime : ''}` : ''}
@@ -357,8 +346,8 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{/* Notes */} {/* Notes */}
{r.notes && ( {r.notes && (
<div> <div>
<div style={fieldLabelStyle}>{t('reservations.notes')}</div> <div className={fieldLabelClass}>{t('reservations.notes')}</div>
<div className="collab-note-md" style={{ ...fieldValueStyle, fontWeight: 400, lineHeight: 1.5, wordBreak: 'break-word', overflowWrap: 'anywhere' }}> <div className={`collab-note-md ${fieldValueClass}`} style={{ fontWeight: 400, lineHeight: 1.5, wordBreak: 'break-word', overflowWrap: 'anywhere' }}>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{r.notes}</Markdown> <Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{r.notes}</Markdown>
</div> </div>
</div> </div>
@@ -367,11 +356,11 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{/* Files */} {/* Files */}
{attachedFiles.length > 0 && ( {attachedFiles.length > 0 && (
<div> <div>
<div style={fieldLabelStyle}>{t('files.title')}</div> <div className={fieldLabelClass}>{t('files.title')}</div>
<div style={{ ...fieldValueStyle, display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 10px' }}> <div className={fieldValueClass} style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 10px' }}>
{attachedFiles.map(f => ( {attachedFiles.map(f => (
<a key={f.id} href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} style={{ display: 'flex', alignItems: 'center', gap: 5, textDecoration: 'none', cursor: 'pointer' }}> <a key={f.id} href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} style={{ display: 'flex', alignItems: 'center', gap: 5, textDecoration: 'none', cursor: 'pointer' }}>
<FileText size={11} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <FileText size={11} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ fontSize: 12, color: 'var(--text-muted)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span> <span style={{ fontSize: 12, color: 'var(--text-muted)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
</a> </a>
))} ))}
@@ -382,37 +371,37 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{/* Delete confirmation */} {/* Delete confirmation */}
{showDeleteConfirm && ReactDOM.createPortal( {showDeleteConfirm && ReactDOM.createPortal(
<div style={{ <div className="bg-[rgba(0,0,0,0.3)]" style={{
position: 'fixed', inset: 0, zIndex: 1000, position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(3px)', backdropFilter: 'blur(3px)',
}} onClick={() => setShowDeleteConfirm(false)}> }} onClick={() => setShowDeleteConfirm(false)}>
<div style={{ <div className="bg-surface-card" style={{
width: 340, background: 'var(--bg-card)', borderRadius: 16, width: 340, borderRadius: 16,
boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px', boxShadow: '0 16px 48px rgba(0,0,0,0.22)', padding: '22px 22px 18px',
display: 'flex', flexDirection: 'column', gap: 12, display: 'flex', flexDirection: 'column', gap: 12,
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ <div className="bg-[rgba(239,68,68,0.12)]" style={{
width: 36, height: 36, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderRadius: '50%', background: 'rgba(239,68,68,0.12)', borderRadius: '50%',
}}> }}>
<Trash2 size={18} strokeWidth={1.8} color="#ef4444" /> <Trash2 size={18} strokeWidth={1.8} color="#ef4444" />
</div> </div>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}> <div className="text-content" style={{ fontSize: 14, fontWeight: 600 }}>
{t('reservations.confirm.deleteTitle')} {t('reservations.confirm.deleteTitle')}
</div> </div>
</div> </div>
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.5 }}> <div className="text-content-secondary" style={{ fontSize: 12.5, lineHeight: 1.5 }}>
{t('reservations.confirm.deleteBody', { name: r.title })} {t('reservations.confirm.deleteBody', { name: r.title })}
</div> </div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}> <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
<button onClick={() => setShowDeleteConfirm(false)} style={{ <button onClick={() => setShowDeleteConfirm(false)} className="text-content-muted" style={{
fontSize: 12, background: 'none', border: '1px solid var(--border-primary)', fontSize: 12, background: 'none', border: '1px solid var(--border-primary)',
borderRadius: 8, padding: '6px 14px', cursor: 'pointer', color: 'var(--text-muted)', fontFamily: 'inherit', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontFamily: 'inherit',
}}>{t('common.cancel')}</button> }}>{t('common.cancel')}</button>
<button onClick={handleDelete} style={{ <button onClick={handleDelete} className="bg-[#ef4444] text-white" style={{
fontSize: 12, background: '#ef4444', color: 'white', fontSize: 12,
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit', border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
}}>{t('common.confirm')}</button> }}>{t('common.confirm')}</button>
</div> </div>
@@ -452,10 +441,9 @@ function Section({ title, count, children, defaultOpen = true, accent, storageKe
userSelect: 'none', userSelect: 'none',
}}> }}>
{open ? <ChevronDown size={14} style={{ color: 'var(--text-faint)' }} /> : <ChevronRight size={14} style={{ color: 'var(--text-faint)' }} />} {open ? <ChevronDown size={14} style={{ color: 'var(--text-faint)' }} /> : <ChevronRight size={14} style={{ color: 'var(--text-faint)' }} />}
<span style={{ fontWeight: 600, fontSize: 12, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{title}</span> <span className="text-content-muted" style={{ fontWeight: 600, fontSize: 12, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{title}</span>
<span style={{ <span className="bg-surface-tertiary text-content-faint" style={{
fontSize: 11, fontWeight: 600, padding: '2px 7px', borderRadius: 99, fontSize: 11, fontWeight: 600, padding: '2px 7px', borderRadius: 99,
background: 'var(--bg-tertiary)', color: 'var(--text-faint)',
minWidth: 20, textAlign: 'center', minWidth: 20, textAlign: 'center',
}}>{count}</span> }}>{count}</span>
</button> </button>
@@ -527,12 +515,12 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
{/* Unified toolbar */} {/* Unified toolbar */}
<div style={{ padding: '24px 28px 0' }} className="max-md:!px-4 max-md:!pt-4"> <div style={{ padding: '24px 28px 0' }} className="max-md:!px-4 max-md:!pt-4">
<div style={{ <div className="bg-surface-tertiary" style={{
background: 'var(--bg-tertiary)', borderRadius: 18, borderRadius: 18,
padding: '14px 16px 14px 22px', padding: '14px 16px 14px 22px',
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap', display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}> }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.01em', flexShrink: 0 }}> <h2 className="text-content" style={{ margin: 0, fontSize: 18, fontWeight: 600, letterSpacing: '-0.01em', flexShrink: 0 }}>
{t(titleKey)} {t(titleKey)}
</h2> </h2>
@@ -542,22 +530,19 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
<div className="hidden md:inline-flex" style={{ gap: 4, flexWrap: 'wrap', flex: 1, minWidth: 0 }}> <div className="hidden md:inline-flex" style={{ gap: 4, flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
<button <button
onClick={() => { setTypeFilters(new Set()); sessionStorage.removeItem(storageKey) }} onClick={() => { setTypeFilters(new Set()); sessionStorage.removeItem(storageKey) }}
className={typeFilters.size === 0 ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
style={{ style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6, display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap', padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap',
background: typeFilters.size === 0 ? 'var(--bg-card)' : 'transparent',
color: typeFilters.size === 0 ? 'var(--text-primary)' : 'var(--text-muted)',
fontWeight: typeFilters.size === 0 ? 500 : 400, fontWeight: typeFilters.size === 0 ? 500 : 400,
boxShadow: typeFilters.size === 0 ? '0 1px 2px rgba(0,0,0,0.06)' : 'none', boxShadow: typeFilters.size === 0 ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
}} }}
> >
{t('common.all')} {t('common.all')}
<span style={{ <span className={`text-content-faint ${typeFilters.size === 0 ? 'bg-surface-tertiary' : 'bg-[rgba(0,0,0,0.06)]'}`} style={{
fontSize: 10, fontWeight: 600, fontSize: 10, fontWeight: 600,
background: typeFilters.size === 0 ? 'var(--bg-tertiary)' : 'rgba(0,0,0,0.06)',
color: 'var(--text-faint)',
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center', padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
}}>{reservations.length}</span> }}>{reservations.length}</span>
</button> </button>
@@ -568,12 +553,11 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
<button <button
key={opt.value} key={opt.value}
onClick={() => toggleTypeFilter(opt.value)} onClick={() => toggleTypeFilter(opt.value)}
className={active ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
style={{ style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6, display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap', padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap',
background: active ? 'var(--bg-card)' : 'transparent',
color: active ? 'var(--text-primary)' : 'var(--text-muted)',
fontWeight: active ? 500 : 400, fontWeight: active ? 500 : 400,
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none', boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
@@ -581,10 +565,8 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
> >
<Icon size={13} style={{ color: active ? opt.color : 'var(--text-faint)' }} /> <Icon size={13} style={{ color: active ? opt.color : 'var(--text-faint)' }} />
{t(opt.labelKey)} {t(opt.labelKey)}
<span style={{ <span className={`text-content-faint ${active ? 'bg-surface-tertiary' : 'bg-[rgba(0,0,0,0.06)]'}`} style={{
fontSize: 10, fontWeight: 600, fontSize: 10, fontWeight: 600,
background: active ? 'var(--bg-tertiary)' : 'rgba(0,0,0,0.06)',
color: 'var(--text-faint)',
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center', padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
}}>{typeCounts[opt.value] || 0}</span> }}>{typeCounts[opt.value] || 0}</span>
</button> </button>
@@ -595,11 +577,11 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
)} )}
{canEdit && ( {canEdit && (
<button onClick={onAdd} style={{ <button onClick={onAdd} className="bg-accent text-accent-text" style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6, display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500, padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0, flexShrink: 0,
marginLeft: 'auto', marginLeft: 'auto',
transition: 'opacity 0.15s ease', transition: 'opacity 0.15s ease',
}} }}
@@ -284,15 +284,8 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
) )
: [] : []
const inputStyle = { const inputClass = 'w-full border border-edge rounded-[10px] px-[12px] py-[8px] text-[13px] font-[inherit] outline-none box-border text-content bg-surface-input'
width: '100%', border: '1px solid var(--border-primary)', borderRadius: 10, const labelClass = 'block text-[11px] font-semibold text-content-faint mb-[5px] uppercase tracking-[0.03em]'
padding: '8px 12px', fontSize: 13, fontFamily: 'inherit',
outline: 'none', boxSizing: 'border-box' as const, color: 'var(--text-primary)', background: 'var(--bg-input)',
}
const labelStyle = {
display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--text-faint)',
marginBottom: 5, textTransform: 'uppercase' as const, letterSpacing: '0.03em',
}
const dayOptions = [ const dayOptions = [
{ value: '', label: '—' }, { value: '', label: '—' },
@@ -315,10 +308,10 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
size="2xl" size="2xl"
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', color: 'var(--text-muted)' }}> <button type="button" onClick={onClose} className="text-content-muted" style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit' }}>
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button type="button" onClick={handleSubmit} disabled={isSaving || !form.title.trim()} style={{ padding: '8px 20px', borderRadius: 10, border: 'none', background: 'var(--text-primary)', color: 'var(--bg-primary)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: isSaving || !form.title.trim() ? 0.5 : 1 }}> <button type="button" onClick={handleSubmit} disabled={isSaving || !form.title.trim()} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: isSaving || !form.title.trim() ? 0.5 : 1 }}>
{isSaving ? t('common.saving') : reservation ? t('common.update') : t('common.add')} {isSaving ? t('common.saving') : reservation ? t('common.update') : t('common.add')}
</button> </button>
</div> </div>
@@ -328,16 +321,14 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{/* Type selector */} {/* Type selector */}
<div> <div>
<label style={labelStyle}>{t('reservations.bookingType')}</label> <label className={labelClass}>{t('reservations.bookingType')}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
{TYPE_OPTIONS.map(({ value, labelKey, Icon }) => ( {TYPE_OPTIONS.map(({ value, labelKey, Icon }) => (
<button key={value} type="button" onClick={() => set('type', value)} style={{ <button key={value} type="button" onClick={() => set('type', value)} className={form.type === value ? 'bg-[var(--text-primary)] text-[var(--bg-primary)]' : 'bg-surface-card text-content-muted'} style={{
display: 'flex', alignItems: 'center', gap: 4, display: 'flex', alignItems: 'center', gap: 4,
padding: '5px 10px', borderRadius: 99, border: '1px solid', padding: '5px 10px', borderRadius: 99, border: '1px solid',
fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.12s', fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.12s',
background: form.type === value ? 'var(--text-primary)' : 'var(--bg-card)',
borderColor: form.type === value ? 'var(--text-primary)' : 'var(--border-primary)', borderColor: form.type === value ? 'var(--text-primary)' : 'var(--border-primary)',
color: form.type === value ? 'var(--bg-primary)' : 'var(--text-muted)',
}}> }}>
<Icon size={11} /> {t(labelKey)} <Icon size={11} /> {t(labelKey)}
</button> </button>
@@ -347,15 +338,15 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{/* Title */} {/* Title */}
<div> <div>
<label style={labelStyle}>{t('reservations.titleLabel')} *</label> <label className={labelClass}>{t('reservations.titleLabel')} *</label>
<input type="text" value={form.title} onChange={e => set('title', e.target.value)} required <input type="text" value={form.title} onChange={e => set('title', e.target.value)} required
placeholder={t('reservations.titlePlaceholder')} style={inputStyle} /> placeholder={t('reservations.titlePlaceholder')} className={inputClass} />
</div> </div>
{/* From / To endpoints */} {/* From / To endpoints */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.meta.from')}</label> <label className={labelClass}>{t('reservations.meta.from')}</label>
{form.type === 'flight' ? ( {form.type === 'flight' ? (
<AirportSelect value={fromPick.airport || null} onChange={a => setFromPick({ airport: a || undefined })} /> <AirportSelect value={fromPick.airport || null} onChange={a => setFromPick({ airport: a || undefined })} />
) : ( ) : (
@@ -363,7 +354,7 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
)} )}
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.to')}</label> <label className={labelClass}>{t('reservations.meta.to')}</label>
{form.type === 'flight' ? ( {form.type === 'flight' ? (
<AirportSelect value={toPick.airport || null} onChange={a => setToPick({ airport: a || undefined })} /> <AirportSelect value={toPick.airport || null} onChange={a => setToPick({ airport: a || undefined })} />
) : ( ) : (
@@ -375,7 +366,7 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{/* Departure row */} {/* Departure row */}
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}> <label className={labelClass}>
{form.type === 'flight' ? t('reservations.departureDate') : form.type === 'car' ? t('reservations.pickupDate') : t('reservations.date')} {form.type === 'flight' ? t('reservations.departureDate') : form.type === 'car' ? t('reservations.pickupDate') : t('reservations.date')}
</label> </label>
<CustomSelect <CustomSelect
@@ -387,15 +378,15 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
/> />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}> <label className={labelClass}>
{form.type === 'flight' ? t('reservations.departureTime') : form.type === 'car' ? t('reservations.pickupTime') : t('reservations.startTime')} {form.type === 'flight' ? t('reservations.departureTime') : form.type === 'car' ? t('reservations.pickupTime') : t('reservations.startTime')}
</label> </label>
<CustomTimePicker value={form.departure_time} onChange={v => set('departure_time', v)} /> <CustomTimePicker value={form.departure_time} onChange={v => set('departure_time', v)} />
</div> </div>
{form.type === 'flight' && fromPick.airport && ( {form.type === 'flight' && fromPick.airport && (
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.meta.departureTimezone')}</label> <label className={labelClass}>{t('reservations.meta.departureTimezone')}</label>
<div style={{ ...inputStyle, padding: '8px 12px', color: 'var(--text-muted)', fontSize: 12, background: 'var(--bg-tertiary)' }}> <div className={inputClass} style={{ padding: '8px 12px', color: 'var(--text-muted)', fontSize: 12, background: 'var(--bg-tertiary)' }}>
{fromPick.airport.tz} {fromPick.airport.tz}
</div> </div>
</div> </div>
@@ -405,7 +396,7 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{/* Arrival row */} {/* Arrival row */}
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}> <label className={labelClass}>
{form.type === 'flight' ? t('reservations.arrivalDate') : form.type === 'car' ? t('reservations.returnDate') : t('reservations.endDate')} {form.type === 'flight' ? t('reservations.arrivalDate') : form.type === 'car' ? t('reservations.returnDate') : t('reservations.endDate')}
</label> </label>
<CustomSelect <CustomSelect
@@ -417,15 +408,15 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
/> />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}> <label className={labelClass}>
{form.type === 'flight' ? t('reservations.arrivalTime') : form.type === 'car' ? t('reservations.returnTime') : t('reservations.endTime')} {form.type === 'flight' ? t('reservations.arrivalTime') : form.type === 'car' ? t('reservations.returnTime') : t('reservations.endTime')}
</label> </label>
<CustomTimePicker value={form.arrival_time} onChange={v => set('arrival_time', v)} /> <CustomTimePicker value={form.arrival_time} onChange={v => set('arrival_time', v)} />
</div> </div>
{form.type === 'flight' && toPick.airport && ( {form.type === 'flight' && toPick.airport && (
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.meta.arrivalTimezone')}</label> <label className={labelClass}>{t('reservations.meta.arrivalTimezone')}</label>
<div style={{ ...inputStyle, padding: '8px 12px', color: 'var(--text-muted)', fontSize: 12, background: 'var(--bg-tertiary)' }}> <div className={inputClass} style={{ padding: '8px 12px', color: 'var(--text-muted)', fontSize: 12, background: 'var(--bg-tertiary)' }}>
{toPick.airport.tz} {toPick.airport.tz}
</div> </div>
</div> </div>
@@ -436,14 +427,14 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{form.type === 'flight' && ( {form.type === 'flight' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.meta.airline')}</label> <label className={labelClass}>{t('reservations.meta.airline')}</label>
<input type="text" value={form.meta_airline} onChange={e => set('meta_airline', e.target.value)} <input type="text" value={form.meta_airline} onChange={e => set('meta_airline', e.target.value)}
placeholder="Lufthansa" style={inputStyle} /> placeholder="Lufthansa" className={inputClass} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.flightNumber')}</label> <label className={labelClass}>{t('reservations.meta.flightNumber')}</label>
<input type="text" value={form.meta_flight_number} onChange={e => set('meta_flight_number', e.target.value)} <input type="text" value={form.meta_flight_number} onChange={e => set('meta_flight_number', e.target.value)}
placeholder="LH 123" style={inputStyle} /> placeholder="LH 123" className={inputClass} />
</div> </div>
</div> </div>
)} )}
@@ -452,19 +443,19 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{form.type === 'train' && ( {form.type === 'train' && (
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.meta.trainNumber')}</label> <label className={labelClass}>{t('reservations.meta.trainNumber')}</label>
<input type="text" value={form.meta_train_number} onChange={e => set('meta_train_number', e.target.value)} <input type="text" value={form.meta_train_number} onChange={e => set('meta_train_number', e.target.value)}
placeholder="ICE 123" style={inputStyle} /> placeholder="ICE 123" className={inputClass} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.platform')}</label> <label className={labelClass}>{t('reservations.meta.platform')}</label>
<input type="text" value={form.meta_platform} onChange={e => set('meta_platform', e.target.value)} <input type="text" value={form.meta_platform} onChange={e => set('meta_platform', e.target.value)}
placeholder="12" style={inputStyle} /> placeholder="12" className={inputClass} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.meta.seat')}</label> <label className={labelClass}>{t('reservations.meta.seat')}</label>
<input type="text" value={form.meta_seat} onChange={e => set('meta_seat', e.target.value)} <input type="text" value={form.meta_seat} onChange={e => set('meta_seat', e.target.value)}
placeholder="42A" style={inputStyle} /> placeholder="42A" className={inputClass} />
</div> </div>
</div> </div>
)} )}
@@ -472,12 +463,12 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{/* Booking Code + Status */} {/* Booking Code + Status */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label style={labelStyle}>{t('reservations.confirmationCode')}</label> <label className={labelClass}>{t('reservations.confirmationCode')}</label>
<input type="text" value={form.confirmation_number} onChange={e => set('confirmation_number', e.target.value)} <input type="text" value={form.confirmation_number} onChange={e => set('confirmation_number', e.target.value)}
placeholder={t('reservations.confirmationPlaceholder')} style={inputStyle} /> placeholder={t('reservations.confirmationPlaceholder')} className={inputClass} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('reservations.status')}</label> <label className={labelClass}>{t('reservations.status')}</label>
<CustomSelect <CustomSelect
value={form.status} value={form.status}
onChange={value => set('status', value)} onChange={value => set('status', value)}
@@ -492,21 +483,21 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
{/* Notes */} {/* Notes */}
<div> <div>
<label style={labelStyle}>{t('reservations.notes')}</label> <label className={labelClass}>{t('reservations.notes')}</label>
<textarea value={form.notes} onChange={e => set('notes', e.target.value)} rows={2} <textarea value={form.notes} onChange={e => set('notes', e.target.value)} rows={2}
placeholder={t('reservations.notesPlaceholder')} placeholder={t('reservations.notesPlaceholder')}
style={{ ...inputStyle, resize: 'none', lineHeight: 1.5 }} /> className={inputClass} style={{ resize: 'none', lineHeight: 1.5 }} />
</div> </div>
{/* Files */} {/* Files */}
<div> <div>
<label style={labelStyle}>{t('files.title')}</label> <label className={labelClass}>{t('files.title')}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{attachedFiles.map(f => ( {attachedFiles.map(f => (
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', background: 'var(--bg-secondary)', borderRadius: 8 }}> <div key={f.id} className="bg-surface-secondary" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', borderRadius: 8 }}>
<FileText size={12} style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> <FileText size={12} className="text-content-muted" style={{ flexShrink: 0 }} />
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span> <span className="text-content-secondary" style={{ flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
<a href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} style={{ color: 'var(--text-faint)', display: 'flex', flexShrink: 0, cursor: 'pointer' }}><ExternalLink size={11} /></a> <a href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} className="text-content-faint" style={{ display: 'flex', flexShrink: 0, cursor: 'pointer' }}><ExternalLink size={11} /></a>
<button type="button" onClick={async () => { <button type="button" onClick={async () => {
if (f.reservation_id === reservation?.id) { if (f.reservation_id === reservation?.id) {
try { await apiClient.put(`/trips/${tripId}/files/${f.id}`, { reservation_id: null }) } catch { toast.error(t('reservations.toast.updateError')) } try { await apiClient.put(`/trips/${tripId}/files/${f.id}`, { reservation_id: null }) } catch { toast.error(t('reservations.toast.updateError')) }
@@ -518,44 +509,44 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
} catch { toast.error(t('reservations.toast.updateError')) } } catch { toast.error(t('reservations.toast.updateError')) }
setLinkedFileIds(prev => prev.filter(id => id !== f.id)) setLinkedFileIds(prev => prev.filter(id => id !== f.id))
if (tripId) loadFiles(tripId) if (tripId) loadFiles(tripId)
}} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 0, flexShrink: 0 }}> }} className="text-content-faint" style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', padding: 0, flexShrink: 0 }}>
<X size={11} /> <X size={11} />
</button> </button>
</div> </div>
))} ))}
{pendingFiles.map((f, i) => ( {pendingFiles.map((f, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', background: 'var(--bg-secondary)', borderRadius: 8 }}> <div key={i} className="bg-surface-secondary" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', borderRadius: 8 }}>
<FileText size={12} style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> <FileText size={12} className="text-content-muted" style={{ flexShrink: 0 }} />
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span> <span className="text-content-secondary" style={{ flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
<button type="button" onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))} <button type="button" onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 0, flexShrink: 0 }}> className="text-content-faint" style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', padding: 0, flexShrink: 0 }}>
<X size={11} /> <X size={11} />
</button> </button>
</div> </div>
))} ))}
<input ref={fileInputRef} type="file" accept=".pdf,.doc,.docx,.txt,image/*" style={{ display: 'none' }} onChange={handleFileChange} /> <input ref={fileInputRef} type="file" accept=".pdf,.doc,.docx,.txt,image/*" style={{ display: 'none' }} onChange={handleFileChange} />
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{onFileUpload && <button type="button" onClick={() => fileInputRef.current?.click()} disabled={uploadingFile} style={{ {onFileUpload && <button type="button" onClick={() => fileInputRef.current?.click()} disabled={uploadingFile} className="text-content-faint" style={{
display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px',
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none', border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
fontSize: 11, color: 'var(--text-faint)', cursor: uploadingFile ? 'default' : 'pointer', fontFamily: 'inherit', fontSize: 11, cursor: uploadingFile ? 'default' : 'pointer', fontFamily: 'inherit',
}}> }}>
<Paperclip size={11} /> <Paperclip size={11} />
{uploadingFile ? t('reservations.uploading') : t('reservations.attachFile')} {uploadingFile ? t('reservations.uploading') : t('reservations.attachFile')}
</button>} </button>}
{reservation?.id && files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).length > 0 && ( {reservation?.id && files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).length > 0 && (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button type="button" onClick={() => setShowFilePicker(v => !v)} style={{ <button type="button" onClick={() => setShowFilePicker(v => !v)} className="text-content-faint" style={{
display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px',
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none', border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
fontSize: 11, color: 'var(--text-faint)', cursor: 'pointer', fontFamily: 'inherit', fontSize: 11, cursor: 'pointer', fontFamily: 'inherit',
}}> }}>
<Link2 size={11} /> {t('reservations.linkExisting')} <Link2 size={11} /> {t('reservations.linkExisting')}
</button> </button>
{showFilePicker && ( {showFilePicker && (
<div style={{ <div className="bg-surface-card" style={{
position: 'absolute', bottom: '100%', left: 0, marginBottom: 4, zIndex: 50, position: 'absolute', bottom: '100%', left: 0, marginBottom: 4, zIndex: 50,
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10, border: '1px solid var(--border-primary)', borderRadius: 10,
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 220, maxHeight: 200, overflowY: 'auto', boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 220, maxHeight: 200, overflowY: 'auto',
}}> }}>
{files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).map(f => ( {files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).map(f => (
@@ -567,14 +558,15 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
if (tripId) loadFiles(tripId) if (tripId) loadFiles(tripId)
} catch { toast.error(t('reservations.toast.updateError')) } } catch { toast.error(t('reservations.toast.updateError')) }
}} }}
className="text-content-secondary"
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 10px',
background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, fontFamily: 'inherit', background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, fontFamily: 'inherit',
color: 'var(--text-secondary)', borderRadius: 7, textAlign: 'left', borderRadius: 7, textAlign: 'left',
}} }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}> onMouseLeave={e => e.currentTarget.style.background = 'none'}>
<FileText size={12} style={{ color: 'var(--text-faint)', flexShrink: 0 }} /> <FileText size={12} className="text-content-faint" style={{ flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
</button> </button>
))} ))}
@@ -591,15 +583,15 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
<> <>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.price')}</label> <label className={labelClass}>{t('reservations.price')}</label>
<input type="text" inputMode="decimal" value={form.price} <input type="text" inputMode="decimal" value={form.price}
onChange={e => { const v = e.target.value; if (v === '' || /^\d*[.,]?\d{0,2}$/.test(v)) set('price', v.replace(',', '.')) }} onChange={e => { const v = e.target.value; if (v === '' || /^\d*[.,]?\d{0,2}$/.test(v)) set('price', v.replace(',', '.')) }}
onPaste={e => { e.preventDefault(); let txt = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = txt.lastIndexOf(','), ld = txt.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { txt = txt.substring(0, dp).replace(/[.,]/g, '') + '.' + txt.substring(dp + 1) } else { txt = txt.replace(/[.,]/g, '') } set('price', txt) }} onPaste={e => { e.preventDefault(); let txt = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = txt.lastIndexOf(','), ld = txt.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { txt = txt.substring(0, dp).replace(/[.,]/g, '') + '.' + txt.substring(dp + 1) } else { txt = txt.replace(/[.,]/g, '') } set('price', txt) }}
placeholder="0.00" placeholder="0.00"
style={inputStyle} /> className={inputClass} />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.budgetCategory')}</label> <label className={labelClass}>{t('reservations.budgetCategory')}</label>
<CustomSelect <CustomSelect
value={form.budget_category} value={form.budget_category}
onChange={v => set('budget_category', v)} onChange={v => set('budget_category', v)}
@@ -613,7 +605,7 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
</div> </div>
</div> </div>
{form.price && parseFloat(form.price) > 0 && ( {form.price && parseFloat(form.price) > 0 && (
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: -4 }}> <div className="text-content-faint" style={{ fontSize: 11, marginTop: -4 }}>
{t('reservations.budgetHint')} {t('reservations.budgetHint')}
</div> </div>
)} )}
+18 -24
View File
@@ -246,7 +246,7 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
{t('settings.about.madeWith')}{' '} {t('settings.about.madeWith')}{' '}
<Heart size={11} fill="#991b1b" stroke="#991b1b" style={{ display: 'inline-block', verticalAlign: '-1px', animation: 'heartPulse 1.5s ease-in-out infinite' }} /> <Heart size={11} fill="#991b1b" stroke="#991b1b" style={{ display: 'inline-block', verticalAlign: '-1px', animation: 'heartPulse 1.5s ease-in-out infinite' }} />
{' '}{t('settings.about.madeBy')}{' '} {' '}{t('settings.about.madeBy')}{' '}
<span className="text-content-faint" style={{ display: 'inline-flex', alignItems: 'center', background: 'var(--bg-tertiary)', borderRadius: 99, padding: '1px 7px', fontSize: 10, fontWeight: 600, verticalAlign: '1px' }}>v{appVersion}</span> <span className="text-content-faint bg-surface-tertiary" style={{ display: 'inline-flex', alignItems: 'center', borderRadius: 99, padding: '1px 7px', fontSize: 10, fontWeight: 600, verticalAlign: '1px' }}>v{appVersion}</span>
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
@@ -254,13 +254,12 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
href="https://ko-fi.com/mauriceboe" href="https://ko-fi.com/mauriceboe"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ff5e5b'; e.currentTarget.style.boxShadow = '0 0 0 1px #ff5e5b22' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ff5e5b'; e.currentTarget.style.boxShadow = '0 0 0 1px #ff5e5b22' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#ff5e5b15', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#ff5e5b15]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Coffee size={20} style={{ color: '#ff5e5b' }} /> <Coffee size={20} className="text-[#ff5e5b]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">Ko-fi</div> <div className="text-sm font-semibold text-content">Ko-fi</div>
@@ -272,13 +271,12 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
href="https://buymeacoffee.com/mauriceboe" href="https://buymeacoffee.com/mauriceboe"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ffdd00'; e.currentTarget.style.boxShadow = '0 0 0 1px #ffdd0022' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ffdd00'; e.currentTarget.style.boxShadow = '0 0 0 1px #ffdd0022' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#ffdd0015', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#ffdd0015]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Heart size={20} style={{ color: '#ffdd00' }} /> <Heart size={20} className="text-[#ffdd00]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">Buy Me a Coffee</div> <div className="text-sm font-semibold text-content">Buy Me a Coffee</div>
@@ -290,12 +288,11 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
href="https://discord.gg/NhZBDSd4qW" href="https://discord.gg/NhZBDSd4qW"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#5865F215', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#5865F215]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</div> </div>
<div> <div>
@@ -311,13 +308,12 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
href="https://github.com/mauriceboe/TREK/issues/new?template=bug_report.yml" href="https://github.com/mauriceboe/TREK/issues/new?template=bug_report.yml"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.boxShadow = '0 0 0 1px #ef444422' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.boxShadow = '0 0 0 1px #ef444422' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#ef444415', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#ef444415]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Bug size={20} style={{ color: '#ef4444' }} /> <Bug size={20} className="text-[#ef4444]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">{t('settings.about.reportBug')}</div> <div className="text-sm font-semibold text-content">{t('settings.about.reportBug')}</div>
@@ -329,13 +325,12 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
href="https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests" href="https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#f59e0b'; e.currentTarget.style.boxShadow = '0 0 0 1px #f59e0b22' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#f59e0b'; e.currentTarget.style.boxShadow = '0 0 0 1px #f59e0b22' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#f59e0b15', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#f59e0b15]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Lightbulb size={20} style={{ color: '#f59e0b' }} /> <Lightbulb size={20} className="text-[#f59e0b]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">{t('settings.about.featureRequest')}</div> <div className="text-sm font-semibold text-content">{t('settings.about.featureRequest')}</div>
@@ -347,13 +342,12 @@ export default function AboutTab({ appVersion }: Props): React.ReactElement {
href="https://github.com/mauriceboe/TREK/wiki" href="https://github.com/mauriceboe/TREK/wiki"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card" className="rounded-xl border overflow-hidden flex items-center gap-4 px-5 py-4 transition-[border-color,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] bg-surface-card border-edge no-underline"
style={{ borderColor: 'var(--border-primary)', textDecoration: 'none' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#6366f1'; e.currentTarget.style.boxShadow = '0 0 0 1px #6366f122' }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#6366f1'; e.currentTarget.style.boxShadow = '0 0 0 1px #6366f122' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }}
> >
<div style={{ width: 40, height: 40, borderRadius: 10, background: '#6366f115', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#6366f115]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<BookOpen size={20} style={{ color: '#6366f1' }} /> <BookOpen size={20} className="text-[#6366f1]" />
</div> </div>
<div> <div>
<div className="text-sm font-semibold text-content">Wiki</div> <div className="text-sm font-semibold text-content">Wiki</div>
+16 -17
View File
@@ -211,8 +211,7 @@ export default function AccountTab(): React.ReactElement {
toast.error(getApiErrorMessage(err, t('common.error'))) toast.error(getApiErrorMessage(err, t('common.error')))
} }
}} }}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors" className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors border border-edge bg-surface-card text-content-secondary"
style={{ border: '1px solid var(--border-primary)', background: 'var(--bg-card)', color: 'var(--text-secondary)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-card)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-card)'}
> >
@@ -374,7 +373,7 @@ export default function AccountTab(): React.ReactElement {
<p className="text-sm font-semibold m-0 text-content">{t('settings.mfa.backupTitle')}</p> <p className="text-sm font-semibold m-0 text-content">{t('settings.mfa.backupTitle')}</p>
<p className="text-xs m-0 text-content-muted">{t('settings.mfa.backupDescription')}</p> <p className="text-xs m-0 text-content-muted">{t('settings.mfa.backupDescription')}</p>
<pre className="text-xs m-0 p-2 rounded border overflow-auto border-edge bg-surface-card text-content" style={{ maxHeight: 220 }}>{backupCodesText}</pre> <pre className="text-xs m-0 p-2 rounded border overflow-auto border-edge bg-surface-card text-content" style={{ maxHeight: 220 }}>{backupCodesText}</pre>
<p className="text-xs m-0" style={{ color: '#b45309' }}>{t('settings.mfa.backupWarning')}</p> <p className="text-xs m-0 text-[#b45309]">{t('settings.mfa.backupWarning')}</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button type="button" onClick={copyBackupCodes} className="px-3 py-2 rounded-lg text-xs border flex items-center gap-1.5 border-edge text-content-secondary"> <button type="button" onClick={copyBackupCodes} className="px-3 py-2 rounded-lg text-xs border flex items-center gap-1.5 border-edge text-content-secondary">
<Copy size={13} /> {t('settings.mfa.backupCopy')} <Copy size={13} /> {t('settings.mfa.backupCopy')}
@@ -429,10 +428,10 @@ export default function AccountTab(): React.ReactElement {
{user?.avatar_url && ( {user?.avatar_url && (
<button <button
onClick={handleAvatarRemove} onClick={handleAvatarRemove}
className="bg-[#ef4444] text-white"
style={{ style={{
position: 'absolute', top: -2, right: -2, position: 'absolute', top: -2, right: -2,
width: 20, height: 20, borderRadius: '50%', width: 20, height: 20, borderRadius: '50%',
background: '#ef4444', color: 'white',
border: '2px solid var(--bg-card)', border: '2px solid var(--bg-card)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', padding: 0, cursor: 'pointer', padding: 0,
@@ -448,10 +447,10 @@ export default function AccountTab(): React.ReactElement {
{user?.role === 'admin' ? <><Shield size={13} /> {t('settings.roleAdmin')}</> : t('settings.roleUser')} {user?.role === 'admin' ? <><Shield size={13} /> {t('settings.roleAdmin')}</> : t('settings.roleUser')}
</span> </span>
{(user as UserWithOidc)?.oidc_issuer && ( {(user as UserWithOidc)?.oidc_issuer && (
<span style={{ <span className="bg-[#dbeafe] text-[#1d4ed8]" style={{
display: 'inline-flex', alignItems: 'center', gap: 4, display: 'inline-flex', alignItems: 'center', gap: 4,
fontSize: 10, fontWeight: 500, padding: '1px 8px', borderRadius: 99, fontSize: 10, fontWeight: 500, padding: '1px 8px', borderRadius: 99,
background: '#dbeafe', color: '#1d4ed8', marginLeft: 6, marginLeft: 6,
}}> }}>
SSO SSO
</span> </span>
@@ -489,8 +488,7 @@ export default function AccountTab(): React.ReactElement {
} }
setShowDeleteConfirm(true) setShowDeleteConfirm(true)
}} }}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors text-red-500 hover:bg-red-50" className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors text-red-500 hover:bg-red-50 border border-[#fecaca]"
style={{ border: '1px solid #fecaca' }}
> >
<Trash2 size={14} /> <Trash2 size={14} />
<span className="hidden sm:inline">{t('settings.deleteAccount')}</span> <span className="hidden sm:inline">{t('settings.deleteAccount')}</span>
@@ -501,9 +499,9 @@ export default function AccountTab(): React.ReactElement {
{/* Delete Account Blocked */} {/* Delete Account Blocked */}
{showDeleteConfirm === 'blocked' && ( {showDeleteConfirm === 'blocked' && (
<div style={{ <div className="bg-[rgba(0,0,0,0.5)]" style={{
position: 'fixed', inset: 0, zIndex: 9999, position: 'fixed', inset: 0, zIndex: 9999,
background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)', backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
}} onClick={() => setShowDeleteConfirm(false)}> }} onClick={() => setShowDeleteConfirm(false)}>
<div className="bg-surface-card" style={{ <div className="bg-surface-card" style={{
@@ -511,8 +509,8 @@ export default function AccountTab(): React.ReactElement {
maxWidth: 400, width: '100%', boxShadow: '0 20px 60px rgba(0,0,0,0.3)', maxWidth: 400, width: '100%', boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: '#fef3c7', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div className="bg-[#fef3c7]" style={{ width: 36, height: 36, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Shield size={18} style={{ color: '#d97706' }} /> <Shield size={18} className="text-[#d97706]" />
</div> </div>
<h3 className="text-content" style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>{t('settings.deleteBlockedTitle')}</h3> <h3 className="text-content" style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>{t('settings.deleteBlockedTitle')}</h3>
</div> </div>
@@ -537,9 +535,9 @@ export default function AccountTab(): React.ReactElement {
{/* Delete Account Confirm */} {/* Delete Account Confirm */}
{showDeleteConfirm === true && ( {showDeleteConfirm === true && (
<div style={{ <div className="bg-[rgba(0,0,0,0.5)]" style={{
position: 'fixed', inset: 0, zIndex: 9999, position: 'fixed', inset: 0, zIndex: 9999,
background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)', backdropFilter: 'blur(4px)',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
}} onClick={() => setShowDeleteConfirm(false)}> }} onClick={() => setShowDeleteConfirm(false)}>
<div className="bg-surface-card" style={{ <div className="bg-surface-card" style={{
@@ -547,8 +545,8 @@ export default function AccountTab(): React.ReactElement {
maxWidth: 400, width: '100%', boxShadow: '0 20px 60px rgba(0,0,0,0.3)', maxWidth: 400, width: '100%', boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
}} onClick={e => e.stopPropagation()}> }} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
<div style={{ width: 36, height: 36, borderRadius: 10, background: '#fef2f2', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div className="bg-[#fef2f2]" style={{ width: 36, height: 36, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Trash2 size={18} style={{ color: '#ef4444' }} /> <Trash2 size={18} className="text-[#ef4444]" />
</div> </div>
<h3 className="text-content" style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>{t('settings.deleteAccountTitle')}</h3> <h3 className="text-content" style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>{t('settings.deleteAccountTitle')}</h3>
</div> </div>
@@ -577,9 +575,10 @@ export default function AccountTab(): React.ReactElement {
setShowDeleteConfirm(false) setShowDeleteConfirm(false)
} }
}} }}
className="bg-[#ef4444] text-white"
style={{ style={{
padding: '8px 16px', borderRadius: 8, fontSize: 13, fontWeight: 600, padding: '8px 16px', borderRadius: 8, fontSize: 13, fontWeight: 600,
border: 'none', background: '#ef4444', color: 'white', border: 'none',
cursor: 'pointer', fontFamily: 'inherit', cursor: 'pointer', fontFamily: 'inherit',
}} }}
> >
@@ -30,7 +30,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
<Section title={t('settings.display')} icon={Palette}> <Section title={t('settings.display')} icon={Palette}>
{/* Color Mode */} {/* Color Mode */}
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.colorMode')}</label> <label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.colorMode')}</label>
<div className="flex gap-3" style={{ flexWrap: 'wrap' }}> <div className="flex gap-3" style={{ flexWrap: 'wrap' }}>
{[ {[
{ value: 'light', label: t('settings.light'), icon: Sun }, { value: 'light', label: t('settings.light'), icon: Sun },
@@ -72,7 +72,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
{/* Language */} {/* Language */}
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.language')}</label> <label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.language')}</label>
{/* Desktop: Button grid */} {/* Desktop: Button grid */}
<div className="hidden sm:flex flex-wrap gap-3"> <div className="hidden sm:flex flex-wrap gap-3">
{SUPPORTED_LANGUAGES.map(opt => ( {SUPPORTED_LANGUAGES.map(opt => (
@@ -113,7 +113,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
}} }}
> >
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{current?.label}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{current?.label}</span>
<ChevronDown size={14} style={{ flexShrink: 0, color: 'var(--text-faint)', transform: langOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} /> <ChevronDown size={14} className="text-content-faint" style={{ flexShrink: 0, transform: langOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
</button> </button>
) )
})()} })()}
@@ -154,7 +154,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
{/* Temperature */} {/* Temperature */}
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.temperature')}</label> <label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.temperature')}</label>
<div className="flex gap-3"> <div className="flex gap-3">
{[ {[
{ value: 'celsius', label: '°C Celsius' }, { value: 'celsius', label: '°C Celsius' },
@@ -185,7 +185,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
{/* Time Format */} {/* Time Format */}
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.timeFormat')}</label> <label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.timeFormat')}</label>
<div className="flex gap-3"> <div className="flex gap-3">
{[ {[
{ value: '24h', short: '24h', example: '14:30' }, { value: '24h', short: '24h', example: '14:30' },
@@ -216,7 +216,7 @@ export default function DisplaySettingsTab(): React.ReactElement {
{/* Booking route labels */} {/* Booking route labels */}
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.bookingLabels')}</label> <label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.bookingLabels')}</label>
<div className="flex gap-3"> <div className="flex gap-3">
{[ {[
{ value: true, label: t('settings.on') || 'On' }, { value: true, label: t('settings.on') || 'On' },
@@ -242,12 +242,12 @@ export default function DisplaySettingsTab(): React.ReactElement {
</button> </button>
))} ))}
</div> </div>
<p className="text-xs mt-1" style={{ color: 'var(--text-faint)' }}>{t('settings.bookingLabelsHint')}</p> <p className="text-xs mt-1 text-content-faint">{t('settings.bookingLabelsHint')}</p>
</div> </div>
{/* Blur Booking Codes */} {/* Blur Booking Codes */}
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>{t('settings.blurBookingCodes')}</label> <label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.blurBookingCodes')}</label>
<div className="flex gap-3"> <div className="flex gap-3">
{[ {[
{ value: true, label: t('settings.on') || 'On' }, { value: true, label: t('settings.on') || 'On' },
@@ -329,8 +329,7 @@ function IntegrationsMcpSection(props: any) {
activeMcpTab === 'apitokens' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-50' activeMcpTab === 'apitokens' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
}`}> }`}>
{t('settings.mcp.apiTokens')} {t('settings.mcp.apiTokens')}
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium" <span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-[rgba(245,158,11,0.15)] text-[#b45309] border border-[rgba(245,158,11,0.4)]">
style={{ background: 'rgba(245,158,11,0.15)', color: '#b45309', border: '1px solid rgba(245,158,11,0.4)' }}>
Deprecated Deprecated
</span> </span>
</button> </button>
@@ -381,16 +380,14 @@ function IntegrationsMcpSection(props: any) {
) : ( ) : (
<div className="rounded-lg border overflow-hidden border-edge"> <div className="rounded-lg border overflow-hidden border-edge">
{oauthClients.map((client, i) => ( {oauthClients.map((client, i) => (
<div key={client.id} className="px-4 py-3" <div key={client.id} className={`px-4 py-3 ${i < oauthClients.length - 1 ? 'border-b border-edge' : ''}`}>
style={{ borderBottom: i < oauthClients.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<KeyRound className="w-4 h-4 flex-shrink-0" style={{ color: 'var(--text-tertiary)' }} /> <KeyRound className="w-4 h-4 flex-shrink-0" style={{ color: 'var(--text-tertiary)' }} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="text-sm font-medium truncate text-content">{client.name}</p> <p className="text-sm font-medium truncate text-content">{client.name}</p>
{client.allows_client_credentials && ( {client.allows_client_credentials && (
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0" <span className="px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0 bg-[rgba(99,102,241,0.12)] text-[#4f46e5] border border-[rgba(99,102,241,0.3)]">
style={{ background: 'rgba(99,102,241,0.12)', color: '#4f46e5', border: '1px solid rgba(99,102,241,0.3)' }}>
{t('settings.oauth.badge.machine')} {t('settings.oauth.badge.machine')}
</span> </span>
)} )}
@@ -436,8 +433,7 @@ function IntegrationsMcpSection(props: any) {
<label className="text-sm font-medium block mb-2 text-content-secondary">{t('settings.oauth.activeSessions')}</label> <label className="text-sm font-medium block mb-2 text-content-secondary">{t('settings.oauth.activeSessions')}</label>
<div className="rounded-lg border overflow-hidden border-edge"> <div className="rounded-lg border overflow-hidden border-edge">
{oauthSessions.map((session, i) => ( {oauthSessions.map((session, i) => (
<div key={session.id} className="flex items-center gap-3 px-4 py-3" <div key={session.id} className={`flex items-center gap-3 px-4 py-3 ${i < oauthSessions.length - 1 ? 'border-b border-edge' : ''}`}>
style={{ borderBottom: i < oauthSessions.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate text-content">{session.client_name}</p> <p className="text-sm font-medium truncate text-content">{session.client_name}</p>
<p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}> <p className="text-xs mt-0.5" style={{ color: 'var(--text-tertiary)' }}>
@@ -461,9 +457,9 @@ function IntegrationsMcpSection(props: any) {
{/* API Tokens tab (deprecated) */} {/* API Tokens tab (deprecated) */}
{activeMcpTab === 'apitokens' && ( {activeMcpTab === 'apitokens' && (
<> <>
<div className="flex items-baseline gap-2 px-3 py-2.5 rounded-lg" style={{ background: 'rgba(245,158,11,0.06)', border: '1px solid rgba(245,158,11,0.3)' }}> <div className="flex items-baseline gap-2 px-3 py-2.5 rounded-lg bg-[rgba(245,158,11,0.06)] border border-[rgba(245,158,11,0.3)]">
<span className="text-amber-500 flex-shrink-0 leading-none"></span> <span className="text-amber-500 flex-shrink-0 leading-none"></span>
<p className="text-xs" style={{ color: '#92400e' }}>{t('settings.mcp.apiTokensDeprecated')}</p> <p className="text-xs text-[#92400e]">{t('settings.mcp.apiTokensDeprecated')}</p>
</div> </div>
{/* JSON config — API Token (collapsible) */} {/* JSON config — API Token (collapsible) */}
@@ -493,8 +489,7 @@ function IntegrationsMcpSection(props: any) {
<div className="flex justify-end"> <div className="flex justify-end">
<button onClick={() => { setMcpModalOpen(true); setMcpCreatedToken(null); setMcpNewName('') }} <button onClick={() => { setMcpModalOpen(true); setMcpCreatedToken(null); setMcpNewName('') }}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors opacity-60 text-content-secondary" className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors opacity-60 text-content-secondary bg-surface-tertiary">
style={{ background: 'var(--bg-tertiary, #e5e7eb)' }}>
<Plus className="w-3.5 h-3.5" /> {t('settings.mcp.createToken')} <Plus className="w-3.5 h-3.5" /> {t('settings.mcp.createToken')}
</button> </button>
</div> </div>
@@ -506,8 +501,7 @@ function IntegrationsMcpSection(props: any) {
) : ( ) : (
<div className="rounded-lg border overflow-hidden border-edge"> <div className="rounded-lg border overflow-hidden border-edge">
{mcpTokens.map((token, i) => ( {mcpTokens.map((token, i) => (
<div key={token.id} className="flex items-center gap-3 px-4 py-3" <div key={token.id} className={`flex items-center gap-3 px-4 py-3 ${i < mcpTokens.length - 1 ? 'border-b border-edge' : ''}`}>
style={{ borderBottom: i < mcpTokens.length - 1 ? '1px solid var(--border-primary)' : undefined }}>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate text-content">{token.name}</p> <p className="text-sm font-medium truncate text-content">{token.name}</p>
<p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}> <p className="text-xs font-mono mt-0.5" style={{ color: 'var(--text-tertiary)' }}>
@@ -541,7 +535,7 @@ function McpTokenModals(props: any) {
<> <>
{/* Create MCP Token modal */} {/* Create MCP Token modal */}
{mcpModalOpen && ( {mcpModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget && !mcpCreatedToken) setMcpModalOpen(false) }}> onClick={e => { if (e.target === e.currentTarget && !mcpCreatedToken) setMcpModalOpen(false) }}>
<div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4 bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4 bg-surface-card">
{!mcpCreatedToken ? ( {!mcpCreatedToken ? (
@@ -569,7 +563,7 @@ function McpTokenModals(props: any) {
) : ( ) : (
<> <>
<h3 className="text-lg font-semibold text-content">{t('settings.mcp.modal.createdTitle')}</h3> <h3 className="text-lg font-semibold text-content">{t('settings.mcp.modal.createdTitle')}</h3>
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}> <div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200 bg-[rgba(251,191,36,0.1)]">
<span className="text-amber-500 mt-0.5"></span> <span className="text-amber-500 mt-0.5"></span>
<p className="text-sm text-content-secondary">{t('settings.mcp.modal.createdWarning')}</p> <p className="text-sm text-content-secondary">{t('settings.mcp.modal.createdWarning')}</p>
</div> </div>
@@ -597,7 +591,7 @@ function McpTokenModals(props: any) {
{/* Delete MCP Token confirm */} {/* Delete MCP Token confirm */}
{mcpDeleteId !== null && ( {mcpDeleteId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget) setMcpDeleteId(null) }}> onClick={e => { if (e.target === e.currentTarget) setMcpDeleteId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
<h3 className="text-base font-semibold text-content">{t('settings.mcp.deleteTokenTitle')}</h3> <h3 className="text-base font-semibold text-content">{t('settings.mcp.deleteTokenTitle')}</h3>
@@ -628,7 +622,7 @@ function OAuthClientModals(props: any) {
<> <>
{/* Create OAuth Client modal */} {/* Create OAuth Client modal */}
{oauthCreateOpen && ( {oauthCreateOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget && !oauthCreatedClient) setOauthCreateOpen(false) }}> onClick={e => { if (e.target === e.currentTarget && !oauthCreatedClient) setOauthCreateOpen(false) }}>
<div className="rounded-xl shadow-xl w-full max-w-lg p-6 space-y-4 overflow-y-auto max-h-[90vh] bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-lg p-6 space-y-4 overflow-y-auto max-h-[90vh] bg-surface-card">
{!oauthCreatedClient ? ( {!oauthCreatedClient ? (
@@ -703,7 +697,7 @@ function OAuthClientModals(props: any) {
) : ( ) : (
<> <>
<h3 className="text-lg font-semibold text-content">{t('settings.oauth.modal.createdTitle')}</h3> <h3 className="text-lg font-semibold text-content">{t('settings.oauth.modal.createdTitle')}</h3>
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}> <div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200 bg-[rgba(251,191,36,0.1)]">
<span className="text-amber-500 mt-0.5"></span> <span className="text-amber-500 mt-0.5"></span>
<p className="text-sm text-content-secondary">{t('settings.oauth.modal.createdWarning')}</p> <p className="text-sm text-content-secondary">{t('settings.oauth.modal.createdWarning')}</p>
</div> </div>
@@ -755,7 +749,7 @@ function OAuthClientModals(props: any) {
{/* Delete OAuth Client confirm */} {/* Delete OAuth Client confirm */}
{oauthDeleteId !== null && ( {oauthDeleteId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget) setOauthDeleteId(null) }}> onClick={e => { if (e.target === e.currentTarget) setOauthDeleteId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
<h3 className="text-base font-semibold text-content">{t('settings.oauth.deleteClient')}</h3> <h3 className="text-base font-semibold text-content">{t('settings.oauth.deleteClient')}</h3>
@@ -776,7 +770,7 @@ function OAuthClientModals(props: any) {
{/* Rotate OAuth Client Secret confirm */} {/* Rotate OAuth Client Secret confirm */}
{oauthRotateId !== null && ( {oauthRotateId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget) setOauthRotateId(null) }}> onClick={e => { if (e.target === e.currentTarget) setOauthRotateId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
<h3 className="text-base font-semibold text-content">{t('settings.oauth.rotateSecret')}</h3> <h3 className="text-base font-semibold text-content">{t('settings.oauth.rotateSecret')}</h3>
@@ -797,10 +791,10 @@ function OAuthClientModals(props: any) {
{/* Rotated Secret display */} {/* Rotated Secret display */}
{oauthRotatedSecret !== null && ( {oauthRotatedSecret !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }}> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]">
<div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4 bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-md p-6 space-y-4 bg-surface-card">
<h3 className="text-lg font-semibold text-content">{t('settings.oauth.rotateSecretDoneTitle')}</h3> <h3 className="text-lg font-semibold text-content">{t('settings.oauth.rotateSecretDoneTitle')}</h3>
<div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200" style={{ background: 'rgba(251,191,36,0.1)' }}> <div className="flex items-start gap-2 p-3 rounded-lg border border-amber-200 bg-[rgba(251,191,36,0.1)]">
<span className="text-amber-500 mt-0.5"></span> <span className="text-amber-500 mt-0.5"></span>
<p className="text-sm text-content-secondary">{t('settings.oauth.rotateSecretDoneWarning')}</p> <p className="text-sm text-content-secondary">{t('settings.oauth.rotateSecretDoneWarning')}</p>
</div> </div>
@@ -828,7 +822,7 @@ function OAuthClientModals(props: any) {
{/* Revoke OAuth Session confirm */} {/* Revoke OAuth Session confirm */}
{oauthRevokeId !== null && ( {oauthRevokeId !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(0,0,0,0.5)' }} <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[rgba(0,0,0,0.5)]"
onClick={e => { if (e.target === e.currentTarget) setOauthRevokeId(null) }}> onClick={e => { if (e.target === e.currentTarget) setOauthRevokeId(null) }}>
<div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card"> <div className="rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4 bg-surface-card">
<h3 className="text-base font-semibold text-content">{t('settings.oauth.revokeSession')}</h3> <h3 className="text-base font-semibold text-content">{t('settings.oauth.revokeSession')}</h3>
+14 -16
View File
@@ -92,10 +92,10 @@ export default function OfflineTab(): React.ReactElement {
<button <button
onClick={handleResync} onClick={handleResync}
disabled={syncing || !navigator.onLine} disabled={syncing || !navigator.onLine}
className="border border-edge bg-surface-secondary text-content"
style={{ style={{
display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px',
borderRadius: 8, border: '1px solid var(--border-primary)', borderRadius: 8,
background: 'var(--bg-secondary)', color: 'var(--text-primary)',
cursor: syncing || !navigator.onLine ? 'not-allowed' : 'pointer', cursor: syncing || !navigator.onLine ? 'not-allowed' : 'pointer',
fontSize: 13, fontWeight: 500, opacity: !navigator.onLine ? 0.5 : 1, fontSize: 13, fontWeight: 500, opacity: !navigator.onLine ? 0.5 : 1,
}} }}
@@ -107,10 +107,10 @@ export default function OfflineTab(): React.ReactElement {
<button <button
onClick={handleClear} onClick={handleClear}
disabled={clearing || rows.length === 0} disabled={clearing || rows.length === 0}
className="border border-edge bg-surface-secondary text-[#ef4444]"
style={{ style={{
display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px',
borderRadius: 8, border: '1px solid var(--border-primary)', borderRadius: 8,
background: 'var(--bg-secondary)', color: '#ef4444',
cursor: clearing || rows.length === 0 ? 'not-allowed' : 'pointer', cursor: clearing || rows.length === 0 ? 'not-allowed' : 'pointer',
fontSize: 13, fontWeight: 500, opacity: rows.length === 0 ? 0.5 : 1, fontSize: 13, fontWeight: 500, opacity: rows.length === 0 ? 0.5 : 1,
}} }}
@@ -122,9 +122,9 @@ export default function OfflineTab(): React.ReactElement {
{/* Cached trip list */} {/* Cached trip list */}
{loading ? ( {loading ? (
<p style={{ color: 'var(--text-muted)', fontSize: 13 }}>Loading</p> <p className="text-content-muted" style={{ fontSize: 13 }}>Loading</p>
) : rows.length === 0 ? ( ) : rows.length === 0 ? (
<p style={{ color: 'var(--text-muted)', fontSize: 13 }}> <p className="text-content-muted" style={{ fontSize: 13 }}>
No trips cached yet. Connect to internet to sync. No trips cached yet. Connect to internet to sync.
</p> </p>
) : ( ) : (
@@ -132,25 +132,24 @@ export default function OfflineTab(): React.ReactElement {
{rows.map(({ trip, meta, placeCount, fileCount }) => ( {rows.map(({ trip, meta, placeCount, fileCount }) => (
<div <div
key={trip.id} key={trip.id}
className="border border-edge bg-surface-secondary"
style={{ style={{
padding: '10px 14px', borderRadius: 8, padding: '10px 14px', borderRadius: 8,
border: '1px solid var(--border-primary)',
background: 'var(--bg-secondary)',
display: 'flex', flexDirection: 'column', gap: 2, display: 'flex', flexDirection: 'column', gap: 2,
}} }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}> <span className="text-content" style={{ fontWeight: 600, fontSize: 14 }}>
{trip.title} {trip.title}
</span> </span>
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}> <span className="text-content-muted" style={{ fontSize: 11 }}>
<Wifi size={10} style={{ display: 'inline', marginRight: 3 }} /> <Wifi size={10} style={{ display: 'inline', marginRight: 3 }} />
{meta.lastSyncedAt {meta.lastSyncedAt
? new Date(meta.lastSyncedAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) ? new Date(meta.lastSyncedAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
: '—'} : '—'}
</span> </span>
</div> </div>
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}> <span className="text-content-muted" style={{ fontSize: 12 }}>
{formatDate(trip.start_date)} {formatDate(trip.end_date)} {formatDate(trip.start_date)} {formatDate(trip.end_date)}
{' · '} {' · '}
{placeCount} place{placeCount !== 1 ? 's' : ''} {placeCount} place{placeCount !== 1 ? 's' : ''}
@@ -168,13 +167,12 @@ export default function OfflineTab(): React.ReactElement {
function Stat({ label, value }: { label: string; value: number }) { function Stat({ label, value }: { label: string; value: number }) {
return ( return (
<div style={{ <div className="border border-edge bg-surface-secondary" style={{
padding: '8px 14px', borderRadius: 8, padding: '8px 14px', borderRadius: 8,
border: '1px solid var(--border-primary)', minWidth: 100,
background: 'var(--bg-secondary)', minWidth: 100,
}}> }}>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)' }}>{value}</div> <div className="text-content" style={{ fontSize: 20, fontWeight: 700 }}>{value}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{label}</div> <div className="text-content-muted" style={{ fontSize: 11 }}>{label}</div>
</div> </div>
) )
} }
+4 -4
View File
@@ -9,10 +9,10 @@ interface SectionProps {
export default function Section({ title, icon: Icon, children }: SectionProps): React.ReactElement { export default function Section({ title, icon: Icon, children }: SectionProps): React.ReactElement {
return ( return (
<div className="rounded-xl border overflow-hidden" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)', marginBottom: 24 }}> <div className="rounded-xl border overflow-hidden bg-surface-card border-edge" style={{ marginBottom: 24 }}>
<div className="px-6 py-4 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-secondary)' }}> <div className="px-6 py-4 border-b flex items-center gap-2 border-edge-secondary">
<Icon className="w-5 h-5" style={{ color: 'var(--text-secondary)' }} /> <Icon className="w-5 h-5 text-content-secondary" />
<h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>{title}</h2> <h2 className="font-semibold text-content">{title}</h2>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{children} {children}
+12 -12
View File
@@ -315,7 +315,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) } } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
} }
const labelStyle: React.CSSProperties = { fontSize: 12, fontWeight: 500, color: 'var(--text-secondary)', marginBottom: 4, display: 'block' } const labelClass = 'block text-xs font-medium text-content-secondary mb-1'
const inputStyle: React.CSSProperties = { const inputStyle: React.CSSProperties = {
width: '100%', fontSize: 13, padding: '8px 10px', border: '1px solid var(--border-primary)', width: '100%', fontSize: 13, padding: '8px 10px', border: '1px solid var(--border-primary)',
borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit',
@@ -345,7 +345,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
{/* Description */} {/* Description */}
<div> <div>
<label style={labelStyle}>{t('todo.detail.description')}</label> <label className={labelClass}>{t('todo.detail.description')}</label>
<textarea value={desc} onChange={e => setDesc(e.target.value)} disabled={!canEdit} rows={4} <textarea value={desc} onChange={e => setDesc(e.target.value)} disabled={!canEdit} rows={4}
placeholder={t('todo.descriptionPlaceholder')} placeholder={t('todo.descriptionPlaceholder')}
style={{ ...inputStyle, resize: 'vertical', minHeight: 80 }} /> style={{ ...inputStyle, resize: 'vertical', minHeight: 80 }} />
@@ -353,7 +353,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
{/* Priority */} {/* Priority */}
<div> <div>
<label style={labelStyle}>{t('todo.detail.priority')}</label> <label className={labelClass}>{t('todo.detail.priority')}</label>
<div style={{ display: 'flex', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
{[0, 1, 2, 3].map(p => { {[0, 1, 2, 3].map(p => {
const cfg = PRIO_CONFIG[p] const cfg = PRIO_CONFIG[p]
@@ -377,7 +377,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
{/* Category */} {/* Category */}
<div> <div>
<label style={labelStyle}>{t('todo.detail.category')}</label> <label className={labelClass}>{t('todo.detail.category')}</label>
<CustomSelect <CustomSelect
value={category} value={category}
onChange={v => setCategory(String(v))} onChange={v => setCategory(String(v))}
@@ -397,7 +397,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
{/* Due date */} {/* Due date */}
<div> <div>
<label style={labelStyle}>{t('todo.detail.dueDate')}</label> <label className={labelClass}>{t('todo.detail.dueDate')}</label>
<CustomDatePicker <CustomDatePicker
value={dueDate} value={dueDate}
onChange={v => setDueDate(v)} onChange={v => setDueDate(v)}
@@ -406,7 +406,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
{/* Assigned to */} {/* Assigned to */}
<div> <div>
<label style={labelStyle}>{t('todo.detail.assignedTo')}</label> <label className={labelClass}>{t('todo.detail.assignedTo')}</label>
<CustomSelect <CustomSelect
value={String(assignedUserId ?? '')} value={String(assignedUserId ?? '')}
onChange={v => setAssignedUserId(v ? Number(v) : null)} onChange={v => setAssignedUserId(v ? Number(v) : null)}
@@ -477,7 +477,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
const [priority, setPriority] = useState(0) const [priority, setPriority] = useState(0)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const labelStyle: React.CSSProperties = { fontSize: 12, fontWeight: 500, color: 'var(--text-secondary)', marginBottom: 4, display: 'block' } const labelClass = 'block text-xs font-medium text-content-secondary mb-1'
const create = async () => { const create = async () => {
if (!name.trim()) return if (!name.trim()) return
@@ -515,14 +515,14 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
</div> </div>
<div> <div>
<label style={labelStyle}>{t('todo.detail.description')}</label> <label className={labelClass}>{t('todo.detail.description')}</label>
<textarea value={desc} onChange={e => setDesc(e.target.value)} rows={4} <textarea value={desc} onChange={e => setDesc(e.target.value)} rows={4}
placeholder={t('todo.descriptionPlaceholder')} placeholder={t('todo.descriptionPlaceholder')}
style={{ width: '100%', fontSize: 13, padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', resize: 'vertical', minHeight: 80 }} /> style={{ width: '100%', fontSize: 13, padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', resize: 'vertical', minHeight: 80 }} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('todo.detail.category')}</label> <label className={labelClass}>{t('todo.detail.category')}</label>
{addingCategory ? ( {addingCategory ? (
<div style={{ display: 'flex', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
<input <input
@@ -569,7 +569,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
</div> </div>
<div> <div>
<label style={labelStyle}>{t('todo.detail.priority')}</label> <label className={labelClass}>{t('todo.detail.priority')}</label>
<div style={{ display: 'flex', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
{[0, 1, 2, 3].map(p => { {[0, 1, 2, 3].map(p => {
const cfg = PRIO_CONFIG[p] const cfg = PRIO_CONFIG[p]
@@ -592,12 +592,12 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
</div> </div>
<div> <div>
<label style={labelStyle}>{t('todo.detail.dueDate')}</label> <label className={labelClass}>{t('todo.detail.dueDate')}</label>
<CustomDatePicker value={dueDate} onChange={v => setDueDate(v)} /> <CustomDatePicker value={dueDate} onChange={v => setDueDate(v)} />
</div> </div>
<div> <div>
<label style={labelStyle}>{t('todo.detail.assignedTo')}</label> <label className={labelClass}>{t('todo.detail.assignedTo')}</label>
<CustomSelect <CustomSelect
value={String(assignedUserId ?? '')} value={String(assignedUserId ?? '')}
onChange={v => setAssignedUserId(v ? Number(v) : null)} onChange={v => setAssignedUserId(v ? Number(v) : null)}
@@ -125,7 +125,7 @@ describe('VacayCalendar', () => {
await user.click(companyBtn) await user.click(companyBtn)
expect(companyBtn).toHaveStyle({ background: '#d97706' }) expect(companyBtn).toHaveClass('bg-[#d97706]')
}) })
it('FE-COMP-VACAYCALENDAR-006: cell click in vacation mode calls toggleEntry', async () => { it('FE-COMP-VACAYCALENDAR-006: cell click in vacation mode calls toggleEntry', async () => {
+2 -12
View File
@@ -94,12 +94,7 @@ export default function VacayCalendar() {
<div className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded-xl border bg-surface-card border-edge" style={{ boxShadow: '0 8px 32px rgba(0,0,0,0.12)' }}> <div className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded-xl border bg-surface-card border-edge" style={{ boxShadow: '0 8px 32px rgba(0,0,0,0.12)' }}>
<button <button
onClick={() => setCompanyMode(false)} onClick={() => setCompanyMode(false)}
className="flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] sm:text-xs font-medium transition-[background-color,color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]" className={`flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] sm:text-xs font-medium transition-[background-color,color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] border ${!companyMode ? 'bg-content text-surface-card border-transparent' : 'bg-transparent text-content-muted border-edge'}`}>
style={{
background: !companyMode ? 'var(--text-primary)' : 'transparent',
color: !companyMode ? 'var(--bg-card)' : 'var(--text-muted)',
border: companyMode ? '1px solid var(--border-primary)' : '1px solid transparent',
}}>
<MousePointer2 size={13} /> <MousePointer2 size={13} />
{selectedUser && <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: selectedUser.color }} />} {selectedUser && <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: selectedUser.color }} />}
{selectedUser ? selectedUser.username : t('vacay.modeVacation')} {selectedUser ? selectedUser.username : t('vacay.modeVacation')}
@@ -107,12 +102,7 @@ export default function VacayCalendar() {
{companyHolidaysEnabled && ( {companyHolidaysEnabled && (
<button <button
onClick={() => setCompanyMode(true)} onClick={() => setCompanyMode(true)}
className="flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] sm:text-xs font-medium transition-[background-color,color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]" className={`flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] sm:text-xs font-medium transition-[background-color,color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] border ${companyMode ? 'bg-[#d97706] text-[#fff] border-transparent' : 'bg-transparent text-content-muted border-edge'}`}>
style={{
background: companyMode ? '#d97706' : 'transparent',
color: companyMode ? '#fff' : 'var(--text-muted)',
border: !companyMode ? '1px solid var(--border-primary)' : '1px solid transparent',
}}>
<Building2 size={13} /> <Building2 size={13} />
{t('vacay.modeCompany')} {t('vacay.modeCompany')}
</button> </button>
@@ -88,7 +88,7 @@ describe('VacayMonthCard', () => {
const cell = daySpan.closest('div') as HTMLElement const cell = daySpan.closest('div') as HTMLElement
// Company overlay is a direct child div with amber background // Company overlay is a direct child div with amber background
const overlayDivs = Array.from(cell.querySelectorAll(':scope > div')) as HTMLElement[] const overlayDivs = Array.from(cell.querySelectorAll(':scope > div')) as HTMLElement[]
const companyOverlay = overlayDivs.find(el => el.style.background.includes('245')) const companyOverlay = overlayDivs.find(el => el.className.includes('bg-[rgba(245,158,11'))
expect(companyOverlay).toBeTruthy() expect(companyOverlay).toBeTruthy()
}) })
@@ -71,7 +71,7 @@ export default function VacayMonthCard({
const jsDay = (i + weekStart) % 7 const jsDay = (i + weekStart) % 7
const isWeekendCol = weekendDays.includes(jsDay) const isWeekendCol = weekendDays.includes(jsDay)
return ( return (
<div key={`${wd}-${i}`} className="text-center text-[10px] font-medium py-1" style={{ color: isWeekendCol ? 'var(--text-faint)' : 'var(--text-muted)' }}> <div key={`${wd}-${i}`} className={`text-center text-[10px] font-medium py-1 ${isWeekendCol ? 'text-content-faint' : 'text-content-muted'}`}>
{wd} {wd}
</div> </div>
) )
@@ -110,7 +110,7 @@ export default function VacayMonthCard({
onMouseLeave={e => { e.currentTarget.style.background = weekend ? 'var(--bg-secondary)' : 'transparent' }} onMouseLeave={e => { e.currentTarget.style.background = weekend ? 'var(--bg-secondary)' : 'transparent' }}
> >
{holiday && <div className="absolute inset-0.5 rounded" style={{ background: hexToRgba(holiday.color, 0.12) }} />} {holiday && <div className="absolute inset-0.5 rounded" style={{ background: hexToRgba(holiday.color, 0.12) }} />}
{isCompany && <div className="absolute inset-0.5 rounded" style={{ background: 'rgba(245,158,11,0.15)' }} />} {isCompany && <div className="absolute inset-0.5 rounded bg-[rgba(245,158,11,0.15)]" />}
{dayEntries.length === 1 && ( {dayEntries.length === 1 && (
<div className="absolute inset-0.5 rounded" style={{ backgroundColor: dayEntries[0].person_color, opacity: 0.4 }} /> <div className="absolute inset-0.5 rounded" style={{ backgroundColor: dayEntries[0].person_color, opacity: 0.4 }} />
@@ -138,7 +138,7 @@ export default function VacayMonthCard({
)} )}
{tripDates?.has(dateStr) && ( {tripDates?.has(dateStr) && (
<span className="absolute top-[3px] right-[3px] w-[5px] h-[5px] rounded-full z-[2]" style={{ background: '#3b82f6' }} /> <span className="absolute top-[3px] right-[3px] w-[5px] h-[5px] rounded-full z-[2] bg-[#3b82f6]" />
)} )}
<span className="relative z-[1] text-[11px]" style={{ <span className="relative z-[1] text-[11px]" style={{
+4 -7
View File
@@ -79,10 +79,8 @@ export default function VacayPersons() {
return ( return (
<div key={u.id} <div key={u.id}
onClick={() => { if (isFused) setSelectedUserId(u.id) }} onClick={() => { if (isFused) setSelectedUserId(u.id) }}
className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg group transition-all" className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg group transition-all border ${isSelected ? 'bg-surface-hover border-edge' : 'bg-transparent border-transparent'}`}
style={{ style={{
background: isSelected ? 'var(--bg-hover)' : 'transparent',
border: isSelected ? '1px solid var(--border-primary)' : '1px solid transparent',
cursor: isFused ? 'pointer' : 'default', cursor: isFused ? 'pointer' : 'default',
}}> }}>
<button <button
@@ -120,7 +118,7 @@ export default function VacayPersons() {
{/* Invite Modal — Portal to body to avoid z-index issues */} {/* Invite Modal — Portal to body to avoid z-index issues */}
{showInvite && ReactDOM.createPortal( {showInvite && ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter" style={{ zIndex: 99990, backgroundColor: 'rgba(15,23,42,0.5)', paddingTop: 70 }} <div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]" style={{ zIndex: 99990, paddingTop: 70 }}
onClick={() => setShowInvite(false)}> onClick={() => setShowInvite(false)}>
<div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-sm bg-surface-card" <div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-sm bg-surface-card"
onClick={e => e.stopPropagation()}> onClick={e => e.stopPropagation()}>
@@ -148,8 +146,7 @@ export default function VacayPersons() {
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button onClick={handleInvite} disabled={!selectedInviteUser || inviting} <button onClick={handleInvite} disabled={!selectedInviteUser || inviting}
className="px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-1.5 disabled:opacity-40" className="px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-1.5 disabled:opacity-40 bg-content text-surface-card">
style={{ background: 'var(--text-primary)', color: 'var(--bg-card)' }}>
{inviting && <Loader2 size={13} className="animate-spin" />} {inviting && <Loader2 size={13} className="animate-spin" />}
{t('vacay.sendInvite')} {t('vacay.sendInvite')}
</button> </button>
@@ -162,7 +159,7 @@ export default function VacayPersons() {
{/* Color Picker Modal — Portal to body */} {/* Color Picker Modal — Portal to body */}
{showColorPicker && ReactDOM.createPortal( {showColorPicker && ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter" style={{ zIndex: 99990, backgroundColor: 'rgba(15,23,42,0.5)', paddingTop: 70 }} <div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]" style={{ zIndex: 99990, paddingTop: 70 }}
onClick={() => { setShowColorPicker(false); setColorEditUserId(null) }}> onClick={() => { setShowColorPicker(false); setColorEditUserId(null) }}>
<div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-xs bg-surface-card" <div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-xs bg-surface-card"
onClick={e => e.stopPropagation()}> onClick={e => e.stopPropagation()}>
+9 -13
View File
@@ -191,9 +191,9 @@ export default function VacaySettings({ onClose }: VacaySettingsProps) {
{/* Dissolve fusion */} {/* Dissolve fusion */}
{isFused && ( {isFused && (
<div className="pt-4 mt-2 border-t border-edge-secondary"> <div className="pt-4 mt-2 border-t border-edge-secondary">
<div className="rounded-xl overflow-hidden" style={{ border: '1px solid rgba(239,68,68,0.2)' }}> <div className="rounded-xl overflow-hidden border border-[rgba(239,68,68,0.2)]">
<div className="px-4 py-3 flex items-center gap-3" style={{ background: 'rgba(239,68,68,0.06)' }}> <div className="px-4 py-3 flex items-center gap-3 bg-[rgba(239,68,68,0.06)]">
<div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ background: 'rgba(239,68,68,0.1)' }}> <div className="w-8 h-8 rounded-lg flex items-center justify-center bg-[rgba(239,68,68,0.1)]">
<Unlink size={16} className="text-red-500" /> <Unlink size={16} className="text-red-500" />
</div> </div>
<div> <div>
@@ -201,7 +201,7 @@ export default function VacaySettings({ onClose }: VacaySettingsProps) {
<p className="text-[11px] text-content-faint">{t('vacay.dissolveHint')}</p> <p className="text-[11px] text-content-faint">{t('vacay.dissolveHint')}</p>
</div> </div>
</div> </div>
<div className="px-4 py-3 flex items-center gap-2 flex-wrap" style={{ borderTop: '1px solid rgba(239,68,68,0.1)' }}> <div className="px-4 py-3 flex items-center gap-2 flex-wrap border-t border-t-[rgba(239,68,68,0.1)]">
{users.map(u => ( {users.map(u => (
<div key={u.id} className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-secondary"> <div key={u.id} className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-secondary">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: u.color || '#6366f1' }} /> <span className="w-2 h-2 rounded-full" style={{ backgroundColor: u.color || '#6366f1' }} />
@@ -209,7 +209,7 @@ export default function VacaySettings({ onClose }: VacaySettingsProps) {
</div> </div>
))} ))}
</div> </div>
<div className="px-4 py-3" style={{ borderTop: '1px solid rgba(239,68,68,0.1)' }}> <div className="px-4 py-3 border-t border-t-[rgba(239,68,68,0.1)]">
<button <button
onClick={async () => { onClick={async () => {
await dissolve() await dissolve()
@@ -247,8 +247,7 @@ function SettingToggle({ icon: Icon, label, hint, value, onChange }: SettingTogg
</div> </div>
</div> </div>
<button onClick={onChange} <button onClick={onChange}
className="relative shrink-0 inline-flex h-6 w-11 items-center rounded-full transition-colors" className={`relative shrink-0 inline-flex h-6 w-11 items-center rounded-full transition-colors ${value ? 'bg-content' : 'bg-edge'}`}>
style={{ background: value ? 'var(--text-primary)' : 'var(--border-primary)' }}>
<span className="absolute left-1 h-4 w-4 rounded-full transition-transform duration-200 bg-surface-card" <span className="absolute left-1 h-4 w-4 rounded-full transition-transform duration-200 bg-surface-card"
style={{ transform: value ? 'translateX(20px)' : 'translateX(0)' }} /> style={{ transform: value ? 'translateX(20px)' : 'translateX(0)' }} />
</button> </button>
@@ -353,8 +352,7 @@ function CalendarRow({ cal, countries, onUpdate, onDelete }: {
</div> </div>
<button <button
onClick={onDelete} onClick={onDelete}
className="shrink-0 p-1.5 rounded-md transition-colors" className="shrink-0 p-1.5 rounded-md transition-colors text-content-faint"
style={{ color: 'var(--text-faint)' }}
onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.background = 'rgba(239,68,68,0.1)' }} onMouseEnter={e => { (e.currentTarget as HTMLButtonElement).style.background = 'rgba(239,68,68,0.1)' }}
onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }} onMouseLeave={e => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }}
> >
@@ -437,15 +435,13 @@ function AddCalendarForm({ countries, onAdd, onCancel }: {
<button <button
disabled={!canAdd} disabled={!canAdd}
onClick={() => onAdd({ region: region || selectedCountry, color, label: label.trim() || null })} onClick={() => onAdd({ region: region || selectedCountry, color, label: label.trim() || null })}
className="flex-1 text-xs px-2 py-1.5 rounded-md font-medium transition-colors disabled:opacity-40" className="flex-1 text-xs px-2 py-1.5 rounded-md font-medium transition-colors disabled:opacity-40 bg-content text-surface-card"
style={{ background: 'var(--text-primary)', color: 'var(--bg-card)' }}
> >
{t('vacay.add')} {t('vacay.add')}
</button> </button>
<button <button
onClick={onCancel} onClick={onCancel}
className="text-xs px-2 py-1.5 rounded-md transition-colors" className="text-xs px-2 py-1.5 rounded-md transition-colors bg-surface-secondary text-content-muted"
style={{ background: 'var(--bg-secondary)', color: 'var(--text-muted)' }}
> >
</button> </button>
+3 -5
View File
@@ -91,10 +91,8 @@ function StatCard({ stat: s, isMe, canEdit, selectedYear, onSave, t }: StatCardP
<div className="grid grid-cols-3 gap-1.5"> <div className="grid grid-cols-3 gap-1.5">
{/* Days — editable */} {/* Days — editable */}
<div <div
className="rounded-md px-2 py-2 group/days" className={`rounded-md px-2 py-2 group/days border ${canEdit ? 'bg-surface-card border-edge' : 'bg-surface-secondary border-transparent'}`}
style={{ style={{
background: canEdit ? 'var(--bg-card)' : 'var(--bg-secondary)',
border: canEdit ? '1px solid var(--border-primary)' : '1px solid transparent',
cursor: canEdit ? 'pointer' : 'default', cursor: canEdit ? 'pointer' : 'default',
}} }}
onClick={() => { if (canEdit && !editing) setEditing(true) }} onClick={() => { if (canEdit && !editing) setEditing(true) }}
@@ -131,8 +129,8 @@ function StatCard({ stat: s, isMe, canEdit, selectedYear, onSave, t }: StatCardP
</div> </div>
</div> </div>
{s.carried_over > 0 && ( {s.carried_over > 0 && (
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md" style={{ background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.15)' }}> <div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-[rgba(245,158,11,0.08)] border border-[rgba(245,158,11,0.15)]">
<span className="text-[10px]" style={{ color: '#d97706' }}>+{s.carried_over} {t('vacay.carriedOver', { year: selectedYear - 1 })}</span> <span className="text-[10px] text-[#d97706]">+{s.carried_over} {t('vacay.carriedOver', { year: selectedYear - 1 })}</span>
</div> </div>
)} )}
</div> </div>
@@ -41,8 +41,8 @@ export default function ConfirmDialog({
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter" className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)', paddingBottom: 'var(--bottom-nav-h)' }} style={{ paddingBottom: 'var(--bottom-nav-h)' }}
onClick={onClose} onClick={onClose}
> >
<div <div
@@ -42,8 +42,8 @@ export default function CopyTripDialog({ isOpen, tripTitle, onClose, onConfirm }
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter" className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)', paddingBottom: 'var(--bottom-nav-h)' }} style={{ paddingBottom: 'var(--bottom-nav-h)' }}
onClick={onClose} onClick={onClose}
> >
<div <div
@@ -59,13 +59,13 @@ export default function CopyTripDialog({ isOpen, tripTitle, onClose, onConfirm }
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="rounded-xl p-3 border border-edge-secondary" style={{ background: 'var(--bg-subtle)' }}> <div className="rounded-xl p-3 border border-edge-secondary" style={{ background: 'var(--bg-subtle)' }}>
<p className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#16a34a' }}> <p className="text-xs font-semibold uppercase tracking-wide mb-2 text-[#16a34a]">
{t('dashboard.confirm.copy.willCopy')} {t('dashboard.confirm.copy.willCopy')}
</p> </p>
<ul className="flex flex-col gap-1"> <ul className="flex flex-col gap-1">
{WILL_COPY_KEYS.map(key => ( {WILL_COPY_KEYS.map(key => (
<li key={key} className="flex items-center gap-2 text-sm text-content-secondary"> <li key={key} className="flex items-center gap-2 text-sm text-content-secondary">
<Check size={13} className="flex-shrink-0" style={{ color: '#16a34a' }} /> <Check size={13} className="flex-shrink-0 text-[#16a34a]" />
{t(key)} {t(key)}
</li> </li>
))} ))}
@@ -96,8 +96,7 @@ export default function CopyTripDialog({ isOpen, tripTitle, onClose, onConfirm }
</button> </button>
<button <button
onClick={() => { onConfirm(); onClose() }} onClick={() => { onConfirm(); onClose() }}
className="px-4 py-2 text-sm font-medium rounded-lg transition-opacity hover:opacity-90" className="px-4 py-2 text-sm font-medium rounded-lg transition-opacity hover:opacity-90 bg-content text-surface-card"
style={{ background: 'var(--text-primary)', color: 'var(--bg-card)' }}
> >
{t('dashboard.confirm.copy.confirm')} {t('dashboard.confirm.copy.confirm')}
</button> </button>
+2 -2
View File
@@ -51,8 +51,8 @@ export default function Modal({
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div <div
className="fixed inset-0 z-[200] flex items-start sm:items-center justify-center px-4 modal-backdrop trek-backdrop-enter" className="fixed inset-0 z-[200] flex items-start sm:items-center justify-center px-4 modal-backdrop trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)', paddingTop: 70, paddingBottom: 'calc(20px + var(--bottom-nav-h))', overflow: 'hidden' }} style={{ paddingTop: 70, paddingBottom: 'calc(20px + var(--bottom-nav-h))', overflow: 'hidden' }}
onMouseDown={e => { mouseDownTarget.current = e.target }} onMouseDown={e => { mouseDownTarget.current = e.target }}
onClick={e => { onClick={e => {
if (e.target === e.currentTarget && mouseDownTarget.current === e.currentTarget) onClose() if (e.target === e.currentTarget && mouseDownTarget.current === e.currentTarget) onClose()
+20 -34
View File
@@ -110,8 +110,7 @@ function AdminNotificationsPanel({ t, toast }: { t: (k: string) => string; toast
<div key={ch} style={{ display: 'flex', justifyContent: 'center' }}> <div key={ch} style={{ display: 'flex', justifyContent: 'center' }}>
<button <button
onClick={() => toggle(eventType, ch)} onClick={() => toggle(eventType, ch)}
className="relative inline-flex h-5 w-9 items-center rounded-full transition-colors flex-shrink-0" className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors flex-shrink-0 ${isOn ? 'bg-content' : 'bg-edge'}`}
style={{ background: isOn ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-4 w-4 rounded-full bg-white transition-transform duration-200" <span className="absolute left-0.5 h-4 w-4 rounded-full bg-white transition-transform duration-200"
style={{ transform: isOn ? 'translateX(16px)' : 'translateX(0)' }} /> style={{ transform: isOn ? 'translateX(16px)' : 'translateX(0)' }} />
@@ -301,7 +300,7 @@ export default function AdminPage(): React.ReactElement {
{u.username.charAt(0).toUpperCase()} {u.username.charAt(0).toUpperCase()}
</div> </div>
)} )}
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-surface-card" style={{ background: u.online ? '#22c55e' : '#94a3b8' }} /> <span className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-surface-card ${u.online ? 'bg-[#22c55e]' : 'bg-[#94a3b8]'}`} />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-slate-900">{u.username}</p> <p className="text-sm font-medium text-slate-900">{u.username}</p>
@@ -383,7 +382,7 @@ export default function AdminPage(): React.ReactElement {
const isActive = !isExpired && !isUsedUp const isActive = !isExpired && !isUsedUp
return ( return (
<div key={inv.id} className="px-5 py-3 flex items-center gap-4"> <div key={inv.id} className="px-5 py-3 flex items-center gap-4">
<Link2 className="w-4 h-4 flex-shrink-0" style={{ color: isActive ? 'var(--text-primary)' : '#d1d5db' }} /> <Link2 className={`w-4 h-4 flex-shrink-0 ${isActive ? 'text-content' : 'text-[#d1d5db]'}`} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="text-xs font-mono text-slate-600 truncate">{inv.token.slice(0, 12)}...</code> <code className="text-xs font-mono text-slate-600 truncate">{inv.token.slice(0, 12)}...</code>
@@ -505,8 +504,7 @@ export default function AdminPage(): React.ReactElement {
disabled={envOverrideOidcOnly || (!passwordLogin && !oidcLogin)} disabled={envOverrideOidcOnly || (!passwordLogin && !oidcLogin)}
onClick={() => handleToggleAuthSetting('password_login', !passwordLogin, setPasswordLogin)} onClick={() => handleToggleAuthSetting('password_login', !passwordLogin, setPasswordLogin)}
title={!passwordLogin && !oidcLogin ? t('admin.lockoutWarning') : undefined} title={!passwordLogin && !oidcLogin ? t('admin.lockoutWarning') : undefined}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${passwordLogin ? 'bg-content' : 'bg-edge'}`}
style={{ background: passwordLogin ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span <span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
@@ -523,8 +521,7 @@ export default function AdminPage(): React.ReactElement {
<button <button
disabled={envOverrideOidcOnly} disabled={envOverrideOidcOnly}
onClick={() => handleToggleAuthSetting('password_registration', !passwordRegistration, setPasswordRegistration)} onClick={() => handleToggleAuthSetting('password_registration', !passwordRegistration, setPasswordRegistration)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${passwordRegistration ? 'bg-content' : 'bg-edge'}`}
style={{ background: passwordRegistration ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span <span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
@@ -543,8 +540,7 @@ export default function AdminPage(): React.ReactElement {
disabled={!passwordLogin && oidcLogin} disabled={!passwordLogin && oidcLogin}
onClick={() => handleToggleAuthSetting('oidc_login', !oidcLogin, setOidcLogin)} onClick={() => handleToggleAuthSetting('oidc_login', !oidcLogin, setOidcLogin)}
title={!passwordLogin && oidcLogin ? t('admin.lockoutWarning') : undefined} title={!passwordLogin && oidcLogin ? t('admin.lockoutWarning') : undefined}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${oidcLogin ? 'bg-content' : 'bg-edge'}`}
style={{ background: oidcLogin ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span <span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
@@ -562,8 +558,7 @@ export default function AdminPage(): React.ReactElement {
</div> </div>
<button <button
onClick={() => handleToggleAuthSetting('oidc_registration', !oidcRegistration, setOidcRegistration)} onClick={() => handleToggleAuthSetting('oidc_registration', !oidcRegistration, setOidcRegistration)}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors" className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${oidcRegistration ? 'bg-content' : 'bg-edge'}`}
style={{ background: oidcRegistration ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span <span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
@@ -589,8 +584,7 @@ export default function AdminPage(): React.ReactElement {
<button <button
type="button" type="button"
onClick={() => handleToggleRequireMfa(!requireMfa)} onClick={() => handleToggleRequireMfa(!requireMfa)}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors" className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${requireMfa ? 'bg-content' : 'bg-edge'}`}
style={{ background: requireMfa ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span <span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
@@ -707,8 +701,7 @@ export default function AdminPage(): React.ReactElement {
setPlacesPhotosEnabled(next) setPlacesPhotosEnabled(next)
try { await adminApi.updatePlacesPhotos(next) } catch { setPlacesPhotosEnabledState(!next); setPlacesPhotosEnabled(!next) } try { await adminApi.updatePlacesPhotos(next) } catch { setPlacesPhotosEnabledState(!next); setPlacesPhotosEnabled(!next) }
}} }}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors" className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesPhotosEnabled ? 'bg-content' : 'bg-edge'}`}
style={{ background: placesPhotosEnabled ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesPhotosEnabled ? 'translateX(20px)' : 'translateX(0)' }} /> <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesPhotosEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button> </button>
@@ -727,8 +720,7 @@ export default function AdminPage(): React.ReactElement {
setPlacesAutocompleteEnabled(next) setPlacesAutocompleteEnabled(next)
try { await adminApi.updatePlacesAutocomplete(next) } catch { setPlacesAutocompleteEnabledState(!next); setPlacesAutocompleteEnabled(!next) } try { await adminApi.updatePlacesAutocomplete(next) } catch { setPlacesAutocompleteEnabledState(!next); setPlacesAutocompleteEnabled(!next) }
}} }}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors" className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesAutocompleteEnabled ? 'bg-content' : 'bg-edge'}`}
style={{ background: placesAutocompleteEnabled ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesAutocompleteEnabled ? 'translateX(20px)' : 'translateX(0)' }} /> <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesAutocompleteEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button> </button>
@@ -747,8 +739,7 @@ export default function AdminPage(): React.ReactElement {
setPlacesDetailsEnabled(next) setPlacesDetailsEnabled(next)
try { await adminApi.updatePlacesDetails(next) } catch { setPlacesDetailsEnabledState(!next); setPlacesDetailsEnabled(!next) } try { await adminApi.updatePlacesDetails(next) } catch { setPlacesDetailsEnabledState(!next); setPlacesDetailsEnabled(!next) }
}} }}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors" className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesDetailsEnabled ? 'bg-content' : 'bg-edge'}`}
style={{ background: placesDetailsEnabled ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesDetailsEnabled ? 'translateX(20px)' : 'translateX(0)' }} /> <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesDetailsEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button> </button>
@@ -952,8 +943,7 @@ export default function AdminPage(): React.ReactElement {
</div> </div>
<button <button
onClick={() => setChannels(!emailActive, webhookActive, ntfyActive)} onClick={() => setChannels(!emailActive, webhookActive, ntfyActive)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0 ${emailActive ? 'bg-content' : 'bg-edge'}`}
style={{ background: emailActive ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: emailActive ? 'translateX(20px)' : 'translateX(0)' }} /> style={{ transform: emailActive ? 'translateX(20px)' : 'translateX(0)' }} />
@@ -987,8 +977,7 @@ export default function AdminPage(): React.ReactElement {
const newVal = smtpValues.smtp_skip_tls_verify === 'true' ? 'false' : 'true' const newVal = smtpValues.smtp_skip_tls_verify === 'true' ? 'false' : 'true'
setSmtpValues(prev => ({ ...prev, smtp_skip_tls_verify: newVal })) setSmtpValues(prev => ({ ...prev, smtp_skip_tls_verify: newVal }))
}} }}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors" className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${smtpValues.smtp_skip_tls_verify === 'true' ? 'bg-content' : 'bg-edge'}`}>
style={{ background: smtpValues.smtp_skip_tls_verify === 'true' ? 'var(--text-primary)' : 'var(--border-primary)' }}>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: smtpValues.smtp_skip_tls_verify === 'true' ? 'translateX(20px)' : 'translateX(0)' }} /> style={{ transform: smtpValues.smtp_skip_tls_verify === 'true' ? 'translateX(20px)' : 'translateX(0)' }} />
</button> </button>
@@ -1028,8 +1017,7 @@ export default function AdminPage(): React.ReactElement {
</div> </div>
<button <button
onClick={() => setChannels(emailActive, !webhookActive, ntfyActive)} onClick={() => setChannels(emailActive, !webhookActive, ntfyActive)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0 ${webhookActive ? 'bg-content' : 'bg-edge'}`}
style={{ background: webhookActive ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: webhookActive ? 'translateX(20px)' : 'translateX(0)' }} /> style={{ transform: webhookActive ? 'translateX(20px)' : 'translateX(0)' }} />
@@ -1046,8 +1034,7 @@ export default function AdminPage(): React.ReactElement {
</div> </div>
<button <button
onClick={() => setChannels(emailActive, webhookActive, !ntfyActive)} onClick={() => setChannels(emailActive, webhookActive, !ntfyActive)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0 ${ntfyActive ? 'bg-content' : 'bg-edge'}`}
style={{ background: ntfyActive ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: ntfyActive ? 'translateX(20px)' : 'translateX(0)' }} /> style={{ transform: ntfyActive ? 'translateX(20px)' : 'translateX(0)' }} />
@@ -1092,8 +1079,7 @@ export default function AdminPage(): React.ReactElement {
toast.error(t('common.error')) toast.error(t('common.error'))
} }
}} }}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors flex-shrink-0 ${tripRemindersActive ? 'bg-content' : 'bg-edge'}`}
style={{ background: tripRemindersActive ? 'var(--text-primary)' : 'var(--border-primary)' }}
> >
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" <span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: tripRemindersActive ? 'translateX(20px)' : 'translateX(0)' }} /> style={{ transform: tripRemindersActive ? 'translateX(20px)' : 'translateX(0)' }} />
@@ -1420,12 +1406,12 @@ export default function AdminPage(): React.ReactElement {
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700" className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
> >
<div style={{ background: 'linear-gradient(135deg, #0f172a, #1e293b)', padding: '20px 24px', display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ background: 'linear-gradient(135deg, #0f172a, #1e293b)', padding: '20px 24px', display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'rgba(255,255,255,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[rgba(255,255,255,0.2)]" style={{ width: 40, height: 40, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<ArrowUpCircle size={20} style={{ color: 'white' }} /> <ArrowUpCircle size={20} className="text-white" />
</div> </div>
<div> <div>
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, color: 'white' }}>{t('admin.update.howTo')}</h3> <h3 className="text-white" style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>{t('admin.update.howTo')}</h3>
<p style={{ margin: '2px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }}> <p className="text-[rgba(255,255,255,0.8)]" style={{ margin: '2px 0 0', fontSize: 12 }}>
v{updateInfo?.current} v{updateInfo?.latest} v{updateInfo?.current} v{updateInfo?.latest}
</p> </p>
</div> </div>
+15 -12
View File
@@ -194,7 +194,7 @@ export default function AtlasPage(): React.ReactElement {
{/* Country action popup */} {/* Country action popup */}
{confirmAction && ( {confirmAction && (
<div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} <div className="bg-[rgba(0,0,0,0.4)]" style={{ position: 'fixed', inset: 0, zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}
onClick={() => setConfirmAction(null)}> onClick={() => setConfirmAction(null)}>
<div className="bg-surface-card" style={{ borderRadius: 16, padding: 24, maxWidth: 340, width: '100%', boxShadow: '0 16px 48px rgba(0,0,0,0.2)', textAlign: 'center' }} <div className="bg-surface-card" style={{ borderRadius: 16, padding: 24, maxWidth: 340, width: '100%', boxShadow: '0 16px 48px rgba(0,0,0,0.2)', textAlign: 'center' }}
onClick={e => e.stopPropagation()}> onClick={e => e.stopPropagation()}>
@@ -234,7 +234,7 @@ export default function AtlasPage(): React.ReactElement {
style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '12px 16px', borderRadius: 12, background: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', transition: 'background 0.12s' }} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '12px 16px', borderRadius: 12, background: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', transition: 'background 0.12s' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-secondary)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-secondary)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}> onMouseLeave={e => e.currentTarget.style.background = 'none'}>
<Star size={18} style={{ color: '#fbbf24', flexShrink: 0 }} /> <Star size={18} className="text-[#fbbf24]" style={{ flexShrink: 0 }} />
<div> <div>
<div className="text-content" style={{ fontSize: 13, fontWeight: 600 }}>{t('atlas.addToBucket')}</div> <div className="text-content" style={{ fontSize: 13, fontWeight: 600 }}>{t('atlas.addToBucket')}</div>
<div className="text-content-muted" style={{ fontSize: 11, marginTop: 1 }}>{t('atlas.addToBucketHint')}</div> <div className="text-content-muted" style={{ fontSize: 11, marginTop: 1 }}>{t('atlas.addToBucketHint')}</div>
@@ -282,7 +282,7 @@ export default function AtlasPage(): React.ReactElement {
style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '12px 16px', borderRadius: 12, background: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', transition: 'background 0.12s' }} style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '12px 16px', borderRadius: 12, background: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', transition: 'background 0.12s' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-secondary)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-secondary)'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}> onMouseLeave={e => e.currentTarget.style.background = 'none'}>
<Star size={18} style={{ color: '#fbbf24', flexShrink: 0 }} /> <Star size={18} className="text-[#fbbf24]" style={{ flexShrink: 0 }} />
<div> <div>
<div className="text-content" style={{ fontSize: 13, fontWeight: 600 }}>{t('atlas.addToBucket')}</div> <div className="text-content" style={{ fontSize: 13, fontWeight: 600 }}>{t('atlas.addToBucket')}</div>
<div className="text-content-muted" style={{ fontSize: 11, marginTop: 1 }}>{t('atlas.addToBucketHint')}</div> <div className="text-content-muted" style={{ fontSize: 11, marginTop: 1 }}>{t('atlas.addToBucketHint')}</div>
@@ -301,7 +301,8 @@ export default function AtlasPage(): React.ReactElement {
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button onClick={executeConfirmAction} <button onClick={executeConfirmAction}
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', background: '#ef4444', color: 'white' }}> className="bg-[#ef4444] text-white"
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
{t('atlas.unmark')} {t('atlas.unmark')}
</button> </button>
</div> </div>
@@ -349,7 +350,8 @@ export default function AtlasPage(): React.ReactElement {
} }
setConfirmAction(null) setConfirmAction(null)
}} }}
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', background: '#ef4444', color: 'white' }}> className="bg-[#ef4444] text-white"
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
{t('atlas.unmark')} {t('atlas.unmark')}
</button> </button>
</div> </div>
@@ -402,7 +404,8 @@ export default function AtlasPage(): React.ReactElement {
setBucketMonth(0); setBucketYear(0) setBucketMonth(0); setBucketYear(0)
setConfirmAction(null) setConfirmAction(null)
}} }}
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', background: '#fbbf24', color: '#1a1a1a' }}> className="bg-[#fbbf24] text-[#1a1a1a]"
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
{t('atlas.addToBucket')} {t('atlas.addToBucket')}
</button> </button>
</div> </div>
@@ -419,8 +422,8 @@ export default function AtlasPage(): React.ReactElement {
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button onClick={executeConfirmAction} <button onClick={executeConfirmAction}
className="bg-content" className="bg-content text-white"
style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', color: 'white' }}> style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
{t('atlas.markVisited')} {t('atlas.markVisited')}
</button> </button>
</div> </div>
@@ -535,7 +538,7 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
const code = item.country_code?.length === 2 ? item.country_code : (Object.entries(A2_TO_A3).find(([, v]) => v === item.country_code)?.[0] || '') const code = item.country_code?.length === 2 ? item.country_code : (Object.entries(A2_TO_A3).find(([, v]) => v === item.country_code)?.[0] || '')
return code ? ( return code ? (
<img src={`https://flagcdn.com/w40/${code.toLowerCase()}.png`} alt={code} style={{ width: 28, height: 20, borderRadius: 4, objectFit: 'cover', marginBottom: 4 }} /> <img src={`https://flagcdn.com/w40/${code.toLowerCase()}.png`} alt={code} style={{ width: 28, height: 20, borderRadius: 4, objectFit: 'cover', marginBottom: 4 }} />
) : <Star size={16} style={{ color: '#fbbf24', marginBottom: 4 }} fill="#fbbf24" /> ) : <Star size={16} className="text-[#fbbf24]" style={{ marginBottom: 4 }} fill="#fbbf24" />
})()} })()}
<span className="text-xs font-semibold text-center leading-tight" style={{ color: tp, maxWidth: 90, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.name}</span> <span className="text-xs font-semibold text-center leading-tight" style={{ color: tp, maxWidth: 90, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.name}</span>
{item.target_date && (() => { {item.target_date && (() => {
@@ -620,7 +623,8 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button onClick={onAddBucket} disabled={!bucketForm.name.trim()} <button onClick={onAddBucket} disabled={!bucketForm.name.trim()}
style={{ fontSize: 11, padding: '4px 12px', borderRadius: 6, border: 'none', background: '#fbbf24', color: '#1a1a1a', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: bucketForm.name.trim() ? 1 : 0.5 }}> className="bg-[#fbbf24] text-[#1a1a1a]"
style={{ fontSize: 11, padding: '4px 12px', borderRadius: 6, border: 'none', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: bucketForm.name.trim() ? 1 : 0.5 }}>
{t('common.add')} {t('common.add')}
</button> </button>
</div> </div>
@@ -733,8 +737,7 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
))} ))}
{countryDetail.manually_marked && onUnmarkCountry && ( {countryDetail.manually_marked && onUnmarkCountry && (
<button onClick={() => onUnmarkCountry(selectedCountry!)} <button onClick={() => onUnmarkCountry(selectedCountry!)}
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold transition-opacity hover:opacity-75" className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold transition-opacity hover:opacity-75 bg-[rgba(239,68,68,0.1)] text-[#ef4444]">
style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
<X size={9} /> <X size={9} />
{t('atlas.unmark')} {t('atlas.unmark')}
</button> </button>
+1 -1
View File
@@ -352,7 +352,7 @@ function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElemen
<section className="atlas"> <section className="atlas">
<div className="atlas-card passport"> <div className="atlas-card passport">
<div className="label">{t('dashboard.atlas.countriesVisited')}</div> <div className="label">{t('dashboard.atlas.countriesVisited')}</div>
<div className="value mono">{countries.length} <span className="unit" style={{ color: 'oklch(1 0 0 / .55)' }}>{t('dashboard.atlas.ofTotal', { total: 195 })}</span></div> <div className="value mono">{countries.length} <span className="unit text-[oklch(1_0_0_/_.55)]">{t('dashboard.atlas.ofTotal', { total: 195 })}</span></div>
<div className="passport-flags"> <div className="passport-flags">
{countries.slice(0, 5).map((c, i) => ( {countries.slice(0, 5).map((c, i) => (
<span key={i} className="flag" title={c}> <span key={i} className="flag" title={c}>
+3 -3
View File
@@ -55,7 +55,7 @@ const ForgotPasswordPage: React.FC = () => {
borderRadius: 10, textAlign: 'left', borderRadius: 10, textAlign: 'left',
display: 'flex', alignItems: 'flex-start', gap: 10, display: 'flex', alignItems: 'flex-start', gap: 10,
}}> }}>
<Terminal size={16} style={{ color: '#92400e', marginTop: 1, flexShrink: 0 }} /> <Terminal size={16} className="text-[#92400e]" style={{ marginTop: 1, flexShrink: 0 }} />
<p style={{ fontSize: 12.5, color: '#92400e', lineHeight: 1.55, margin: 0 }}> <p style={{ fontSize: 12.5, color: '#92400e', lineHeight: 1.55, margin: 0 }}>
{t('login.forgotPasswordSmtpHintOff')} {t('login.forgotPasswordSmtpHintOff')}
</p> </p>
@@ -81,7 +81,7 @@ const ForgotPasswordPage: React.FC = () => {
background: '#fffbeb', border: '1px solid #fde68a', background: '#fffbeb', border: '1px solid #fde68a',
borderRadius: 10, display: 'flex', alignItems: 'flex-start', gap: 10, borderRadius: 10, display: 'flex', alignItems: 'flex-start', gap: 10,
}}> }}>
<Terminal size={15} style={{ color: '#92400e', marginTop: 1, flexShrink: 0 }} /> <Terminal size={15} className="text-[#92400e]" style={{ marginTop: 1, flexShrink: 0 }} />
<p style={{ fontSize: 12.5, color: '#92400e', lineHeight: 1.5, margin: 0 }}> <p style={{ fontSize: 12.5, color: '#92400e', lineHeight: 1.5, margin: 0 }}>
{t('login.forgotPasswordSmtpHintOff')} {t('login.forgotPasswordSmtpHintOff')}
</p> </p>
@@ -93,7 +93,7 @@ const ForgotPasswordPage: React.FC = () => {
{t('common.email')} {t('common.email')}
</label> </label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Mail size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Mail size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="email" value={email} type="email" value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
+3 -12
View File
@@ -39,8 +39,7 @@ export default function InAppNotificationsPage(): React.ReactElement {
{unreadCount > 0 && ( {unreadCount > 0 && (
<button <button
onClick={markAllRead} onClick={markAllRead}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors text-content-secondary" className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors text-content-secondary bg-surface-hover"
style={{ background: 'var(--bg-hover)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-hover)'}
> >
@@ -64,21 +63,13 @@ export default function InAppNotificationsPage(): React.ReactElement {
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">
<button <button
onClick={() => setUnreadOnly(false)} onClick={() => setUnreadOnly(false)}
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors" className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${!unreadOnly ? 'bg-content text-surface' : 'bg-surface-hover text-content-secondary'}`}
style={{
background: !unreadOnly ? 'var(--text-primary)' : 'var(--bg-hover)',
color: !unreadOnly ? 'var(--bg-primary)' : 'var(--text-secondary)',
}}
> >
{t('notifications.all')} {t('notifications.all')}
</button> </button>
<button <button
onClick={() => setUnreadOnly(true)} onClick={() => setUnreadOnly(true)}
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors" className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${unreadOnly ? 'bg-content text-surface' : 'bg-surface-hover text-content-secondary'}`}
style={{
background: unreadOnly ? 'var(--text-primary)' : 'var(--bg-hover)',
color: unreadOnly ? 'var(--bg-primary)' : 'var(--text-secondary)',
}}
> >
{t('notifications.unreadOnly')} {t('notifications.unreadOnly')}
</button> </button>
+3 -3
View File
@@ -1558,7 +1558,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
: t('journey.picker.newGallery') : t('journey.picker.newGallery')
return ( return (
<div className="fixed inset-0 z-[9999] flex items-end md:items-center justify-center md:p-5 overscroll-none" style={{ background: 'rgba(9,9,11,0.75)' }} onClick={onClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}> <div className="fixed inset-0 z-[9999] flex items-end md:items-center justify-center md:p-5 overscroll-none bg-[rgba(9,9,11,0.75)]" onClick={onClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}>
<div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[720px] md:max-w-[960px] w-full max-h-[calc(100dvh-var(--bottom-nav-h)-20px)] md:max-h-[85vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} onClick={e => e.stopPropagation()}> <div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[720px] md:max-w-[960px] w-full max-h-[calc(100dvh-var(--bottom-nav-h)-20px)] md:max-h-[85vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} onClick={e => e.stopPropagation()}>
{/* Header */} {/* Header */}
@@ -2476,7 +2476,7 @@ function AddTripDialog({ journeyId, existingTripIds, onClose, onAdded }: {
} }
return ( return (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-5" style={{ background: 'rgba(9,9,11,0.75)' }}> <div className="fixed inset-0 z-[200] flex items-center justify-center p-5 bg-[rgba(9,9,11,0.75)]">
<div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[420px] w-full flex flex-col overflow-hidden"> <div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[420px] w-full flex flex-col overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700"> <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
@@ -2608,7 +2608,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, onRefr
} }
return ( return (
<div className="fixed inset-0 z-[200] flex items-end md:items-center justify-center md:p-5 overscroll-none" style={{ background: 'rgba(9,9,11,0.75)' }} onClick={handleClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}> <div className="fixed inset-0 z-[200] flex items-end md:items-center justify-center md:p-5 overscroll-none bg-[rgba(9,9,11,0.75)]" onClick={handleClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}>
<div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[480px] w-full max-h-[85vh] md:max-h-[90vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} onClick={e => e.stopPropagation()}> <div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[480px] w-full max-h-[85vh] md:max-h-[90vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700"> <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
+1 -1
View File
@@ -95,7 +95,7 @@ export default function JourneyPage() {
<h2 className="text-content" style={{ margin: 0, fontSize: 18, fontWeight: 600, letterSpacing: '-0.01em', flexShrink: 0 }}> <h2 className="text-content" style={{ margin: 0, fontSize: 18, fontWeight: 600, letterSpacing: '-0.01em', flexShrink: 0 }}>
{t('journey.title')} {t('journey.title')}
</h2> </h2>
<div style={{ width: 1, height: 22, background: 'var(--border-faint)', flexShrink: 0 }} /> <div className="bg-edge-faint" style={{ width: 1, height: 22, flexShrink: 0 }} />
<span className="text-content-muted" style={{ fontSize: 13 }}> <span className="text-content-muted" style={{ fontSize: 13 }}>
{t('journey.frontpage.subtitle')} {t('journey.frontpage.subtitle')}
</span> </span>
+6 -6
View File
@@ -450,7 +450,7 @@ export default function LoginPage(): React.ReactElement {
<div> <div>
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('settings.newPassword')}</label> <label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('settings.newPassword')}</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Lock size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="password" value={newPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewPassword(e.target.value)} required type="password" value={newPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewPassword(e.target.value)} required
placeholder={t('settings.newPassword')} style={inputBase} placeholder={t('settings.newPassword')} style={inputBase}
@@ -462,7 +462,7 @@ export default function LoginPage(): React.ReactElement {
<div> <div>
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('settings.confirmPassword')}</label> <label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('settings.confirmPassword')}</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Lock size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="password" value={confirmPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfirmPassword(e.target.value)} required type="password" value={confirmPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfirmPassword(e.target.value)} required
placeholder={t('settings.confirmPassword')} style={inputBase} placeholder={t('settings.confirmPassword')} style={inputBase}
@@ -478,7 +478,7 @@ export default function LoginPage(): React.ReactElement {
<div> <div>
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('login.mfaCodeLabel')}</label> <label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('login.mfaCodeLabel')}</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<KeyRound size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <KeyRound size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="text" type="text"
inputMode="text" inputMode="text"
@@ -508,7 +508,7 @@ export default function LoginPage(): React.ReactElement {
<div> <div>
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('login.username')}</label> <label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('login.username')}</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<User size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <User size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="text" value={username} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUsername(e.target.value)} required type="text" value={username} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUsername(e.target.value)} required
placeholder="admin" style={inputBase} placeholder="admin" style={inputBase}
@@ -524,7 +524,7 @@ export default function LoginPage(): React.ReactElement {
<div> <div>
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('common.email')}</label> <label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('common.email')}</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Mail size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Mail size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="email" value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} required type="email" value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} required
placeholder={t('login.emailPlaceholder')} style={inputBase} placeholder={t('login.emailPlaceholder')} style={inputBase}
@@ -540,7 +540,7 @@ export default function LoginPage(): React.ReactElement {
<div> <div>
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('common.password')}</label> <label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('common.password')}</label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Lock size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type={showPassword ? 'text' : 'password'} value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)} required type={showPassword ? 'text' : 'password'} value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)} required
placeholder="••••••••" style={{ ...inputBase, paddingRight: 44 }} placeholder="••••••••" style={{ ...inputBase, paddingRight: 44 }}
+3 -3
View File
@@ -101,7 +101,7 @@ const ResetPasswordPage: React.FC = () => {
{t('login.newPassword')} {t('login.newPassword')}
</label> </label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Lock size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type={showPw ? 'text' : 'password'} value={pw} type={showPw ? 'text' : 'password'} value={pw}
onChange={(e: ChangeEvent<HTMLInputElement>) => setPw(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setPw(e.target.value)}
@@ -120,7 +120,7 @@ const ResetPasswordPage: React.FC = () => {
{t('login.confirmPassword')} {t('login.confirmPassword')}
</label> </label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <Lock size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type={showPw ? 'text' : 'password'} value={pw2} type={showPw ? 'text' : 'password'} value={pw2}
onChange={(e: ChangeEvent<HTMLInputElement>) => setPw2(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setPw2(e.target.value)}
@@ -138,7 +138,7 @@ const ResetPasswordPage: React.FC = () => {
{t('login.mfaCode')} {t('login.mfaCode')}
</label> </label>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<KeyRound size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} /> <KeyRound size={15} className="text-[#9ca3af]" style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }} />
<input <input
type="text" inputMode="numeric" value={mfaCode} type="text" inputMode="numeric" value={mfaCode}
onChange={(e: ChangeEvent<HTMLInputElement>) => setMfaCode(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setMfaCode(e.target.value)}
+56 -53
View File
@@ -44,17 +44,17 @@ export default function SharedTripPage() {
const { data, error, selectedDay, setSelectedDay, activeTab, setActiveTab, showLangPicker, setShowLangPicker } = useSharedTrip() const { data, error, selectedDay, setSelectedDay, activeTab, setActiveTab, showLangPicker, setShowLangPicker } = useSharedTrip()
if (error) return ( if (error) return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f3f4f6' }}> <div className="bg-[#f3f4f6]" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}>
<div style={{ textAlign: 'center', padding: 40 }}> <div style={{ textAlign: 'center', padding: 40 }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>🔒</div> <div style={{ fontSize: 48, marginBottom: 16 }}>🔒</div>
<h1 style={{ fontSize: 20, fontWeight: 700, color: '#111827' }}>{t('shared.expired')}</h1> <h1 className="text-[#111827]" style={{ fontSize: 20, fontWeight: 700 }}>{t('shared.expired')}</h1>
<p style={{ color: '#6b7280', marginTop: 8 }}>{t('shared.expiredHint')}</p> <p className="text-[#6b7280]" style={{ marginTop: 8 }}>{t('shared.expiredHint')}</p>
</div> </div>
</div> </div>
) )
if (!data) return ( if (!data) return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f3f4f6' }}> <div className="bg-[#f3f4f6]" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}>
<div style={{ width: 32, height: 32, border: '3px solid #e5e7eb', borderTopColor: '#111827', borderRadius: '50%', animation: 'spin 0.6s linear infinite' }} /> <div style={{ width: 32, height: 32, border: '3px solid #e5e7eb', borderTopColor: '#111827', borderRadius: '50%', animation: 'spin 0.6s linear infinite' }} />
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style> <style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
</div> </div>
@@ -73,17 +73,17 @@ export default function SharedTripPage() {
return ( return (
<div className="bg-surface-secondary" style={{ minHeight: '100vh', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}> <div className="bg-surface-secondary" style={{ minHeight: '100vh', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
{/* Header */} {/* Header */}
<div style={{ background: 'linear-gradient(135deg, #000 0%, #0f172a 50%, #1e293b 100%)', color: 'white', padding: '32px 20px 28px', textAlign: 'center', position: 'relative' }}> <div className="text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #0f172a 50%, #1e293b 100%)', padding: '32px 20px 28px', textAlign: 'center', position: 'relative' }}>
{/* Cover image background */} {/* Cover image background */}
{trip.cover_image && ( {trip.cover_image && (
<div style={{ position: 'absolute', inset: 0, backgroundImage: `url(${trip.cover_image.startsWith('http') ? trip.cover_image : trip.cover_image.startsWith('/') ? trip.cover_image : '/uploads/' + trip.cover_image})`, backgroundSize: 'cover', backgroundPosition: 'center', opacity: 0.15 }} /> <div style={{ position: 'absolute', inset: 0, backgroundImage: `url(${trip.cover_image.startsWith('http') ? trip.cover_image : trip.cover_image.startsWith('/') ? trip.cover_image : '/uploads/' + trip.cover_image})`, backgroundSize: 'cover', backgroundPosition: 'center', opacity: 0.15 }} />
)} )}
{/* Background decoration */} {/* Background decoration */}
<div style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%', background: 'rgba(255,255,255,0.03)' }} /> <div className="bg-[rgba(255,255,255,0.03)]" style={{ position: 'absolute', top: -60, right: -60, width: 200, height: 200, borderRadius: '50%' }} />
<div style={{ position: 'absolute', bottom: -40, left: -40, width: 150, height: 150, borderRadius: '50%', background: 'rgba(255,255,255,0.02)' }} /> <div className="bg-[rgba(255,255,255,0.02)]" style={{ position: 'absolute', bottom: -40, left: -40, width: 150, height: 150, borderRadius: '50%' }} />
{/* Logo */} {/* Logo */}
<div style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 44, height: 44, borderRadius: 12, background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(8px)', marginBottom: 12, border: '1px solid rgba(255,255,255,0.1)' }}> <div className="bg-[rgba(255,255,255,0.08)]" style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 44, height: 44, borderRadius: 12, backdropFilter: 'blur(8px)', marginBottom: 12, border: '1px solid rgba(255,255,255,0.1)' }}>
<img src="/icons/icon-white.svg" alt="TREK" width="26" height="26" /> <img src="/icons/icon-white.svg" alt="TREK" width="26" height="26" />
</div> </div>
@@ -96,7 +96,7 @@ export default function SharedTripPage() {
)} )}
{(trip.start_date || trip.end_date) && ( {(trip.start_date || trip.end_date) && (
<div style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8, padding: '6px 14px', borderRadius: 20, background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.08)' }}> <div className="bg-[rgba(255,255,255,0.08)]" style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8, padding: '6px 14px', borderRadius: 20, backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.08)' }}>
<span style={{ fontSize: 12, fontWeight: 500, opacity: 0.8 }}> <span style={{ fontSize: 12, fontWeight: 500, opacity: 0.8 }}>
{[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })).join(' — ')} {[trip.start_date, trip.end_date].filter(Boolean).map((d: string) => new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' })).join(' — ')}
</span> </span>
@@ -109,22 +109,25 @@ export default function SharedTripPage() {
{/* Language picker - top right */} {/* Language picker - top right */}
<div style={{ position: 'absolute', top: 12, right: 12, zIndex: 10 }}> <div style={{ position: 'absolute', top: 12, right: 12, zIndex: 10 }}>
<button onClick={() => setShowLangPicker(v => !v)} style={{ <button onClick={() => setShowLangPicker(v => !v)}
className="bg-[rgba(255,255,255,0.1)] text-[rgba(255,255,255,0.7)]"
style={{
padding: '5px 12px', borderRadius: 20, border: '1px solid rgba(255,255,255,0.15)', padding: '5px 12px', borderRadius: 20, border: '1px solid rgba(255,255,255,0.15)',
background: 'rgba(255,255,255,0.1)', backdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
color: 'rgba(255,255,255,0.7)', fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit', fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit',
}}> }}>
{SUPPORTED_LANGUAGES.find(l => l.value === (locale?.split('-')[0] || 'en'))?.label || 'Language'} {SUPPORTED_LANGUAGES.find(l => l.value === (locale?.split('-')[0] || 'en'))?.label || 'Language'}
</button> </button>
{showLangPicker && ( {showLangPicker && (
<div style={{ position: 'absolute', top: '100%', right: 0, marginTop: 6, background: 'white', borderRadius: 10, boxShadow: '0 4px 16px rgba(0,0,0,0.2)', padding: 4, zIndex: 50, minWidth: 150 }}> <div className="bg-white" style={{ position: 'absolute', top: '100%', right: 0, marginTop: 6, borderRadius: 10, boxShadow: '0 4px 16px rgba(0,0,0,0.2)', padding: 4, zIndex: 50, minWidth: 150 }}>
{SUPPORTED_LANGUAGES.map(lang => ( {SUPPORTED_LANGUAGES.map(lang => (
<button key={lang.value} onClick={() => { <button key={lang.value} onClick={() => {
// Set language locally without API call (shared page has no auth) // Set language locally without API call (shared page has no auth)
useSettingsStore.setState(s => ({ settings: { ...s.settings, language: lang.value } })) useSettingsStore.setState(s => ({ settings: { ...s.settings, language: lang.value } }))
setShowLangPicker(false) setShowLangPicker(false)
}} }}
style={{ display: 'block', width: '100%', padding: '6px 12px', border: 'none', background: 'none', textAlign: 'left', cursor: 'pointer', fontSize: 12, color: '#374151', borderRadius: 6, fontFamily: 'inherit' }} className="text-[#374151]"
style={{ display: 'block', width: '100%', padding: '6px 12px', border: 'none', background: 'none', textAlign: 'left', cursor: 'pointer', fontSize: 12, borderRadius: 6, fontFamily: 'inherit' }}
onMouseEnter={e => e.currentTarget.style.background = '#f3f4f6'} onMouseEnter={e => e.currentTarget.style.background = '#f3f4f6'}
onMouseLeave={e => e.currentTarget.style.background = 'none'} onMouseLeave={e => e.currentTarget.style.background = 'none'}
>{lang.label}</button> >{lang.label}</button>
@@ -144,13 +147,13 @@ export default function SharedTripPage() {
...(permissions?.share_budget ? [{ id: 'budget', label: t('shared.tabBudget'), Icon: Wallet }] : []), ...(permissions?.share_budget ? [{ id: 'budget', label: t('shared.tabBudget'), Icon: Wallet }] : []),
...(permissions?.share_collab ? [{ id: 'collab', label: t('shared.tabChat'), Icon: MessageCircle }] : []), ...(permissions?.share_collab ? [{ id: 'collab', label: t('shared.tabChat'), Icon: MessageCircle }] : []),
].map(tab => ( ].map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{ <button key={tab.id} onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'bg-[#111827] text-white' : 'bg-surface-card text-[#6b7280]'}
style={{
padding: '8px 18px', borderRadius: 12, border: '1.5px solid', cursor: 'pointer', padding: '8px 18px', borderRadius: 12, border: '1.5px solid', cursor: 'pointer',
fontSize: 12, fontWeight: 600, fontFamily: 'inherit', transition: 'all 0.15s', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 600, fontFamily: 'inherit', transition: 'all 0.15s', whiteSpace: 'nowrap',
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
background: activeTab === tab.id ? '#111827' : 'var(--bg-card, white)',
borderColor: activeTab === tab.id ? '#111827' : 'var(--border-faint, #e5e7eb)', borderColor: activeTab === tab.id ? '#111827' : 'var(--border-faint, #e5e7eb)',
color: activeTab === tab.id ? 'white' : '#6b7280',
boxShadow: activeTab === tab.id ? '0 2px 8px rgba(0,0,0,0.15)' : '0 1px 3px rgba(0,0,0,0.04)', boxShadow: activeTab === tab.id ? '0 2px 8px rgba(0,0,0,0.15)' : '0 1px 3px rgba(0,0,0,0.04)',
}}><tab.Icon size={13} /><span className="hidden sm:inline">{tab.label}</span></button> }}><tab.Icon size={13} /><span className="hidden sm:inline">{tab.label}</span></button>
))} ))}
@@ -190,17 +193,17 @@ export default function SharedTripPage() {
<div key={day.id} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}> <div key={day.id} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}>
<div onClick={() => setSelectedDay(selectedDay === day.id ? null : day.id)} <div onClick={() => setSelectedDay(selectedDay === day.id ? null : day.id)}
style={{ padding: '12px 16px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 }}> style={{ padding: '12px 16px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 28, height: 28, borderRadius: '50%', background: selectedDay === day.id ? '#111827' : '#f3f4f6', color: selectedDay === day.id ? 'white' : '#6b7280', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0 }}>{di + 1}</div> <div className={selectedDay === day.id ? 'bg-[#111827] text-white' : 'bg-[#f3f4f6] text-[#6b7280]'} style={{ width: 28, height: 28, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0 }}>{di + 1}</div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{day.title || `Day ${day.day_number}`}</div> <div className="text-[#111827]" style={{ fontSize: 14, fontWeight: 600 }}>{day.title || `Day ${day.day_number}`}</div>
{day.date && <div style={{ fontSize: 11, color: '#9ca3af', marginTop: 1 }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div>} {day.date && <div className="text-[#9ca3af]" style={{ fontSize: 11, marginTop: 1 }}>{new Date(day.date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}</div>}
</div> </div>
{dayAccs.map((acc: any) => ( {dayAccs.map((acc: any) => (
<span key={acc.id} style={{ fontSize: 9, padding: '2px 6px', borderRadius: 4, background: '#f3f4f6', color: '#6b7280', display: 'flex', alignItems: 'center', gap: 3 }}> <span key={acc.id} className="bg-[#f3f4f6] text-[#6b7280]" style={{ fontSize: 9, padding: '2px 6px', borderRadius: 4, display: 'flex', alignItems: 'center', gap: 3 }}>
<Hotel size={8} /> {acc.place_name} <Hotel size={8} /> {acc.place_name}
</span> </span>
))} ))}
<span style={{ fontSize: 11, color: '#9ca3af' }}>{da.length} {t('shared.places')}</span> <span className="text-[#9ca3af]" style={{ fontSize: 11 }}>{da.length} {t('shared.places')}</span>
</div> </div>
{selectedDay === day.id && merged.length > 0 && ( {selectedDay === day.id && merged.length > 0 && (
@@ -215,24 +218,24 @@ export default function SharedTripPage() {
if (r.type === 'flight') sub = [meta.airline, meta.flight_number, meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport}${meta.arrival_airport}` : ''].filter(Boolean).join(' · ') if (r.type === 'flight') sub = [meta.airline, meta.flight_number, meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport}${meta.arrival_airport}` : ''].filter(Boolean).join(' · ')
else if (r.type === 'train') sub = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : ''].filter(Boolean).join(' · ') else if (r.type === 'train') sub = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : ''].filter(Boolean).join(' · ')
return ( return (
<div key={`t-${r.id}`} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', borderRadius: 6, background: 'rgba(59,130,246,0.06)', border: '1px solid rgba(59,130,246,0.15)' }}> <div key={`t-${r.id}`} className="bg-[rgba(59,130,246,0.06)]" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px', borderRadius: 6, border: '1px solid rgba(59,130,246,0.15)' }}>
<div style={{ width: 24, height: 24, borderRadius: '50%', background: 'rgba(59,130,246,0.12)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[rgba(59,130,246,0.12)]" style={{ width: 24, height: 24, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<TIcon size={12} color="#3b82f6" /> <TIcon size={12} color="#3b82f6" />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 500, color: '#111827' }}>{r.title}{time ? ` · ${time}` : ''}</div> <div className="text-[#111827]" style={{ fontSize: 12, fontWeight: 500 }}>{r.title}{time ? ` · ${time}` : ''}</div>
{sub && <div style={{ fontSize: 10, color: '#6b7280' }}>{sub}</div>} {sub && <div className="text-[#6b7280]" style={{ fontSize: 10 }}>{sub}</div>}
</div> </div>
</div> </div>
) )
} }
if (item.type === 'note') { if (item.type === 'note') {
return ( return (
<div key={`n-${item.data.id}`} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 8px', borderRadius: 6, background: '#f9fafb', border: '1px solid #f3f4f6' }}> <div key={`n-${item.data.id}`} className="bg-[#f9fafb]" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 8px', borderRadius: 6, border: '1px solid #f3f4f6' }}>
<FileText size={12} color="#9ca3af" /> <FileText size={12} color="#9ca3af" />
<div> <div>
<div style={{ fontSize: 12, color: '#374151' }}>{item.data.text}</div> <div className="text-[#374151]" style={{ fontSize: 12 }}>{item.data.text}</div>
{item.data.time && <div style={{ fontSize: 10, color: '#9ca3af' }}>{item.data.time}</div>} {item.data.time && <div className="text-[#9ca3af]" style={{ fontSize: 10 }}>{item.data.time}</div>}
</div> </div>
</div> </div>
) )
@@ -246,10 +249,10 @@ export default function SharedTripPage() {
{place.image_url ? <img src={place.image_url} style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover' }} /> : <MapPin size={13} color="white" />} {place.image_url ? <img src={place.image_url} style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover' }} /> : <MapPin size={13} color="white" />}
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12.5, fontWeight: 500, color: '#111827' }}>{place.name}</div> <div className="text-[#111827]" style={{ fontSize: 12.5, fontWeight: 500 }}>{place.name}</div>
{(place.address || place.description) && <div style={{ fontSize: 10, color: '#9ca3af', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{place.address || place.description}</div>} {(place.address || place.description) && <div className="text-[#9ca3af]" style={{ fontSize: 10, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{place.address || place.description}</div>}
</div> </div>
{place.place_time && <span style={{ fontSize: 10, color: '#6b7280', display: 'flex', alignItems: 'center', gap: 3, flexShrink: 0 }}><Clock size={9} />{place.place_time}{place.end_time ? ` ${place.end_time}` : ''}</span>} {place.place_time && <span className="text-[#6b7280]" style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 3, flexShrink: 0 }}><Clock size={9} />{place.place_time}{place.end_time ? ` ${place.end_time}` : ''}</span>}
</div> </div>
) )
})} })}
@@ -272,12 +275,12 @@ export default function SharedTripPage() {
const date = rDate ? new Date(rDate + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) : '' const date = rDate ? new Date(rDate + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) : ''
return ( return (
<div key={r.id} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 10, padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 }}> <div key={r.id} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 10, padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 32, height: 32, borderRadius: '50%', background: '#f3f4f6', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> <div className="bg-[#f3f4f6]" style={{ width: 32, height: 32, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<TIcon size={15} color="#6b7280" /> <TIcon size={15} color="#6b7280" />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: '#111827' }}>{r.title}</div> <div className="text-[#111827]" style={{ fontSize: 13, fontWeight: 600 }}>{r.title}</div>
<div style={{ fontSize: 11, color: '#9ca3af', display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 2 }}> <div className="text-[#9ca3af]" style={{ fontSize: 11, display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 2 }}>
{date && <span>{date}</span>} {date && <span>{date}</span>}
{time && <span>{time}</span>} {time && <span>{time}</span>}
{r.location && <span>{r.location}</span>} {r.location && <span>{r.location}</span>}
@@ -285,7 +288,7 @@ export default function SharedTripPage() {
{meta.train_number && <span>{meta.train_number}</span>} {meta.train_number && <span>{meta.train_number}</span>}
</div> </div>
</div> </div>
<span style={{ fontSize: 10, padding: '2px 8px', borderRadius: 20, fontWeight: 600, background: r.status === 'confirmed' ? 'rgba(22,163,74,0.1)' : 'rgba(217,119,6,0.1)', color: r.status === 'confirmed' ? '#16a34a' : '#d97706' }}> <span className={r.status === 'confirmed' ? 'bg-[rgba(22,163,74,0.1)] text-[#16a34a]' : 'bg-[rgba(217,119,6,0.1)] text-[#d97706]'} style={{ fontSize: 10, padding: '2px 8px', borderRadius: 20, fontWeight: 600 }}>
{r.status === 'confirmed' ? t('shared.confirmed') : t('shared.pending')} {r.status === 'confirmed' ? t('shared.confirmed') : t('shared.pending')}
</span> </span>
</div> </div>
@@ -299,10 +302,10 @@ export default function SharedTripPage() {
<div className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}> <div className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}>
{Object.entries((packing || []).reduce((g: any, i: any) => { const c = i.category || t('shared.other'); (g[c] = g[c] || []).push(i); return g }, {})).map(([cat, items]: [string, any]) => ( {Object.entries((packing || []).reduce((g: any, i: any) => { const c = i.category || t('shared.other'); (g[c] = g[c] || []).push(i); return g }, {})).map(([cat, items]: [string, any]) => (
<div key={cat}> <div key={cat}>
<div style={{ padding: '8px 16px', background: '#f9fafb', fontSize: 11, fontWeight: 700, color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '1px solid #f3f4f6' }}>{cat}</div> <div className="bg-[#f9fafb] text-[#6b7280]" style={{ padding: '8px 16px', fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '1px solid #f3f4f6' }}>{cat}</div>
{items.map((item: any) => ( {items.map((item: any) => (
<div key={item.id} style={{ padding: '6px 16px', display: 'flex', alignItems: 'center', gap: 8, borderBottom: '1px solid #f9fafb' }}> <div key={item.id} style={{ padding: '6px 16px', display: 'flex', alignItems: 'center', gap: 8, borderBottom: '1px solid #f9fafb' }}>
<span style={{ fontSize: 13, color: item.checked ? '#9ca3af' : '#111827', textDecoration: item.checked ? 'line-through' : 'none' }}>{item.name}</span> <span className={item.checked ? 'text-[#9ca3af]' : 'text-[#111827]'} style={{ fontSize: 13, textDecoration: item.checked ? 'line-through' : 'none' }}>{item.name}</span>
</div> </div>
))} ))}
</div> </div>
@@ -317,21 +320,21 @@ export default function SharedTripPage() {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Total card */} {/* Total card */}
<div style={{ background: 'linear-gradient(135deg, #000 0%, #1a1a2e 100%)', borderRadius: 14, padding: '20px 24px', color: 'white' }}> <div className="text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #1a1a2e 100%)', borderRadius: 14, padding: '20px 24px' }}>
<div style={{ fontSize: 10, fontWeight: 500, letterSpacing: 1, textTransform: 'uppercase', opacity: 0.5 }}>{t('shared.totalBudget')}</div> <div style={{ fontSize: 10, fontWeight: 500, letterSpacing: 1, textTransform: 'uppercase', opacity: 0.5 }}>{t('shared.totalBudget')}</div>
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 4 }}>{total.toLocaleString(locale, { minimumFractionDigits: 2 })} {trip.currency || 'EUR'}</div> <div style={{ fontSize: 28, fontWeight: 700, marginTop: 4 }}>{total.toLocaleString(locale, { minimumFractionDigits: 2 })} {trip.currency || 'EUR'}</div>
</div> </div>
{/* By category */} {/* By category */}
{Object.entries(grouped).map(([cat, items]: [string, any]) => ( {Object.entries(grouped).map(([cat, items]: [string, any]) => (
<div key={cat} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 12, overflow: 'hidden' }}> <div key={cat} className="bg-surface-card border border-edge-faint" style={{ borderRadius: 12, overflow: 'hidden' }}>
<div style={{ padding: '10px 16px', background: '#f9fafb', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #f3f4f6' }}> <div className="bg-[#f9fafb]" style={{ padding: '10px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #f3f4f6' }}>
<span style={{ fontSize: 12, fontWeight: 700, color: '#374151' }}>{cat}</span> <span className="text-[#374151]" style={{ fontSize: 12, fontWeight: 700 }}>{cat}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#6b7280' }}>{items.reduce((s: number, i: any) => s + (parseFloat(i.total_price) || 0), 0).toLocaleString(locale, { minimumFractionDigits: 2 })} {trip.currency || ''}</span> <span className="text-[#6b7280]" style={{ fontSize: 12, fontWeight: 600 }}>{items.reduce((s: number, i: any) => s + (parseFloat(i.total_price) || 0), 0).toLocaleString(locale, { minimumFractionDigits: 2 })} {trip.currency || ''}</span>
</div> </div>
{items.map((item: any) => ( {items.map((item: any) => (
<div key={item.id} style={{ padding: '8px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #fafafa' }}> <div key={item.id} style={{ padding: '8px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #fafafa' }}>
<span style={{ fontSize: 13, color: '#111827' }}>{item.name}</span> <span className="text-[#111827]" style={{ fontSize: 13 }}>{item.name}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: '#111827' }}>{item.total_price ? Number(item.total_price).toLocaleString(locale, { minimumFractionDigits: 2 }) : '—'}</span> <span className="text-[#111827]" style={{ fontSize: 13, fontWeight: 600 }}>{item.total_price ? Number(item.total_price).toLocaleString(locale, { minimumFractionDigits: 2 }) : '—'}</span>
</div> </div>
))} ))}
</div> </div>
@@ -343,9 +346,9 @@ export default function SharedTripPage() {
{/* Collab Chat */} {/* Collab Chat */}
{activeTab === 'collab' && (collab || []).length > 0 && ( {activeTab === 'collab' && (collab || []).length > 0 && (
<div className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}> <div className="bg-surface-card border border-edge-faint" style={{ borderRadius: 14, overflow: 'hidden' }}>
<div style={{ padding: '12px 16px', background: '#f9fafb', borderBottom: '1px solid #f3f4f6', display: 'flex', alignItems: 'center', gap: 8 }}> <div className="bg-[#f9fafb]" style={{ padding: '12px 16px', borderBottom: '1px solid #f3f4f6', display: 'flex', alignItems: 'center', gap: 8 }}>
<MessageCircle size={14} color="#6b7280" /> <MessageCircle size={14} color="#6b7280" />
<span style={{ fontSize: 12, fontWeight: 700, color: '#374151' }}>{t('shared.tabChat')} · {(collab || []).length} {t('shared.messages')}</span> <span className="text-[#374151]" style={{ fontSize: 12, fontWeight: 700 }}>{t('shared.tabChat')} · {(collab || []).length} {t('shared.messages')}</span>
</div> </div>
<div style={{ maxHeight: 500, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ maxHeight: 500, overflowY: 'auto', padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 10 }}>
{(collab || []).map((msg: any, i: number) => { {(collab || []).map((msg: any, i: number) => {
@@ -354,20 +357,20 @@ export default function SharedTripPage() {
return ( return (
<div key={msg.id}> <div key={msg.id}>
{showDate && ( {showDate && (
<div style={{ textAlign: 'center', margin: '8px 0', fontSize: 10, fontWeight: 600, color: '#9ca3af' }}> <div className="text-[#9ca3af]" style={{ textAlign: 'center', margin: '8px 0', fontSize: 10, fontWeight: 600 }}>
{new Date(msg.created_at).toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })} {new Date(msg.created_at).toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })}
</div> </div>
)} )}
<div style={{ display: 'flex', gap: 10 }}> <div style={{ display: 'flex', gap: 10 }}>
<div style={{ width: 32, height: 32, borderRadius: '50%', background: '#e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, color: '#6b7280', flexShrink: 0, overflow: 'hidden' }}> <div className="bg-[#e5e7eb] text-[#6b7280]" style={{ width: 32, height: 32, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0, overflow: 'hidden' }}>
{msg.avatar ? <img src={`/uploads/avatars/${msg.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : (msg.username || '?')[0].toUpperCase()} {msg.avatar ? <img src={`/uploads/avatars/${msg.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : (msg.username || '?')[0].toUpperCase()}
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111827' }}>{msg.username}</span> <span className="text-[#111827]" style={{ fontSize: 12, fontWeight: 600 }}>{msg.username}</span>
<span style={{ fontSize: 10, color: '#9ca3af' }}>{new Date(msg.created_at).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })}</span> <span className="text-[#9ca3af]" style={{ fontSize: 10 }}>{new Date(msg.created_at).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })}</span>
</div> </div>
<div style={{ fontSize: 13, color: '#374151', marginTop: 3, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{msg.text}</div> <div className="text-[#374151]" style={{ fontSize: 13, marginTop: 3, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{msg.text}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -381,9 +384,9 @@ export default function SharedTripPage() {
<div style={{ textAlign: 'center', padding: '40px 0 20px' }}> <div style={{ textAlign: 'center', padding: '40px 0 20px' }}>
<div className="bg-surface-card border border-edge-faint" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 16px', borderRadius: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}> <div className="bg-surface-card border border-edge-faint" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '8px 16px', borderRadius: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}>
<img src="/icons/icon.svg" alt="TREK" width="18" height="18" style={{ borderRadius: 4 }} /> <img src="/icons/icon.svg" alt="TREK" width="18" height="18" style={{ borderRadius: 4 }} />
<span style={{ fontSize: 11, color: '#9ca3af' }}>{t('shared.sharedVia')} <strong style={{ color: '#6b7280' }}>TREK</strong></span> <span className="text-[#9ca3af]" style={{ fontSize: 11 }}>{t('shared.sharedVia')} <strong className="text-[#6b7280]">TREK</strong></span>
</div> </div>
<div style={{ marginTop: 8, fontSize: 10, color: '#d1d5db' }}>Made with <span style={{ color: '#ef4444' }}>&hearts;</span> by Maurice · <a href="https://github.com/mauriceboe/TREK" style={{ color: '#9ca3af', textDecoration: 'none' }}>GitHub</a></div> <div className="text-[#d1d5db]" style={{ marginTop: 8, fontSize: 10 }}>Made with <span className="text-[#ef4444]">&hearts;</span> by Maurice · <a href="https://github.com/mauriceboe/TREK" className="text-[#9ca3af]" style={{ textDecoration: 'none' }}>GitHub</a></div>
</div> </div>
</div> </div>
</div> </div>
+15 -18
View File
@@ -76,23 +76,20 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
const Icon = tab.icon const Icon = tab.icon
return ( return (
<button key={tab.id} onClick={() => setSubTabPersist(tab.id)} <button key={tab.id} onClick={() => setSubTabPersist(tab.id)}
className={active ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
style={{ style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6, display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap', padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap',
background: active ? 'var(--bg-card)' : 'transparent',
color: active ? 'var(--text-primary)' : 'var(--text-muted)',
fontWeight: active ? 500 : 400, fontWeight: active ? 500 : 400,
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none', boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
transition: 'background 180ms cubic-bezier(0.23,1,0.32,1), color 180ms cubic-bezier(0.23,1,0.32,1), box-shadow 180ms cubic-bezier(0.23,1,0.32,1)', transition: 'background 180ms cubic-bezier(0.23,1,0.32,1), color 180ms cubic-bezier(0.23,1,0.32,1), box-shadow 180ms cubic-bezier(0.23,1,0.32,1)',
}} }}
> >
<Icon size={13} style={{ color: active ? 'var(--text-primary)' : 'var(--text-faint)' }} /> <Icon size={13} className={active ? 'text-content' : 'text-content-faint'} />
<span className="hidden sm:inline">{tab.label}</span> <span className="hidden sm:inline">{tab.label}</span>
<span style={{ <span className={`text-content-faint ${active ? 'bg-surface-tertiary' : 'bg-[rgba(0,0,0,0.06)]'}`} style={{
fontSize: 10, fontWeight: 600, fontSize: 10, fontWeight: 600,
background: active ? 'var(--bg-tertiary)' : 'rgba(0,0,0,0.06)',
color: 'var(--text-faint)',
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center', padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
}}>{tab.count}</span> }}>{tab.count}</span>
</button> </button>
@@ -111,8 +108,8 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
<div style={{ display: 'flex', gap: 6, flexShrink: 0, marginLeft: 'auto', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 6, flexShrink: 0, marginLeft: 'auto', flexWrap: 'wrap' }}>
{packingAbgehakt > 0 && ( {packingAbgehakt > 0 && (
<button onClick={() => setClearCheckedSignal(s => s + 1)} <button onClick={() => setClearCheckedSignal(s => s + 1)}
className={`hidden sm:inline-flex items-center gap-1.5 px-[14px] py-[9px] hover:opacity-[0.88]`} className={`hidden sm:inline-flex items-center gap-1.5 px-[14px] py-[9px] hover:opacity-[0.88] bg-[rgba(239,68,68,0.14)] text-[#ef4444]`}
style={{ ...sharedBtnStyle, background: 'rgba(239,68,68,0.14)', color: '#ef4444' }} style={sharedBtnStyle}
> >
<Trash2 size={14} strokeWidth={2.5} /> <Trash2 size={14} strokeWidth={2.5} />
<span>{t('packing.clearChecked', { count: packingAbgehakt })}</span> <span>{t('packing.clearChecked', { count: packingAbgehakt })}</span>
@@ -120,21 +117,21 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
)} )}
<ApplyTemplateButton <ApplyTemplateButton
tripId={tripId} tripId={tripId}
className={sharedBtnClass} className={`${sharedBtnClass} bg-accent text-accent-text`}
style={{ ...sharedBtnStyle, background: 'var(--accent)', color: 'var(--accent-text)' }} style={sharedBtnStyle}
/> />
{packingItems.length > 0 && ( {packingItems.length > 0 && (
<button onClick={() => setSaveTemplateSignal(s => s + 1)} <button onClick={() => setSaveTemplateSignal(s => s + 1)}
className={sharedBtnClass} className={`${sharedBtnClass} bg-accent text-accent-text`}
style={{ ...sharedBtnStyle, background: 'var(--accent)', color: 'var(--accent-text)' }} style={sharedBtnStyle}
> >
<FolderPlus size={14} strokeWidth={2.5} /> <FolderPlus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.saveAsTemplate')}</span> <span className="hidden sm:inline">{t('packing.saveAsTemplate')}</span>
</button> </button>
)} )}
<button onClick={() => setImportPackingSignal(s => s + 1)} <button onClick={() => setImportPackingSignal(s => s + 1)}
className={sharedBtnClass} className={`${sharedBtnClass} bg-accent text-accent-text`}
style={{ ...sharedBtnStyle, background: 'var(--accent)', color: 'var(--accent-text)' }} style={sharedBtnStyle}
> >
<Upload size={14} strokeWidth={2.5} /> <Upload size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.import')}</span> <span className="hidden sm:inline">{t('packing.import')}</span>
@@ -144,12 +141,12 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
})()} })()}
{subTab === 'todo' && ( {subTab === 'todo' && (
<button onClick={() => setAddTodoSignal(s => s + 1)} <button onClick={() => setAddTodoSignal(s => s + 1)}
className="hover:opacity-[0.88]" className="hover:opacity-[0.88] bg-accent text-accent-text"
style={{ style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6, display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500, padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0, flexShrink: 0,
marginLeft: 'auto', marginLeft: 'auto',
}} }}
> >
@@ -531,7 +528,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
)} )}
{selectedPlace && isMobile && ReactDOM.createPortal( {selectedPlace && isMobile && ReactDOM.createPortal(
<div style={{ position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', background: 'rgba(0,0,0,0.3)', paddingBottom: 'var(--bottom-nav-h)' }} onClick={() => setSelectedPlaceId(null)}> <div className="bg-[rgba(0,0,0,0.3)]" style={{ position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', paddingBottom: 'var(--bottom-nav-h)' }} onClick={() => setSelectedPlaceId(null)}>
<div style={{ width: '100%', maxHeight: '85vh' }} onClick={e => e.stopPropagation()}> <div style={{ width: '100%', maxHeight: '85vh' }} onClick={e => e.stopPropagation()}>
<PlaceInspector <PlaceInspector
place={selectedPlace} place={selectedPlace}
@@ -583,7 +580,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
)} )}
{mobileSidebarOpen && ReactDOM.createPortal( {mobileSidebarOpen && ReactDOM.createPortal(
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.3)', zIndex: 9999 }} onClick={() => setMobileSidebarOpen(null)}> <div className="bg-[rgba(0,0,0,0.3)]" style={{ position: 'fixed', inset: 0, zIndex: 9999 }} onClick={() => setMobileSidebarOpen(null)}>
<div className="bg-surface-card" style={{ position: 'absolute', top: 'var(--nav-h)', left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} onClick={e => e.stopPropagation()}> <div className="bg-surface-card" style={{ position: 'absolute', top: 'var(--nav-h)', left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} onClick={e => e.stopPropagation()}>
<div className="border-b border-edge-secondary" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px' }}> <div className="border-b border-edge-secondary" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px' }}>
<span className="text-content" style={{ fontWeight: 600, fontSize: 14 }}>{mobileSidebarOpen === 'left' ? t('trip.mobilePlan') : t('trip.mobilePlaces')}</span> <span className="text-content" style={{ fontWeight: 600, fontSize: 14 }}>{mobileSidebarOpen === 'left' ? t('trip.mobilePlan') : t('trip.mobilePlaces')}</span>
+5 -9
View File
@@ -59,11 +59,7 @@ export default function VacayPage(): React.ReactElement {
<div className="grid grid-cols-4 gap-1"> <div className="grid grid-cols-4 gap-1">
{years.map(y => ( {years.map(y => (
<div key={y} onClick={() => setSelectedYear(y)} <div key={y} onClick={() => setSelectedYear(y)}
className="group relative py-1.5 rounded-lg text-xs font-medium transition-[background-color,color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] text-center cursor-pointer" className={`group relative py-1.5 rounded-lg text-xs font-medium transition-[background-color,color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] text-center cursor-pointer ${y === selectedYear ? 'bg-content text-surface-card' : 'bg-surface-secondary text-content-muted'}`}>
style={{
background: y === selectedYear ? 'var(--text-primary)' : 'var(--bg-secondary)',
color: y === selectedYear ? 'var(--bg-card)' : 'var(--text-muted)',
}}>
{y} {y}
{years.length > 1 && ( {years.length > 1 && (
<span onClick={e => { e.stopPropagation(); setDeleteYear(y); setShowMobileSidebar(false) }} <span onClick={e => { e.stopPropagation(); setDeleteYear(y); setShowMobileSidebar(false) }}
@@ -179,7 +175,7 @@ export default function VacayPage(): React.ReactElement {
{/* Mobile Sidebar Drawer */} {/* Mobile Sidebar Drawer */}
{showMobileSidebar && ReactDOM.createPortal( {showMobileSidebar && ReactDOM.createPortal(
<div className="fixed inset-0 lg:hidden" style={{ zIndex: 99980 }}> <div className="fixed inset-0 lg:hidden" style={{ zIndex: 99980 }}>
<div className="absolute inset-0" style={{ background: 'rgba(0,0,0,0.4)' }} onClick={() => setShowMobileSidebar(false)} /> <div className="absolute inset-0 bg-[rgba(0,0,0,0.4)]" onClick={() => setShowMobileSidebar(false)} />
<div className="absolute left-0 top-0 bottom-0 w-[280px] overflow-y-auto p-3 flex flex-col gap-3 bg-surface" <div className="absolute left-0 top-0 bottom-0 w-[280px] overflow-y-auto p-3 flex flex-col gap-3 bg-surface"
style={{ boxShadow: '4px 0 24px rgba(0,0,0,0.15)', animation: 'slideInLeft 0.2s ease-out' }}> style={{ boxShadow: '4px 0 24px rgba(0,0,0,0.15)', animation: 'slideInLeft 0.2s ease-out' }}>
{sidebarContent} {sidebarContent}
@@ -196,7 +192,7 @@ export default function VacayPage(): React.ReactElement {
{/* Delete Year Modal */} {/* Delete Year Modal */}
<Modal isOpen={deleteYear !== null} onClose={() => setDeleteYear(null)} title={t('vacay.removeYear')} size="sm"> <Modal isOpen={deleteYear !== null} onClose={() => setDeleteYear(null)} title={t('vacay.removeYear')} size="sm">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-3 p-3 rounded-lg" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.15)' }}> <div className="flex gap-3 p-3 rounded-lg bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.15)]">
<AlertTriangle size={18} className="text-red-500 shrink-0 mt-0.5" /> <AlertTriangle size={18} className="text-red-500 shrink-0 mt-0.5" />
<div> <div>
<p className="text-sm font-medium text-content"> <p className="text-sm font-medium text-content">
@@ -220,8 +216,8 @@ export default function VacayPage(): React.ReactElement {
{/* Incoming invite — forced fullscreen modal */} {/* Incoming invite — forced fullscreen modal */}
{incomingInvites.length > 0 && ReactDOM.createPortal( {incomingInvites.length > 0 && ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center px-4" <div className="fixed inset-0 flex items-center justify-center px-4 bg-[rgba(0,0,0,0.7)]"
style={{ zIndex: 99995, backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(8px)' }}> style={{ zIndex: 99995, backdropFilter: 'blur(8px)' }}>
{incomingInvites.map(inv => ( {incomingInvites.map(inv => (
<div key={inv.plan_id} className="trek-modal-enter w-full max-w-md rounded-2xl shadow-2xl overflow-hidden bg-surface-card"> <div key={inv.plan_id} className="trek-modal-enter w-full max-w-md rounded-2xl shadow-2xl overflow-hidden bg-surface-card">
<div className="px-6 pt-6 pb-4 text-center"> <div className="px-6 pt-6 pb-4 text-center">