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:
co-authored by
mergetest
Claude Opus 4.8
parent
d0aa2fbd52
commit
8f7b242610
@@ -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();
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) })}
|
||||
>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />{' '}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -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)]';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user