feat(welcome): apply step wiring + Mercury styling
Wire ApplyStep with public StartAsync(), IProgress<ApplyProgress> marshalled via InvokeAsync(StateHasChanged), OnComplete EventCallback (host advances to Done), and failure surface + Retry button. Add _Imports.razor Apply using. Wire Routes.razor AdvanceToDone handler. Add Mercury CSS: slate-void palette, DM Mono typography, layered radial gradients, staggered step-enter animation, styled wizard chrome/cards/fields/progress bar/buttons. 17/17 tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
<PrefsStep />
|
||||
break;
|
||||
case 4:
|
||||
<ApplyStep />
|
||||
<ApplyStep OnComplete="AdvanceToDone" />
|
||||
break;
|
||||
case 5:
|
||||
<DoneStep />
|
||||
@@ -124,4 +124,12 @@
|
||||
if (_currentStep > 0)
|
||||
_currentStep--;
|
||||
}
|
||||
|
||||
void AdvanceToDone()
|
||||
{
|
||||
// Called by ApplyStep when configuration completes successfully.
|
||||
// Step index 5 = Done.
|
||||
_currentStep = 5;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,98 @@
|
||||
@* Minimal placeholder — full wiring in Task 11 *@
|
||||
@inject IApplyService ApplyService
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step apply-step">
|
||||
<h1>Applying Configuration</h1>
|
||||
<p class="step-subtitle">Your settings will be applied now.</p>
|
||||
<button class="btn-primary" @onclick="Start" disabled="@_started">Start</button>
|
||||
@if (_started)
|
||||
<div class="apply-header">
|
||||
<h1>Applying Configuration</h1>
|
||||
<p class="step-subtitle">Your SilverOS is being configured. This may take a few minutes.</p>
|
||||
</div>
|
||||
|
||||
@if (_errorMessage is not null)
|
||||
{
|
||||
<p class="apply-status">Working… please wait.</p>
|
||||
<div class="apply-error">
|
||||
<div class="apply-error-icon">⚠</div>
|
||||
<p class="apply-error-title">Configuration failed</p>
|
||||
<p class="apply-error-detail">@_errorMessage</p>
|
||||
<button class="btn-primary btn-retry" @onclick="() => _ = StartAsync()">Retry</button>
|
||||
</div>
|
||||
}
|
||||
else if (!_running && !_complete)
|
||||
{
|
||||
<div class="apply-ready">
|
||||
<p class="apply-ready-text">Ready to apply your selections. Click Start to begin.</p>
|
||||
<button class="btn-primary btn-start" @onclick="() => _ = StartAsync()" disabled="@_running">Start</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="apply-progress-container">
|
||||
<div class="apply-stage-label">@_stageLabel</div>
|
||||
<div class="apply-progress-track">
|
||||
<div class="apply-progress-bar" style="width: @(_percent)%"></div>
|
||||
</div>
|
||||
<div class="apply-percent-label">@(_percent)%</div>
|
||||
</div>
|
||||
|
||||
@if (_complete)
|
||||
{
|
||||
<div class="apply-complete">
|
||||
<div class="apply-complete-icon">✓</div>
|
||||
<p>Configuration complete.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@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<ApplyProgress>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<IApplyService>();
|
||||
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<ApplyStep>();
|
||||
await cut.InvokeAsync(() => cut.Instance.StartAsync());
|
||||
apply.Verify(a => a.RunAsync(
|
||||
It.Is<ApplyRequest>(r => r.Username == "alice" && r.Flavour.Id == "daily-driver"),
|
||||
It.IsAny<IProgress<ApplyProgress>>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnComplete_invoked_when_apply_succeeds()
|
||||
{
|
||||
var apply = new Mock<IApplyService>();
|
||||
apply.Setup(a => a.RunAsync(It.IsAny<ApplyRequest>(), It.IsAny<IProgress<ApplyProgress>>(), It.IsAny<CancellationToken>()))
|
||||
.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<ApplyStep>(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<IApplyService>();
|
||||
apply.Setup(a => a.RunAsync(It.IsAny<ApplyRequest>(), It.IsAny<IProgress<ApplyProgress>>(), It.IsAny<CancellationToken>()))
|
||||
.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<ApplyStep>();
|
||||
await cut.InvokeAsync(() => cut.Instance.StartAsync());
|
||||
Assert.Contains("Module 03 failed", cut.Markup);
|
||||
Assert.NotNull(cut.Find(".btn-retry"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user