Nouvelle app Vite/React independante (workspace apps/*), calquee sur apps/web : port 5174, sa propre image Docker (nginx statique), son propre domaine/deploiement. - AdminApiClient (VITE_ADMIN_API_URL, credentials: include) + ApiError. - AdminAuthContext / RequireAdmin : restaure la session admin via GET /admin/auth/me, garde de route (miroir de AuthContext/RequireAuth). - LoginPage (/login) : validation cliente via adminLoginSchema partage, erreurs traduites via ErrorMessageService. - AdminLayout : sidebar (Tableau de bord / Monitoring / Corrections) + deconnexion, rendu une fois autour du groupe RequireAdmin. - Pages Dashboard / Monitoring / Corrections en placeholder (remplies aux PR 3-5). - i18n fr (bloc admin.* + sous-ensemble errors.*), tokens _theme.scss copies de apps/web (extraction en package partage : suivi separe). - Dockerfile multi-stage (node build -> nginx:alpine) + nginx.conf (SPA fallback). Service admin-web dans docker-compose.yml (port ADMIN_WEB_PORT, VITE_ADMIN_API_URL en build arg). - Cypress : login.feature (KO -> message, OK -> dashboard) + admin-layout .cy.ts (redirection /login sans session, nav entre sections, logout). Job "Run admin-web E2E tests" ajoute a ci.yml. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
5.4 KiB
SCSS
149 lines
5.4 KiB
SCSS
// =============================================================================
|
|
// Global stylesheet — imported exactly once, in main.tsx. Contains only
|
|
// truly app-wide rules: the theme tokens and a minimal reset/base styling
|
|
// that every page inherits. Anything specific to one component or page
|
|
// belongs in a .scss file colocated next to that component/page instead.
|
|
// =============================================================================
|
|
|
|
@use "./theme";
|
|
|
|
// Include borders/padding in an element's declared width/height everywhere,
|
|
// rather than the browser default of adding them on top.
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
// Minimal reset: remove the default body margin so pages can control their
|
|
// own layout without fighting the browser's default 8px margin.
|
|
body {
|
|
margin: 0;
|
|
font-family: var(--font-body);
|
|
font-size: var(--font-size-base);
|
|
line-height: 1.55;
|
|
color: var(--color-text);
|
|
background: var(--color-background);
|
|
}
|
|
|
|
// Headings use the condensed "label" face app-wide — see _theme.scss for
|
|
// the rationale. `text-wrap: balance` avoids a lone short word wrapping
|
|
// onto its own line in multi-line titles.
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6 {
|
|
margin: 0;
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
// Default to the page-title size; a heading used as a smaller component
|
|
// title (e.g. the auth card's <h1>) overrides this in its own stylesheet.
|
|
h1 {
|
|
font-size: var(--font-size-2xl);
|
|
}
|
|
|
|
// A visible, consistent focus ring for keyboard navigation — the browser
|
|
// default varies a lot between elements and browsers.
|
|
:focus-visible {
|
|
outline: 2px solid var(--color-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
// Checkbox/radio appearance, app-wide — "selectable card" style: the native
|
|
// control itself is visually hidden (still real, focusable and
|
|
// screen-reader-visible — see the `input[type=...]` rule below, not
|
|
// `display: none`) and the whole label row it lives in becomes the
|
|
// interactive surface instead: a flat bordered box that fills in with a
|
|
// tinted background + primary border once selected, with a checkmark
|
|
// fading in on the leading edge.
|
|
//
|
|
// The base (unselected) look below is detected structurally with `:has()`
|
|
// — safe, since "does this label contain a checkbox/radio" never changes
|
|
// after mount. The *selected* look is instead driven by the `is-selected`
|
|
// class {@link CheckboxOption}/{@link RadioOption} (components/ui/) toggle
|
|
// in JS from the same boolean their caller already passes to `checked` —
|
|
// chaining a second `:has(:checked)` to react to that live state turned
|
|
// out to be unreliable across browsers, so this only needs one
|
|
// always-true `:has()`.
|
|
//
|
|
// Every checkbox/radio in the app goes through this one place (the allergy
|
|
// grid, the theme picker, anywhere future) rather than each feature styling
|
|
// its own — see profile-forms.scss / settings-pages.scss, which only
|
|
// arrange these within their own layout (grid vs. stacked list) and
|
|
// intentionally don't re-style the control/label look itself.
|
|
label:has(> input[type="checkbox"]),
|
|
label:has(> input[type="radio"]) {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
padding: var(--space-xs) var(--space-sm);
|
|
// Overrides the generic `label { font-weight: 600 }` base rule
|
|
// (profile-forms.scss) — without this, an *unselected* row reads just as
|
|
// bold as a selected one (only `.allergy-select__option` happened to set
|
|
// its own 400 already; `.theme-select__option` didn't, so its rows were
|
|
// all permanently bold until this was centralized here).
|
|
font-weight: 400;
|
|
border: 1.5px solid var(--color-border);
|
|
border-radius: var(--radius-base);
|
|
background: var(--color-surface);
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 0.15s ease,
|
|
border-color 0.15s ease;
|
|
|
|
&:hover {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
&.is-selected {
|
|
border-color: var(--color-primary);
|
|
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
|
|
color: var(--color-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
&:has(:focus-visible) {
|
|
outline: 2px solid var(--color-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
}
|
|
|
|
// The control itself is removed from the visual flow — hidden the
|
|
// "sr-only" way (not `display: none`) so it stays focusable/tabbable and
|
|
// announced correctly by screen readers; the label above carries the
|
|
// entire visible selected/unchecked look.
|
|
input[type="checkbox"],
|
|
input[type="radio"] {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
margin: 0;
|
|
opacity: 0;
|
|
}
|
|
|
|
// The checkmark — a real element (see components/ui/Checkbox.tsx /
|
|
// Radio.tsx) shown via the same `is-selected` class as the label's own
|
|
// look above, not a separate CSS-only trigger. Scaled in from nothing so
|
|
// toggling has a bit of motion. Same mark for both checkbox and radio: one
|
|
// consistent "selected" language app-wide rather than a checkmark here and
|
|
// a dot there. Sits first in the row (before the label text, per DOM
|
|
// order) — a classic "control on the left" layout rather than trailing.
|
|
.check-mark {
|
|
flex: none;
|
|
width: 0.9rem;
|
|
height: 0.9rem;
|
|
background: var(--color-primary);
|
|
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
|
transform: scale(0);
|
|
transition: transform 0.1s ease;
|
|
}
|
|
|
|
.is-selected .check-mark {
|
|
transform: scale(1);
|
|
}
|