Files
HealthLog/src/app/globals.css
T
Marc-André Bombeck 61d156e71b fix(api-handler): narrow safeRequestProp catch + harden @source path resolution
Two carry-overs from the W7d dev-server repair, flagged for v1.4.26 in
the post-W7d follow-up list.

1. `safeRequestProp` previously caught every exception from the read
   callback so logging instrumentation never crashed the wrapped
   handler. That was overly broad — a regression in a header parser
   or a `null` access inside `read` would silently fall back to the
   default and hide the bug. The catch is now narrowed to the V8
   private-field `TypeError` that Next 16 raises when the request is
   a synthetic placeholder on force-static handlers; every other
   error rethrows. Three message-shape variants are recognised
   (`private member`, `private field`, `private name`) to cover
   current V8 + Bun + older runtimes.

2. `src/app/globals.css` now imports Tailwind with an explicit
   `source("../..")` base. Tailwind v4 auto-detection otherwise walks
   up from the CSS file's resolver root, which can land at an
   unexpected level depending on how the build tool reaches the
   stylesheet. Pinning the base mirrors the cwd-anchored fix the
   safety-contracts loader landed in commit `dd44a0d` and stops the
   planning / docs directories from leaking class-shaped fragments
   into the generated CSS regardless of where the build is invoked
   from.

The narrow-catch helper is exposed through a `__testables` export so
its contract can be unit-tested directly. The new test file covers:
  - the three private-field message variants classify as match
  - generic TypeErrors, RangeErrors, and bare Errors classify as
    non-match (must rethrow)
  - safeRequestProp returns the read result on a real request
  - safeRequestProp returns the fallback on a private-field TypeError
  - safeRequestProp rethrows any other exception

Verification:
  pnpm typecheck                           → clean
  pnpm lint                                → clean
  pnpm vitest run api-handler-safe-request-prop → 9 / 9 pass
2026-05-14 21:45:56 +02:00

333 lines
11 KiB
CSS

