diff --git a/docs/frontend-design-cookbook.md b/docs/frontend-design-cookbook.md index ef98dba..c33af2d 100644 --- a/docs/frontend-design-cookbook.md +++ b/docs/frontend-design-cookbook.md @@ -80,10 +80,12 @@ Use these wrappers before introducing route-specific structure: - `editorial-page`: standard constrained content width for route pages. - `editorial-hero`: top summary strip for title, subtitle, and primary actions. +- `PageHeader.svelte`: preferred reusable wrapper for page-level hero sections; use it to keep title hierarchy, backlinks, meta rows, and action placement consistent. - `editorial-panel`: primary surface for forms, tables, and ledgers. - `editorial-toolbar`: compact action row under hero copy. - `editorial-backlink`: standard top-left back navigation style. - `editorial-alert`, `editorial-alert--error`, `editorial-alert--success`, `editorial-alert--warning`, `editorial-alert--info`: feedback banners. +- `page-header-meta`, `page-header-foot`, `hero-strip`: shared secondary rows inside page headers for compact metadata and summary stats. ### Collapsible panels @@ -117,10 +119,14 @@ This matches the warm editorial aesthetic and maintains visual consistency with These classes are already in use and should be reused: -- Lists and ledgers: `routine-ledger-row`, `products-mobile-card`, `health-entry-row` -- Group headers: `products-section-title` -- Table shell: `products-table-shell` +- Lists and ledgers: `routine-ledger-row`, `editorial-mobile-card`, `health-entry-row` +- Group headers: `editorial-section-title` +- Table shell: `editorial-table-shell` +- Compact metadata rows: `editorial-meta-strip` - Tabs shell: `products-tabs`, `editorial-tabs` +- App shell/navigation: `app-mobile-header`, `app-drawer`, `app-nav-list`, `app-nav-link`, `app-sidebar-footer` +- Reusable locale control: `LanguageSwitcher.svelte` with `language-switcher*` classes +- Dashboard summary patterns: `dashboard-stat-strip`, `dashboard-stat-card`, `dashboard-attention-list`, `dashboard-attention-item` - Health semantic pills: `health-kind-pill*`, `health-flag-pill*` - Lab results utilities: - metadata chips: `lab-results-meta-strip`, `lab-results-meta-pill` @@ -145,6 +151,7 @@ These classes are already in use and should be reused: - In dense row-based lists, prefer `ghost` action controls; use icon-only buttons on desktop tables and short text+icon `ghost` actions on mobile cards to keep row actions subordinate to data. - For editable data tables, open a dedicated inline edit panel above the list (instead of per-row expanded forms) and prefill it from row actions; keep users on the same filtered/paginated context after save. - When a list is narrowed to a single entity key (for example `test_code`), display an explicit "filtered by" banner with a one-click clear action and avoid extra grouping wrappers that add no context. +- For dashboard-style summaries, prefer compact stat strips and attention rows over large decorative cards; each item should pair one strong value with one short explanatory line. ### DRY form primitives @@ -211,6 +218,7 @@ These classes are already in use and should be reused: - Core tokens and global look: `frontend/src/app.css` - App shell and route domain mapping: `frontend/src/routes/+layout.svelte` +- Shared page header: `frontend/src/lib/components/PageHeader.svelte` - Route examples using the pattern: - `frontend/src/routes/+page.svelte` - `frontend/src/routes/products/+page.svelte` diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 5efb7b6..9dcc24a 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -32,8 +32,74 @@ "dashboard_title": "Dashboard", "dashboard_subtitle": "Your recent health & skincare overview", + "dashboard_dailyBriefing": "A quick read on what changed, what is missing, and where to look next.", "dashboard_latestSnapshot": "Latest Skin Snapshot", "dashboard_recentRoutines": "Recent Routines", + "dashboard_requiresAttention": "Requires attention", + "dashboard_healthPulse": "Health Pulse", + "dashboard_viewSkinHistory": "Open skin history", + "dashboard_viewLabResults": "Open lab results", + "dashboard_heroFreshness": "Freshness", + "dashboard_sinceLastSnapshot": "since last snapshot", + "dashboard_skinFreshness": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "Last snapshot was {count} day ago.", + "countPlural=*": "Last snapshot was {count} days ago." + } + } + ], + "dashboard_daysAgo": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} day ago", + "countPlural=*": "{count} days ago" + } + } + ], + "dashboard_daysAgoShort": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count}d", + "countPlural=*": "{count}d" + } + } + ], + "dashboard_flaggedResults": "flagged results", + "dashboard_flaggedLabsCount": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} flagged result", + "countPlural=*": "{count} flagged results" + } + } + ], + "dashboard_attentionSnapshot": "Skin log", + "dashboard_attentionRoutineAM": "AM routine", + "dashboard_attentionRoutinePM": "PM routine", + "dashboard_attentionLabs": "Lab review", + "dashboard_attentionMissing": "Missing", + "dashboard_attentionToday": "Today", + "dashboard_attentionStable": "No flagged items", + "dashboard_statusLogged": "Logged", + "dashboard_statusOpen": "Open", + "dashboard_metricHydration": "Hydration", + "dashboard_metricSensitivity": "Sensitivity", + "dashboard_metricSebumTzone": "Sebum T-zone", + "dashboard_metricSebumCheeks": "Sebum cheeks", + "dashboard_metricDelta": "Delta {delta}", + "dashboard_averageSteps": "Avg. {count} steps", + "dashboard_lastRoutine": "Last routine", + "dashboard_lastLabDate": "Last collection", + "dashboard_noLabResults": "No lab results yet.", "dashboard_noSnapshots": "No skin snapshots yet.", "dashboard_noRoutines": "No routines in the past 2 weeks.", @@ -152,6 +218,9 @@ "grooming_title": "Grooming Schedule", "grooming_backToRoutines": "Routines", "grooming_addEntry": "+ Add entry", + "grooming_newTitle": "New grooming entry", + "grooming_newSubtitle": "Add a recurring entry to your weekly grooming schedule.", + "grooming_newSectionIntro": "Choose the day, action, and an optional note.", "grooming_entryAdded": "Entry added.", "grooming_entryUpdated": "Entry updated.", "grooming_entryDeleted": "Entry deleted.", @@ -254,6 +323,8 @@ ], "medications_addNew": "+ Add medication", "medications_newTitle": "New medication", + "medications_newSubtitle": "Add a basic medication or supplement record for later tracking.", + "medications_newSectionIntro": "Start with the type, product name, and active substance.", "medications_kind": "Kind", "medications_productName": "Product name *", "medications_productNamePlaceholder": "e.g. Vitamin D3", @@ -291,6 +362,8 @@ ], "labResults_addNew": "+ Add result", "labResults_newTitle": "New lab result", + "labResults_newSubtitle": "Save a single lab result and add it to your health history.", + "labResults_newSectionIntro": "Start with the date and LOINC code, then add the remaining details.", "labResults_flagFilter": "Flag:", "labResults_flagAll": "All", "labResults_flagNone": "None", @@ -374,6 +447,8 @@ "skin_analyzePhotos": "Analyze photos", "skin_analyzing": "Analyzing…", "skin_newSnapshotTitle": "New skin snapshot", + "skin_newSubtitle": "Capture today’s skin state manually or prefill the form with AI photo analysis.", + "skin_newSectionIntro": "Start with the date and overall condition, then refine the details.", "skin_date": "Date *", "skin_overallState": "Overall state", "skin_texture": "Texture", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index cfd8308..b0fca21 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -32,8 +32,82 @@ "dashboard_title": "Dashboard", "dashboard_subtitle": "Przegląd zdrowia i pielęgnacji", + "dashboard_dailyBriefing": "Szybki rzut oka na zmiany, braki i miejsca, które warto teraz sprawdzić.", "dashboard_latestSnapshot": "Ostatni stan skóry", "dashboard_recentRoutines": "Ostatnie rutyny", + "dashboard_requiresAttention": "Wymaga uwagi", + "dashboard_healthPulse": "Puls zdrowia", + "dashboard_viewSkinHistory": "Otwórz historię skóry", + "dashboard_viewLabResults": "Otwórz wyniki badań", + "dashboard_heroFreshness": "Świeżość", + "dashboard_sinceLastSnapshot": "od ostatniego wpisu", + "dashboard_skinFreshness": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "Ostatni wpis był {count} dzień temu.", + "countPlural=few": "Ostatni wpis był {count} dni temu.", + "countPlural=many": "Ostatni wpis był {count} dni temu.", + "countPlural=*": "Ostatni wpis był {count} dni temu." + } + } + ], + "dashboard_daysAgo": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} dzień temu", + "countPlural=few": "{count} dni temu", + "countPlural=many": "{count} dni temu", + "countPlural=*": "{count} dni temu" + } + } + ], + "dashboard_daysAgoShort": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} d", + "countPlural=few": "{count} d", + "countPlural=many": "{count} d", + "countPlural=*": "{count} d" + } + } + ], + "dashboard_flaggedResults": "wyników oznaczonych flagą", + "dashboard_flaggedLabsCount": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} wynik z flagą", + "countPlural=few": "{count} wyniki z flagą", + "countPlural=many": "{count} wyników z flagą", + "countPlural=*": "{count} wyników z flagą" + } + } + ], + "dashboard_attentionSnapshot": "Dziennik skóry", + "dashboard_attentionRoutineAM": "Rutyna AM", + "dashboard_attentionRoutinePM": "Rutyna PM", + "dashboard_attentionLabs": "Przegląd badań", + "dashboard_attentionMissing": "Brak", + "dashboard_attentionToday": "Dzisiaj", + "dashboard_attentionStable": "Brak flagowanych pozycji", + "dashboard_statusLogged": "Zapisane", + "dashboard_statusOpen": "Do uzupełnienia", + "dashboard_metricHydration": "Nawodnienie", + "dashboard_metricSensitivity": "Wrażliwość", + "dashboard_metricSebumTzone": "Sebum T-zone", + "dashboard_metricSebumCheeks": "Sebum policzki", + "dashboard_metricDelta": "Zmiana {delta}", + "dashboard_averageSteps": "Śr. {count} kroków", + "dashboard_lastRoutine": "Ostatnia rutyna", + "dashboard_lastLabDate": "Ostatnie badanie", + "dashboard_noLabResults": "Brak wyników badań.", "dashboard_noSnapshots": "Brak wpisów o stanie skóry.", "dashboard_noRoutines": "Brak rutyn w ciągu ostatnich 2 tygodni.", @@ -156,6 +230,9 @@ "grooming_title": "Harmonogram pielęgnacji", "grooming_backToRoutines": "Rutyny", "grooming_addEntry": "+ Dodaj wpis", + "grooming_newTitle": "Nowy wpis pielęgnacyjny", + "grooming_newSubtitle": "Dodaj stały wpis do tygodniowego harmonogramu pielęgnacji.", + "grooming_newSectionIntro": "Ustal dzień, czynność i krótką notatkę, jeśli chcesz.", "grooming_entryAdded": "Wpis dodany.", "grooming_entryUpdated": "Wpis zaktualizowany.", "grooming_entryDeleted": "Wpis usunięty.", @@ -262,6 +339,8 @@ ], "medications_addNew": "+ Dodaj lek", "medications_newTitle": "Nowy lek", + "medications_newSubtitle": "Dodaj podstawowy rekord leku lub suplementu do dalszego śledzenia.", + "medications_newSectionIntro": "Zacznij od rodzaju, nazwy i substancji czynnej.", "medications_kind": "Rodzaj", "medications_productName": "Nazwa produktu *", "medications_productNamePlaceholder": "np. Witamina D3", @@ -303,6 +382,8 @@ ], "labResults_addNew": "+ Dodaj wynik", "labResults_newTitle": "Nowy wynik badania", + "labResults_newSubtitle": "Zapisz pojedynczy wynik badania, aby dołączyć go do historii zdrowia.", + "labResults_newSectionIntro": "Najpierw podaj datę i kod LOINC, resztę możesz uzupełnić skrótowo.", "labResults_flagFilter": "Flaga:", "labResults_flagAll": "Wszystkie", "labResults_flagNone": "Brak", @@ -388,6 +469,8 @@ "skin_analyzePhotos": "Analizuj zdjęcia", "skin_analyzing": "Analizuję…", "skin_newSnapshotTitle": "Nowy wpis", + "skin_newSubtitle": "Zapisz bieżący stan skóry ręcznie lub uzupełnij pola analizą AI ze zdjęć.", + "skin_newSectionIntro": "Zacznij od daty i ogólnej oceny, a potem doprecyzuj szczegóły.", "skin_date": "Data *", "skin_overallState": "Ogólny stan", "skin_texture": "Tekstura", diff --git a/frontend/src/app.css b/frontend/src/app.css index 93a802d..4aa2272 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -153,40 +153,182 @@ body { } .app-mobile-header { + position: sticky; + top: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; border-bottom: 1px solid hsl(35 22% 76% / 0.7); - background: linear-gradient(180deg, hsl(44 35% 97%), hsl(44 25% 94%)); + background: linear-gradient(180deg, hsl(44 35% 97% / 0.92), hsl(44 25% 94% / 0.96)); + backdrop-filter: blur(16px); + padding: 0.9rem 1rem; +} + +.app-mobile-titleblock { + min-width: 0; +} + +.app-mobile-overline, +.app-sidebar-subtitle { + margin: 0; + color: var(--muted-foreground); + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.16em; + line-height: 1.35; + overflow-wrap: anywhere; + text-transform: uppercase; } .app-mobile-title, .app-brand { + display: block; font-family: 'Cormorant Infant', 'Times New Roman', serif; font-size: 1.2rem; font-weight: 600; letter-spacing: 0.02em; } +.app-mobile-toggle, .app-icon-button { display: flex; - height: 2rem; - width: 2rem; + height: 2.6rem; + width: 2.6rem; align-items: center; justify-content: center; - border: 1px solid hsl(34 21% 75%); - border-radius: 0.45rem; + border: 1px solid hsl(34 21% 75% / 0.95); + border-radius: 999px; + background: hsl(42 32% 95% / 0.92); color: var(--muted-foreground); + box-shadow: 0 10px 24px -20px hsl(220 32% 14% / 0.55); } +.app-mobile-toggle:hover, .app-icon-button:hover { color: var(--foreground); border-color: var(--page-accent); background: var(--page-accent-soft); } +.app-drawer-backdrop { + position: fixed; + inset: 0; + z-index: 50; + background: hsl(220 40% 8% / 0.42); + backdrop-filter: blur(4px); +} + .app-sidebar { border-right: 1px solid hsl(36 20% 73% / 0.75); background: linear-gradient(180deg, hsl(44 34% 97%), hsl(42 28% 94%)); } +.app-drawer { + position: fixed; + inset: 0 auto 0 0; + z-index: 60; + display: flex; + width: min(20rem, calc(100vw - 1.5rem)); + flex-direction: column; + gap: 1rem; + overflow-y: auto; + border-right: 1px solid hsl(36 20% 73% / 0.75); + background: linear-gradient(180deg, hsl(44 35% 97%), hsl(42 29% 94%)); + padding: 1.1rem 0.85rem 1rem; + box-shadow: 0 28px 56px -28px hsl(220 34% 14% / 0.42); +} + +.app-sidebar-brandblock { + margin-bottom: 0.8rem; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; + padding: 0 0.8rem; +} + +.app-sidebar-brandcopy { + min-width: 0; + max-width: 100%; +} + +.app-mobile-header--menu-open { + border-bottom-color: transparent; + background: transparent; + backdrop-filter: none; +} + +.app-nav-list { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.app-nav-link { + display: flex; + align-items: center; + gap: 0.75rem; + border: 1px solid transparent; + border-radius: 0.95rem; + padding: 0.8rem 0.9rem; + color: var(--muted-foreground); + font-size: 0.93rem; + text-decoration: none; + transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease, transform 140ms ease; +} + +.app-nav-link:hover { + transform: translateX(2px); + border-color: hsl(35 23% 76% / 0.75); + background: hsl(42 28% 92% / 0.78); + color: var(--foreground); +} + +.app-nav-link--active { + border-color: color-mix(in srgb, var(--page-accent) 45%, white); + background: color-mix(in srgb, var(--page-accent) 13%, white); + color: var(--foreground); + box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.7); +} + +.app-sidebar-footer { + margin-top: auto; + padding: 0.8rem; +} + +.language-switcher { + display: inline-flex; + width: 100%; + border: 1px solid hsl(35 22% 75% / 0.82); + border-radius: 999px; + background: hsl(42 30% 93% / 0.9); + padding: 0.2rem; +} + +.language-switcher__button { + flex: 1; + border-radius: 999px; + padding: 0.45rem 0.7rem; + color: var(--muted-foreground); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + transition: background-color 140ms ease, color 140ms ease, box-shadow 140ms ease; +} + +.language-switcher__button:hover { + color: var(--foreground); +} + +.language-switcher__button--active { + background: color-mix(in srgb, var(--page-accent) 14%, white); + color: var(--foreground); + box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.74); +} + .app-sidebar a { border: 1px solid transparent; } @@ -212,6 +354,7 @@ body { width: min(1160px, 100%); } +.app-main h1, .app-main h2 { font-family: 'Cormorant Infant', 'Times New Roman', serif; font-size: clamp(1.9rem, 3.3vw, 2.7rem); @@ -241,6 +384,19 @@ body { color: var(--foreground); } +.page-header-meta, +.page-header-foot { + grid-column: 1 / -1; +} + +.page-header-meta { + margin-top: 0.65rem; +} + +.page-header-foot { + margin-top: 1rem; +} + .editorial-toolbar { margin-top: 0.9rem; display: flex; @@ -289,7 +445,7 @@ body { color: hsl(207 78% 28%); } -.products-table-shell { +.editorial-table-shell { border: 1px solid hsl(35 24% 74% / 0.85); border-radius: 0.9rem; overflow: hidden; @@ -299,14 +455,14 @@ body { background: color-mix(in srgb, var(--page-accent) 10%, white); } -.products-mobile-card { +.editorial-mobile-card { display: block; border: 1px solid hsl(35 21% 76% / 0.85); border-radius: 0.8rem; padding: 0.95rem; } -.products-section-title { +.editorial-section-title { border-bottom: 1px dashed color-mix(in srgb, var(--page-accent) 35%, var(--border)); padding-bottom: 0.3rem; padding-top: 0.5rem; @@ -317,11 +473,7 @@ body { text-transform: uppercase; } -.products-sticky-actions { - border-color: color-mix(in srgb, var(--page-accent) 25%, var(--border)); -} - -.products-meta-strip { +.editorial-meta-strip { display: flex; flex-wrap: wrap; align-items: center; @@ -488,7 +640,7 @@ body { font-feature-settings: 'tnum'; } -.lab-results-mobile-grid .products-section-title { +.lab-results-mobile-grid .editorial-section-title { margin-top: 0.15rem; } @@ -525,6 +677,20 @@ body { } @media (min-width: 768px) { + .app-mobile-header, + .app-drawer, + .app-drawer-backdrop { + display: none !important; + } + + .app-sidebar { + position: sticky; + top: 0; + align-self: flex-start; + height: 100vh; + overflow-y: auto; + } + .app-shell { flex-direction: row; } @@ -556,6 +722,10 @@ body { grid-area: subtitle; } + .dashboard-stat-strip { + grid-column: 1 / -1; + } + .editorial-toolbar { grid-area: actions; margin-top: 0; @@ -661,6 +831,122 @@ body { font-weight: 600; } +.dashboard-stat-strip { + margin-top: 1.8rem; + display: grid; + gap: 0.65rem; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-top: 1px dashed color-mix(in srgb, var(--page-accent) 24%, var(--editorial-line)); + padding-top: 1rem; +} + +.dashboard-stat-card { + border: 1px solid hsl(36 18% 77% / 0.62); + border-radius: 0.85rem; + background: linear-gradient(180deg, hsl(44 32% 96% / 0.74), hsl(45 24% 93% / 0.66)); + padding: 0.72rem 0.78rem; +} + +.dashboard-stat-label, +.dashboard-metric-label, +.dashboard-featured-label, +.dashboard-health-subline, +.dashboard-attention-label { + margin: 0; + color: var(--editorial-muted); + font-size: 0.73rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.dashboard-stat-value, +.dashboard-metric-value, +.dashboard-featured-value { + margin: 0.28rem 0 0; + font-family: 'Cormorant Infant', 'Times New Roman', serif; + font-size: 1.28rem; + font-weight: 600; + line-height: 1; +} + +.dashboard-stat-detail, +.dashboard-featured-meta, +.dashboard-featured-notes, +.dashboard-health-value, +.dashboard-attention-value, +.dashboard-panel-note, +.dashboard-metric-trend { + margin: 0.28rem 0 0; + color: var(--editorial-muted); + font-size: 0.8rem; +} + +.dashboard-attention-panel { + position: relative; + z-index: 1; + margin-bottom: 1rem; + border: 1px solid hsl(36 25% 74% / 0.8); + border-radius: 1.2rem; + background: linear-gradient(180deg, hsl(44 40% 95% / 0.92), hsl(42 32% 93% / 0.94)); + box-shadow: + 0 20px 40px -34px hsl(219 32% 14% / 0.38), + inset 0 1px 0 hsl(0 0% 100% / 0.7); + padding: 0.9rem; +} + +.dashboard-attention-header { + display: flex; + align-items: baseline; + gap: 0.75rem; + margin-bottom: 0.85rem; +} + +.dashboard-attention-header h3 { + margin: 0; + font-family: 'Cormorant Infant', 'Times New Roman', serif; + font-size: clamp(1.3rem, 2.3vw, 1.6rem); + font-weight: 600; +} + +.dashboard-attention-list { + display: grid; + gap: 0.6rem; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.dashboard-attention-item { + display: flex; + min-height: 4.35rem; + flex-direction: column; + justify-content: space-between; + border: 1px solid hsl(36 22% 74% / 0.75); + border-radius: 0.9rem; + padding: 0.72rem 0.8rem; + text-decoration: none; + color: inherit; + transition: transform 140ms ease, border-color 140ms ease, background-color 140ms ease; +} + +.dashboard-attention-item:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--page-accent) 42%, var(--border)); +} + +.dashboard-attention-item--alert { + background: linear-gradient(180deg, hsl(27 76% 95%), hsl(18 60% 91%)); +} + +.dashboard-attention-item--calm { + background: linear-gradient(180deg, hsl(130 24% 95%), hsl(136 26% 91%)); +} + +.dashboard-attention-value { + color: var(--foreground); + font-size: 0.88rem; + font-weight: 600; +} + .editorial-grid { position: relative; z-index: 1; @@ -671,11 +957,16 @@ body { .editorial-panel { border-radius: 1.2rem; - padding: 1rem; + padding: 0.9rem; +} + +.dashboard-grid { + grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr) minmax(0, 0.92fr); + align-items: start; } .panel-header { - margin-bottom: 0.9rem; + margin-bottom: 0.75rem; display: flex; align-items: baseline; justify-content: space-between; @@ -687,7 +978,7 @@ body { .panel-header h3 { margin: 0; font-family: 'Cormorant Infant', 'Times New Roman', serif; - font-size: clamp(1.35rem, 2.4vw, 1.7rem); + font-size: clamp(1.22rem, 2.1vw, 1.56rem); font-weight: 600; } @@ -714,7 +1005,7 @@ body { .snapshot-date { color: var(--editorial-muted); - font-size: 0.9rem; + font-size: 0.84rem; font-weight: 600; } @@ -786,33 +1077,51 @@ body { list-style: none; } +.dashboard-metric-row { + margin-top: 0.85rem; + display: grid; + gap: 0.55rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.dashboard-metric-card { + border: 1px solid hsl(36 23% 74% / 0.76); + border-radius: 0.8rem; + background: hsl(42 36% 93% / 0.7); + padding: 0.65rem 0.72rem; +} + +.dashboard-metric-value { + font-size: 1.08rem; +} + .routine-summary-strip { - margin-bottom: 0.7rem; + margin-bottom: 0.6rem; display: flex; flex-wrap: wrap; align-items: center; - gap: 0.4rem; + gap: 0.35rem; } .routine-summary-chip { border: 1px solid hsl(35 24% 71% / 0.85); border-radius: 999px; - padding: 0.22rem 0.62rem; + padding: 0.16rem 0.5rem; color: var(--editorial-muted); - font-size: 0.74rem; + font-size: 0.68rem; font-weight: 700; - letter-spacing: 0.08em; + letter-spacing: 0.06em; } .panel-action-link, .routine-summary-link { border: 1px solid color-mix(in srgb, var(--page-accent) 38%, var(--editorial-line)); border-radius: 999px; - padding: 0.24rem 0.64rem; + padding: 0.2rem 0.56rem; color: var(--page-accent); - font-size: 0.76rem; + font-size: 0.68rem; font-weight: 700; - letter-spacing: 0.08em; + letter-spacing: 0.06em; text-decoration: none; text-transform: uppercase; } @@ -826,6 +1135,32 @@ body { background: var(--page-accent-soft); } +.dashboard-featured-routine { + display: block; + margin-bottom: 0.7rem; + border: 1px solid hsl(36 24% 73% / 0.82); + border-radius: 0.88rem; + background: linear-gradient(155deg, color-mix(in srgb, var(--page-accent) 5%, white), hsl(44 30% 94%)); + padding: 0.78rem 0.84rem; + text-decoration: none; + color: inherit; +} + +.dashboard-featured-routine:hover { + border-color: color-mix(in srgb, var(--page-accent) 44%, var(--border)); +} + +.dashboard-featured-routine-topline { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.dashboard-featured-notes { + line-height: 1.4; +} + .routine-item + .routine-item { border-top: 1px dashed hsl(36 26% 72% / 0.7); } @@ -834,7 +1169,7 @@ body { display: flex; align-items: center; gap: 0.6rem; - padding: 0.78rem 0; + padding: 0.68rem 0; text-decoration: none; color: inherit; transition: transform 140ms ease, color 160ms ease; @@ -858,9 +1193,9 @@ body { .routine-meta { display: flex; flex-wrap: wrap; - gap: 0.75rem; + gap: 0.6rem; color: var(--editorial-muted); - font-size: 0.8rem; + font-size: 0.76rem; } .routine-note-inline { @@ -882,7 +1217,7 @@ body { } .routine-date { - font-size: 0.93rem; + font-size: 0.88rem; font-weight: 600; } @@ -909,6 +1244,56 @@ body { display: flex; } +.dashboard-health-meta-strip { + margin-bottom: 0.65rem; +} + +.dashboard-health-list { + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.dashboard-health-item { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.65rem; + border: 1px solid hsl(36 22% 75% / 0.78); + border-radius: 0.85rem; + background: hsl(44 34% 95% / 0.75); + padding: 0.68rem 0.78rem; + text-decoration: none; + color: inherit; +} + +.dashboard-health-item:hover { + border-color: color-mix(in srgb, var(--page-accent) 36%, var(--border)); + background: var(--page-accent-soft); +} + +.dashboard-health-test { + margin: 0; + font-size: 0.88rem; + font-weight: 600; +} + +.dashboard-health-value-wrap { + display: flex; + min-width: 0; + flex-direction: column; + align-items: flex-end; + gap: 0.35rem; +} + +.dashboard-health-value { + margin-top: 0; + color: var(--foreground); + font-size: 0.8rem; + font-weight: 600; + text-align: right; +} + .reveal-1, .reveal-2, .reveal-3 { @@ -933,12 +1318,23 @@ body { } @media (max-width: 1024px) { + .dashboard-stat-strip, + .dashboard-attention-list, .editorial-grid { grid-template-columns: minmax(0, 1fr); } + + .dashboard-grid { + grid-template-columns: minmax(0, 1fr); + } } @media (max-width: 640px) { + .dashboard-stat-strip { + margin-top: 1.35rem; + padding-top: 0.8rem; + } + .editorial-title { font-size: 2.05rem; } @@ -951,6 +1347,21 @@ body { font-size: 1.4rem; } + .dashboard-metric-row { + grid-template-columns: minmax(0, 1fr); + } + + .dashboard-health-item, + .dashboard-featured-routine-topline, + .dashboard-attention-item { + align-items: flex-start; + flex-direction: column; + } + + .dashboard-health-value-wrap { + align-items: flex-start; + } + .state-pill, .routine-pill { letter-spacing: 0.08em; diff --git a/frontend/src/lib/components/AutoFixBadge.svelte b/frontend/src/lib/components/AutoFixBadge.svelte index 5a5797d..3b481f5 100644 --- a/frontend/src/lib/components/AutoFixBadge.svelte +++ b/frontend/src/lib/components/AutoFixBadge.svelte @@ -1,6 +1,6 @@ + +{#if messages.length} +
+ {#each messages as message, index (`${message.kind}-${index}-${message.text}`)} +
+ {message.text} +
+ {/each} +
+{/if} diff --git a/frontend/src/lib/components/LanguageSwitcher.svelte b/frontend/src/lib/components/LanguageSwitcher.svelte index 7b19090..ef30711 100644 --- a/frontend/src/lib/components/LanguageSwitcher.svelte +++ b/frontend/src/lib/components/LanguageSwitcher.svelte @@ -1,15 +1,21 @@ -
- - | - +
+ {#each locales as locale (locale.code)} + + {/each}
diff --git a/frontend/src/lib/components/MetadataDebugPanel.svelte b/frontend/src/lib/components/MetadataDebugPanel.svelte index 385a8f4..2c6780c 100644 --- a/frontend/src/lib/components/MetadataDebugPanel.svelte +++ b/frontend/src/lib/components/MetadataDebugPanel.svelte @@ -1,6 +1,6 @@ + +
+ {#if backHref && backLabel} + {backLabel} + {/if} + + {#if kicker} +

{kicker}

+ {/if} + +

{title}

+ + {#if subtitle} +

{subtitle}

+ {/if} + + {#if meta} +
+ {@render meta()} +
+ {/if} + + {#if actions} +
+ {@render actions()} +
+ {/if} + + {#if children} +
+ {@render children()} +
+ {/if} +
diff --git a/frontend/src/lib/components/ProductForm.svelte b/frontend/src/lib/components/ProductForm.svelte index 80a0b73..1eba4a0 100644 --- a/frontend/src/lib/components/ProductForm.svelte +++ b/frontend/src/lib/components/ProductForm.svelte @@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; import { baseSelectClass, baseTextareaClass } from '$lib/components/forms/form-classes'; import type { ProductParseResponse } from '$lib/api'; - import { m } from '$lib/paraglide/messages.js'; + import * as m from '$lib/paraglide/messages.js'; import { Sparkles, X } from 'lucide-svelte'; let { diff --git a/frontend/src/lib/components/ProductFormAiModal.svelte b/frontend/src/lib/components/ProductFormAiModal.svelte index 921db52..fd3a0d9 100644 --- a/frontend/src/lib/components/ProductFormAiModal.svelte +++ b/frontend/src/lib/components/ProductFormAiModal.svelte @@ -1,5 +1,5 @@
- -
-
+
+
+

{m["nav_appSubtitle"]()}

{m["nav_appName"]()}
- {#if mobileMenuOpen} - - - + {/if} - -
{@render children()}
diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 78a29b1..acab86d 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -1,15 +1,18 @@ -import { getRoutines, getSkinSnapshots } from '$lib/api'; -import type { SkinConditionSnapshot } from '$lib/types'; +import { getLabResults, getRoutines, getSkinSnapshots } from '$lib/api'; +import type { Routine, SkinConditionSnapshot } from '$lib/types'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { - const [routines, snapshots] = await Promise.all([ + const [routines, snapshots, labResults] = await Promise.all([ getRoutines({ from_date: recentDate(14) }), - getSkinSnapshots({ from_date: recentDate(60) }) + getSkinSnapshots({ from_date: recentDate(60) }), + getLabResults({ latest_only: true, limit: 8 }) ]); return { - recentRoutines: routines.slice(0, 10), - latestSnapshot: getFreshestSnapshot(snapshots) + recentRoutines: getFreshestRoutines(routines).slice(0, 10), + recentSnapshots: snapshots, + latestSnapshot: getFreshestSnapshot(snapshots), + recentLabResults: labResults.items }; }; @@ -27,3 +30,11 @@ function getFreshestSnapshot(snapshots: SkinConditionSnapshot[]): SkinConditionS return current.created_at > freshest.created_at ? current : freshest; }); } + +function getFreshestRoutines(routines: Routine[]): Routine[] { + return [...routines].sort((a, b) => { + if (a.routine_date !== b.routine_date) return b.routine_date.localeCompare(a.routine_date); + if (a.part_of_day !== b.part_of_day) return b.part_of_day.localeCompare(a.part_of_day); + return b.created_at.localeCompare(a.created_at); + }); +} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ae00c63..4a4718c 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,7 +1,56 @@ -{m.dashboard_title()} — innercontext +{dashboard_title()} - innercontext
-

{m["nav_appSubtitle"]()}

-

{m.dashboard_title()}

-

{m.dashboard_subtitle()}

+

{nav_appSubtitle()}

+

{dashboard_title()}

+

{dashboard_dailyBriefing()}

- {#if data.latestSnapshot} - {@const snapshot = data.latestSnapshot} -
-
-

{m["dashboard_latestSnapshot"]()}

-

{snapshot.snapshot_date}

+
+ {#each heroStats as stat (stat.label)} +
+

{stat.label}

+

{stat.value}

+

{stat.detail}

- {#if snapshot.overall_state} - - {humanize(snapshot.overall_state)} - - {/if} -
- {/if} + {/each} +
-
+
+
+

01

+

{dashboard_requiresAttention()}

+
+
+ {#each attentionItems as item (item.label)} + + {item.label} + {item.value} + + {/each} +
+
+ +
-

01

-

{m["dashboard_latestSnapshot"]()}

+

02

+

{dashboard_latestSnapshot()}

- {#if data.latestSnapshot} - {@const s = data.latestSnapshot} + {#if latestSnapshot}
- {s.snapshot_date} - {#if s.overall_state} - {humanize(s.overall_state)} + {latestSnapshot.snapshot_date} + {#if latestSnapshot.overall_state} + + {formatState(latestSnapshot.overall_state)} + {/if}
- {#if s.active_concerns.length} -
- {#each s.active_concerns as concern (concern)} - {humanize(concern)} + {#if daysSinceSnapshot != null} +

{dashboard_skinFreshness({ count: daysSinceSnapshot })}

+ {/if} + + {#if latestSnapshot.active_concerns.length} +
+ {#each latestSnapshot.active_concerns as concern (concern)} + {formatConcern(concern)} {/each}
{/if} - {#if s.notes} -

{s.notes}

+ {#if snapshotMetrics.length} +
+ {#each snapshotMetrics as metric (metric.label)} +
+

{metric.label}

+

{metric.value}

+ {#if metric.trend} +

{dashboard_metricDelta({ delta: metric.trend })}

+ {/if} +
+ {/each} +
+ {/if} + + {#if latestSnapshot.notes} +

{latestSnapshot.notes}

{/if} {:else} -

{m["dashboard_noSnapshots"]()}

+

{dashboard_noSnapshots()}

+ {/if}
-

02

-

{m["dashboard_recentRoutines"]()}

+

03

+

{dashboard_recentRoutines()}

- {#if data.recentRoutines.length} + {#if sortedRoutines.length}
AM {routineStats.amCount} PM {routineStats.pmCount} - {m["routines_addNew"]()} + {dashboard_averageSteps({ count: routineStats.averageSteps })} + {routines_addNew()}
+ + {#if latestRoutine} + + + + + {#if latestRoutine.notes} + + {/if} + + {/if} +
    - {#each data.recentRoutines as routine (routine.id)} + {#each sortedRoutines.slice(0, 5) as routine (routine.id)}
  1. @@ -116,9 +376,9 @@
    - {routine.steps?.length ?? 0} {m.common_steps()} + {routine.steps?.length ?? 0} {common_steps()} {#if routine.notes} - {m.routines_notes()}: {routine.notes} + {routines_notes()}: {routine.notes} {/if}
@@ -127,11 +387,49 @@ {/each} {:else} -

{m["dashboard_noRoutines"]()}

+

{dashboard_noRoutines()}

{/if} + +
+
+

04

+

{dashboard_healthPulse()}

+
+ + + {#if sortedLabResults.length} +
+ {dashboard_lastLabDate()}: {latestLabDate} + {dashboard_flaggedLabsCount({ count: flaggedLabs.length })} +
+ + + {:else} +

{dashboard_noLabResults()}

+ {/if} +
diff --git a/frontend/src/routes/health/lab-results/+page.server.ts b/frontend/src/routes/health/lab-results/+page.server.ts index c38458f..13cd386 100644 --- a/frontend/src/routes/health/lab-results/+page.server.ts +++ b/frontend/src/routes/health/lab-results/+page.server.ts @@ -1,4 +1,4 @@ -import { createLabResult, deleteLabResult, getLabResults, updateLabResult } from '$lib/api'; +import { deleteLabResult, getLabResults, updateLabResult } from '$lib/api'; import { fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -41,38 +41,6 @@ export const load: PageServerLoad = async ({ url }) => { }; export const actions: Actions = { - create: async ({ request }) => { - const form = await request.formData(); - const collected_at = form.get('collected_at') as string; - const test_code = form.get('test_code') as string; - const test_name_original = form.get('test_name_original') as string; - const value_num = form.get('value_num') as string; - const unit_original = form.get('unit_original') as string; - const flag = form.get('flag') as string; - const lab = form.get('lab') as string; - - if (!collected_at || !test_code) { - return fail(400, { error: 'Date and test code are required' }); - } - - const body: Record = { - collected_at, - test_code - }; - if (test_name_original) body.test_name_original = test_name_original; - if (value_num) body.value_num = Number(value_num); - if (unit_original) body.unit_original = unit_original; - if (flag) body.flag = flag; - if (lab) body.lab = lab; - - try { - await createLabResult(body); - return { created: true }; - } catch (e) { - return fail(500, { error: (e as Error).message }); - } - }, - update: async ({ request }) => { const form = await request.formData(); const id = form.get('id') as string; diff --git a/frontend/src/routes/health/lab-results/+page.svelte b/frontend/src/routes/health/lab-results/+page.svelte index fe3218e..84f205d 100644 --- a/frontend/src/routes/health/lab-results/+page.svelte +++ b/frontend/src/routes/health/lab-results/+page.svelte @@ -1,14 +1,16 @@ {m["labResults_title"]()} — innercontext
-
-

{m["nav_appSubtitle"]()}

-

{m["labResults_title"]()}

-

{m["labResults_count"]({ count: data.resultPage.total })}

-
- - {m['labResults_view']()}: {data.latestOnly ? m['labResults_viewLatest']() : m['labResults_viewAll']()} - {#if data.flag} - · {m['labResults_flagFilter']()} {data.flag} + + {#snippet meta()} +
+ + {m['labResults_view']()}: {data.latestOnly ? m['labResults_viewLatest']() : m['labResults_viewAll']()} + {#if data.flag} + · {m['labResults_flagFilter']()} {data.flag} + {/if} + + + {m['labResults_flag']()}: {flaggedCount} + + {#if data.test_code} + {data.test_code} {/if} - - - {m['labResults_flag']()}: {flaggedCount} - - {#if data.test_code} - {data.test_code} - {/if} -
-
- -
-
+
+ {/snippet} - {#if form?.error} -
{form.error}
- {/if} - {#if form?.created} -
{m["labResults_added"]()}
- {/if} - {#if form?.deleted} -
{m["labResults_deleted"]()}
- {/if} - {#if form?.updated} -
{m["labResults_updated"]()}
- {/if} + {#snippet actions()} + + {/snippet} + + + {#if editingId} @@ -457,56 +452,6 @@
{/if} - {#if showForm} - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
- -
- {/if} - {#if data.totalPages > 1}