agent-orchestrator/ui-electrobun/src/mainview/SplashScreen.svelte
Hibryda aae86a4001 feat(electrobun): i18n system — @formatjs/intl + Svelte 5 runes + 3 locales
- i18n.svelte.ts: store with $state locale + createIntl(), t() function,
  formatDate/Number/RelativeTime, getDir() for RTL, async setLocale()
- i18n.types.ts: TranslationKey union (codegen from en.json)
- locales/en.json: 200+ strings in ICU MessageFormat
- locales/pl.json: full Polish translation
- locales/ar.json: partial Arabic (validates 6-form plural + RTL)
- scripts/i18n-types.ts: codegen script for type-safe keys
- 6 components wired: StatusBar, AgentPane, CommandPalette,
  SettingsDrawer, SplashScreen, ChatInput
- Language selector in AppearanceSettings
- App.svelte: document.dir reactive for RTL
- CONTRIBUTING_I18N.md: guide for adding languages

Note: currently Electrobun-only. Will extract to @agor/i18n shared
package for both Tauri and Electrobun.
2026-03-22 10:28:13 +01:00

140 lines
3.1 KiB
Svelte

<script lang="ts">
/**
* Full-screen splash overlay shown on app startup.
* Auto-dismisses when the `ready` prop becomes true.
* Fade-out transition: 300ms opacity.
*/
import { t } from './i18n.svelte.ts';
interface Props {
/** Set to true when app initialization is complete. */
ready: boolean;
}
let { ready }: Props = $props();
let visible = $state(true);
let fading = $state(false);
// When ready flips to true, start fade-out then hide
$effect(() => {
if (ready && visible && !fading) {
fading = true;
setTimeout(() => {
visible = false;
}, 300);
}
});
</script>
<div
class="splash"
style:display={visible ? 'flex' : 'none'}
class:fading
role="status"
aria-label="Loading application"
>
<div class="splash-content">
<div class="logo-text" aria-hidden="true">AGOR</div>
<div class="version">v0.0.1</div>
<div class="loading-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<div class="loading-label">{t('splash.loading')}</div>
</div>
</div>
<style>
.splash {
position: fixed;
inset: 0;
z-index: 10000;
background: var(--ctp-base);
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 300ms ease-out;
}
.splash.fading {
opacity: 0;
pointer-events: none;
}
.splash-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.logo-text {
font-family: 'Inter', system-ui, sans-serif;
font-weight: 900;
font-size: 4rem;
letter-spacing: 0.3em;
background: linear-gradient(
135deg,
var(--ctp-mauve),
var(--ctp-blue),
var(--ctp-sapphire),
var(--ctp-teal)
);
background-size: 300% 300%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient-shift 3s ease infinite;
user-select: none;
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.version {
font-size: 0.875rem;
color: var(--ctp-overlay0);
font-family: var(--term-font-family, monospace);
letter-spacing: 0.05em;
}
.loading-indicator {
display: flex;
gap: 0.375rem;
margin-top: 0.5rem;
}
.dot {
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
background: var(--ctp-overlay1);
animation: pulse-dot 1.2s ease-in-out infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse-dot {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
.loading-label {
font-size: 0.75rem;
color: var(--ctp-subtext0);
font-family: var(--ui-font-family, system-ui, sans-serif);
animation: pulse-text 2s ease-in-out infinite;
}
@keyframes pulse-text {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
</style>