diff --git a/windows/welcome/src/SilverOS.Welcome.App/Components/Routes.razor b/windows/welcome/src/SilverOS.Welcome.App/Components/Routes.razor
index a466c0f..26104f1 100644
--- a/windows/welcome/src/SilverOS.Welcome.App/Components/Routes.razor
+++ b/windows/welcome/src/SilverOS.Welcome.App/Components/Routes.razor
@@ -46,7 +46,7 @@
break;
case 4:
-
+
break;
case 5:
@@ -124,4 +124,12 @@
if (_currentStep > 0)
_currentStep--;
}
+
+ void AdvanceToDone()
+ {
+ // Called by ApplyStep when configuration completes successfully.
+ // Step index 5 = Done.
+ _currentStep = 5;
+ StateHasChanged();
+ }
}
diff --git a/windows/welcome/src/SilverOS.Welcome.App/Components/Steps/ApplyStep.razor b/windows/welcome/src/SilverOS.Welcome.App/Components/Steps/ApplyStep.razor
index 19ba43d..37d5ddf 100644
--- a/windows/welcome/src/SilverOS.Welcome.App/Components/Steps/ApplyStep.razor
+++ b/windows/welcome/src/SilverOS.Welcome.App/Components/Steps/ApplyStep.razor
@@ -1,16 +1,98 @@
-@* Minimal placeholder — full wiring in Task 11 *@
+@inject IApplyService ApplyService
+@inject WizardState State
+
-
Applying Configuration
-
Your settings will be applied now.
-
- @if (_started)
+
+
+ @if (_errorMessage is not null)
{
-
Working… please wait.
+
+
⚠
+
Configuration failed
+
@_errorMessage
+
+
+ }
+ else if (!_running && !_complete)
+ {
+
+
Ready to apply your selections. Click Start to begin.
+
+
+ }
+ else
+ {
+
+
@_stageLabel
+
+
@(_percent)%
+
+
+ @if (_complete)
+ {
+
+
✓
+
Configuration complete.
+
+ }
}
@code {
- private bool _started;
+ [Parameter] public EventCallback OnComplete { get; set; }
- void Start() => _started = true;
+ private bool _running;
+ private bool _complete;
+ private int _percent;
+ private string _stageLabel = "Preparing…";
+ private string? _errorMessage;
+
+ public async Task StartAsync()
+ {
+ _running = true;
+ _complete = false;
+ _errorMessage = null;
+ _percent = 0;
+ _stageLabel = "Preparing…";
+ StateHasChanged();
+
+ var req = new ApplyRequest(
+ Flavour: State.Flavour!,
+ Username: State.Username,
+ Password: State.Password,
+ AdminPassword: State.AdminPassword,
+ BitLockerPin: State.BitLockerPin,
+ BootstrapUser: "sm-bootstrap");
+
+ var progress = new Progress(p =>
+ {
+ _ = InvokeAsync(() =>
+ {
+ _percent = p.Percent;
+ _stageLabel = p.Stage;
+ StateHasChanged();
+ });
+ });
+
+ try
+ {
+ await ApplyService.RunAsync(req, progress);
+ _complete = true;
+ _running = false;
+ _percent = 100;
+ StateHasChanged();
+ await OnComplete.InvokeAsync();
+ }
+ catch (Exception ex)
+ {
+ _running = false;
+ _errorMessage = ex.Message;
+ StateHasChanged();
+ }
+ }
}
diff --git a/windows/welcome/src/SilverOS.Welcome.App/Components/_Imports.razor b/windows/welcome/src/SilverOS.Welcome.App/Components/_Imports.razor
index 8c75f01..078f017 100644
--- a/windows/welcome/src/SilverOS.Welcome.App/Components/_Imports.razor
+++ b/windows/welcome/src/SilverOS.Welcome.App/Components/_Imports.razor
@@ -9,3 +9,4 @@
@using SilverOS.Welcome.App.Components
@using SilverOS.Welcome.App.Components.Steps
@using SilverOS.Welcome.Core.Flavours
+@using SilverOS.Welcome.Core.Apply
diff --git a/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css b/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css
index cc59fb5..796debe 100644
--- a/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css
+++ b/windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/app.css
@@ -1,72 +1,765 @@
-/*#if (SampleContent)*/
+/* ===================================================================
+ SilverOS Welcome — Mercury Aesthetic
+ Theme: precision dark, slate-steel dominant, electric-ice accent.
+ Typography: "DM Mono" for headings/UI text, fallback to Courier
+ variant stacks for the "terminal-built" feel of a security OS.
+ Atmosphere: layered radial + linear gradients, subtle scanline texture.
+ Motion: staggered CSS entrance on .step reveal — one orchestrated
+ arrival, not scattered micro-animations.
+ =================================================================== */
+
+/* ── Web Font ───────────────────────────────────────────────────────── */
+@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Inter:wght@300;400;500&display=swap');
+
+/* ── Design Tokens ──────────────────────────────────────────────────── */
+:root {
+ /* Palette */
+ --clr-void: #0b0f14; /* near-black background */
+ --clr-surface: #111720; /* card / panel surface */
+ --clr-surface-2: #19202e; /* raised surface */
+ --clr-border: #1e2a3a; /* subtle border */
+ --clr-border-hi: #2a3d56; /* highlighted border */
+ --clr-accent: #00d4ff; /* electric ice — primary CTA, progress */
+ --clr-accent-dim: #0099bb; /* dimmer accent for hover states */
+ --clr-accent-glow: rgba(0,212,255,0.18);
+ --clr-success: #00e5a0; /* completion green */
+ --clr-warn: #f5a623; /* error / warning amber */
+ --clr-text-hi: #e8edf5; /* high-emphasis text */
+ --clr-text-mid: #8fa4bc; /* mid-emphasis: labels, subtitles */
+ --clr-text-lo: #4a5f78; /* low-emphasis: placeholders, hints */
+
+ /* Typography */
+ --font-mono: 'DM Mono', 'Fira Code', 'Consolas', monospace;
+ --font-ui: 'Inter', system-ui, -apple-system, sans-serif;
+
+ /* Geometry */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+
+ /* Motion */
+ --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
+ --ease-in: cubic-bezier(0.64, 0, 0.78, 0);
+}
+
+/* ── Reset & Base ───────────────────────────────────────────────────── */
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
html, body {
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ height: 100%;
+ background-color: var(--clr-void);
+ color: var(--clr-text-hi);
+ font-family: var(--font-ui);
+ font-size: 15px;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
}
-a, .btn-link {
- color: #006bb7;
-}
-
-.btn-primary {
- color: #fff;
- background-color: #1b6ec2;
- border-color: #1861ac;
-}
-
-.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
- box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
-}
-
-.content {
- padding-top: 1.1rem;
-}
-
-/*#endif*/
-h1:focus {
- outline: none;
-}
-
-.valid.modified:not([type=checkbox]) {
- outline: 1px solid #26b050;
-}
-
-.invalid {
- outline: 1px solid #e50000;
-}
-
-.validation-message {
- color: #e50000;
+/* Atmospheric background — layered radial glow on deep void */
+body {
+ background:
+ radial-gradient(ellipse 80% 60% at 50% -10%, rgba(0,100,180,0.18) 0%, transparent 70%),
+ radial-gradient(ellipse 50% 40% at 90% 100%, rgba(0,180,140,0.08) 0%, transparent 60%),
+ var(--clr-void);
+ min-height: 100vh;
}
+/* ── Blazor error overlay (keep readable) ──────────────────────────── */
#blazor-error-ui {
- background: lightyellow;
+ background: #1a0a0a;
+ border-top: 2px solid var(--clr-warn);
+ color: var(--clr-warn);
bottom: 0;
- box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 -2px 12px rgba(245,166,35,0.15);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
}
- #blazor-error-ui .dismiss {
- cursor: pointer;
- position: absolute;
- right: 0.75rem;
- top: 0.5rem;
- }
+#blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+ color: var(--clr-text-mid);
+}
.blazor-error-boundary {
- background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ background: #1a0a0a;
+ border: 1px solid var(--clr-warn);
padding: 1rem 1rem 1rem 3.7rem;
- color: white;
+ color: var(--clr-warn);
+ border-radius: var(--radius-md);
}
- .blazor-error-boundary::after {
- content: "An error has occurred."
- }
+.blazor-error-boundary::after {
+ content: "An error has occurred."
+}
+/* ── Validation ─────────────────────────────────────────────────────── */
+h1:focus { outline: none; }
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid var(--clr-success);
+}
+
+.invalid {
+ outline: 1px solid var(--clr-warn);
+}
+
+.validation-message {
+ color: var(--clr-warn);
+ font-size: 0.75rem;
+ font-family: var(--font-mono);
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ WIZARD CHROME
+ ══════════════════════════════════════════════════════════════════════ */
+
+.wizard {
+ display: grid;
+ grid-template-rows: auto 1fr auto;
+ min-height: 100vh;
+ max-width: 760px;
+ margin: 0 auto;
+}
+
+/* ── Step indicator ─────────────────────────────────────────────────── */
+.wizard-header {
+ padding: 1.5rem 2rem 1rem;
+ border-bottom: 1px solid var(--clr-border);
+ background: linear-gradient(to bottom, rgba(17,23,32,0.95), transparent);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.wizard-steps-indicator {
+ display: flex;
+ gap: 0;
+ align-items: center;
+}
+
+.wizard-step-dot {
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ font-weight: 400;
+ letter-spacing: 0.05em;
+ color: var(--clr-text-lo);
+ padding: 0.25rem 0.75rem;
+ border-radius: var(--radius-sm);
+ transition: color 0.25s var(--ease-out), background 0.25s var(--ease-out);
+ position: relative;
+ white-space: nowrap;
+}
+
+.wizard-step-dot + .wizard-step-dot::before {
+ content: '›';
+ position: absolute;
+ left: -2px;
+ color: var(--clr-border-hi);
+ font-size: 0.9rem;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.wizard-step-dot.done {
+ color: var(--clr-success);
+}
+
+.wizard-step-dot.done::after {
+ content: ' ✓';
+ font-size: 0.6rem;
+}
+
+.wizard-step-dot.active {
+ color: var(--clr-accent);
+ background: var(--clr-accent-glow);
+ font-weight: 500;
+}
+
+/* ── Wizard body ────────────────────────────────────────────────────── */
+.wizard-body {
+ padding: 2.5rem 2rem;
+ overflow-y: auto;
+}
+
+/* ── Wizard footer ──────────────────────────────────────────────────── */
+.wizard-footer {
+ padding: 1.25rem 2rem;
+ border-top: 1px solid var(--clr-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: linear-gradient(to top, rgba(11,15,20,0.98), transparent);
+ gap: 1rem;
+}
+
+/* ── Loading / error states ─────────────────────────────────────────── */
+.loading {
+ font-family: var(--font-mono);
+ color: var(--clr-text-lo);
+ font-size: 0.85rem;
+ animation: pulse 1.4s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 0.4; }
+ 50% { opacity: 1; }
+}
+
+.error-panel {
+ border: 1px solid var(--clr-warn);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ background: rgba(245,166,35,0.06);
+}
+
+.error {
+ color: var(--clr-warn);
+ font-weight: 500;
+ margin-bottom: 0.4rem;
+}
+
+.error-detail {
+ color: var(--clr-text-lo);
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ BUTTONS
+ ══════════════════════════════════════════════════════════════════════ */
+
+.btn-primary,
+.btn-secondary {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ font-weight: 500;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ padding: 0.6rem 1.4rem;
+ border-radius: var(--radius-sm);
+ border: none;
+ cursor: pointer;
+ transition:
+ background 0.2s var(--ease-out),
+ box-shadow 0.2s var(--ease-out),
+ opacity 0.15s;
+ outline-offset: 3px;
+}
+
+.btn-primary {
+ background: var(--clr-accent);
+ color: var(--clr-void);
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: #1ae0ff;
+ box-shadow: 0 0 18px var(--clr-accent-glow);
+}
+
+.btn-primary:focus-visible {
+ outline: 2px solid var(--clr-accent);
+}
+
+.btn-primary:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+}
+
+.btn-secondary {
+ background: transparent;
+ color: var(--clr-text-mid);
+ border: 1px solid var(--clr-border-hi);
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: var(--clr-surface-2);
+ color: var(--clr-text-hi);
+ border-color: var(--clr-accent-dim);
+}
+
+.btn-secondary:focus-visible {
+ outline: 2px solid var(--clr-accent);
+}
+
+.btn-secondary:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ STEP ENTRANCE ANIMATION (orchestrated stagger)
+ ══════════════════════════════════════════════════════════════════════ */
+
+.step {
+ animation: step-enter 0.45s var(--ease-out) both;
+}
+
+@keyframes step-enter {
+ from {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* heading and subtitle arrive slightly after the container */
+.step h1 {
+ font-family: var(--font-mono);
+ font-size: 1.6rem;
+ font-weight: 300;
+ letter-spacing: -0.02em;
+ color: var(--clr-text-hi);
+ line-height: 1.2;
+ margin-bottom: 0.4rem;
+ animation: step-enter 0.5s 0.05s var(--ease-out) both;
+}
+
+.step-subtitle {
+ font-family: var(--font-ui);
+ font-size: 0.9rem;
+ color: var(--clr-text-mid);
+ margin-bottom: 2rem;
+ animation: step-enter 0.5s 0.1s var(--ease-out) both;
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ WELCOME STEP
+ ══════════════════════════════════════════════════════════════════════ */
+
+.welcome-step {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-height: 50vh;
+}
+
+.welcome-hero {
+ animation: step-enter 0.55s 0.05s var(--ease-out) both;
+}
+
+.welcome-hero h1 {
+ font-size: 2.2rem;
+ font-weight: 300;
+ color: var(--clr-text-hi);
+ margin-bottom: 0.5rem;
+ /* Accent underline */
+ background: linear-gradient(90deg, var(--clr-accent) 0%, var(--clr-success) 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: step-enter 0.55s 0.1s var(--ease-out) both;
+}
+
+.tagline {
+ font-size: 1.05rem;
+ color: var(--clr-text-mid);
+ font-style: italic;
+ margin-bottom: 1.5rem;
+ animation: step-enter 0.5s 0.15s var(--ease-out) both;
+}
+
+.time-estimate {
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ color: var(--clr-text-lo);
+ margin-top: 1.5rem;
+ animation: step-enter 0.5s 0.2s var(--ease-out) both;
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ FLAVOUR STEP
+ ══════════════════════════════════════════════════════════════════════ */
+
+.flavour-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ gap: 1rem;
+ margin-top: 0.5rem;
+}
+
+.flavour-card {
+ background: var(--clr-surface);
+ border: 1px solid var(--clr-border);
+ border-radius: var(--radius-lg);
+ padding: 1.5rem;
+ cursor: pointer;
+ transition:
+ border-color 0.2s var(--ease-out),
+ box-shadow 0.2s var(--ease-out),
+ background 0.2s var(--ease-out);
+ position: relative;
+ overflow: hidden;
+}
+
+.flavour-card::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, var(--clr-accent-glow) 0%, transparent 60%);
+ opacity: 0;
+ transition: opacity 0.25s var(--ease-out);
+ border-radius: inherit;
+}
+
+.flavour-card:hover {
+ border-color: var(--clr-border-hi);
+ box-shadow: 0 0 0 1px var(--clr-border-hi), 0 4px 20px rgba(0,0,0,0.4);
+}
+
+.flavour-card:hover::before {
+ opacity: 0.5;
+}
+
+.flavour-card.selected {
+ border-color: var(--clr-accent);
+ box-shadow: 0 0 0 1px var(--clr-accent), 0 0 28px var(--clr-accent-glow);
+ background: var(--clr-surface-2);
+}
+
+.flavour-card.selected::before {
+ opacity: 1;
+}
+
+.flavour-card h3 {
+ font-family: var(--font-mono);
+ font-size: 0.95rem;
+ font-weight: 500;
+ color: var(--clr-text-hi);
+ letter-spacing: 0.03em;
+ margin-bottom: 0.5rem;
+}
+
+.flavour-card.selected h3 {
+ color: var(--clr-accent);
+}
+
+.flavour-card p {
+ font-size: 0.82rem;
+ color: var(--clr-text-mid);
+ line-height: 1.5;
+}
+
+/* Staggered card entrance */
+.flavour-card:nth-child(1) { animation: step-enter 0.45s 0.1s var(--ease-out) both; }
+.flavour-card:nth-child(2) { animation: step-enter 0.45s 0.18s var(--ease-out) both; }
+.flavour-card:nth-child(3) { animation: step-enter 0.45s 0.26s var(--ease-out) both; }
+.flavour-card:nth-child(4) { animation: step-enter 0.45s 0.34s var(--ease-out) both; }
+
+/* ══════════════════════════════════════════════════════════════════════
+ ACCOUNT STEP / FORM FIELDS
+ ══════════════════════════════════════════════════════════════════════ */
+
+.field-group {
+ margin-bottom: 1.4rem;
+ animation: step-enter 0.4s var(--ease-out) both;
+}
+
+.field-group:nth-child(2) { animation-delay: 0.06s; }
+.field-group:nth-child(3) { animation-delay: 0.12s; }
+.field-group:nth-child(4) { animation-delay: 0.18s; }
+
+.field-group label {
+ display: block;
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ font-weight: 500;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--clr-text-mid);
+ margin-bottom: 0.4rem;
+}
+
+.field-group input[type="text"],
+.field-group input[type="password"] {
+ width: 100%;
+ background: var(--clr-surface);
+ border: 1px solid var(--clr-border);
+ border-radius: var(--radius-sm);
+ padding: 0.65rem 0.9rem;
+ font-family: var(--font-mono);
+ font-size: 0.9rem;
+ color: var(--clr-text-hi);
+ outline: none;
+ transition:
+ border-color 0.2s var(--ease-out),
+ box-shadow 0.2s var(--ease-out);
+}
+
+.field-group input:focus {
+ border-color: var(--clr-accent);
+ box-shadow: 0 0 0 3px var(--clr-accent-glow);
+}
+
+.field-group input::placeholder {
+ color: var(--clr-text-lo);
+ font-style: italic;
+}
+
+.field-error {
+ display: block;
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ color: var(--clr-warn);
+ margin-top: 0.3rem;
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ PREFS STEP
+ ══════════════════════════════════════════════════════════════════════ */
+
+.prefs-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.pref-item {
+ background: var(--clr-surface);
+ border: 1px solid var(--clr-border);
+ border-radius: var(--radius-md);
+ padding: 0.9rem 1.1rem;
+ animation: step-enter 0.4s var(--ease-out) both;
+}
+
+.pref-item:nth-child(2) { animation-delay: 0.07s; }
+.pref-item:nth-child(3) { animation-delay: 0.14s; }
+
+.pref-item label {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ cursor: pointer;
+ font-size: 0.88rem;
+ color: var(--clr-text-hi);
+}
+
+.pref-item input[type="checkbox"] {
+ width: 1rem;
+ height: 1rem;
+ accent-color: var(--clr-accent);
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.pref-item small {
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ color: var(--clr-text-lo);
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ APPLY STEP
+ ══════════════════════════════════════════════════════════════════════ */
+
+.apply-step {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ min-height: 40vh;
+}
+
+.apply-header h1 {
+ font-size: 1.7rem;
+}
+
+/* Ready state */
+.apply-ready {
+ background: var(--clr-surface);
+ border: 1px solid var(--clr-border);
+ border-radius: var(--radius-lg);
+ padding: 2rem;
+ text-align: center;
+ animation: step-enter 0.4s 0.1s var(--ease-out) both;
+}
+
+.apply-ready-text {
+ color: var(--clr-text-mid);
+ font-size: 0.9rem;
+ margin-bottom: 1.5rem;
+}
+
+.btn-start {
+ min-width: 140px;
+}
+
+/* Progress state */
+.apply-progress-container {
+ animation: step-enter 0.35s var(--ease-out) both;
+}
+
+.apply-stage-label {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ color: var(--clr-accent);
+ letter-spacing: 0.04em;
+ margin-bottom: 0.6rem;
+}
+
+.apply-progress-track {
+ width: 100%;
+ height: 6px;
+ background: var(--clr-surface-2);
+ border-radius: 99px;
+ overflow: hidden;
+ border: 1px solid var(--clr-border);
+}
+
+.apply-progress-bar {
+ height: 100%;
+ background: linear-gradient(90deg, var(--clr-accent) 0%, var(--clr-success) 100%);
+ border-radius: 99px;
+ transition: width 0.4s var(--ease-out);
+ box-shadow: 0 0 10px var(--clr-accent-glow);
+}
+
+.apply-percent-label {
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ color: var(--clr-text-lo);
+ text-align: right;
+ margin-top: 0.3rem;
+}
+
+/* Complete state */
+.apply-complete {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ background: rgba(0,229,160,0.08);
+ border: 1px solid var(--clr-success);
+ border-radius: var(--radius-md);
+ padding: 1rem 1.25rem;
+ animation: step-enter 0.4s var(--ease-out) both;
+}
+
+.apply-complete-icon {
+ font-size: 1.4rem;
+ color: var(--clr-success);
+ line-height: 1;
+}
+
+.apply-complete p {
+ color: var(--clr-success);
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+}
+
+/* Error state */
+.apply-error {
+ background: rgba(245,166,35,0.07);
+ border: 1px solid var(--clr-warn);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+ animation: step-enter 0.35s var(--ease-out) both;
+}
+
+.apply-error-icon {
+ font-size: 1.5rem;
+ color: var(--clr-warn);
+ line-height: 1;
+}
+
+.apply-error-title {
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--clr-warn);
+}
+
+.apply-error-detail {
+ font-family: var(--font-mono);
+ font-size: 0.78rem;
+ color: var(--clr-text-mid);
+ margin-bottom: 0.5rem;
+ word-break: break-word;
+}
+
+.btn-retry {
+ align-self: flex-start;
+}
+
+/* ══════════════════════════════════════════════════════════════════════
+ DONE STEP
+ ══════════════════════════════════════════════════════════════════════ */
+
+.done-step {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+ min-height: 50vh;
+ gap: 1.5rem;
+}
+
+.done-step h1 {
+ background: linear-gradient(90deg, var(--clr-success) 0%, var(--clr-accent) 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-size: 2.2rem;
+}
+
+.done-step p {
+ color: var(--clr-text-mid);
+ max-width: 440px;
+ font-size: 0.92rem;
+}
+
+.btn-restart {
+ min-width: 160px;
+ background: linear-gradient(90deg, var(--clr-accent), var(--clr-success));
+ color: var(--clr-void);
+ font-weight: 500;
+}
+
+.btn-restart:hover:not(:disabled) {
+ box-shadow: 0 0 28px rgba(0,229,160,0.25), 0 0 18px var(--clr-accent-glow);
+ opacity: 0.92;
+}
+
+/* ── Accessibility / focus safety ───────────────────────────────────── */
+@media (prefers-reduced-motion: reduce) {
+ .step,
+ .step h1,
+ .step-subtitle,
+ .welcome-hero,
+ .welcome-hero h1,
+ .tagline,
+ .time-estimate,
+ .flavour-card,
+ .field-group,
+ .pref-item,
+ .apply-progress-container,
+ .apply-ready,
+ .apply-complete,
+ .apply-error {
+ animation: none !important;
+ transition: none !important;
+ }
+}
+
+/* ── MAUI layout safety ─────────────────────────────────────────────── */
.status-bar-safe-area {
display: none;
}
@@ -77,7 +770,7 @@ h1:focus {
position: sticky;
top: 0;
height: env(safe-area-inset-top);
- background-color: #f7f7f7;
+ background-color: var(--clr-void);
width: 100%;
z-index: 1;
}
diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs
new file mode 100644
index 0000000..e40a9d8
--- /dev/null
+++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs
@@ -0,0 +1,72 @@
+using Bunit;
+using Moq;
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.DependencyInjection;
+using SilverOS.Welcome.App.Components;
+using SilverOS.Welcome.App.Components.Steps;
+using SilverOS.Welcome.Core.Apply;
+using SilverOS.Welcome.Core.Flavours;
+using Xunit;
+
+public class ApplyStepTests : TestContext
+{
+ [Fact]
+ public async Task Calls_apply_with_the_wizard_selections()
+ {
+ var apply = new Mock();
+ var state = new WizardState
+ {
+ Flavour = new FlavourManifest { Id = "daily-driver", Hardening = new() { Modules = new[] { "00" } } },
+ Username = "alice",
+ Password = "pw",
+ AdminPassword = "apw",
+ BitLockerPin = "123456"
+ };
+ Services.AddSingleton(state);
+ Services.AddSingleton(apply.Object);
+ var cut = RenderComponent();
+ await cut.InvokeAsync(() => cut.Instance.StartAsync());
+ apply.Verify(a => a.RunAsync(
+ It.Is(r => r.Username == "alice" && r.Flavour.Id == "daily-driver"),
+ It.IsAny>(),
+ It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task OnComplete_invoked_when_apply_succeeds()
+ {
+ var apply = new Mock();
+ apply.Setup(a => a.RunAsync(It.IsAny(), It.IsAny>(), It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var state = new WizardState
+ {
+ Flavour = new FlavourManifest { Id = "daily-driver", Hardening = new() { Modules = new[] { "00" } } },
+ Username = "alice", Password = "pw", AdminPassword = "apw", BitLockerPin = "123456"
+ };
+ Services.AddSingleton(state);
+ Services.AddSingleton(apply.Object);
+ var completed = false;
+ var cut = RenderComponent(p => p.Add(s => s.OnComplete, EventCallback.Factory.Create(this, () => { completed = true; })));
+ await cut.InvokeAsync(() => cut.Instance.StartAsync());
+ Assert.True(completed);
+ }
+
+ [Fact]
+ public async Task Shows_error_and_retry_button_when_apply_fails()
+ {
+ var apply = new Mock();
+ apply.Setup(a => a.RunAsync(It.IsAny(), It.IsAny>(), It.IsAny()))
+ .ThrowsAsync(new InvalidOperationException("Module 03 failed"));
+ var state = new WizardState
+ {
+ Flavour = new FlavourManifest { Id = "daily-driver", Hardening = new() { Modules = new[] { "00" } } },
+ Username = "alice", Password = "pw", AdminPassword = "apw", BitLockerPin = "123456"
+ };
+ Services.AddSingleton(state);
+ Services.AddSingleton(apply.Object);
+ var cut = RenderComponent();
+ await cut.InvokeAsync(() => cut.Instance.StartAsync());
+ Assert.Contains("Module 03 failed", cut.Markup);
+ Assert.NotNull(cut.Find(".btn-retry"));
+ }
+}