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:
co-authored by
mergetest
Claude Opus 4.8
parent
1650a121db
commit
cfe00c5c1c
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user