@import "tailwindcss" source("../..");
@import "tw-animate-css";
@import "shadcn/tailwind.css";
/*
* Exclude planning + docs directories from Tailwind v4's content scan.
*
* v4's scanner walks the entire project root by default and picks any
* class-shaped token out of every text file. Planning reports and
* design docs often quote literal class-name fragments such as
* `bg-[var(...)]/12` as examples, and the bracketed-CSS-variable form
* combined with an opacity modifier generates an escaped selector that
* Turbopack's CSS parser rejects with `Unexpected token Delim('.')`,
* 500-ing every page. Constraining the scan to source code avoids
* fixture-style strings poisoning the generated CSS.
*
* The `source("../..")` argument on the `@import "tailwindcss"` line
* above pins auto-detection to the project root (resolved from this
* stylesheet) instead of letting Tailwind walk up from whatever cwd
* the build tool happens to be sitting at. That mirrors the
* cwd-anchored fix the safety-contracts loader landed in
* commit `dd44a0d` and stops the planning / docs directories from
* leaking class-shaped fragments into the generated CSS regardless of
* whether the build runs from the repo root or a tooling subdir.
*
* The `@source not` directives below are resolved relative to this
* stylesheet, so they point at `<repo-root>/.planning` and
* `<repo-root>/docs`.
*/
@source not "../../.planning";
@source not "../../docs";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
/* Dracula semantic colors for charts */
--color-dracula-purple: var(--dracula-purple);
--color-dracula-pink: var(--dracula-pink);
--color-dracula-green: var(--dracula-green);
--color-dracula-cyan: var(--dracula-cyan);
--color-dracula-orange: var(--dracula-orange);
--color-dracula-red: var(--dracula-red);
--color-dracula-yellow: var(--dracula-yellow);
--color-dracula-lavender: var(--dracula-lavender);
/*
* Semantic feedback tokens — aliases over the Dracula palette so badges,
* banners, and status pills can stop hard-coding `text-dracula-green` /
* `bg-dracula-orange/15` and ask for the *meaning* instead. Each is a
* straight alias of the existing colour at the time of introduction so
* the visual stays pixel-identical to v1.4.6 — what changes is intent.
* - success → dracula-green (positive outcome, e.g. published)
* - warning → dracula-orange (caution / non-blocking attention)
* - info → dracula-cyan (neutral informational note)
*/
--color-success: var(--success);
--color-warning: var(--warning);
--color-info: var(--info);
}
/*
* Dracula Theme
* Based on https://draculatheme.com/contribute
* Mapped to shadcn/ui CSS variable system
*/
/* ── Dracula palette (shared) ────────────────────── */
:root {
--dracula-bg: #282a36;
--dracula-current: #44475a;
--dracula-fg: #f8f8f2;
--dracula-comment: #9aa3b3;
--dracula-cyan: #8be9fd;
--dracula-green: #50fa7b;
--dracula-orange: #ffb86c;
--dracula-pink: #ff79c6;
--dracula-purple: #bd93f9;
--dracula-red: #ff5555;
--dracula-yellow: #f1fa8c;
--dracula-lavender: #d6acff;
/*
* Semantic feedback tokens (see @theme `--color-success` / `--color-warning`
* / `--color-info` re-exports). Aliased over the Dracula palette so existing
* dark-mode visuals stay identical; light mode (Alucard) below provides the
* higher-contrast counterpart so AA contrast lands on the white card.
*/
--success: var(--dracula-green);
--warning: var(--dracula-orange);
--info: var(--dracula-cyan);
--radius: 0.625rem;
}
/* ── Dark mode (default) ─────────────────────────── */
:root.dark {
--background: #282a36;
--foreground: #f8f8f2;
--card: #1e1f29;
--card-foreground: #f8f8f2;
--popover: #1e1f29;
--popover-foreground: #f8f8f2;
--primary: #bd93f9;
--primary-foreground: #282a36;
--secondary: #44475a;
--secondary-foreground: #f8f8f2;
--muted: #44475a;
--muted-foreground: #9aa3b3;
--accent: #44475a;
--accent-foreground: #f8f8f2;
--destructive: #ff5555;
--border: #44475a;
--input: #44475a;
--ring: #bd93f9;
--chart-1: #bd93f9;
--chart-2: #50fa7b;
--chart-3: #ff79c6;
--chart-4: #8be9fd;
--chart-5: #ffb86c;
--sidebar: #1e1f29;
--sidebar-foreground: #f8f8f2;
--sidebar-primary: #bd93f9;
--sidebar-primary-foreground: #f8f8f2;
--sidebar-accent: #44475a;
--sidebar-accent-foreground: #f8f8f2;
--sidebar-border: #44475a;
--sidebar-ring: #bd93f9;
color-scheme: dark;
}
/* ── Light mode (Alucard — official Dracula light) ── */
:root.light {
--background: #f5f5f5;
--foreground: #1f1f1f;
--card: #ffffff;
--card-foreground: #1f1f1f;
--popover: #ffffff;
--popover-foreground: #1f1f1f;
--primary: #644ac9;
--primary-foreground: #ffffff;
--secondary: #dcdeef;
--secondary-foreground: #1f1f1f;
--muted: #dcdeef;
--muted-foreground: #5b6273;
--accent: #dcdeef;
--accent-foreground: #1f1f1f;
--destructive: #cb3a2a;
/*
* Alucard light-mode overrides for the semantic tokens — straight Dracula
* green/orange/cyan would fail AA contrast on a white card. These match the
* darker palette already used for `--destructive`/`--chart-*` in light mode.
*/
--success: #14720a;
--warning: #a34d14;
--info: #036a96;
--border: #cfcfde;
--input: #cfcfde;
--ring: #644ac9;
--chart-1: #644ac9;
--chart-2: #14720a;
--chart-3: #a3134d;
--chart-4: #036a96;
--chart-5: #a34d14;
--sidebar: #ffffff;
--sidebar-foreground: #1f1f1f;
--sidebar-primary: #644ac9;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #dcdeef;
--sidebar-accent-foreground: #1f1f1f;
--sidebar-border: #cfcfde;
--sidebar-ring: #644ac9;
color-scheme: light;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
p {
color: var(--foreground);
}
}
/*
* `.no-scrollbar` — keeps a container scrollable but hides the visible
* bar. Used by the mobile section strips in `<AdminShell>` and
* `<SettingsShell>` so 13 admin / 10 settings entries can swipe
* horizontally without painting an always-on scrollbar at the top of
* every admin/settings page (a recurring maintainer complaint — most
* recently mistaken for a `/admin/api-tokens` table scrollbar in
* v1.4.16/17 because the strip sits right above the api-tokens card).
*
* Standard `scrollbar-width: none` (Firefox) + `::-webkit-scrollbar`
* (Chromium/WebKit) covers every browser HealthLog targets. Touch
* scrolling, keyboard arrow scrolling, and `Tab` focus traversal all
* keep working — this is a purely cosmetic suppression, not an
* `overflow: hidden`.
*/
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* ── Insight card animations ─────────────────────── */
@keyframes insight-fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-insight-in {
animation: insight-fade-in 0.4s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.animate-insight-in {
animation: none;
}
}
/* ── v1.4.20 phase B1 — Insights redesign hero strip ─────────────── */
/*
* `.hero-gradient` — purple → pink linear gradient at low opacity, for
* the new <HeroStrip> band on /insights. Sits above `var(--card)` so
* the band reads as a richer surface than the card it overlays.
*
* Matches the design handoff (`prototype/artboard-fullpage.jsx →
* BriefingHero` background) — purple/pink at ~12% opacity over the
* card colour, with a subtle border-tint to anchor it on the dark bg.
*/
.hero-gradient {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--dracula-purple) 18%, var(--card)) 0%,
color-mix(in srgb, var(--dracula-pink) 12%, var(--card)) 100%
);
border: 1px solid color-mix(in srgb, var(--dracula-purple) 24%, transparent);
}
/*
* `.glow-purple` — soft purple aura behind the hero card, painted via
* `box-shadow` so it stays GPU-cheap and respects the existing card
* border radius without an extra wrapper element.
*/
.glow-purple {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--dracula-purple) 16%, transparent),
0 18px 48px -28px color-mix(in srgb, var(--dracula-purple) 50%, transparent);
}
/*
* `pulseDot` keyframes — the "live, refreshed N min ago" green dot in
* the hero meta row. Used as `.pulse-dot { animation: pulseDot ... }`.
* Reduced-motion users get a static dot via the prefers-reduced-motion
* guard below.
*/
@keyframes pulseDot {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.85);
}
}
.pulse-dot {
animation: pulseDot 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.pulse-dot {
animation: none;
}
}