diff --git a/docs/design/edition-color-vocabulary.md b/docs/design/edition-color-vocabulary.md index a0f818fcc..0377741d5 100644 --- a/docs/design/edition-color-vocabulary.md +++ b/docs/design/edition-color-vocabulary.md @@ -9,7 +9,7 @@ This names the **roles** colors play in the voter-facing edition views, independ - **Reuse and retheme the existing shadcn token names** where a role already exists (`background`, `foreground`, `muted-foreground`, `border`, `ring`, `popover`, `accent`, `destructive`). New names are added only for roles shadcn doesn't cover. No `ed-*` namespace. - **No numeric ramps.** Each role is one value per theme; "lighter/darker" variants are expressed as alpha on the base token (Tailwind `/NN` over an HSL var) or as an explicit `-soft` companion, not as `-100…-900` scales. - **`/NN` opacity modifiers work only on opaque roles** (`foreground`, `accent`, `live`, `vote-must`, …). The inherently translucent roles (`surface`/`surface-raised`/`surface-active`, `accent-soft`, `border`/`border-strong`, `*-soft`) carry their alpha inside the CSS variable, so `bg-surface/50` expands to a double-alpha `hsl(… / 0.05 / .5)` — silently invalid CSS. Use the next step in the role family (`surface` → `surface-raised` → `surface-active`) instead of a modifier. -- **Portals escape the scope.** The variables live on the edition root (`EditionViewRoot`), but Radix `Select`/`Popover`/`Sheet` content portals under `document.body` and resolves the global values there. Before tokenizing portaled content, give it the scope (portal `container` inside the edition root, or the scope class on the content element). +- **The tokens live on `:root`, not a scope class (#367).** The whole app renders on the same dark identity today, so there's one set of values for the whole app — including admin and Radix portal content — and no scope class to remember. The flip (#359) will attach `data-edition-theme="light"` to the document root. - Values below reference today's hardcoded classes only to define the mapping; the actual palette is chosen in #320/#321. ## Token set @@ -32,6 +32,8 @@ This names the **roles** colors play in the voter-facing edition views, independ | `surface-active` **(new)** | Hover/pressed/selected layer | `bg-white/20`, `bg-white/30` | | `popover` (existing) | Solid floating sheets, dropdowns, drawers | `bg-gray-800`, `bg-gray-900`, `bg-gray-900/95` | +`paper-background` / `paper-foreground` / `paper-muted-foreground` / `paper-border` **(new, admin/auth only)** are a stopgap for shared shadcn primitives (`Dialog`, `Input`, `InputOTP`, `Table`, toasts, `AuthDialog`) that render on a plain white chrome surface outside the edition views and were left invisible when `background`/`foreground`/`muted-foreground`/`border` were repointed to the dark identity. Named `paper-*` (not `surface-*`) specifically to avoid colliding with the `surface`/`surface-raised`/`surface-active` overlay-fill family above — those are translucent layers on the dark ground, `paper-*` is an opaque light page/dialog background, and the two are unrelated roles that happen to share a domain vocabulary. They're hardcoded grays/white today, not theme-aware. Once the light/dark flip (#359) lands, fold them into `background`/`foreground`/`muted-foreground`/`border` (scoped to the light theme value) and delete this family. + ### Borders & focus | Token | Role | Absorbs today | @@ -40,11 +42,14 @@ This names the **roles** colors play in the voter-facing edition views, independ | `border-strong` **(new)** | Emphasis / selected outlines | `border-purple-400/40`–`/50`, `border-purple-400`, `border-white/50`–`/80` | | `ring` | Focus/selection ring | `ring-purple-400`, `ring-white` | +A hairline drawn in `border-white/NN` (not purple) doesn't fit either border role — reach for `border-foreground/NN` instead: `foreground` is opaque and already carries today's white value, so the opacity modifier reproduces the exact color without borrowing a fill role (`surface`) for a border. + ### Brand accent (interactive) | Token | Role | Absorbs today | | ------------------------------ | ------------------------------------------------ | -------------------------------------------------- | -| `accent` / `accent-foreground` | Primary interactive fill: selected toggles, CTAs | `bg-purple-600`, `bg-purple-700`, `bg-purple-400` | +| `accent` / `accent-foreground` | Primary interactive fill: selected toggles, CTAs | `bg-purple-600`, `bg-purple-400` | +| `accent-hover` | Hover state for the accent fill | `hover:bg-purple-700` | | `accent-soft` **(new)** | Translucent accent wash for selected/hover chips | `bg-purple-600/30`–`/60`, `bg-purple-400/10`–`/30` | | `text-accent` | Accent-colored text/icons | `text-purple-400` | diff --git a/docs/design/edition-identity-spec.md b/docs/design/edition-identity-spec.md index a5b9d822a..0483153b9 100644 --- a/docs/design/edition-identity-spec.md +++ b/docs/design/edition-identity-spec.md @@ -2,7 +2,7 @@ Resolution of [#321](https://github.com/chiptus/UpLine/issues/321), part of the visual-identity wayfinder map [#317](https://github.com/chiptus/UpLine/issues/317). Written in the semantic token vocabulary from [`edition-color-vocabulary.md`](./edition-color-vocabulary.md); implementation tickets are cut from this document. -**Scope:** the voter-facing edition views only (EditionView hero/tabs, schedule Now/Timeline/List, artists, explore, set details). Admin, groups/settings, and landing/auth keep the old skin. +**Scope:** the voter-facing edition views (EditionView hero/tabs, schedule Now/Timeline/List, artists, explore, set details). The tokens themselves live on `:root` globally (#367), so admin and Radix portal content pick them up too — but the palette and component work described here targets the edition views. **Provenance:** direction decided in [#320](https://github.com/chiptus/UpLine/issues/320) (Festival v2 winner, soft-border light graft), confirmed against real screens by the in-app prototype pass on PR #354 (`src/pages/EditionView/prototype/`, deleted after rollout). Values below are the prototype's confirmed values, extended to the full #319 role set. @@ -12,7 +12,7 @@ One skeleton, two color themes. **Dark is the default**: a violet poster ground ## Theme mechanics (CSS contract) -- Token values are CSS variables scoped to the edition-view root. Dark values are the default; light values apply under an explicit `data-edition-theme="light"` scope on that root. +- Token values are CSS variables on `:root` (#367). Dark values are the default; light values apply under an explicit `data-edition-theme="light"` attribute on the document root. - Both themes ship. How the theme is chosen (system preference vs in-app toggle) and persisted is a rollout decision — [#322](https://github.com/chiptus/UpLine/issues/322)'s territory. The CSS contract above is what it plugs into. - Values are stored as HSL channels to match the existing shadcn `hsl(var(--…))` plumbing; hex given here for legibility. Opaque roles are plain triples (`H S% L%`); the inherently translucent roles (`surface*`, `accent-soft`, `border*`, `*-soft`) carry their alpha inside the variable (`H S% L% / A`) — see the vocabulary's ground rules for why those roles must not take Tailwind `/NN` modifiers. @@ -107,7 +107,7 @@ Same in both themes. ## What implementation consumes -1. Token values above land in `src/index.css` under the edition scope, replacing the roles named in `edition-color-vocabulary.md`'s "absorbs today" mapping (~350 occurrences / 60 files per the #319 audit). +1. Token values above land in `src/index.css` under `:root`, replacing the roles named in `edition-color-vocabulary.md`'s "absorbs today" mapping (~350 occurrences / 60 files per the #319 audit). 2. `tailwind.config.ts` gains the new role names (`surface*`, `subtle-foreground`, `border-strong`, `accent-soft`, `live*`, `notice*`, `vote-*`). 3. Font families wired once at the edition-view root; display styling via the heading rules above. 4. The prototype directory `src/pages/EditionView/prototype/` and its wiring are deleted once the real implementation lands. diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 5cb5a1a36..4b40f13a7 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef< (({ className, ...props }, ref) => ( )); diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 13219e774..44f5854cf 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -4,11 +4,11 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-paper-foreground", { variants: { variant: { - default: "bg-background text-foreground", + default: "bg-paper-background text-paper-foreground", destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", }, diff --git a/src/components/ui/badge.test.tsx b/src/components/ui/badge.test.tsx index 8889dbef2..7304bfc36 100644 --- a/src/components/ui/badge.test.tsx +++ b/src/components/ui/badge.test.tsx @@ -31,7 +31,7 @@ describe("Badge", () => { it("renders with outline variant", () => { const { container } = render(Outline); const badge = container.querySelector("div"); - expect(badge).toHaveClass("text-foreground"); + expect(badge).toHaveClass("text-paper-foreground", "border-paper-border"); }); it("applies custom className", () => { diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index d3d5d6040..bcbdb4153 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -14,7 +14,7 @@ const badgeVariants = cva( "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", + outline: "text-paper-foreground border-paper-border", }, }, defaultVariants: { diff --git a/src/components/ui/card.test.tsx b/src/components/ui/card.test.tsx index 14b02e6f9..bfcb4dd80 100644 --- a/src/components/ui/card.test.tsx +++ b/src/components/ui/card.test.tsx @@ -118,7 +118,7 @@ describe("CardDescription", () => { it("has base classes", () => { const { container } = render(Test); const description = container.firstChild as HTMLElement; - expect(description).toHaveClass("text-sm", "text-muted-foreground"); + expect(description).toHaveClass("text-sm", "text-paper-muted-foreground"); }); it("applies custom className", () => { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index b42ae32b9..10c1c4815 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -50,7 +50,7 @@ const CardDescription = React.forwardRef< >(({ className, ...props }, ref) => (
)); diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index b2a80de5c..05fab5150 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -36,13 +36,13 @@ const DialogContent = React.forwardRef< {children} - + Close @@ -104,7 +104,7 @@ const DialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx index 1475c7297..49fe45704 100644 --- a/src/components/ui/input-otp.tsx +++ b/src/components/ui/input-otp.tsx @@ -39,7 +39,7 @@ const InputOTPSlot = React.forwardRef<
-
+
)}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 09700f605..0a9818c9d 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( (({ className, ...props }, ref) => ( )); @@ -99,7 +102,7 @@ const TableCaption = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 7de5c4a2a..7e253684d 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -9,7 +9,7 @@ const Textarea = React.forwardRef( return (