Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
82fd997
feat(admin): migrate admin surfaces onto edition-view role tokens
claude Aug 23, 2026
d937bed
fix(admin): keep border-white/20 literal instead of misusing surface-…
claude Aug 23, 2026
96c0212
fix(admin): address PR review — move role tokens to :root, pair accen…
claude Aug 23, 2026
e1a95cb
chore: drop stray CSS comment block and unused EditionViewRoot in admin
claude Aug 23, 2026
7093b35
fix: restore legible text/borders on light-background surfaces after …
claude Aug 23, 2026
a630a2a
fix: restore legible placeholder text color in ImageCell
claude Aug 23, 2026
1ed193b
refactor: tokenize light-surface text/border colors instead of litera…
claude Aug 23, 2026
0dbefd8
fix: restore legible text on OTP slots and shared Input primitive
claude Aug 23, 2026
297985e
docs: mark surface-* tokens as a pre-theming stopgap
claude Aug 23, 2026
84bc215
refactor: rename surface-* light-chrome tokens to paper-*
claude Aug 23, 2026
7d1421b
docs(design): trim :root note, fix stale scope language; fix(toast): …
claude Aug 23, 2026
15372cf
fix(theme): apply paper roles to dialog/sonner, fix destructive contrast
claude Aug 23, 2026
9d917fb
Remove redundant paper-token overrides now handled by primitives
claude Aug 23, 2026
6e61fb4
Fix illegible outline badges and muted text on light admin surfaces
claude Aug 23, 2026
95438de
Use neutral paper roles for input focus ring and card/textarea muted …
claude Aug 23, 2026
86347f4
Use paper-background for the default toast fill
claude Aug 23, 2026
3c9b14d
Update stale tests for paper-token class names
claude Aug 23, 2026
a5025ca
Fix white-on-white contrast in toast close button, Alert, and AlertDi…
claude Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/design/edition-color-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
Expand All @@ -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` |

Expand Down
6 changes: 3 additions & 3 deletions docs/design/edition-identity-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-paper-background text-paper-foreground p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
Expand Down Expand Up @@ -93,7 +93,7 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm text-paper-muted-foreground", className)}
{...props}
/>
));
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/badge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("Badge", () => {
it("renders with outline variant", () => {
const { container } = render(<Badge variant="outline">Outline</Badge>);
const badge = container.querySelector("div");
expect(badge).toHaveClass("text-foreground");
expect(badge).toHaveClass("text-paper-foreground", "border-paper-border");
});

it("applies custom className", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
chiptus marked this conversation as resolved.
Comment thread
chiptus marked this conversation as resolved.
},
},
defaultVariants: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("CardDescription", () => {
it("has base classes", () => {
const { container } = render(<CardDescription>Test</CardDescription>);
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", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm text-paper-muted-foreground", className)}
{...props}
/>
));
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-paper-border bg-paper-background p-6 text-paper-foreground shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-paper-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
Expand Down Expand Up @@ -104,7 +104,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm text-paper-muted-foreground", className)}
{...props}
/>
));
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/input-otp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const InputOTPSlot = React.forwardRef<
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm text-paper-foreground transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
Expand All @@ -48,7 +48,7 @@ const InputOTPSlot = React.forwardRef<
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
<div className="h-4 w-px animate-caret-blink bg-paper-foreground duration-1000" />
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base text-paper-foreground ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-paper-foreground placeholder:text-paper-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-paper-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ function Toaster({ ...props }: ToasterProps) {
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
"group toast group-[.toaster]:bg-paper-background group-[.toaster]:text-paper-foreground group-[.toaster]:border-paper-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-paper-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
Expand Down
9 changes: 6 additions & 3 deletions src/components/ui/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
"h-12 px-4 text-left align-middle font-medium text-paper-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
Expand All @@ -87,7 +87,10 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn(
"p-4 align-middle text-paper-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
Expand All @@ -99,7 +102,7 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
className={cn("mt-4 text-sm text-paper-muted-foreground", className)}
{...props}
/>
));
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-paper-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
Expand Down
Loading
Loading