fix(ui): clone popover/CTA clipping + textarea resize (#481, #476) (#489)

Bug #481 — Clone "+Insert" popover was clipped offscreen and the script
textarea couldn't be resized:
- Apply the existing `.clone-panel--overflow-visible` helper to the script
  `.studio-panel` so the upward-opening popover escapes the panel's
  `overflow:auto` box instead of being shoved into its scroll region.
- Cap the popover at `max-width: min(360px, calc(100vw - 16px))` so the
  14-chip grid can never spill past the viewport edge.
- Re-enable the textarea corner grip (`resize: vertical`, matching the base
  `textarea.input-base`) and lift the ⊕ Insert button off the bottom-right so
  it no longer physically covers the drag handle.

Bug #476 — the design-mode "Synthesize Audio" CTA dropped below the fold on
narrow shells:
- Replace the raw `@media (max-width: 900px)` reflow rules with the app's
  shell-width classes (`.shell-narrow` / `.shell-mini`, set in App.jsx from
  `app-container.clientWidth`). The shell scales via `zoom`, so a viewport
  media query fired at the wrong threshold whenever `--ui-scale ≠ 1`.
- When stacked, let `.studio-with-history__main` grow (drop its `overflow:hidden`
  clip) and pin the action bar `position: sticky; bottom: 0` so the Synthesize
  CTA stays on-screen.

Pure CSS + one className; no component restructuring. Added a regression test
guarding the shell-class reflow + sticky CTA against the viewport-`@media`
anti-pattern. typecheck:ci clean; vitest 506/506 green.

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-16 15:39:34 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 1650a121db
commit cfe00c5c1c
4 changed files with 94 additions and 18 deletions
+37 -13
View File
@@ -20,11 +20,34 @@
overflow: hidden;
}
@media (max-width: 900px) {
.studio-with-history {
flex-direction: column;
overflow-y: auto;
}
/* On a narrow shell the workspace + history aside stack vertically. Driven by
the shell-width class set in App.jsx from `app-container.clientWidth`, NOT a
raw `@media (max-width)`: the shell scales via `zoom`, so a viewport media
query fires at the wrong threshold whenever --ui-scale ≠ 1 and dropped the
action bar (Synthesize CTA) below the fold (#476). `.shell-narrow`/`.shell-mini`
track the real scaled layout width on every engine (index.css:294). */
.shell-narrow .studio-with-history,
.shell-mini .studio-with-history {
flex-direction: column;
overflow-y: auto;
}
/* When stacked, the main column must NOT clip its own overflow — its
`overflow:hidden` (needed for the side-by-side scroll panes) otherwise trapped
the pinned action bar below the fold of the now-scrolling outer column (#476).
Letting it grow naturally hands scrolling to `.studio-with-history`, and the
action bar below stays reachable; `position:sticky` keeps the Synthesize CTA
pinned to the viewport bottom while the script content scrolls. */
.shell-narrow .studio-with-history__main,
.shell-mini .studio-with-history__main {
overflow: visible;
flex: 0 0 auto;
}
.shell-narrow .studio-with-history__main .studio-action-bar,
.shell-mini .studio-with-history__main .studio-action-bar {
position: sticky;
bottom: 0;
z-index: 3;
}
/* Right column: Saved voices (top) + History (bottom) stacked, each scrolls. */
@@ -115,12 +138,13 @@
padding: 32px 16px;
}
/* On narrow windows the right column drops below the definition column. */
@media (max-width: 900px) {
.studio-right {
flex-basis: auto;
border-left: none;
border-top: 1px solid var(--chrome-border);
max-height: 60vh;
}
/* 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
lockstep with the stacked layout at the scaled width (#476). */
.shell-narrow .studio-right,
.shell-mini .studio-right {
flex-basis: auto;
border-left: none;
border-top: 1px solid var(--chrome-border);
max-height: 60vh;
}
+15 -4
View File
@@ -136,7 +136,10 @@
.clone-insert-btn {
position: absolute;
right: 8px;
bottom: 8px;
/* Lifted off the textarea's bottom-right resize grip so the ⊕ button no
longer covers the drag handle (#481). The popover's `bottom` offset below
is anchored to this row, so they move together. */
bottom: 30px;
display: inline-flex;
align-items: center;
gap: 4px;
@@ -154,12 +157,17 @@
.clone-insert-pop {
position: absolute;
right: 8px;
bottom: 38px;
/* Opens upward, clearing the lifted ⊕ button (bottom:30px + its height). */
bottom: 60px;
z-index: 20;
display: flex;
flex-wrap: wrap;
gap: 4px;
max-width: 360px;
/* Viewport-edge guard (#481): the 14-chip grid can't spill past the window
even when the script panel is narrow. The script panel's
`.clone-panel--overflow-visible` (applied in the JSX) stops the parent
`overflow:auto` from clipping it. */
max-width: min(360px, calc(100vw - 16px));
padding: 8px;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border-strong);
@@ -265,7 +273,10 @@
/* Text input + textarea tweaks */
.clone-text-area {
flex: 1;
resize: none;
/* Re-enable the corner grip (matches base textarea.input-base). The ⊕ Insert
button is lifted off the bottom-right so it no longer covers the handle
(#481). */
resize: vertical;
min-height: 60px;
margin-bottom: 6px;
}
+4 -1
View File
@@ -266,7 +266,10 @@ export default function CloneDesignTab(props) {
{/* ═══ SCRIPT — what should it say ═══ */}
<div className="studio-column">
<div className="studio-panel">
{/* overflow-visible: the ⊕ Insert popover opens above the textarea and
must escape the panel's `overflow:auto` box instead of being clipped
into its scroll region (#481). */}
<div className="studio-panel clone-panel--overflow-visible">
<div className="label-row label-row--center">
<Command className="label-icon" size={14} /> {t('clone.script', { defaultValue: 'Script' })}
</div>
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
/**
* Regression guard for the design-mode "Synthesize Audio" CTA clipping (#476).
*
* The studio workspace stacks vertically on narrow shells. That reflow MUST be
* driven by the shell-width classes (`.shell-narrow` / `.shell-mini`, set in
* App.jsx from `app-container.clientWidth`), NOT a raw `@media (max-width)`:
* the shell scales via `zoom`, so a viewport media query fires at the wrong
* threshold whenever `--ui-scale ≠ 1` and dropped the action bar below the fold
* (the same anti-pattern documented in index.css:294). This test fails CI if a
* future change reintroduces a `@media (max-width)` in this file or drops the
* shell-class reflow / sticky action bar.
*/
// Strip /* … */ comments so the guard checks real declarations, not the
// warning comment that quotes the forbidden `@media (max-width)` pattern.
const raw = readFileSync(
resolve(process.cwd(), 'src/components/WorkspaceHistory.css'),
'utf8',
);
const css = raw.replace(/\/\*[\s\S]*?\*\//g, '');
describe('workspace narrow-shell reflow (#476 CTA-clipping guard)', () => {
it('does NOT use a raw viewport @media (max-width) query', () => {
expect(css).not.toMatch(/@media[^{]*max-width/);
});
it('stacks the workspace via the shell-width classes', () => {
expect(css).toMatch(/\.shell-narrow\s+\.studio-with-history/);
expect(css).toMatch(/\.shell-mini\s+\.studio-with-history/);
});
it('pins the action bar (Synthesize CTA) sticky so it stays on-screen', () => {
expect(css).toMatch(/\.studio-action-bar\s*\{[^}]*position:\s*sticky/s);
});
});