feat(story-1.2): Vue 3 SPA scaffold, base component library, User entity, SpaController

Frontend:
- Vue 3 + Vite + TypeScript strict in frontend/; builds to public/build/
- Vue Router (hash-history, requiresAuth guard → /login) and Pinia
- Global SCSS design tokens with 6 full themes (Warm Craft, Playful Pop, Sage & Cream, Dusty Mauve, Ocean Dusk, Honey & Slate)
- Base components: BaseButton (5 variants), BaseInput (floating label, error state),
  BaseBottomSheet (slide-up, focus trap, tap-outside dismiss), BaseCard, BaseChip,
  BaseToast (2.5s auto-dismiss, aria-live polite), BottomNav (4 tabs, hides at 960px)
- Type stubs for all API shapes: User, Device, Image, StickerLayer, RenderedAsset, Token

Backend:
- SpaController catch-all serves public/build/index.html; excludes api/setup/token/login/register
- User entity (email+password+roles+theme); UserRepository with PasswordUpgrader
- SecurityController with /login, /logout, /register stubs; Twig login form
- security.yaml: form_login firewall, remember_me, role_hierarchy, access_control
- Migration: create user table

Verified: npm run build succeeds, GET / → 302 /login (unauthenticated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 23:21:29 -04:00
parent 378b0b858b
commit a55b3bd187
39 changed files with 3243 additions and 19 deletions
+187
View File
@@ -0,0 +1,187 @@
// ─── Design tokens ───────────────────────────────────────────────────────────
:root {
// Typography
--font-family: 'Nunito Variable', 'Nunito', sans-serif;
--text-xs: 11px;
--text-sm: 13px;
--text-base: 15px;
--text-md: 17px;
--text-lg: 20px;
--text-xl: 24px;
--text-2xl: 28px;
// Spacing
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
// Radius
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-full: 9999px;
// Motion
--duration-fast: 150ms;
--duration-base: 250ms;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
// Touch target minimum
--touch-min: 44px;
}
// ─── Themes ──────────────────────────────────────────────────────────────────
[data-theme="warm-craft"],
:root {
--color-bg: #fdf6ee;
--color-surface: #fff9f2;
--color-surface-2: #f5ead8;
--color-border: #e8d9c4;
--color-text: #3a2e22;
--color-text-muted: #8a7060;
--color-primary: #c97c3a;
--color-primary-fg: #ffffff;
--color-secondary: #e8d9c4;
--color-secondary-fg:#3a2e22;
--color-destructive: #c0392b;
--color-destructive-fg: #ffffff;
--color-focus-ring: #c97c3a;
}
[data-theme="playful-pop"] {
--color-bg: #fff0fb;
--color-surface: #fff8fe;
--color-surface-2: #ffe4f7;
--color-border: #f0c8ea;
--color-text: #2d0a28;
--color-text-muted: #7a4272;
--color-primary: #d63aab;
--color-primary-fg: #ffffff;
--color-secondary: #ffe4f7;
--color-secondary-fg:#2d0a28;
--color-destructive: #e03030;
--color-destructive-fg: #ffffff;
--color-focus-ring: #d63aab;
}
[data-theme="sage-cream"] {
--color-bg: #f6f8f3;
--color-surface: #fafcf7;
--color-surface-2: #e4ede0;
--color-border: #ccd9c4;
--color-text: #1e2b1a;
--color-text-muted: #607050;
--color-primary: #4e7c3a;
--color-primary-fg: #ffffff;
--color-secondary: #e4ede0;
--color-secondary-fg:#1e2b1a;
--color-destructive: #a83020;
--color-destructive-fg: #ffffff;
--color-focus-ring: #4e7c3a;
}
[data-theme="dusty-mauve"] {
--color-bg: #f6f0f4;
--color-surface: #fdf8fb;
--color-surface-2: #ead8e8;
--color-border: #d8c4d4;
--color-text: #2a1828;
--color-text-muted: #7a5874;
--color-primary: #8e4a84;
--color-primary-fg: #ffffff;
--color-secondary: #ead8e8;
--color-secondary-fg:#2a1828;
--color-destructive: #b83030;
--color-destructive-fg: #ffffff;
--color-focus-ring: #8e4a84;
}
[data-theme="ocean-dusk"] {
--color-bg: #eef3f8;
--color-surface: #f4f8fc;
--color-surface-2: #d4e4f0;
--color-border: #b8d0e4;
--color-text: #0e2030;
--color-text-muted: #4a6880;
--color-primary: #1a6ea8;
--color-primary-fg: #ffffff;
--color-secondary: #d4e4f0;
--color-secondary-fg:#0e2030;
--color-destructive: #b83020;
--color-destructive-fg: #ffffff;
--color-focus-ring: #1a6ea8;
}
[data-theme="honey-slate"] {
--color-bg: #f2f2ee;
--color-surface: #f8f8f4;
--color-surface-2: #e4e0d4;
--color-border: #d0cc bc;
--color-text: #1c1c18;
--color-text-muted: #6c6858;
--color-primary: #c49a20;
--color-primary-fg: #1c1c18;
--color-secondary: #e4e0d4;
--color-secondary-fg:#1c1c18;
--color-destructive: #b03020;
--color-destructive-fg: #ffffff;
--color-focus-ring: #c49a20;
}
// ─── Reset & base ─────────────────────────────────────────────────────────────
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: var(--font-family);
font-size: var(--text-base);
color: var(--color-text);
background: var(--color-bg);
-webkit-font-smoothing: antialiased;
}
body {
min-height: 100dvh;
}
#app {
min-height: 100dvh;
display: flex;
flex-direction: column;
}
// ─── Focus visible ────────────────────────────────────────────────────────────
:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
// ─── Utility mixins ───────────────────────────────────────────────────────────
@mixin touch-target {
min-height: var(--touch-min);
min-width: var(--touch-min);
}
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}