feat(ui): convert standalone widget CSS to Tailwind utilities (#781)

Move the clearly-mechanical CSS (flex/grid, spacing, sizing, typography,
simple colors/borders/radii, and simple hover/disabled states) for seven
standalone widgets onto their JSX as Tailwind v4 utilities. Animation
(@keyframes), glass/backdrop-filter, pseudo-element/compound/sibling
selectors, !important, media queries, and any class referenced from
another file are left in CSS verbatim. Exact pixels/colors are preserved
via @theme token utilities plus arbitrary var()/px values; transitions use
arbitrary-property syntax so the timing function stays identical (Tailwind's
transition utilities inject a different default ease). No preflight is loaded,
so every converted border pairs an explicit border-solid/border-dashed + color.

- NetworkToggle: fully converted; NetworkToggle.css deleted and its import
  removed (all classes were local).
- FloatingPill: converted the static content/label/meta/timer/error/progress
  track + dismiss button; kept the pill base (animation+glass+fixed pos), dot,
  progress-fill (base + indeterminate !important/animation), keyframes, the
  prefers-reduced-motion block, and the --done/--error descendant overrides.
- AudioTrimmer: converted all audio-trimmer__* parts + trim-field*; kept the
  .audio-trimmer base rule (also targeted by unlayered overrides in index.css).
- ReadinessChecklist: converted title/list/item/status-layout/label/detail/
  fix/all-pass; kept the glass base, the rc-spin keyframe, and the dynamic
  status--pass/warn/fail/loading color+animation modifiers.
- MultiLangPicker: converted chips/add/summary/search/list/section/option;
  kept the .multi-lang__drop dropdown (animation + shadow) and mlp-in keyframe.
- WorkspaceVoices: converted only the local wv__active*/wv__empty-cta active-
  voice card; kept wv/wv__head/wv__title/wv__search*/wv__scroll/wv__empty/
  wv--collapsed/wv__rename-input (shared with WorkspaceProjects.jsx).
- WorkspaceHistory: converted the local wh/wh__* panel chrome (active chip
  state expressed as a mutually-exclusive ternary since utilities are equal
  specificity); kept the studio-with-history/studio-right/shell-narrow/
  shell-mini layout rules (referenced by App.jsx, index.css, and tests).

Verified: oxlint exit 0, oxfmt --check clean, vite build OK, 641 vitest pass,
bun install --frozen-lockfile clean.

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-06-30 17:35:53 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 5b6f49ee0c
commit 349a40a261
14 changed files with 193 additions and 819 deletions
-111
View File
@@ -6,114 +6,3 @@
gap: var(--space-5);
outline: none;
}
.audio-trimmer__meta {
display: flex;
gap: var(--space-6);
align-items: center;
font-size: var(--text-base);
color: var(--color-fg-muted);
}
.audio-trimmer__hint {
margin-left: auto;
color: var(--color-fg-subtle);
font-size: var(--text-2xs);
}
.audio-trimmer__error {
color: var(--color-danger);
font-size: var(--text-md);
}
.audio-trimmer__toolbar {
display: flex;
gap: var(--space-2);
align-items: center;
}
.audio-trimmer__view-info {
margin-left: auto;
font-size: var(--text-sm);
color: var(--color-fg-subtle);
font-variant-numeric: tabular-nums;
}
.audio-trimmer__ruler {
width: 100%;
height: 18px;
background: #141414;
border-radius: var(--radius-md);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.audio-trimmer__wave {
width: 100%;
height: 160px;
background: #0f1112;
border-radius: var(--radius-lg);
border: 1px solid rgba(255, 255, 255, 0.05);
cursor: crosshair;
touch-action: none;
}
.audio-trimmer__fields {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: var(--space-4);
align-items: center;
}
.trim-field {
display: flex;
align-items: center;
gap: var(--space-2);
background: #0f1112;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 2px 6px;
}
.trim-field--readonly { cursor: default; }
.trim-field__label {
font-size: var(--text-xs);
color: var(--color-fg-subtle);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: var(--weight-semibold);
}
.trim-field__input {
background: transparent;
border: none;
outline: none;
color: var(--color-fg);
font-size: var(--text-md);
padding: 4px 6px;
width: 100%;
min-width: 60px;
font-family: var(--font-mono);
}
.trim-field__value {
padding: 4px 6px;
color: #b8bb26;
font-family: var(--font-mono);
font-size: var(--text-md);
flex: 1;
}
.trim-field__value.is-err { color: var(--color-danger); }
.trim-field__unit { font-size: var(--text-xs); color: var(--color-fg-subtle); }
.audio-trimmer__actions {
display: flex;
gap: var(--space-4);
align-items: center;
flex-wrap: wrap;
}
.audio-trimmer__kbd-hint {
font-size: var(--text-base);
color: var(--color-fg-subtle);
}
.audio-trimmer__actions-right {
margin-left: auto;
display: flex;
gap: var(--space-4);
}
.audio-trimmer__audio { display: none; }
+47 -29
View File
@@ -703,7 +703,7 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
}
>
<div ref={containerRef} tabIndex={-1} className="audio-trimmer">
<div className="audio-trimmer__meta">
<div className="flex gap-[var(--space-6)] items-center [font-size:var(--text-base)] text-fg-muted">
<span>
{decoding
? t('trimmer.decoding')
@@ -711,13 +711,15 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
? `${t('trimmer.meta_length', { duration: fmtHMS(audioMeta.duration), sampleRate: audioMeta.sampleRate })}${peakProgress > 0 && peakProgress < 1 ? ` · ${t('trimmer.meta_rendering', { percent: Math.round(peakProgress * 100) })}` : ''}`
: '…'}
</span>
<span className="audio-trimmer__hint">{t('trimmer.keyboard_hint')}</span>
<span className="ml-auto text-fg-subtle [font-size:var(--text-2xs)]">
{t('trimmer.keyboard_hint')}
</span>
</div>
{error && <div className="audio-trimmer__error">{error}</div>}
{error && <div className="text-danger [font-size:var(--text-md)]">{error}</div>}
{/* Zoom controls */}
<div className="audio-trimmer__toolbar">
<div className="flex gap-[var(--space-2)] items-center">
<Button
variant="subtle"
iconSize="md"
@@ -754,7 +756,7 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
>
{t('trimmer.fit_sel_btn')}
</Button>
<div className="audio-trimmer__view-info">
<div className="ml-auto [font-size:var(--text-sm)] text-fg-subtle tabular-nums">
{t('trimmer.view_range', {
start: fmtHMS(viewStart),
end: fmtHMS(viewEnd),
@@ -764,15 +766,24 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
</div>
{/* Ruler */}
<canvas ref={rulerRef} className="audio-trimmer__ruler" />
<canvas
ref={rulerRef}
className="w-full h-[18px] bg-[#141414] rounded-md border-b border-solid border-b-[rgba(255,255,255,0.05)]"
/>
{/* Waveform */}
<canvas ref={waveRef} onMouseDown={onCanvasDown} className="audio-trimmer__wave" />
<canvas
ref={waveRef}
onMouseDown={onCanvasDown}
className="w-full h-[160px] bg-[#0f1112] rounded-lg border border-solid border-[rgba(255,255,255,0.05)] cursor-crosshair touch-none"
/>
{/* Numeric fields */}
<div className="audio-trimmer__fields">
<label className="trim-field">
<span className="trim-field__label">{t('trimmer.start_label')}</span>
<div className="grid grid-cols-[1fr_1fr_1fr_auto] gap-[var(--space-4)] items-center">
<label className="flex items-center gap-[var(--space-2)] bg-[#0f1112] border border-solid border-border rounded-md px-[6px] py-[2px]">
<span className="[font-size:var(--text-xs)] text-fg-subtle uppercase [letter-spacing:0.05em] font-semibold">
{t('trimmer.start_label')}
</span>
<input
type="text"
inputMode="decimal"
@@ -785,12 +796,16 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
commitStartInput();
}
}}
className="trim-field__input"
className="bg-transparent border-0 outline-none text-fg [font-size:var(--text-md)] px-[6px] py-[4px] w-full min-w-[60px] font-mono"
/>
<span className="trim-field__unit">{t('trimmer.unit_seconds')}</span>
<span className="[font-size:var(--text-xs)] text-fg-subtle">
{t('trimmer.unit_seconds')}
</span>
</label>
<label className="trim-field">
<span className="trim-field__label">{t('trimmer.end_label')}</span>
<label className="flex items-center gap-[var(--space-2)] bg-[#0f1112] border border-solid border-border rounded-md px-[6px] py-[2px]">
<span className="[font-size:var(--text-xs)] text-fg-subtle uppercase [letter-spacing:0.05em] font-semibold">
{t('trimmer.end_label')}
</span>
<input
type="text"
inputMode="decimal"
@@ -803,17 +818,23 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
commitEndInput();
}
}}
className="trim-field__input"
className="bg-transparent border-0 outline-none text-fg [font-size:var(--text-md)] px-[6px] py-[4px] w-full min-w-[60px] font-mono"
/>
<span className="trim-field__unit">{t('trimmer.unit_seconds')}</span>
<span className="[font-size:var(--text-xs)] text-fg-subtle">
{t('trimmer.unit_seconds')}
</span>
</label>
<div className="trim-field trim-field--readonly">
<span className="trim-field__label">{t('trimmer.length_label')}</span>
<span className={`trim-field__value ${tooLong ? 'is-err' : ''}`}>
<div className="flex items-center gap-[var(--space-2)] bg-[#0f1112] border border-solid border-border rounded-md px-[6px] py-[2px] cursor-default">
<span className="[font-size:var(--text-xs)] text-fg-subtle uppercase [letter-spacing:0.05em] font-semibold">
{t('trimmer.length_label')}
</span>
<span
className={`px-[6px] py-[4px] font-mono [font-size:var(--text-md)] flex-1 ${tooLong ? 'text-danger' : 'text-[#b8bb26]'}`}
>
{(duration_ms / 1000).toFixed(2)}
{t('trimmer.unit_seconds')}
</span>
<span className="trim-field__unit">
<span className="[font-size:var(--text-xs)] text-fg-subtle">
{tooLong
? t('trimmer.too_long', { max: maxSeconds })
: tooShort
@@ -833,7 +854,7 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
</div>
{/* Play / Action row */}
<div className="audio-trimmer__actions">
<div className="flex gap-[var(--space-4)] items-center flex-wrap">
<Button
variant="subtle"
onClick={togglePlay}
@@ -843,9 +864,11 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
>
{playing ? t('trimmer.pause') : t('trimmer.preview_selection')}
</Button>
<span className="audio-trimmer__kbd-hint">{t('trimmer.play_hint')}</span>
<span className="[font-size:var(--text-base)] text-fg-subtle">
{t('trimmer.play_hint')}
</span>
<div className="audio-trimmer__actions-right">
<div className="ml-auto flex gap-[var(--space-4)]">
<Button variant="ghost" onClick={onCancel}>
{t('trimmer.cancel')}
</Button>
@@ -860,12 +883,7 @@ export default function AudioTrimmer({ file, maxSeconds = 15, onConfirm, onCance
</div>
</div>
<audio
ref={audioRef}
preload="auto"
onEnded={() => setPlaying(false)}
className="audio-trimmer__audio"
/>
<audio ref={audioRef} preload="auto" onEnded={() => setPlaying(false)} className="hidden" />
</div>
</Dialog>
);
+7 -68
View File
@@ -98,55 +98,14 @@
}
/* ── Content area ───────────────────────────────────────────────────── */
.floating-pill__content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.floating-pill__label {
font-weight: var(--weight-medium);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.floating-pill__meta {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--text-xs);
color: var(--color-fg-muted);
}
.floating-pill__timer {
font-family: var(--font-mono);
font-size: var(--text-2xs);
color: var(--color-fg-subtle);
letter-spacing: 0.03em;
}
.floating-pill__error {
font-size: var(--text-xs);
color: var(--color-danger);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* .floating-pill__content / __label / __meta / __timer / __error layout
moved to Tailwind utilities on the JSX. The base __label rule lives there
too; the unlayered `.floating-pill--done .floating-pill__label` override
below still wins (utilities are low-priority @layer). */
/* ── Mini progress bar ──────────────────────────────────────────────── */
.floating-pill__progress {
width: 100%;
height: 3px;
border-radius: 2px;
background: var(--color-bg-elev-2);
overflow: hidden;
margin-top: 2px;
}
/* .floating-pill__progress (track) moved to Tailwind utilities on the JSX.
The fill (base + indeterminate) stays here — it has !important + animation. */
.floating-pill__progress-fill {
height: 100%;
@@ -166,27 +125,7 @@
}
/* ── Dismiss button ─────────────────────────────────────────────────── */
.floating-pill__dismiss {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
background: transparent;
color: var(--color-fg-subtle);
cursor: pointer;
flex-shrink: 0;
transition: background var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out);
}
.floating-pill__dismiss:hover {
background: rgba(255,255,255,0.08);
color: var(--color-fg);
}
/* .floating-pill__dismiss + :hover moved to Tailwind utilities on the JSX. */
/* ── Done stage — green tint ────────────────────────────────────────── */
+12 -7
View File
@@ -101,19 +101,24 @@ export default function FloatingPill() {
<span className={`floating-pill__dot floating-pill__dot--${stage}`} />
{/* Content */}
<div className="floating-pill__content">
<span className="floating-pill__label">
<div className="floating-pill__content flex-1 min-w-0 flex flex-col gap-[2px]">
<span className="floating-pill__label font-medium whitespace-nowrap overflow-hidden text-ellipsis">
{stageEmoji} {label}
</span>
{/* Meta row: timer + progress text */}
<div className="floating-pill__meta">
<div className="floating-pill__meta flex items-center gap-[var(--space-3)] [font-size:var(--text-xs)] text-fg-muted">
{isActive && elapsed > 0 && (
<span className="floating-pill__timer">{formatElapsed(elapsed)}</span>
<span className="floating-pill__timer font-mono [font-size:var(--text-2xs)] text-fg-subtle [letter-spacing:0.03em]">
{formatElapsed(elapsed)}
</span>
)}
{progress !== null && isActive && <span>{Math.round(progress)}%</span>}
{isError && error && (
<span className="floating-pill__error" title={error}>
<span
className="floating-pill__error [font-size:var(--text-xs)] text-danger whitespace-nowrap overflow-hidden text-ellipsis"
title={error}
>
{error}
</span>
)}
@@ -121,7 +126,7 @@ export default function FloatingPill() {
{/* Mini progress bar */}
{isActive && (
<div className="floating-pill__progress">
<div className="floating-pill__progress w-full h-[3px] rounded-[2px] bg-bg-elev-2 overflow-hidden mt-[2px]">
<div
className={[
'floating-pill__progress-fill',
@@ -137,7 +142,7 @@ export default function FloatingPill() {
{/* Dismiss / cancel button */}
<button
className="floating-pill__dismiss"
className="floating-pill__dismiss flex items-center justify-center w-[20px] h-[20px] rounded-full border-0 bg-transparent text-fg-subtle cursor-pointer flex-shrink-0 [transition:background_var(--dur-fast)_var(--ease-out),color_var(--dur-fast)_var(--ease-out)] hover:bg-[rgba(255,255,255,0.08)] hover:text-fg"
onClick={handleDismiss}
title={cancellable ? t('common.cancel') : t('common.dismiss')}
aria-label={cancellable ? t('common.cancelOp') : t('common.dismissStatus')}
-126
View File
@@ -1,66 +1,3 @@
.multi-lang {
position: relative;
}
.multi-lang__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
min-height: 28px;
}
.multi-lang__chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: var(--chrome-hover-bg);
border: 1px solid var(--chrome-border);
border-radius: 999px;
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 500;
color: var(--chrome-fg);
text-transform: uppercase;
}
.multi-lang__chip-x {
background: none;
border: none;
color: var(--chrome-fg-muted);
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
border-radius: 999px;
transition: color 0.15s;
}
.multi-lang__chip-x:hover {
color: var(--color-danger);
}
.multi-lang__add {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 999px;
border: 1px dashed var(--chrome-border);
background: none;
color: var(--chrome-fg-muted);
cursor: pointer;
transition: all 0.15s;
}
.multi-lang__add:hover {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
border-style: solid;
}
.multi-lang__summary {
font-family: var(--font-mono);
font-size: 0.62rem;
color: var(--chrome-fg-dim);
margin-top: 4px;
}
/* Dropdown */
.multi-lang__drop {
position: absolute;
@@ -83,66 +20,3 @@
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.multi-lang__search {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-bottom: 1px solid var(--chrome-border);
color: var(--chrome-fg-muted);
}
.multi-lang__search input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--chrome-fg);
font-family: var(--font-sans);
font-size: 0.78rem;
}
.multi-lang__list {
overflow-y: auto;
flex: 1;
padding: 4px 0;
}
.multi-lang__section {
font-family: var(--font-mono);
font-size: 0.62rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--chrome-fg-dim);
padding: 6px 10px 2px;
}
.multi-lang__option {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 5px 10px;
background: none;
border: none;
color: var(--chrome-fg);
font-family: var(--font-sans);
font-size: 0.76rem;
cursor: pointer;
text-align: left;
transition: background 0.1s;
}
.multi-lang__option:hover {
background: var(--chrome-hover-bg);
}
.multi-lang__option-code {
font-family: var(--font-mono);
font-size: 0.68rem;
color: var(--chrome-accent);
min-width: 28px;
font-weight: 600;
}
.multi-lang__more,
.multi-lang__empty {
padding: 8px 10px;
font-size: 0.7rem;
color: var(--chrome-fg-dim);
text-align: center;
}
+30 -16
View File
@@ -72,16 +72,19 @@ export default function MultiLangPicker({
}, [query, selectedCodes]);
return (
<div className="multi-lang" ref={dropRef}>
<div className="multi-lang__chips">
<div className="relative" ref={dropRef}>
<div className="flex flex-wrap gap-[4px] items-center min-h-[28px]">
{selected.map((s) => (
<span key={s.code} className="multi-lang__chip">
<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"
>
<Globe size={9} />
<span>{s.code}</span>
{!disabled && (
<button
type="button"
className="multi-lang__chip-x"
className="bg-transparent border-0 text-[color:var(--chrome-fg-muted)] cursor-pointer p-0 flex items-center rounded-full [transition:color_0.15s] hover:text-danger"
onClick={() => removeLang(s.code)}
aria-label={`Remove ${s.lang}`}
>
@@ -93,7 +96,7 @@ export default function MultiLangPicker({
{!disabled && (
<button
type="button"
className="multi-lang__add"
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"
onClick={() => setDropOpen(!dropOpen)}
title={t('dub.add_language')}
>
@@ -103,14 +106,14 @@ export default function MultiLangPicker({
</div>
{selected.length > 0 && (
<div className="multi-lang__summary">
<div className="[font-family:var(--font-mono)] text-[0.62rem] text-[color:var(--chrome-fg-dim)] mt-[4px]">
{t('dub.languages_selected', { count: selected.length })}
</div>
)}
{dropOpen && (
<div className="multi-lang__drop">
<div className="multi-lang__search">
<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)]">
<Search size={10} />
<input
ref={inputRef}
@@ -118,44 +121,55 @@ export default function MultiLangPicker({
onChange={(e) => setQuery(e.target.value)}
placeholder={t('dub.search_languages')}
spellCheck={false}
className="flex-1 bg-transparent border-0 outline-none text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[0.78rem]"
/>
</div>
<div className="multi-lang__list">
<div className="overflow-y-auto flex-1 py-[4px]">
{popularFiltered.length > 0 && (
<>
<div className="multi-lang__section">{t('dub.popular')}</div>
<div className="[font-family:var(--font-mono)] text-[0.62rem] font-semibold uppercase [letter-spacing:0.04em] text-[color:var(--chrome-fg-dim)] pt-[6px] px-[10px] pb-[2px]">
{t('dub.popular')}
</div>
{popularFiltered.map((item) => (
<button
key={item.code}
type="button"
className="multi-lang__option"
className="flex items-center gap-[8px] w-full px-[10px] py-[5px] bg-transparent border-0 text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[0.76rem] cursor-pointer text-left [transition:background_0.1s] hover:bg-[var(--chrome-hover-bg)]"
onClick={() => addLang(item.lang, item.code)}
>
<span className="multi-lang__option-code">{item.code}</span>
<span className="[font-family:var(--font-mono)] text-[0.68rem] text-[color:var(--chrome-accent)] min-w-[28px] font-semibold">
{item.code}
</span>
<span>{item.lang}</span>
</button>
))}
</>
)}
<div className="multi-lang__section">{t('dub.all_languages')}</div>
<div className="[font-family:var(--font-mono)] text-[0.62rem] font-semibold uppercase [letter-spacing:0.04em] text-[color:var(--chrome-fg-dim)] pt-[6px] px-[10px] pb-[2px]">
{t('dub.all_languages')}
</div>
{filteredLangs.slice(0, 50).map((lc) => (
<button
key={lc.code}
type="button"
className="multi-lang__option"
className="flex items-center gap-[8px] w-full px-[10px] py-[5px] bg-transparent border-0 text-[color:var(--chrome-fg)] [font-family:var(--font-sans)] text-[0.76rem] cursor-pointer text-left [transition:background_0.1s] hover:bg-[var(--chrome-hover-bg)]"
onClick={() => addLang(lc.label, lc.code)}
>
<span className="multi-lang__option-code">{lc.code}</span>
<span className="[font-family:var(--font-mono)] text-[0.68rem] text-[color:var(--chrome-accent)] min-w-[28px] font-semibold">
{lc.code}
</span>
<span>{lc.label}</span>
</button>
))}
{filteredLangs.length > 50 && (
<div className="multi-lang__more">
<div className="px-[10px] py-[8px] text-[0.7rem] text-[color:var(--chrome-fg-dim)] text-center">
{t('dub.more_to_narrow', { count: filteredLangs.length - 50 })}
</div>
)}
{filteredLangs.length === 0 && popularFiltered.length === 0 && (
<div className="multi-lang__empty">{t('dub.no_matches')}</div>
<div className="px-[10px] py-[8px] text-[0.7rem] text-[color:var(--chrome-fg-dim)] text-center">
{t('dub.no_matches')}
</div>
)}
</div>
</div>
-197
View File
@@ -1,197 +0,0 @@
/* Footer Local/Network share toggle. Matches the logs-footer pill chrome
(gruvbox palette, 20px pill height); the "Network" active state uses the
accent green #b8bb26. The panel is an upward-opening dropdown card. */
.net-toggle {
position: relative;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
/* ── Pill ─────────────────────────────────────────────────────────────── */
.net-toggle__pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
height: 20px;
border-radius: 3px;
background: none;
border: 1px solid transparent;
color: #a89984;
font-size: 11px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: all 0.1s;
}
.net-toggle__pill:hover {
background: rgba(255, 255, 255, 0.04);
color: #ebdbb2;
}
.net-toggle__pill:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.net-toggle__pill--on {
background: rgba(184, 187, 38, 0.12);
border-color: rgba(184, 187, 38, 0.4);
color: #b8bb26;
}
.net-toggle__pill--on:hover {
background: rgba(184, 187, 38, 0.18);
color: #b8bb26;
}
/* ── Dropdown panel (opens upward, since the footer is pinned to bottom) ── */
.net-toggle__panel {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
z-index: 60;
width: 248px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--chrome-bg, #1d2021);
border: 1px solid rgba(184, 187, 38, 0.35);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
color: #ebdbb2;
}
.net-toggle__panel-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #b8bb26;
}
.net-toggle__hint {
margin: 0;
font-size: 11px;
line-height: 1.5;
color: #a89984;
}
/* ── Address row ──────────────────────────────────────────────────────── */
.net-toggle__row {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.net-toggle__row-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
}
.net-toggle__addr {
font-family: var(--chrome-font-mono, var(--font-mono, monospace));
font-size: 11.5px;
color: #ebdbb2;
word-break: break-all;
}
.net-toggle__row-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.net-toggle__iconbtn {
background: none;
border: none;
color: #a89984;
cursor: pointer;
width: 22px;
height: 22px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s;
}
.net-toggle__iconbtn:hover {
color: #b8bb26;
background: rgba(255, 255, 255, 0.06);
}
.net-toggle__qr {
display: block;
width: 104px;
height: 104px;
border-radius: 4px;
background: #fff;
padding: 4px;
}
/* ── PIN + stop ───────────────────────────────────────────────────────── */
.net-toggle__pin {
font-size: 12px;
color: #a89984;
text-align: center;
}
.net-toggle__pin strong {
font-family: var(--chrome-font-mono, var(--font-mono, monospace));
font-size: 14px;
letter-spacing: 0.12em;
color: #b8bb26;
}
.net-toggle__off {
background: rgba(251, 73, 52, 0.12);
border: 1px solid rgba(251, 73, 52, 0.35);
color: #fb4934;
font-size: 11px;
font-weight: 600;
font-family: inherit;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.1s;
}
.net-toggle__off:hover {
background: rgba(251, 73, 52, 0.2);
}
.net-toggle__off:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.net-toggle__confirm-actions {
display: flex;
gap: 6px;
margin-top: 8px;
}
.net-toggle__enable {
background: rgba(184, 187, 38, 0.15);
border: 1px solid rgba(184, 187, 38, 0.4);
color: #b8bb26;
font-size: 11px;
font-weight: 600;
font-family: inherit;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
}
.net-toggle__enable:hover { background: rgba(184, 187, 38, 0.25); }
.net-toggle__cancel {
background: transparent;
border: 1px solid var(--border, #504945);
color: inherit;
font-size: 11px;
font-family: inherit;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
}
.net-toggle__enable:disabled,
.net-toggle__cancel:disabled { opacity: 0.5; cursor: not-allowed; }
+45 -22
View File
@@ -7,7 +7,6 @@ import { Wifi, WifiOff, Copy, ExternalLink } from 'lucide-react';
import toast from 'react-hot-toast';
import { apiJson, apiPost } from '../api/client';
import { openExternal } from '../api/external';
import './NetworkToggle.css';
export default function NetworkToggle() {
const { t } = useTranslation();
@@ -79,9 +78,9 @@ export default function NetworkToggle() {
};
return (
<div className="net-toggle">
<div className="relative inline-flex items-center flex-shrink-0">
<button
className={`net-toggle__pill ${st.enabled ? 'net-toggle__pill--on' : ''}`}
className={`inline-flex items-center gap-[5px] py-[2px] px-[8px] h-[20px] rounded-sm font-medium text-[11px] [font-family:inherit] cursor-pointer [transition:all_0.1s] border border-solid disabled:opacity-50 disabled:cursor-not-allowed ${st.enabled ? 'bg-[rgba(184,187,38,0.12)] border-[rgba(184,187,38,0.4)] text-[#b8bb26] hover:bg-[rgba(184,187,38,0.18)]' : 'bg-transparent border-transparent text-[#a89984] hover:bg-[rgba(255,255,255,0.04)] hover:text-fg'}`}
onClick={st.enabled ? () => setOpen((o) => !o) : () => setConfirming((c) => !c)}
disabled={busy}
title={st.enabled ? t('network.sharing_on_title') : t('network.share_on_network')}
@@ -93,19 +92,28 @@ export default function NetworkToggle() {
</button>
{!st.enabled && confirming && (
<div className="net-toggle__panel net-toggle__panel--confirm">
<div className="net-toggle__panel-title">{t('network.share_confirm_title')}</div>
<p className="net-toggle__hint">{t('network.share_confirm_hint')}</p>
<div className="net-toggle__confirm-actions">
<div className="absolute bottom-[calc(100%+8px)] right-0 z-[60] w-[248px] flex flex-col gap-[8px] p-[12px] bg-[var(--chrome-bg,#1d2021)] border border-solid border-[rgba(184,187,38,0.35)] rounded-[8px] shadow-[0_8px_24px_rgba(0,0,0,0.45)] text-fg">
<div className="text-[11px] font-semibold uppercase [letter-spacing:0.06em] text-[#b8bb26]">
{t('network.share_confirm_title')}
</div>
<p className="m-0 text-[11px] [line-height:1.5] text-[#a89984]">
{t('network.share_confirm_hint')}
</p>
<div className="flex gap-[6px] mt-[8px]">
<button
type="button"
className="net-toggle__cancel"
className="bg-transparent border border-solid border-[var(--border,#504945)] [color:inherit] text-[11px] [font-family:inherit] py-[5px] px-[12px] rounded-md cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setConfirming(false)}
disabled={busy}
>
{t('common.cancel')}
</button>
<button type="button" className="net-toggle__enable" onClick={enable} disabled={busy}>
<button
type="button"
className="bg-[rgba(184,187,38,0.15)] border border-solid border-[rgba(184,187,38,0.4)] text-[#b8bb26] text-[11px] font-semibold [font-family:inherit] py-[5px] px-[12px] rounded-md cursor-pointer hover:bg-[rgba(184,187,38,0.25)] disabled:opacity-50 disabled:cursor-not-allowed"
onClick={enable}
disabled={busy}
>
{busy ? t('network.enabling') : t('network.enable')}
</button>
</div>
@@ -113,23 +121,30 @@ export default function NetworkToggle() {
)}
{st.enabled && open && (
<div className="net-toggle__panel">
<div className="net-toggle__panel-title">{t('network.shared_title')}</div>
<div className="absolute bottom-[calc(100%+8px)] right-0 z-[60] w-[248px] flex flex-col gap-[8px] p-[12px] bg-[var(--chrome-bg,#1d2021)] border border-solid border-[rgba(184,187,38,0.35)] rounded-[8px] shadow-[0_8px_24px_rgba(0,0,0,0.45)] text-fg">
<div className="text-[11px] font-semibold uppercase [letter-spacing:0.06em] text-[#b8bb26]">
{t('network.shared_title')}
</div>
{(st.lan_addresses || []).length === 0 && (
<p className="net-toggle__hint">{t('network.no_interface')}</p>
<p className="m-0 text-[11px] [line-height:1.5] text-[#a89984]">
{t('network.no_interface')}
</p>
)}
{(st.lan_addresses || []).map((ip) => {
const url = `http://${ip}:${st.share_port}/?pin=${st.pin}`;
return (
<div key={ip} className="net-toggle__row">
<div className="net-toggle__row-main">
<code className="net-toggle__addr">
<div
key={ip}
className="flex flex-col items-center gap-[8px] p-[8px] rounded-lg bg-[rgba(255,255,255,0.03)] border border-solid border-[rgba(255,255,255,0.05)]"
>
<div className="flex items-center justify-between gap-[8px] w-full">
<code className="[font-family:var(--chrome-font-mono,var(--font-mono,monospace))] text-[11.5px] text-fg break-all">
{ip}:{st.share_port}
</code>
<div className="net-toggle__row-actions">
<div className="flex items-center gap-[2px] flex-shrink-0">
<button
type="button"
className="net-toggle__iconbtn"
className="bg-transparent border-0 text-[#a89984] cursor-pointer w-[22px] h-[22px] rounded-sm flex items-center justify-center [transition:all_0.1s] hover:text-[#b8bb26] hover:bg-[rgba(255,255,255,0.06)]"
onClick={() => copy(url)}
aria-label={`Copy ${ip}`}
title={t('network.copy_link')}
@@ -138,7 +153,7 @@ export default function NetworkToggle() {
</button>
<button
type="button"
className="net-toggle__iconbtn"
className="bg-transparent border-0 text-[#a89984] cursor-pointer w-[22px] h-[22px] rounded-sm flex items-center justify-center [transition:all_0.1s] hover:text-[#b8bb26] hover:bg-[rgba(255,255,255,0.06)]"
onClick={() => openExternal(url)}
aria-label={`Open ${ip}`}
title={t('network.open_in_browser')}
@@ -149,7 +164,7 @@ export default function NetworkToggle() {
</div>
{qrs[ip] && (
<img
className="net-toggle__qr"
className="block w-[104px] h-[104px] rounded-md bg-[#fff] p-[4px]"
src={qrs[ip]}
alt={t('network.qr_alt', { ip })}
width={104}
@@ -159,10 +174,18 @@ export default function NetworkToggle() {
</div>
);
})}
<div className="net-toggle__pin">
{t('network.pin')} <strong>{st.pin}</strong>
<div className="text-[12px] text-[#a89984] text-center">
{t('network.pin')}{' '}
<strong className="[font-family:var(--chrome-font-mono,var(--font-mono,monospace))] text-[14px] [letter-spacing:0.12em] text-[#b8bb26]">
{st.pin}
</strong>
</div>
<button type="button" className="net-toggle__off" onClick={disable} disabled={busy}>
<button
type="button"
className="bg-[rgba(251,73,52,0.12)] border border-solid border-[rgba(251,73,52,0.35)] text-danger text-[11px] font-semibold [font-family:inherit] py-[5px] px-[10px] rounded-md cursor-pointer [transition:all_0.1s] hover:bg-[rgba(251,73,52,0.2)] disabled:opacity-50 disabled:cursor-not-allowed"
onClick={disable}
disabled={busy}
>
{t('network.stop_sharing')}
</button>
</div>
@@ -17,54 +17,8 @@
font-size: var(--text-sm);
}
.readiness-checklist__title {
font-weight: var(--weight-semibold);
font-size: var(--text-md);
color: var(--color-fg);
margin: 0 0 var(--space-2) 0;
display: flex;
align-items: center;
gap: var(--space-3);
}
.readiness-checklist__title-icon {
font-size: var(--text-lg);
}
.readiness-checklist__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.readiness-checklist__item {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
transition: background var(--dur-fast) var(--ease-out);
}
.readiness-checklist__item:hover {
background: var(--color-bg-elev-3);
}
/* ── Status icons ───────────────────────────────────────────────────── */
.readiness-checklist__status {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1px;
}
.readiness-checklist__status--pass { color: var(--color-success); }
.readiness-checklist__status--warn { color: var(--color-accent); }
.readiness-checklist__status--fail { color: var(--color-danger); }
@@ -76,37 +30,3 @@
@keyframes rc-spin {
to { transform: rotate(360deg); }
}
/* ── Content ────────────────────────────────────────────────────────── */
.readiness-checklist__label {
font-weight: var(--weight-medium);
color: var(--color-fg);
}
.readiness-checklist__detail {
font-size: var(--text-xs);
color: var(--color-fg-muted);
margin-top: 1px;
}
.readiness-checklist__fix {
font-size: var(--text-xs);
color: var(--color-accent);
margin-top: 2px;
}
/* ── Compact summary when all pass ──────────────────────────────────── */
.readiness-checklist__all-pass {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: rgba(142, 192, 124, 0.08);
border: 1px solid rgba(142, 192, 124, 0.15);
border-radius: var(--radius-md);
color: var(--color-success);
font-weight: var(--weight-medium);
font-size: var(--text-sm);
}
+26 -16
View File
@@ -111,8 +111,8 @@ export default function ReadinessChecklist({ compact = false, showWhenAllPass =
if (isLoading) {
return (
<div className="readiness-checklist">
<div className="readiness-checklist__title">
<span className="readiness-checklist__title-icon">
<div className="flex items-center gap-[var(--space-3)] font-semibold [font-size:var(--text-md)] text-fg m-0 mb-[var(--space-2)]">
<span className="[font-size:var(--text-lg)]">
<Search size={14} />
</span>
{t('readiness.checking_system')}
@@ -126,7 +126,7 @@ export default function ReadinessChecklist({ compact = false, showWhenAllPass =
const issues = checks.filter((c) => c.status !== 'pass');
if (issues.length === 0) {
return (
<div className="readiness-checklist__all-pass">
<div className="flex items-center gap-[var(--space-3)] py-[var(--space-3)] px-[var(--space-4)] bg-[rgba(142,192,124,0.08)] border border-solid border-[rgba(142,192,124,0.15)] rounded-md text-success font-medium [font-size:var(--text-sm)]">
<CheckCircle size={14} />
{t('readiness.all_ready')}
</div>
@@ -134,17 +134,22 @@ export default function ReadinessChecklist({ compact = false, showWhenAllPass =
}
return (
<div className="readiness-checklist">
<ul className="readiness-checklist__list">
<ul className="list-none m-0 p-0 flex flex-col gap-[var(--space-2)]">
{issues.map((check) => (
<li key={check.id} className="readiness-checklist__item">
<li
key={check.id}
className="flex items-start gap-[var(--space-3)] py-[var(--space-2)] px-[var(--space-3)] rounded-sm [transition:background_var(--dur-fast)_var(--ease-out)] hover:bg-bg-elev-3"
>
<span
className={`readiness-checklist__status readiness-checklist__status--${check.status}`}
className={`shrink-0 w-[16px] h-[16px] flex items-center justify-center mt-[1px] readiness-checklist__status readiness-checklist__status--${check.status}`}
>
<StatusIcon status={check.status} />
</span>
<div>
<div className="readiness-checklist__label">{check.label}</div>
{check.fix && <div className="readiness-checklist__fix">{check.fix}</div>}
<div className="font-medium text-fg">{check.label}</div>
{check.fix && (
<div className="[font-size:var(--text-xs)] text-accent mt-[2px]">{check.fix}</div>
)}
</div>
</li>
))}
@@ -155,25 +160,30 @@ export default function ReadinessChecklist({ compact = false, showWhenAllPass =
return (
<div className="readiness-checklist">
<div className="readiness-checklist__title">
<span className="readiness-checklist__title-icon">
<div className="flex items-center gap-[var(--space-3)] font-semibold [font-size:var(--text-md)] text-fg m-0 mb-[var(--space-2)]">
<span className="[font-size:var(--text-lg)]">
{anyFail ? <AlertTriangle size={14} /> : <CheckCircle size={14} />}
</span>
{t('readiness.system_readiness')}
</div>
<ul className="readiness-checklist__list">
<ul className="list-none m-0 p-0 flex flex-col gap-[var(--space-2)]">
{checks.map((check) => (
<li key={check.id} className="readiness-checklist__item">
<li
key={check.id}
className="flex items-start gap-[var(--space-3)] py-[var(--space-2)] px-[var(--space-3)] rounded-sm [transition:background_var(--dur-fast)_var(--ease-out)] hover:bg-bg-elev-3"
>
<span
className={`readiness-checklist__status readiness-checklist__status--${check.status}`}
className={`shrink-0 w-[16px] h-[16px] flex items-center justify-center mt-[1px] readiness-checklist__status readiness-checklist__status--${check.status}`}
>
<StatusIcon status={check.status} />
</span>
<div>
<div className="readiness-checklist__label">{check.label}</div>
<div className="readiness-checklist__detail">{check.detail}</div>
<div className="font-medium text-fg">{check.label}</div>
<div className="[font-size:var(--text-xs)] text-fg-muted mt-[1px]">
{check.detail}
</div>
{check.fix && (
<div className="readiness-checklist__fix">
<div className="[font-size:var(--text-xs)] text-accent mt-[2px]">
<Lightbulb size={12} /> {check.fix}
</div>
)}
+1 -77
View File
@@ -60,83 +60,7 @@
background: var(--chrome-bg);
}
.wh {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
border-top: 1px solid var(--chrome-border-strong, var(--chrome-border));
}
.wh__head {
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--chrome-border);
}
.wh__title {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--chrome-font-mono, var(--font-mono));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--chrome-fg-muted);
}
.wh__filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.wh__chip {
flex: 0 0 auto;
padding: 2px 10px;
font-size: 0.68rem;
font-weight: 500;
color: var(--chrome-fg-muted);
background: transparent;
border: 1px solid var(--chrome-border-strong);
border-radius: var(--chrome-radius-pill, 999px);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.wh__chip:hover {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
}
.wh__chip.is-active {
color: var(--color-brand, #d3869b);
background: color-mix(in srgb, var(--color-brand, #d3869b) 12%, transparent);
border-color: color-mix(in srgb, var(--color-brand, #d3869b) 35%, transparent);
}
.wh__scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
.wh__empty {
color: var(--chrome-fg-dim);
font-size: 0.72rem;
line-height: 1.5;
text-align: center;
padding: 32px 16px;
}
/* Panel chrome (.wh / .wh__*) moved to Tailwind utilities on WorkspaceHistory.jsx. */
/* On a narrow shell the right column drops below the definition column. Same
shell-class trigger as above (not a viewport media query) so it reflows in
+11 -7
View File
@@ -93,15 +93,15 @@ export default function WorkspaceHistory({
// ── Dub variant: a flat list of dub jobs, no clone/design filter. ──
if (variant === 'dub') {
return (
<aside className="wh">
<div className="wh__head">
<span className="wh__title">
<aside className="flex-[1_1_0] flex flex-col min-h-0 overflow-hidden border-t border-solid border-t-[var(--chrome-border-strong,var(--chrome-border))]">
<div className="flex-[0_0_auto] flex flex-col gap-[8px] py-[10px] px-[12px] border-b border-solid border-b-[var(--chrome-border)]">
<span className="inline-flex items-center gap-[6px] [font-family:var(--chrome-font-mono,var(--font-mono))] text-[0.72rem] font-semibold [letter-spacing:0.04em] uppercase text-[color:var(--chrome-fg-muted)]">
<History size={13} /> {t('history.dub_title', { defaultValue: 'Dub history' })}
</span>
</div>
<div className="wh__scroll">
<div className="flex-[1_1_auto] min-h-0 overflow-y-auto flex flex-col gap-[8px] p-[8px]">
{dubHistory.length === 0 ? (
<div className="wh__empty">
<div className="text-[color:var(--chrome-fg-dim)] text-[0.72rem] [line-height:1.5] text-center py-[32px] px-[16px]">
{t('history.empty_dub', { defaultValue: 'Your dubs will appear here.' })}
</div>
) : (
@@ -160,12 +160,16 @@ export default function WorkspaceHistory({
<span className="wh__title">
<History size={13} /> {t('history.title', { defaultValue: 'History' })}
</span>
<div className="wh__filters">
<div className="flex flex-wrap gap-[4px]">
{FILTERS.map((f) => (
<button
key={f.id}
type="button"
className={`wh__chip ${filter === f.id ? 'is-active' : ''}`}
className={`flex-[0_0_auto] py-[2px] px-[10px] text-[0.68rem] font-medium border border-solid rounded-[var(--chrome-radius-pill,999px)] cursor-pointer [transition:background_0.15s_ease,color_0.15s_ease,border-color_0.15s_ease] ${
filter === f.id
? 'text-[color:var(--color-brand,#d3869b)] bg-[color-mix(in_srgb,var(--color-brand,#d3869b)_12%,transparent)] border-[color-mix(in_srgb,var(--color-brand,#d3869b)_35%,transparent)]'
: 'bg-transparent text-[color:var(--chrome-fg-muted)] border-[var(--chrome-border-strong)] hover:bg-[var(--chrome-hover-bg)] hover:text-[color:var(--chrome-fg)]'
}`}
onClick={() => setFilter(f.id)}
>
{t(`history.filter_${f.id}`, { defaultValue: f.label })}
+3 -54
View File
@@ -16,60 +16,9 @@
flex: 0 0 auto;
}
/* ── Active voice card (10x §2) — identity always visible ─────── */
.wv__active {
flex: 0 0 auto;
padding: 10px 12px;
border-bottom: 1px solid var(--chrome-border);
}
.wv__active-kicker {
font-family: var(--chrome-font-mono, var(--font-mono));
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.06em;
/* 10x P4 contrast: readable kicker — dim (3.91:1 default theme) → muted. */
color: var(--chrome-fg-muted, #a89984);
margin-bottom: 6px;
}
.wv__active-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
border: 1px solid var(--chrome-accent-border, rgba(211, 134, 155, 0.35));
background: var(--chrome-accent-bg, rgba(211, 134, 155, 0.08));
border-radius: 10px;
}
.wv__active-row { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.wv__active-name { font-size: 0.8rem; font-weight: 600; color: var(--chrome-fg); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wv__active-recipe {
font-size: 0.68rem;
color: var(--chrome-fg-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wv__active-actions { display: flex; gap: 6px; }
.wv__active-empty {
font-size: 0.7rem;
line-height: 1.5;
color: var(--chrome-fg-muted);
padding: 8px 10px;
border: 1px dashed var(--chrome-border);
border-radius: 10px;
}
.wv__empty-cta {
display: block;
margin: 8px auto 0;
padding: 4px 10px;
font-size: 0.66rem;
color: var(--chrome-fg-muted);
background: transparent;
border: 1px dashed var(--chrome-border);
border-radius: var(--chrome-radius-pill, 999px);
cursor: default;
}
/* Active-voice card + empty-state CTA (.wv__active*, .wv__empty-cta) converted
to Tailwind utilities on WorkspaceVoices.jsx — these classes were local to
this component, so their rules were removed here. */
.wv__head {
flex: 0 0 auto;
+11 -9
View File
@@ -70,14 +70,16 @@ export default function WorkspaceVoices({
return (
<section className={`wv ${items.length === 0 ? 'wv--collapsed' : ''}`}>
{/* ── ACTIVE VOICE ─────────────────────────────────────────────── */}
<div className="wv__active">
<div className="wv__active-kicker">
<div className="flex-[0_0_auto] py-[10px] px-[12px] border-b border-solid border-b-[var(--chrome-border)]">
<div className="[font-family:var(--chrome-font-mono,var(--font-mono))] text-[0.62rem] uppercase [letter-spacing:0.06em] text-[color:var(--chrome-fg-muted,#a89984)] mb-[6px]">
{t('voices.active', { defaultValue: 'Active voice' })}
</div>
{active ? (
<div className="wv__active-card">
<div className="wv__active-row">
<span className="wv__active-name">{active.name}</span>
<div className="flex flex-col gap-[6px] py-[8px] px-[10px] border border-solid border-[var(--chrome-accent-border,rgba(211,134,155,0.35))] bg-[var(--chrome-accent-bg,rgba(211,134,155,0.08))] rounded-[10px]">
<div className="flex items-center gap-[8px] justify-between">
<span className="text-[0.8rem] font-semibold text-[color:var(--chrome-fg)] min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{active.name}
</span>
<span
className="history-kind"
style={{
@@ -88,7 +90,7 @@ export default function WorkspaceVoices({
{active.instruct ? t('sidebar.design_label') : t('sidebar.clone_label')}
</span>
</div>
<div className="wv__active-recipe">
<div className="text-[0.68rem] text-[color:var(--chrome-fg-muted)] whitespace-nowrap overflow-hidden text-ellipsis">
{active.instruct ||
t('voices.active_clone_recipe', {
defaultValue: 'Cloned from your reference clip',
@@ -102,7 +104,7 @@ export default function WorkspaceVoices({
compact
/>
) : null}
<div className="wv__active-actions">
<div className="flex gap-[6px]">
<button
type="button"
className="history-action-btn"
@@ -113,7 +115,7 @@ export default function WorkspaceVoices({
</div>
</div>
) : (
<div className="wv__active-empty">
<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]">
{t('voices.none_selected', {
defaultValue: 'No voice selected — describe one, drop audio, or pick below.',
})}
@@ -143,7 +145,7 @@ export default function WorkspaceVoices({
{/* Empty states carry verbs (10x §2). */}
<button
type="button"
className="wv__empty-cta"
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"
onClick={() => setDefineMethod(defineMethod === 'audio' ? 'audio' : 'design')}
>
{defineMethod === 'audio'