- PlexBrowse PLAYABLE_TINT: transcode was collapsed onto remux's bg-warn; give it
bg-danger so direct/remux/transcode read as a distinct good->heavy green/amber/red
gradient again.
- Live indicators (VideoCard row + thumbnail badge, playlistColumns badge): revert to
fixed live-red + white instead of the --danger token. 'live' is a broadcast marker,
not an error state, and the badge sits over a thumbnail (no app-bg contrast need);
restores the conventional red/white LIVE look and decouples it from error retunes.
- ChannelPage exploring badge: drop the redundant dark:text-warn (no-op after the
token rename — --warn is already theme-scoped).
error/fatal toast shade merge left as-is (both are 'bad'; the 400/500 step was subtle).
Add theme-scoped --danger/--warn/--success (+ -fg) tokens wired into the v4
@theme inline, replacing ~110 raw red/amber/emerald/green palette refs across
34 files. Fixes light-theme contrast failures (fixed Tailwind shades fell to
~1.7-3:1 on white). Status colors are scheme-independent — recognised by hue,
shifting only by theme to hold contrast (bright on dark bg, darkened on light).
Solid fills use the paired -fg text; @source inline guarantees the full -fg set
exists. IMDb rating gold kept as a deliberate decorative non-status exception.
Root-fixes the opacity-modifier seed bug behind R10 S0: the theme colors were
bare `var(--x)` hex with no <alpha-value>, so `bg-accent/40`, `ring-accent/40`,
`border-border/60`, `text-muted/70`, … (~99 sites across 45 files) emitted no
CSS rule at all — latent no-ops. v4 implements the opacity modifier via
`color-mix`, so the modifier now works with the existing hex vars, no per-scheme
token duplication needed.
- `npx @tailwindcss/upgrade`: `@tailwind` directives → `@import "tailwindcss"`,
JS config → CSS `@theme` (colors, --font-sans, the z-index tokens preserved),
classname renames (rounded/shadow/blur scale, outline-none→outline-hidden,
ring→ring-2) across 56 files; PostCSS plugin → @tailwindcss/postcss,
autoprefixer removed (v4 prefixes for you).
- `@theme` → `@theme inline` (load-bearing): the color vars are redefined at
runtime under html[data-scheme]/[data-theme]; inline resolves them at the use
site so per-scheme opacity tints track the active scheme (the shadcn v4 pattern).
- Revert one codemod false-positive: it rewrote the English word "rounded" →
"rounded-sm" inside a releaseNotes.ts prose sentence.
Gate green: tsc(app+node) / eslint 0-err / prettier / vitest 79 / knip.
Verified in-browser: bg-accent/15 now computes a scheme-tracking color at 0.15
alpha across all 6 schemes × 2 themes; the hand-rolled glass border/shadow/
radius/backdrop-filter render unchanged under v4.
- The dragged row trailed the cursor by 150ms. The row carried Tailwind's bare
`transition` utility (copied from DataTable's <tr>), which animates TRANSFORM
— exactly the property dnd-kit drives from the pointer. transition-colors now,
and the row being dragged drops dnd-kit's own transition too.
- The list looked sorted by Channel when it was really in its stored order that
happens to be channel-grouped, and nothing said so. Sorting by "#" ascending
IS the stored order, so it is now the neutral state: the header shows a real
arrow for it, cycling off any column lands back on it, and it stays the state
where drag is enabled and no save is offered.
- A choice dialog with two or more actions stacks vertically. Side by side they
wrapped raggedly, and the wrapped one read as a lesser second group when the
two are peers. Cancel goes last, actions run least to most destructive.
A truncate cell is white-space:nowrap, so its min-content width is the whole
title — under table-layout:auto the Title column grew until Length and the
remove button hung off the scroller (measured: table 1059px inside an 855px
wrapper, main.scrollWidth 1074 vs clientWidth 887). Fixed layout with a declared
width on every column but Title, which absorbs the rest.
The column sort and the filters are a VIEW; the playlist's stored order only
changes when the user presses "Save this order", which still goes through the
undoable path. Picking a sort used to write straight to the server, so merely
looking at the list by title rewrote the playlist and marked a YouTube-linked
mirror dirty.
The table:
- playlistColumns.tsx holds the column defs; the body renders them as table rows
and fills the grip cell itself, since the handle needs the row's sortable
listeners that a render closure can't reach.
- The # column shows the STORED position, so a view-sort never renumbers the
playlist under the reader.
- Saving is withheld while a filter is active (it would drop the hidden items),
and drag is disabled under any view, with the reason spelled out rather than
silently doing nothing.
- applyView/sameOrder live in lib/playlistView.ts with tests: the sort and the
channel grouping compose, and getting that wrong produces a plausible order
that the save button would then persist.
- dnd-kit KeyboardSensor: the grip was focusable and called itself "reorder",
but only a pointer could move it.
The guards:
- Delete is ONE dialog with every outcome. It was two chained confirms where the
second one's Cancel/Escape/backdrop meant "delete here only" — dismissing the
dialog deleted the playlist. ConfirmProvider gained useChoice() for it.
- ConfirmProvider focuses Cancel when an offered action is destructive; the
confirm button took autoFocus unconditionally, so a stray Enter on an open
dialog ran "Delete user" or "Clear audit log".
- Removing an item asks first, and rebases the undo history instead of throwing
it away: useUndoable.rebase maps every snapshot, so the reorder history stays
valid for the remaining items.
- A failed detail query showed "Loading…" forever; it now says so and offers a
retry. Rename and reorder failures are surfaced instead of swallowed (reorder
was a bare .then(), which also produced an unhandled rejection).
- Local reorder/remove no longer require YouTube write scope.
- playlistName() replaces the watch-later localizer copied into three files.