fix(theme): remove stray token-border frames + make accent family theme-track (#864)

Task 1 — physically remove the token-based structural border utilities that
kept rendering stray frames (history panels, cards, rows, settings) whenever a
`--*-border` token didn't resolve transparent (theme re-declare, or bare
`border` = currentColor under Tailwind v4). Converted every
`border[-trbl]-[var(--chrome-border…)]` / `[var(--color-border…)]` (83
occurrences across 32 components/pages) to `border-transparent` — keeps the 1px
box (no layout shift, matches the badge.tsx convention), drops the frame, and
active/selected state stays visible via the existing bg-tint/text cues. Also
converted button.tsx's `border-border`/`border-input` variants and Panel's
header divider. Kept: focus-visible rings, aria-invalid, dashed drop-zones, and
the waveform/segment editor. Strengthened tests/test_no_literal_borders.py with
`test_no_token_border_utilities_in_jsx` so a reintroduced token border fails CI
(allowlists the editor + shadcn form-control primitives).

Task 2 — aliased the accent family in the base :root to the themed brand token
(`--chrome-accent: var(--color-brand)`, `-bg`/`-border` via color-mix), so
donate/support/commercial CTAs, active tabs, .btn-primary, status pills and
GoalBar/Pip track the active theme instead of the fixed pink. Replaced the
hardcoded `#d3869b`/`#f3a5b6`/`rgba(243,165,182,…)` pinks and the DONATE_HUE
constant in SupportPage.jsx with `var(--color-brand)` tints.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-07-01 22:06:56 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent d0aa2fbd52
commit 8f7b242610
35 changed files with 146 additions and 95 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ export default function BatchAddDialog({
className={`flex cursor-pointer flex-col items-center justify-center gap-[6px] rounded-[10px] border-2 border-dashed px-4 py-7 text-[0.82rem] transition-all hover:border-[var(--chrome-accent)] hover:bg-white/[0.02] hover:text-[var(--chrome-fg)] ${
dragOver
? 'border-[var(--chrome-accent)] bg-white/[0.02] text-[var(--chrome-fg)]'
: 'border-[var(--chrome-border)] text-[var(--chrome-fg-muted)]'
: 'border-transparent text-[var(--chrome-fg-muted)]'
}`}
onDragOver={(e) => {
e.preventDefault();
+1 -1
View File
@@ -128,7 +128,7 @@ export default function CompareModal({
</span>
<button
type="button"
className="ml-auto inline-flex h-[var(--chrome-icon-btn,22px)] w-[var(--chrome-icon-btn,22px)] cursor-pointer items-center justify-center rounded-[var(--chrome-radius-pill)] bg-transparent text-[var(--chrome-fg-muted)] [border:1px_solid_transparent] transition-[background,color,border-color] duration-[var(--dur-fast)] hover:border-[var(--chrome-border-strong)] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
className="ml-auto inline-flex h-[var(--chrome-icon-btn,22px)] w-[var(--chrome-icon-btn,22px)] cursor-pointer items-center justify-center rounded-[var(--chrome-radius-pill)] bg-transparent text-[var(--chrome-fg-muted)] [border:1px_solid_transparent] transition-[background,color,border-color] duration-[var(--dur-fast)] hover:border-transparent hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
onClick={onClose}
aria-label={t('compare.close')}
>
+1 -1
View File
@@ -76,7 +76,7 @@ export default class ErrorBoundary extends React.Component {
<p className="m-0 mb-3 text-[0.82rem] leading-[1.5] text-[var(--chrome-fg-muted)]">
{i18next.t('errors.desc')}
</p>
<pre className="m-0 mb-3.5 max-h-[140px] overflow-auto rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] bg-[var(--chrome-hover-bg)] px-2.5 py-2 text-left font-mono text-[0.72rem] text-[var(--chrome-severity-err)]">
<pre className="m-0 mb-3.5 max-h-[140px] overflow-auto rounded-[var(--chrome-radius-pill)] border border-transparent bg-[var(--chrome-hover-bg)] px-2.5 py-2 text-left font-mono text-[0.72rem] text-[var(--chrome-severity-err)]">
{msg}
</pre>
<div className="flex flex-wrap justify-center gap-2">
+5 -5
View File
@@ -26,8 +26,8 @@ const trackCls = (on, kind) => {
if (on && kind === 'dub')
return `${TRACK_BASE} border-[var(--chrome-accent-border)] bg-[var(--chrome-accent-bg)] text-[var(--chrome-accent)]`;
if (on)
return `${TRACK_BASE} border-[var(--chrome-border-strong)] bg-[var(--chrome-hover-bg)] text-[var(--chrome-fg)]`;
return `${TRACK_BASE} border-[var(--chrome-border)] text-[var(--chrome-fg-muted)]`;
return `${TRACK_BASE} border-transparent bg-[var(--chrome-hover-bg)] text-[var(--chrome-fg)]`;
return `${TRACK_BASE} border-transparent text-[var(--chrome-fg-muted)]`;
};
const TAB_BASE =
'inline-flex items-center gap-[6px] px-[12px] py-[6px] bg-transparent border-0 border-b-2 cursor-pointer text-[length:var(--text-sm)] transition-[color,border-color] duration-[var(--dur-fast)]';
@@ -288,7 +288,7 @@ export default function ExportModal({
aria-label={t('exportModal.export_options')}
>
<div
className="pointer-events-auto flex w-[min(880px,calc(100vw-24px))] max-h-[min(70vh,560px)] flex-col overflow-hidden rounded-t-lg border border-b-0 border-[var(--chrome-border-strong)] bg-[var(--chrome-bg)] shadow-[0_-8px_24px_rgba(0,0,0,0.45),0_-1px_0_var(--chrome-border)_inset] animate-in fade-in slide-in-from-bottom-full duration-200"
className="pointer-events-auto flex w-[min(880px,calc(100vw-24px))] max-h-[min(70vh,560px)] flex-col overflow-hidden rounded-t-lg border border-b-0 border-transparent bg-[var(--chrome-bg)] shadow-[0_-8px_24px_rgba(0,0,0,0.45),0_-1px_0_var(--chrome-border)_inset] animate-in fade-in slide-in-from-bottom-full duration-200"
ref={drawerRef}
>
<header className="relative flex items-center gap-[var(--space-3)] p-[6px_var(--space-4)_10px] [border-bottom:1px_solid_var(--chrome-border)] [background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent)]">
@@ -306,7 +306,7 @@ export default function ExportModal({
</span>
<button
type="button"
className="ml-auto inline-flex h-[var(--chrome-icon-btn,22px)] w-[var(--chrome-icon-btn,22px)] cursor-pointer items-center justify-center rounded-[var(--chrome-radius-pill)] bg-transparent text-[var(--chrome-fg-muted)] [border:1px_solid_transparent] transition-[background,color,border-color] duration-[var(--dur-fast)] hover:border-[var(--chrome-border-strong)] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
className="ml-auto inline-flex h-[var(--chrome-icon-btn,22px)] w-[var(--chrome-icon-btn,22px)] cursor-pointer items-center justify-center rounded-[var(--chrome-radius-pill)] bg-transparent text-[var(--chrome-fg-muted)] [border:1px_solid_transparent] transition-[background,color,border-color] duration-[var(--dur-fast)] hover:border-transparent hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
onClick={onClose}
aria-label={t('exportModal.close_drawer')}
>
@@ -323,7 +323,7 @@ export default function ExportModal({
<button
key={k}
type="button"
className="inline-flex cursor-pointer items-center gap-[4px] rounded-[var(--chrome-radius-pill)] bg-transparent px-[8px] py-[3px] font-sans text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] [border:1px_solid_var(--chrome-border)] transition-[background,color,border-color] duration-[var(--dur-fast)] hover:border-[var(--chrome-border-strong)] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
className="inline-flex cursor-pointer items-center gap-[4px] rounded-[var(--chrome-radius-pill)] bg-transparent px-[8px] py-[3px] font-sans text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] [border:1px_solid_var(--chrome-border)] transition-[background,color,border-color] duration-[var(--dur-fast)] hover:border-transparent hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)]"
onClick={() => applyPreset(k)}
title={t('exportModal.preset_title', { tab: v.tab, label: t(v.labelKey) })}
>
+2 -2
View File
@@ -189,7 +189,7 @@ export default function GlossaryPanel({
</div>
) : (
<>
<table className="w-full border-collapse text-[length:var(--text-sm)] [&_td]:border-b [&_td]:border-b-transparent [&_td]:px-[6px] [&_td]:py-[3px] [&_td]:text-left [&_td]:align-middle [&_th]:border-b [&_th]:border-b-[var(--color-border)] [&_th]:px-[6px] [&_th]:py-[3px] [&_th]:text-left [&_th]:align-middle [&_th]:text-[length:var(--text-xs)] [&_th]:font-semibold [&_th]:uppercase [&_th]:tracking-[0.04em] [&_th]:text-[var(--color-fg-subtle)]">
<table className="w-full border-collapse text-[length:var(--text-sm)] [&_td]:border-b [&_td]:border-b-transparent [&_td]:px-[6px] [&_td]:py-[3px] [&_td]:text-left [&_td]:align-middle [&_th]:border-b [&_th]:border-b-transparent [&_th]:px-[6px] [&_th]:py-[3px] [&_th]:text-left [&_th]:align-middle [&_th]:text-[length:var(--text-xs)] [&_th]:font-semibold [&_th]:uppercase [&_th]:tracking-[0.04em] [&_th]:text-[var(--color-fg-subtle)]">
<thead>
<tr>
<th>{t('glossary.source')}</th>
@@ -228,7 +228,7 @@ export default function GlossaryPanel({
onDelete={() => onDelete(term.id)}
/>
))}
<tr className="border-t border-dashed border-[var(--color-border)] [&>td]:py-[4px]">
<tr className="border-t border-dashed border-transparent [&>td]:py-[4px]">
<td>
<Input
size="sm"
@@ -5,7 +5,7 @@ import { Dialog } from '../ui';
function Kbd({ children }) {
return (
<span className="inline-flex h-[22px] min-w-[28px] items-center justify-center gap-[2px] rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border-strong)] bg-[var(--chrome-hover-bg)] px-2 py-[2px] font-mono text-[0.7rem] font-medium text-[var(--chrome-fg)]">
<span className="inline-flex h-[22px] min-w-[28px] items-center justify-center gap-[2px] rounded-[var(--chrome-radius-pill)] border border-transparent bg-[var(--chrome-hover-bg)] px-2 py-[2px] font-mono text-[0.7rem] font-medium text-[var(--chrome-fg)]">
{children}
</span>
);
@@ -78,7 +78,7 @@ export default function KeyboardCheatsheet({ open, onClose }) {
<div className="grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-[18px]">
{SECTIONS.map((sec) => (
<div key={sec.title}>
<div className="mb-[10px] border-b border-[var(--chrome-border)] pb-[6px] font-mono text-[length:var(--chrome-label-size)] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
<div className="mb-[10px] border-b border-transparent pb-[6px] font-mono text-[length:var(--chrome-label-size)] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
{sec.title}
</div>
<div className="flex flex-col gap-[6px]">
+3 -3
View File
@@ -76,7 +76,7 @@ export default function MultiLangPicker({
{selected.map((s) => (
<span
key={s.code}
className="inline-flex items-center gap-[4px] px-[8px] py-[2px] bg-[var(--chrome-hover-bg)] border border-solid border-[var(--chrome-border)] rounded-full [font-family:var(--font-mono)] text-[0.68rem] font-medium text-[color:var(--chrome-fg)] uppercase"
className="inline-flex items-center gap-[4px] px-[8px] py-[2px] bg-[var(--chrome-hover-bg)] border border-solid border-transparent rounded-full [font-family:var(--font-mono)] text-[0.68rem] font-medium text-[color:var(--chrome-fg)] uppercase"
>
<Globe size={9} />
<span>{s.code}</span>
@@ -95,7 +95,7 @@ export default function MultiLangPicker({
{!disabled && (
<button
type="button"
className="flex items-center justify-center w-[24px] h-[24px] rounded-full border border-dashed border-[var(--chrome-border)] bg-transparent text-[color:var(--chrome-fg-muted)] cursor-pointer [transition:all_0.15s] hover:bg-[var(--chrome-hover-bg)] hover:text-[color:var(--chrome-fg)] hover:border-solid"
className="flex items-center justify-center w-[24px] h-[24px] rounded-full border border-dashed border-transparent bg-transparent text-[color:var(--chrome-fg-muted)] cursor-pointer [transition:all_0.15s] hover:bg-[var(--chrome-hover-bg)] hover:text-[color:var(--chrome-fg)] hover:border-solid"
onClick={() => setDropOpen(!dropOpen)}
title={t('dub.add_language')}
>
@@ -112,7 +112,7 @@ export default function MultiLangPicker({
{dropOpen && (
<div className="multi-lang__drop">
<div className="flex items-center gap-[6px] px-[10px] py-[8px] border-b border-solid border-b-[var(--chrome-border)] text-[color:var(--chrome-fg-muted)]">
<div className="flex items-center gap-[6px] px-[10px] py-[8px] border-b border-solid border-b-transparent text-[color:var(--chrome-fg-muted)]">
<Search size={10} />
<input
ref={inputRef}
+3 -3
View File
@@ -97,8 +97,8 @@ export default function VoicePreview({
if (!open) return null;
return (
<div className="fixed bottom-[calc(var(--logs-footer-height,28px)+16px)] right-[16px] z-[900] w-[320px] bg-[var(--chrome-bg)] border border-solid border-[var(--chrome-border-strong)] rounded-[12px] [box-shadow:0_8px_32px_rgba(0,0,0,0.4)] flex flex-col overflow-hidden animate-[voice-preview-in_0.2s_ease-out]">
<div className="flex items-center justify-between py-[10px] px-[14px] border-b border-solid border-b-[var(--chrome-border)]">
<div className="fixed bottom-[calc(var(--logs-footer-height,28px)+16px)] right-[16px] z-[900] w-[320px] bg-[var(--chrome-bg)] border border-solid border-transparent rounded-[12px] [box-shadow:0_8px_32px_rgba(0,0,0,0.4)] flex flex-col overflow-hidden animate-[voice-preview-in_0.2s_ease-out]">
<div className="flex items-center justify-between py-[10px] px-[14px] border-b border-solid border-b-transparent">
<span className="flex items-center gap-[6px] [font-family:var(--font-mono)] text-[0.72rem] font-semibold uppercase [letter-spacing:0.04em] text-[color:var(--chrome-fg)]">
<Volume2 size={13} /> {t('voicePreview.title')}
</span>
@@ -171,7 +171,7 @@ export default function VoicePreview({
)}
</div>
<div className="flex items-center justify-between pt-[8px] px-[14px] pb-[10px] border-t border-solid border-t-[var(--chrome-border)]">
<div className="flex items-center justify-between pt-[8px] px-[14px] pb-[10px] border-t border-solid border-t-transparent">
{loading ? (
<Button variant="ghost" size="sm" onClick={handleStop} leading={<Square size={10} />}>
{t('voicePreview.stop')}
+2 -2
View File
@@ -114,7 +114,7 @@ export default function WorkspaceVoices({
</div>
</div>
) : (
<div className="text-[0.7rem] [line-height:1.5] text-[color:var(--chrome-fg-muted)] py-[8px] px-[10px] border border-dashed border-[var(--chrome-border)] rounded-[10px]">
<div className="text-[0.7rem] [line-height:1.5] text-[color:var(--chrome-fg-muted)] py-[8px] px-[10px] border border-dashed border-transparent rounded-[10px]">
{t('voices.none_selected', {
defaultValue: 'No voice selected — describe one, drop audio, or pick below.',
})}
@@ -144,7 +144,7 @@ export default function WorkspaceVoices({
{/* Empty states carry verbs (10x §2). */}
<button
type="button"
className="block mt-[8px] mx-auto py-[4px] px-[10px] text-[0.66rem] text-[color:var(--chrome-fg-muted)] bg-transparent border border-dashed border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill,999px)] cursor-default"
className="block mt-[8px] mx-auto py-[4px] px-[10px] text-[0.66rem] text-[color:var(--chrome-fg-muted)] bg-transparent border border-dashed border-transparent rounded-[var(--chrome-radius-pill,999px)] cursor-default"
onClick={() => setDefineMethod(defineMethod === 'audio' ? 'audio' : 'design')}
>
{defineMethod === 'audio'
+1 -1
View File
@@ -216,7 +216,7 @@ export default function ActionBar({
</label>
<button
type="button"
className="inline-flex items-center gap-[4px] px-[10px] py-[4px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] cursor-pointer whitespace-nowrap flex-none transition-[color,border-color] duration-[var(--dur-fast)] hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)] focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
className="inline-flex items-center gap-[4px] px-[10px] py-[4px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-transparent rounded-[var(--chrome-radius-pill)] cursor-pointer whitespace-nowrap flex-none transition-[color,border-color] duration-[var(--dur-fast)] hover:text-[var(--chrome-fg)] hover:border-transparent focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
onClick={() => setShowOverrides(!showOverrides)}
aria-expanded={showOverrides}
>
@@ -21,12 +21,12 @@ const CHIP_FOCUS =
'focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]';
const PCHIP_BASE = `inline-flex items-center gap-[5px] px-[12px] py-[5px] font-[var(--font-sans)] text-[0.72rem] font-medium rounded-[var(--chrome-radius-pill)] border bg-transparent flex-none cursor-pointer transition-colors duration-[120ms] ${CHIP_FOCUS}`;
const PCHIP_INACTIVE =
'border-[var(--chrome-border)] text-[var(--chrome-fg-muted)] hover:bg-[var(--chrome-hover-bg)] hover:border-[var(--chrome-border-strong)] hover:text-[var(--chrome-fg)]';
'border-transparent text-[var(--chrome-fg-muted)] hover:bg-[var(--chrome-hover-bg)] hover:border-transparent hover:text-[var(--chrome-fg)]';
const PCHIP_ACTIVE =
'bg-[var(--chrome-accent-bg)] border-[var(--chrome-accent-border)] text-[var(--chrome-accent)]';
const CHIP_BASE = `font-[var(--font-sans)] font-medium text-[0.68rem] px-[10px] py-[3px] rounded-[var(--chrome-radius-pill)] border bg-transparent whitespace-nowrap cursor-pointer transition-colors duration-[120ms] ${CHIP_FOCUS}`;
const CHIP_INACTIVE =
'border-[var(--chrome-border)] text-[var(--chrome-fg-muted)] hover:text-[var(--chrome-fg)] hover:bg-[var(--chrome-hover-bg)] hover:border-[var(--chrome-border-strong)]';
'border-transparent text-[var(--chrome-fg-muted)] hover:text-[var(--chrome-fg)] hover:bg-[var(--chrome-hover-bg)] hover:border-transparent';
const CHIP_ACTIVE =
'bg-[var(--chrome-accent-bg)] border-[var(--chrome-accent-border)] text-[var(--chrome-accent)]';
@@ -130,7 +130,7 @@ export default function DesignMethodPanel({
(first run) starts expanded. */}
<button
type="button"
className="flex items-center gap-[8px] w-full mt-[4px] mb-[8px] px-[10px] py-[6px] bg-[var(--chrome-hover-bg)] border border-[var(--chrome-border)] rounded-[8px] cursor-pointer text-left transition-[border-color] duration-[var(--dur-fast)] hover:border-[var(--chrome-border-strong)] focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
className="flex items-center gap-[8px] w-full mt-[4px] mb-[8px] px-[10px] py-[6px] bg-[var(--chrome-hover-bg)] border border-transparent rounded-[8px] cursor-pointer text-left transition-[border-color] duration-[var(--dur-fast)] hover:border-transparent focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
onClick={() => setIdentityOpen((o) => !o)}
aria-expanded={identityOpen}
>
@@ -6,14 +6,14 @@ import { TAGS } from '../../utils/constants';
// Tailwind utilities (shadcn P4). Flat chrome pill, mono face — token utilities
// reference the same --chrome-* vars the old rule used, so the look is unchanged.
const TAG_BTN =
'border border-[var(--chrome-border)] bg-transparent text-[var(--chrome-fg-muted)] px-[9px] py-[3px] rounded-[var(--chrome-radius-pill)] font-[var(--chrome-font-mono)] font-medium text-[0.66rem] whitespace-nowrap cursor-pointer transition-colors duration-[120ms] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)]';
'border border-transparent bg-transparent text-[var(--chrome-fg-muted)] px-[9px] py-[3px] rounded-[var(--chrome-radius-pill)] font-[var(--chrome-font-mono)] font-medium text-[0.66rem] whitespace-nowrap cursor-pointer transition-colors duration-[120ms] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)] hover:border-transparent';
// Studio shell migrated from the `studio-*` classes + CloneDesignTab.css to
// utilities (fast shadcn). `.studio-panel` stays defined in index.css for the
// dub area; clone reproduces it inline so its bespoke restack (flat stack,
// overflow-visible insert popover) is self-contained.
const STUDIO_PANEL =
'flex flex-col min-h-0 bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-none py-[10px] px-[12px] max-[800px]:px-[10px] max-[600px]:px-[6px] max-[600px]:py-[8px]';
'flex flex-col min-h-0 bg-[var(--chrome-bg)] border border-transparent rounded-none py-[10px] px-[12px] max-[800px]:px-[10px] max-[600px]:px-[6px] max-[600px]:py-[8px]';
export default function ScriptPanel({
t,
@@ -85,8 +85,8 @@ export default function ScriptPanel({
type="button"
className={`absolute right-[8px] bottom-[30px] inline-flex items-center gap-[4px] px-2 py-1 text-[0.66rem] bg-[var(--chrome-bg)] border rounded-[var(--chrome-radius-pill)] cursor-pointer transition-[color,border-color] duration-[var(--dur-fast)] focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px] ${
insertOpen
? 'text-[var(--chrome-fg)] border-[var(--chrome-border-strong)]'
: 'text-[var(--chrome-fg-muted)] border-[var(--chrome-border)] hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)]'
? 'text-[var(--chrome-fg)] border-transparent'
: 'text-[var(--chrome-fg-muted)] border-transparent hover:text-[var(--chrome-fg)] hover:border-transparent'
}`}
onClick={() => setInsertOpen((o) => !o)}
aria-expanded={insertOpen}
@@ -100,7 +100,7 @@ export default function ScriptPanel({
)}
{insertOpen && (
<div
className="absolute right-[8px] bottom-[60px] z-20 flex flex-wrap gap-1 max-w-[min(360px,calc(100vw-16px))] max-h-[min(280px,calc(100vh-120px))] overflow-y-auto overscroll-contain p-2 bg-[var(--chrome-bg)] border border-[var(--chrome-border-strong)] rounded-[10px] shadow-[0_8px_24px_rgba(0,0,0,0.45)]"
className="absolute right-[8px] bottom-[60px] z-20 flex flex-wrap gap-1 max-w-[min(360px,calc(100vw-16px))] max-h-[min(280px,calc(100vh-120px))] overflow-y-auto overscroll-contain p-2 bg-[var(--chrome-bg)] border border-transparent rounded-[10px] shadow-[0_8px_24px_rgba(0,0,0,0.45)]"
role="menu"
>
{TAGS.map((tag) => (
+3 -4
View File
@@ -11,8 +11,7 @@ const ERROR_AUTOCLEAR_MS = 12000;
// Export-track toggle chips: flat pill outline, tinted by on/off/success state.
const TRACK_LABEL =
'inline-flex items-center gap-[4px] px-[8px] py-[2px] border border-transparent rounded-[var(--chrome-radius-pill)] cursor-pointer transition-colors';
const TRACK_ON =
'text-[var(--chrome-fg)] border-[var(--chrome-border-strong)] bg-[var(--chrome-hover-bg)]';
const TRACK_ON = 'text-[var(--chrome-fg)] border-transparent bg-[var(--chrome-hover-bg)]';
const TRACK_OFF = 'text-[var(--chrome-fg-dim)]';
const TRACK_ON_SUCCESS =
'text-[var(--chrome-severity-ok)] border-transparent bg-[color-mix(in_srgb,var(--chrome-severity-ok)_10%,transparent)]';
@@ -43,7 +42,7 @@ export default function DubFooter({
}, [canAutoClear, dubError, onDismissError]);
return (
<div className="px-[var(--space-3)] py-[4px] shrink-0 bg-[var(--chrome-bg)] border border-[var(--chrome-border)]">
<div className="px-[var(--space-3)] py-[4px] shrink-0 bg-[var(--chrome-bg)] border border-transparent">
{dubStep === 'done' && (
<div className="mb-[var(--space-2)]">
<Badge tone="success">
@@ -86,7 +85,7 @@ export default function DubFooter({
)}
{/* Output options + Timing moved to the top of the right (transcript) section. */}
{dubTracks.length > 0 && (
<div className="flex items-center gap-[var(--space-2)] mb-[2px] px-[var(--space-3)] py-[3px] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] font-[family-name:var(--font-sans)] bg-[var(--chrome-bg)] rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] flex-wrap">
<div className="flex items-center gap-[var(--space-2)] mb-[2px] px-[var(--space-3)] py-[3px] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] font-[family-name:var(--font-sans)] bg-[var(--chrome-bg)] rounded-[var(--chrome-radius-pill)] border border-transparent flex-wrap">
<span className="font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] tracking-[var(--chrome-label-track)] uppercase text-[var(--chrome-fg-muted)] font-semibold">
{t('dub.export_tracks')}
</span>
@@ -29,11 +29,11 @@ import toast from 'react-hot-toast';
// ── Translation-settings bar utility class clusters ──────────────────────
const SETTINGS_SUMMARY =
'flex items-center gap-[var(--space-2)] px-[var(--space-3)] py-[3px] mb-[3px] bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] font-[family-name:var(--font-sans)] text-[0.66rem] text-[var(--chrome-fg-muted)]';
'flex items-center gap-[var(--space-2)] px-[var(--space-3)] py-[3px] mb-[3px] bg-[var(--chrome-bg)] border border-transparent rounded-[var(--chrome-radius-pill)] font-[family-name:var(--font-sans)] text-[0.66rem] text-[var(--chrome-fg-muted)]';
const SUMMARY_TRIGGER =
'inline-flex items-center gap-[5px] flex-1 min-w-0 bg-transparent border-none text-fg-muted cursor-pointer py-[2px] px-0 [font:inherit] text-left';
const SETTINGS_BAR =
'flex flex-col gap-[3px] max-[900px]:gap-[6px] mb-[4px] px-[8px] py-[4px] bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)]';
'flex flex-col gap-[3px] max-[900px]:gap-[6px] mb-[4px] px-[8px] py-[4px] bg-[var(--chrome-bg)] border border-transparent rounded-[var(--chrome-radius-pill)]';
const FIELD = 'flex flex-col gap-[1px] min-w-0';
const FIELD_RESP = 'max-[960px]:basis-full max-[960px]:min-w-0';
const FIELD_LABEL =
@@ -245,7 +245,7 @@ export default function DubLeftColumn({
dropdown. It's also pre-selected on the segments so "new
language = same speaker's voice" works by default. */}
{dubSegments.some((s) => s.speaker_id) && (
<div className="mt-[2px] px-[var(--space-3)] py-[3px] bg-[var(--chrome-bg)] rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)]">
<div className="mt-[2px] px-[var(--space-3)] py-[3px] bg-[var(--chrome-bg)] rounded-[var(--chrome-radius-pill)] border border-transparent">
<div className="flex gap-[var(--space-2)] items-center flex-wrap">
<span
className="font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] text-[var(--chrome-fg-muted)] tracking-[var(--chrome-label-track)] uppercase font-semibold"
@@ -492,7 +492,7 @@ export default function DubLeftColumn({
<div
role="dialog"
aria-label={t('dub.install_popover_title')}
className="absolute z-20 top-[calc(100%+6px)] left-0 w-[290px] max-w-[80vw] p-[10px] flex flex-col gap-[8px] bg-[var(--chrome-bg,#282828)] border border-[var(--chrome-border-strong,#504945)] rounded-[8px] shadow-[0_8px_24px_rgba(0,0,0,0.45)] normal-case text-left"
className="absolute z-20 top-[calc(100%+6px)] left-0 w-[290px] max-w-[80vw] p-[10px] flex flex-col gap-[8px] bg-[var(--chrome-bg,#282828)] border border-transparent rounded-[8px] shadow-[0_8px_24px_rgba(0,0,0,0.45)] normal-case text-left"
>
<div className="text-[0.68rem] font-semibold text-[var(--chrome-fg,#ebdbb2)] normal-case tracking-normal">
{t('dub.install_popover_title')}
@@ -502,12 +502,12 @@ export default function DubLeftColumn({
</p>
{installCmd && (
<div className="flex items-stretch gap-[4px]">
<code className="flex-1 min-w-0 px-[6px] py-[4px] text-[0.6rem] leading-[1.4] font-[family-name:var(--chrome-font-mono,monospace)] text-[var(--chrome-fg,#ebdbb2)] bg-[rgba(0,0,0,0.35)] border border-[var(--chrome-border,#3c3836)] rounded-[5px] overflow-x-auto whitespace-nowrap">
<code className="flex-1 min-w-0 px-[6px] py-[4px] text-[0.6rem] leading-[1.4] font-[family-name:var(--chrome-font-mono,monospace)] text-[var(--chrome-fg,#ebdbb2)] bg-[rgba(0,0,0,0.35)] border border-transparent rounded-[5px] overflow-x-auto whitespace-nowrap">
{installCmd}
</code>
<button
type="button"
className="shrink-0 inline-flex items-center justify-center px-[6px] rounded-[5px] border border-[var(--chrome-border,#3c3836)] text-[var(--chrome-fg-muted,#a89984)] hover:text-[var(--chrome-fg,#ebdbb2)] hover:border-[var(--chrome-border-strong,#504945)] cursor-pointer bg-transparent"
className="shrink-0 inline-flex items-center justify-center px-[6px] rounded-[5px] border border-transparent text-[var(--chrome-fg-muted,#a89984)] hover:text-[var(--chrome-fg,#ebdbb2)] hover:border-transparent cursor-pointer bg-transparent"
onClick={copyInstallCmd}
title={t('dub.copy_command')}
aria-label={t('dub.copy_command')}
@@ -616,9 +616,7 @@ export default function DubLeftColumn({
onChange={(e) => setDubInstruct(e.target.value)}
/>
</div>
<div
className={`${FIELD} basis-full pt-[3px] border-t border-[var(--chrome-border)] mt-[1px]`}
>
<div className={`${FIELD} basis-full pt-[3px] border-t border-transparent mt-[1px]`}>
<label className="flex items-center gap-[6px] text-[0.65rem] text-[var(--chrome-fg-muted)] cursor-pointer mb-[2px]">
<input
type="checkbox"
@@ -173,7 +173,7 @@ export default function DubRightColumn({
{showTranscript ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
</div>
{showTranscript && (
<div className="bg-[var(--chrome-bg)] border border-[var(--chrome-border)] border-t-0 rounded-b-[var(--chrome-radius-pill)] p-[var(--space-3)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] leading-[1.5] max-h-[80px] overflow-y-auto">
<div className="bg-[var(--chrome-bg)] border border-transparent border-t-0 rounded-b-[var(--chrome-radius-pill)] p-[var(--space-3)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] leading-[1.5] max-h-[80px] overflow-y-auto">
{dubTranscript}
</div>
)}
@@ -185,7 +185,7 @@ export default function DubRightColumn({
{dubJobId && !glossaryVisible && (
<button
type="button"
className="inline-flex items-center px-[var(--space-3)] py-[3px] mb-[4px] font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] tracking-[var(--chrome-label-track)] uppercase text-[var(--chrome-fg-muted)] bg-transparent border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] cursor-pointer transition-colors hover:bg-[var(--chrome-hover-bg)] hover:border-[var(--chrome-border-strong)] hover:text-[var(--chrome-fg)]"
className="inline-flex items-center px-[var(--space-3)] py-[3px] mb-[4px] font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] tracking-[var(--chrome-label-track)] uppercase text-[var(--chrome-fg-muted)] bg-transparent border border-transparent rounded-[var(--chrome-radius-pill)] cursor-pointer transition-colors hover:bg-[var(--chrome-hover-bg)] hover:border-transparent hover:text-[var(--chrome-fg)]"
onClick={() => {
setGlossaryOpen(true);
setGlossaryHidden(false);
+2 -3
View File
@@ -14,9 +14,8 @@ const BASE =
'disabled:opacity-45 disabled:cursor-not-allowed';
const TONES = {
idle: 'text-[var(--chrome-fg-muted)] border-[var(--chrome-border)] hover:bg-[var(--chrome-hover-bg)]',
stopping:
'text-[var(--chrome-fg-muted)] border-[var(--chrome-border)] hover:bg-[var(--chrome-hover-bg)]',
idle: 'text-[var(--chrome-fg-muted)] border-transparent hover:bg-[var(--chrome-hover-bg)]',
stopping: 'text-[var(--chrome-fg-muted)] border-transparent hover:bg-[var(--chrome-hover-bg)]',
danger:
'text-[var(--chrome-severity-err)] border-transparent bg-[color-mix(in_srgb,var(--chrome-severity-err)_10%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-severity-err)_18%,transparent)]',
green:
+2 -2
View File
@@ -402,7 +402,7 @@ export default function IdleSkeleton({
</label>
<button
type="button"
className="inline-flex items-center gap-[5px] px-[10px] py-[5px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill,999px)] cursor-pointer transition-colors hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)]"
className="inline-flex items-center gap-[5px] px-[10px] py-[5px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-transparent rounded-[var(--chrome-radius-pill,999px)] cursor-pointer transition-colors hover:text-[var(--chrome-fg)] hover:border-transparent"
onClick={() => setLandingAdvOpen((o) => !o)}
aria-expanded={landingAdvOpen}
>
@@ -581,7 +581,7 @@ export default function IdleSkeleton({
<Play size={11} /> {t('dub.generate_dub')}
</Button>
<button
className="inline-flex items-center gap-[5px] bg-transparent border border-[var(--chrome-border)] text-[var(--chrome-fg-muted)] rounded-[8px] flex-[0_0_auto] px-[8px] py-[4px] text-[0.7rem] opacity-40"
className="inline-flex items-center gap-[5px] bg-transparent border border-transparent text-[var(--chrome-fg-muted)] rounded-[8px] flex-[0_0_auto] px-[8px] py-[4px] text-[0.7rem] opacity-40"
disabled
>
<Download size={11} /> {t('dub.export_btn', { defaultValue: 'Export' })}{' '}
@@ -117,7 +117,7 @@ export default function ArchetypesZone({
const facetGroup =
'flex items-center gap-[5px] flex-nowrap min-w-0 overflow-x-auto overflow-y-hidden [scrollbar-width:thin]';
const facetToggle =
'inline-flex items-center gap-[5px] h-[26px] box-border px-[9px] rounded-[7px] border border-[var(--chrome-border)] bg-[var(--chrome-hover-bg)] text-[var(--chrome-fg-muted)] text-[0.68rem] whitespace-nowrap cursor-pointer hover:text-[var(--chrome-fg)] hover:border-[color:var(--chrome-border-strong)]';
'inline-flex items-center gap-[5px] h-[26px] box-border px-[9px] rounded-[7px] border border-transparent bg-[var(--chrome-hover-bg)] text-[var(--chrome-fg-muted)] text-[0.68rem] whitespace-nowrap cursor-pointer hover:text-[var(--chrome-fg)] hover:border-[color:var(--chrome-border-strong)]';
const gridClass =
viewMode === 'grid'
? 'grid grid-cols-[repeat(auto-fill,minmax(248px,1fr))] gap-[10px]'
@@ -125,7 +125,7 @@ export default function ArchetypesZone({
return (
<div className="flex-1 min-h-0 flex flex-col overflow-y-auto">
<div className="flex flex-row items-center gap-[10px] flex-nowrap shrink-0 pt-[2px] pb-[10px] mb-[8px] border-b border-[var(--chrome-border)]">
<div className="flex flex-row items-center gap-[10px] flex-nowrap shrink-0 pt-[2px] pb-[10px] mb-[8px] border-b border-transparent">
{/* Three filter lanes (categories · facets · toggles), each its own
horizontally-scrollable portion; the view toggle is pinned right. */}
<div className={`${facetGroup} flex-[2.4_1_0]`}>
@@ -150,9 +150,7 @@ export default function ArchetypesZone({
))}
</div>
<div
className={`${facetGroup} flex-[1.6_1_0] pl-[10px] border-l border-[var(--chrome-border)]`}
>
<div className={`${facetGroup} flex-[1.6_1_0] pl-[10px] border-l border-transparent`}>
{['gender', 'age', 'pitch', 'accent', 'lang'].map((dim) => (
<Select
key={dim}
@@ -172,9 +170,7 @@ export default function ArchetypesZone({
))}
</div>
<div
className={`${facetGroup} flex-[1_1_0] pl-[10px] border-l border-[var(--chrome-border)]`}
>
<div className={`${facetGroup} flex-[1_1_0] pl-[10px] border-l border-transparent`}>
<label className={facetToggle}>
<input
type="checkbox"
@@ -214,7 +214,7 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
};
const voicePlay =
'flex items-center justify-center w-[28px] h-[28px] rounded-full border border-[var(--chrome-border)] bg-bg-elev-1 text-[var(--text-primary)] cursor-pointer flex-shrink-0 hover:bg-[var(--accent)] hover:border-[color:var(--accent)] hover:text-white';
'flex items-center justify-center w-[28px] h-[28px] rounded-full border border-transparent bg-bg-elev-1 text-[var(--text-primary)] cursor-pointer flex-shrink-0 hover:bg-[var(--accent)] hover:border-[color:var(--accent)] hover:text-white';
const actionBtn =
'flex items-center justify-center w-[24px] h-[24px] bg-transparent text-[var(--text-secondary)] rounded-[4px] cursor-pointer hover:bg-bg-elev-2 hover:text-[var(--text-primary)]';
@@ -305,7 +305,7 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
{results.map((r, i) => (
<div
key={i}
className="flex justify-between items-center px-[10px] py-[8px] gap-[8px] border-b border-[var(--chrome-border)] last:border-0"
className="flex justify-between items-center px-[10px] py-[8px] gap-[8px] border-b border-transparent last:border-0"
>
<div className="flex-1 min-w-0 flex flex-col gap-[2px]">
<span className="text-[0.75rem] truncate">{r.title}</span>
@@ -85,7 +85,7 @@ export default function ProfileActivity({
<button
type="button"
onClick={() => onOpenProject?.(p.project_id)}
className="flex w-full cursor-pointer items-center gap-[var(--space-3)] rounded-[var(--radius-md)] border border-border bg-[rgba(255,255,255,0.02)] px-[var(--space-4)] py-[var(--space-3)] text-fg [font-size:var(--text-md)] transition-[background,border-color] duration-[var(--dur-fast)] ease-[var(--ease-out)] hover:border-[var(--color-border-strong)] hover:bg-[rgba(255,255,255,0.06)]"
className="flex w-full cursor-pointer items-center gap-[var(--space-3)] rounded-[var(--radius-md)] border border-border bg-[rgba(255,255,255,0.02)] px-[var(--space-4)] py-[var(--space-3)] text-fg [font-size:var(--text-md)] transition-[background,border-color] duration-[var(--dur-fast)] ease-[var(--ease-out)] hover:border-transparent hover:bg-[rgba(255,255,255,0.06)]"
>
<FolderOpen size={11} />
<span className="flex-1 text-left">{p.project_name}</span>
@@ -29,7 +29,7 @@ export default function SettingsSearch({ value, onChange, onClear }) {
placeholder={t('settings.search_placeholder', { defaultValue: 'Search settings…' })}
aria-label={t('settings.search_placeholder', { defaultValue: 'Search settings…' })}
data-testid="settings-search"
className="w-full min-w-0 box-border rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] bg-[color-mix(in_srgb,var(--chrome-bg)_94%,white)] py-[var(--space-2)] pl-[calc(var(--space-3)*2+13px)] pr-[calc(var(--space-3)*2+13px)] text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[length:var(--text-sm)] focus:border-[var(--chrome-accent)] focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
className="w-full min-w-0 box-border rounded-[var(--chrome-radius-pill)] border border-transparent bg-[color-mix(in_srgb,var(--chrome-bg)_94%,white)] py-[var(--space-2)] pl-[calc(var(--space-3)*2+13px)] pr-[calc(var(--space-3)*2+13px)] text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[length:var(--text-sm)] focus:border-[var(--chrome-accent)] focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
/>
{value && (
<button
@@ -35,7 +35,7 @@ export default function SettingsSidebar({ visibleIds, active, onSelect }) {
onChange={(e) => onSelect(e.target.value)}
aria-label={t('settings.title', { defaultValue: 'Settings' })}
data-testid="settings-nav-select"
className="w-full min-w-0 box-border rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] bg-[color-mix(in_srgb,var(--chrome-bg)_94%,white)] px-[var(--space-4)] py-[var(--space-3)] text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[length:var(--text-sm)] focus:border-[var(--chrome-accent)] focus:outline-none"
className="w-full min-w-0 box-border rounded-[var(--chrome-radius-pill)] border border-transparent bg-[color-mix(in_srgb,var(--chrome-bg)_94%,white)] px-[var(--space-4)] py-[var(--space-3)] text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[length:var(--text-sm)] focus:border-[var(--chrome-accent)] focus:outline-none"
>
{GROUPS.map((g) => {
const items = g.items.filter((it) => isVisible(it.id));
@@ -22,7 +22,7 @@ export default function Collapsible({ title, icon: Icon, defaultOpen = false, ba
return (
<div
className={cn(
'border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] mt-[var(--space-4)] overflow-hidden',
'border border-transparent rounded-[var(--chrome-radius-pill)] mt-[var(--space-4)] overflow-hidden',
open && 'is-open',
)}
>
@@ -50,7 +50,7 @@ export default function Collapsible({ title, icon: Icon, defaultOpen = false, ba
)}
</button>
{open && (
<div className="px-[var(--space-5)] pt-[var(--space-2)] pb-[var(--space-4)] border-t border-[var(--chrome-border)] [&>[data-slot=setting-row]:last-child]:pb-0">
<div className="px-[var(--space-5)] pt-[var(--space-2)] pb-[var(--space-4)] border-t border-transparent [&>[data-slot=setting-row]:last-child]:pb-0">
{children}
</div>
)}
@@ -45,7 +45,7 @@ export default function SettingRow({
data-slot="setting-row"
data-mono={mono ? '' : undefined}
className={cn(
'grid gap-y-[1px] py-[var(--space-4)] min-h-0 border-b border-[var(--chrome-border)] last:border-b-0 [font-family:var(--font-sans)]',
'grid gap-y-[1px] py-[var(--space-4)] min-h-0 border-b border-transparent last:border-b-0 [font-family:var(--font-sans)]',
align === 'start' ? 'items-start' : 'items-center',
stack
? 'grid-cols-[1fr] gap-[var(--space-3)]'
@@ -36,7 +36,7 @@ export default function SettingsInput({
data-slot="settings-input"
type={type}
className={cn(
'w-full min-w-0 max-w-[min(360px,100%)] box-border rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] bg-[color-mix(in_srgb,var(--chrome-bg)_94%,white)] px-[var(--space-4)] py-[var(--space-3)] text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[length:var(--text-sm)] focus:outline-none focus:border-[var(--chrome-accent)] disabled:opacity-50 disabled:cursor-not-allowed',
'w-full min-w-0 max-w-[min(360px,100%)] box-border rounded-[var(--chrome-radius-pill)] border border-transparent bg-[color-mix(in_srgb,var(--chrome-bg)_94%,white)] px-[var(--space-4)] py-[var(--space-3)] text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[length:var(--text-sm)] focus:outline-none focus:border-[var(--chrome-accent)] disabled:opacity-50 disabled:cursor-not-allowed',
mono && '[font-family:var(--chrome-font-mono)]',
className,
)}
@@ -25,7 +25,7 @@ import React from 'react';
// to the primitive without re-deriving the token string. `data-slot` is the
// stable hook Settings.css / panel CSS reach into.
export const SETTINGS_SECTION_SURFACE =
'bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] px-[var(--space-6)] py-[var(--space-5)] mb-[var(--space-5)] last:mb-0';
'bg-[var(--chrome-bg)] border border-transparent rounded-[var(--chrome-radius-pill)] px-[var(--space-6)] py-[var(--space-5)] mb-[var(--space-5)] last:mb-0';
export default function SettingsSection({
icon: Icon,
@@ -41,7 +41,7 @@ export default function SettingsSection({
data-slot="settings-section"
className={`${SETTINGS_SECTION_SURFACE} ${className}`.trim()}
>
<header className="flex items-center gap-[var(--space-3)] mb-[var(--space-3)] pb-[var(--space-3)] border-b border-[var(--chrome-border)]">
<header className="flex items-center gap-[var(--space-3)] mb-[var(--space-3)] pb-[var(--space-3)] border-b border-transparent">
{Icon && (
<span
className="shrink-0 inline-flex items-center justify-center w-[20px] h-[20px] rounded-[var(--chrome-radius-pill)] text-[color:var(--chrome-fg-muted)] bg-[color-mix(in_srgb,currentColor_12%,var(--chrome-bg))] border border-transparent"
+5 -5
View File
@@ -32,7 +32,7 @@ const buttonVariants = cva(
destructive:
'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20',
outline:
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
'border border-transparent bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
@@ -44,19 +44,19 @@ const buttonVariants = cva(
primary:
'border border-transparent bg-primary text-primary-foreground font-semibold shadow-xs hover:bg-primary/90 active:scale-[0.98]',
subtle:
'border border-border bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
'border border-transparent bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
softGhost:
'border border-transparent bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground',
danger:
'text-destructive bg-destructive/10 border border-transparent hover:bg-destructive/20 hover:border-transparent',
chip: 'border border-border bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
chip: 'border border-transparent bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
chipActive: 'text-success bg-success/10 border border-transparent',
preset:
'justify-start text-left border border-border bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
'justify-start text-left border border-transparent bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
presetActive:
'justify-start text-left text-primary bg-primary/[0.12] border border-transparent',
iconBtn:
'border border-border bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
'border border-transparent bg-transparent text-muted-foreground hover:bg-[var(--chrome-hover-bg)] hover:text-foreground hover:border-transparent',
iconBtnActive: 'text-primary bg-primary/[0.12] border border-transparent',
},
size: {
+9 -3
View File
@@ -321,9 +321,15 @@
--chrome-fg: #d5c4a1;
--chrome-fg-muted: #a89984;
--chrome-fg-dim: #7c6f64;
--chrome-accent: #f3a5b6;
--chrome-accent-bg: rgba(243, 165, 182, 0.12);
--chrome-accent-border: rgba(243, 165, 182, 0.35);
/* Accent family aliases the THEMED brand token (--color-brand, re-declared per
[data-theme]) so every accent surface — donate/support/commercial CTAs,
active tabs, .btn-primary, status pills, GoalBar/Pip — tracks the active
theme instead of the old fixed pink. (--chrome-accent-border stays zeroed by
the app-wide border-removal block below; the visible accent cue is the fg
color + the -bg tint.) */
--chrome-accent: var(--color-brand);
--chrome-accent-bg: color-mix(in srgb, var(--color-brand) 12%, transparent);
--chrome-accent-border: color-mix(in srgb, var(--color-brand) 35%, transparent);
--chrome-severity-err: #fb4934;
--chrome-severity-warn: #fabd2f;
--chrome-severity-ok: #8ec07c;
+1 -1
View File
@@ -316,7 +316,7 @@ export default function CloneDesignTab(props) {
{/* ═══ VOICE — who says it ═══ */}
<div className="flex flex-col gap-[6px] flex-none min-h-0 relative z-[1]">
<div className="flex flex-col min-h-0 overflow-auto bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-none py-[10px] px-[12px] max-[800px]:px-[10px] max-[600px]:px-[6px] max-[600px]:py-[8px]">
<div className="flex flex-col min-h-0 overflow-auto bg-[var(--chrome-bg)] border border-transparent rounded-none py-[10px] px-[12px] max-[800px]:px-[10px] max-[600px]:px-[6px] max-[600px]:py-[8px]">
<div className="label-row justify-between">
<span className="label-row mb-0">
<Volume2 className="label-icon" size={14} />{' '}
+2 -2
View File
@@ -23,7 +23,7 @@ import ReadinessChecklist from '../components/ReadinessChecklist';
// (P4 shadcn/Tailwind pass). Defined once here so the four card instances stay
// in lockstep; Tailwind's scanner picks the literals up from this file.
const projCard =
'bg-[var(--chrome-bg)] border border-solid border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] py-[10px] px-[14px] [transition:background_0.15s,border-color_0.15s] flex items-center gap-[12px] hover:bg-[var(--chrome-hover-bg)] hover:border-[var(--chrome-border-strong)]';
'bg-[var(--chrome-bg)] border border-solid border-transparent rounded-[var(--chrome-radius-pill)] py-[10px] px-[14px] [transition:background_0.15s,border-color_0.15s] flex items-center gap-[12px] hover:bg-[var(--chrome-hover-bg)] hover:border-transparent';
const projIcon =
'w-[32px] h-[32px] rounded-[var(--chrome-radius-pill)] flex items-center justify-center shrink-0';
const projInfo = 'flex-1 min-w-0';
@@ -32,7 +32,7 @@ const projName =
const projMeta =
'[font-family:var(--chrome-font-mono)] text-[0.62rem] text-[color:var(--chrome-fg-dim)] mt-[2px] font-normal whitespace-nowrap overflow-hidden text-ellipsis';
const projAction =
'[font-family:var(--font-sans)] text-[0.7rem] font-medium py-[4px] px-[12px] rounded-[var(--chrome-radius-pill)] bg-transparent border border-solid border-[var(--chrome-border-strong)] text-[color:var(--chrome-fg-muted)] cursor-pointer [transition:background_var(--dur-fast),color_var(--dur-fast),border-color_var(--dur-fast)] shrink-0 whitespace-nowrap [letter-spacing:0.02em] hover:bg-[var(--chrome-hover-bg)] hover:text-[color:var(--chrome-fg)] hover:border-[var(--chrome-fg-muted)]';
'[font-family:var(--font-sans)] text-[0.7rem] font-medium py-[4px] px-[12px] rounded-[var(--chrome-radius-pill)] bg-transparent border border-solid border-transparent text-[color:var(--chrome-fg-muted)] cursor-pointer [transition:background_var(--dur-fast),color_var(--dur-fast),border-color_var(--dur-fast)] shrink-0 whitespace-nowrap [letter-spacing:0.02em] hover:bg-[var(--chrome-hover-bg)] hover:text-[color:var(--chrome-fg)] hover:border-[var(--chrome-fg-muted)]';
// Section divider label ("Cloned Voices" etc.) the trailing dotted rule lives
// on an ::after pseudo, expressed via the after: variant.
const sectionTitle =
+1 -1
View File
@@ -87,7 +87,7 @@ function Card({ kind, accent, title, subtitle, trailing, onClick, IconC, view })
return (
<button
type="button"
className={`flex cursor-pointer rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] [border-left:3px_solid_var(--card-accent,var(--chrome-accent))] bg-[var(--chrome-bg)] text-left [font-family:inherit] text-[var(--chrome-fg)] transition-[border-color,background,transform] duration-[0.12s] hover:border-[var(--chrome-border-strong)] hover:bg-[color-mix(in_srgb,var(--card-accent,var(--chrome-accent))_5%,var(--chrome-bg))] active:translate-y-[1px] ${
className={`flex cursor-pointer rounded-[var(--chrome-radius-pill)] border border-transparent [border-left:3px_solid_var(--card-accent,var(--chrome-accent))] bg-[var(--chrome-bg)] text-left [font-family:inherit] text-[var(--chrome-fg)] transition-[border-color,background,transform] duration-[0.12s] hover:border-transparent hover:bg-[color-mix(in_srgb,var(--card-accent,var(--chrome-accent))_5%,var(--chrome-bg))] active:translate-y-[1px] ${
list
? 'flex-row items-center gap-[14px] px-[12px] py-[6px]'
: 'flex-col gap-[6px] px-[12px] py-[10px]'
+7 -5
View File
@@ -41,7 +41,9 @@ const METHODS = [
},
];
const DONATE_HUE = '#d3869b';
// Donate/support accent tracks the themed brand token (per-[data-theme]) so the
// panel recolors with the app theme instead of the old fixed pink.
const DONATE_HUE = 'var(--color-brand)';
// PayPal.me carries the chosen amount straight into the checkout; Ko-fi opens
// its tip page (no reliable preset-amount URL). A non-numeric/"custom" amount
@@ -115,10 +117,10 @@ function SupportView() {
return (
<div className="flex flex-col gap-6">
<div className="text-center">
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-transparent bg-[color-mix(in_srgb,#d3869b_12%,transparent)]">
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-transparent bg-[color-mix(in_srgb,var(--color-brand)_12%,transparent)]">
<Heart
size={24}
className="text-[#f3a5b6] [fill:rgba(243,165,182,0.35)] drop-shadow-[0_0_12px_rgba(243,165,182,0.5)]"
className="text-[var(--color-brand)] [fill:color-mix(in_srgb,var(--color-brand)_35%,transparent)] drop-shadow-[0_0_12px_color-mix(in_srgb,var(--color-brand)_50%,transparent)]"
/>
</span>
<h2 className="relative inline-block font-serif text-[2rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
@@ -298,7 +300,7 @@ function LicenseView() {
key={label}
className="gap-0 rounded-md border-border bg-transparent p-4 shadow-none transition-colors hover:border-border-strong hover:bg-[var(--chrome-hover-bg)]"
>
<span className="mb-2.5 flex size-[30px] items-center justify-center rounded-md border border-transparent bg-[color-mix(in_srgb,#d3869b_10%,transparent)] text-[#d3869b]">
<span className="mb-2.5 flex size-[30px] items-center justify-center rounded-md border border-transparent bg-[color-mix(in_srgb,var(--color-brand)_10%,transparent)] text-[var(--color-brand)]">
<Icon size={16} />
</span>
<div className="mb-1 font-mono text-[0.75rem] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
@@ -360,7 +362,7 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
const TAB_INACTIVE =
'border-transparent text-[var(--chrome-fg-muted)] hover:text-[var(--chrome-fg)]';
const TAB_SUPPORT_ACTIVE =
'border-transparent bg-[color-mix(in_srgb,#d3869b_18%,transparent)] text-[var(--chrome-fg)]';
'border-transparent bg-[color-mix(in_srgb,var(--color-brand)_18%,transparent)] text-[var(--chrome-fg)]';
const TAB_LICENSE_ACTIVE =
'border-transparent bg-[color-mix(in_srgb,#83a598_18%,transparent)] text-[var(--chrome-fg)]';
+4 -4
View File
@@ -15,11 +15,11 @@ import { Card } from '@/components/ui/card';
const VARIANT = {
// glass: surface (gradients + backdrop-filter) + ::before stay in residual.css.
glass: 'border border-[var(--color-border-warm)]',
glass: 'border border-transparent',
solid:
'border border-[var(--color-border-warm)] ' +
'border border-transparent ' +
'[background-image:linear-gradient(160deg,#2a2624_0%,#201c1b_100%)] [box-shadow:var(--shadow-md)]',
flat: 'border border-[var(--color-border)] [background-color:rgba(0,0,0,0.08)]',
flat: 'border border-transparent [background-color:rgba(0,0,0,0.08)]',
};
const PAD = {
@@ -72,7 +72,7 @@ const Panel = forwardRef(function Panel(
<Card asChild className={classes}>
<Tag ref={ref} {...rest}>
{hasHeader && (
<header className="ui-panel__header flex items-center justify-between py-[var(--space-4)] px-[var(--space-5)] gap-[var(--space-4)] [border-bottom:1px_solid_var(--color-border)]">
<header className="ui-panel__header flex items-center justify-between py-[var(--space-4)] px-[var(--space-5)] gap-[var(--space-4)] [border-bottom:1px_solid_transparent]">
{title != null && (
<div className="ui-panel__title flex items-center gap-[var(--space-3)] min-w-0 [font-size:var(--text-md)] font-bold text-fg tracking-[-0.01em]">
{title}
+2 -2
View File
@@ -39,8 +39,8 @@ export default function Tabs({
// The leading utilities also reset shadcn's list/trigger box-model defaults
// (h-9, bg-muted, rounded-lg, flex-1, active shadow) back to OmniVoice's.
const listClass = isPill
? 'h-auto inline-flex shrink-0 gap-[3px] rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] bg-[var(--chrome-bg)] p-[3px]'
: 'h-auto inline-flex shrink-0 gap-[var(--space-5)] rounded-none border-0 border-b border-[var(--chrome-border)] bg-transparent p-0';
? 'h-auto inline-flex shrink-0 gap-[3px] rounded-[var(--chrome-radius-pill)] border border-transparent bg-[var(--chrome-bg)] p-[3px]'
: 'h-auto inline-flex shrink-0 gap-[var(--space-5)] rounded-none border-0 border-b border-transparent bg-transparent p-0';
return (
<ShadcnTabs
+55 -4
View File
@@ -15,13 +15,21 @@ reach:
arbitrary utilities, in ``*.jsx`` / ``*.tsx``.
3. Inline ``style={{ borderColor: }}`` (non-transparent) in ``*.jsx`` /
``*.tsx``.
4. Token-based STRUCTURAL border utilities in ``*.jsx`` / ``*.tsx``
``border[-trbl]-[var(--chrome-border)]`` and ``-[var(--color-border)]``
(incl. ``-strong`` / ``-warm``). These render a hairline the moment the
``--*-border`` token doesn't resolve transparent (a theme re-declare, or a
bare ``border`` with no color under Tailwind v4's currentColor default), so
the panel/aside/card/row frames were physically removed and converted to
``border-transparent``. This catches their reappearance.
It deliberately does NOT flag: ``:focus-visible`` / ring rules, the
``--color-ring`` / ``--focus-ring`` focus tokens, ``border-transparent`` /
``border-0``, ``var(--)``-driven borders (the ``--*-border`` tokens are zeroed
centrally; accent/severity token borders are intentional semantic state cues),
or colored non-neutral literal CSS borders kept as functional editor / severity
affordances (e.g. the waveform segment editor).
``border-0``; the intentional ``--chrome-accent-border`` accent state cue and
severity token borders; the ``border-border`` / ``border-input`` design-system
aliases (also zeroed) and the arbitrary ``[border:var(---border)]`` property
form; the dashed drop-zone affordance; or borders kept in the functional
waveform / segment editor and shadcn form-control primitives (allowlisted).
"""
import re
from pathlib import Path
@@ -53,6 +61,30 @@ _JSX_BORDER_UTIL = re.compile(
# ── 3) Inline borderColor (non-transparent) ──────────────────────────────────
_JSX_BORDERCOLOR = re.compile(r"borderColor\s*:")
# ── 4) Token-based structural border utilities in JSX/TSX ────────────────────
# `border[-trbl]-[var(--chrome-border…)]` / `…-[var(--color-border…)]` (any
# direction, incl. `-strong` / `-warm`, and any variant prefix). NOT matched:
# `--chrome-accent-border` (intentional accent state cue) — the char after
# `--chrome-`/`--color-` must be `border`.
_JSX_TOKEN_BORDER = re.compile(
r"border(?:-[trblxy])?-\[var\(--(?:chrome|color)-border",
)
# Functional-affordance files whose remaining borders are intentional and must
# NOT be stripped: the waveform / segment editor (selection ring, drag handles,
# segment boundaries) and the shadcn form-control primitives (the border IS the
# control's own outline — removing it makes the field invisible).
_BORDER_ALLOW = {
"components/AudioTrimmer.jsx",
"components/WaveformPlayer.jsx",
"components/SegmentTrack.jsx",
"components/ui/input.tsx",
"components/ui/textarea.tsx",
"components/ui/select.tsx",
"components/ui/slider.tsx",
"components/ui/table.tsx",
}
def _iter_frontend_files(suffixes):
for p in _SRC.rglob("*"):
@@ -101,6 +133,25 @@ def test_no_inline_border_color_in_jsx():
)
def test_no_token_border_utilities_in_jsx():
offenders = []
for p in _iter_frontend_files({".jsx", ".tsx"}):
if p.relative_to(_SRC).as_posix() in _BORDER_ALLOW:
continue
for i, line in enumerate(p.read_text().splitlines(), 1):
if _JSX_TOKEN_BORDER.search(line):
offenders.append(f"{p.relative_to(_REPO)}:{i}: {line.strip()[:120]}")
assert not offenders, (
"Token-based structural border utilities reappeared. A "
"`border-[var(--chrome-border…)]` / `border-[var(--color-border…)]` "
"renders a stray frame the moment the token isn't transparent. Drop the "
"border or use `border-transparent` (keep the width to suppress the "
"no-Preflight UA border); convey active/selected state with a background "
"tint (`--chrome-accent-bg` / `--chrome-hover-bg` / `--color-bg-elev-*`)."
"\n" + "\n".join(offenders)
)
def test_focus_indicators_are_preserved():
"""Regression guard: the border removal must not strip focus a11y."""
css = _INDEX_CSS.read_text()