Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions assets/css/builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -1359,23 +1359,31 @@
overflow: hidden;
}

/* Two stacked rows, not two side-by-side columns: the heading and the actions
share the first, and the picker strip gets the whole width of the second.
Sitting beside the strip, the actions took ~280px off the row the chips
scroll in — enough that the last chip was always cut down the middle, which
reads as a broken card rather than as an invitation to scroll. */
.atf-studio__top {
display: flex;
align-items: flex-end;
gap: var( --atfb-gap );
display: grid;
gap: 8px;
padding: 12px 14px;
border-block-end: 1px solid var( --atfb-border );
background: var( --atfb-surface );
min-inline-size: 0;
}

.atf-studio__picker {
.atf-studio__topbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px var( --atfb-gap );
min-inline-size: 0;
flex: 1 1 auto;
}

.atf-studio__heading {
margin: 0 0 8px;
margin: 0;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
Expand Down Expand Up @@ -1423,17 +1431,22 @@
/* --- The picker strip ----------------------------------------------------- */

/* A single scrolling row. Ten chips do not fit on a narrow window and wrapping
them would put the strip back to the height the column layout had. */
them would put the strip back to the height the column layout had.
Snapping to chip boundaries is what keeps a scroll from *stopping* on half a
card — the strip is allowed to run out of room, but never to look cut. */
.atfs-themes {
display: flex;
gap: 8px;
overflow-x: auto;
padding-block-end: 4px;
scrollbar-width: thin;
scroll-snap-type: x proximity;
scroll-padding-inline: 2px;
}

.atfs-theme {
flex: 0 0 auto;
scroll-snap-align: start;
inline-size: 92px;
display: grid;
gap: 5px;
Expand All @@ -1451,12 +1464,14 @@
block-size: 30px;
}

/* On the heading's line now, so nothing has to be nudged up off the strip's
scrollbar gutter. */
.atfs__actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 0 0 auto;
margin-block-end: 4px;
margin-block-end: 0;
}

.atfs-controls__body {
Expand Down
26 changes: 15 additions & 11 deletions assets/js/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2106,22 +2106,26 @@ var allTerrainFormsBuilder = function(exports) {
el("div", {
class: "atf-studio__top",
children: [
// The actions share the heading's line, not the strip's.
// Beside the strip they took the width the last chip needed
// and cut it down the middle, which reads as a broken card
// rather than as "there is more, scroll".
el("div", {
class: "atf-studio__picker",
class: "atf-studio__topbar",
children: [
el("h2", { class: "atf-studio__heading", text: "Theme" }),
swatches
el("div", {
class: "atfs__actions",
children: [
button("Save as a theme", () => void saveAsTheme(), "primary"),
button("Export", () => void exportTheme()),
button("Import", importTheme),
deleteButton
]
})
]
}),
el("div", {
class: "atfs__actions",
children: [
button("Save as a theme", () => void saveAsTheme(), "primary"),
button("Export", () => void exportTheme()),
button("Import", importTheme),
deleteButton
]
})
swatches
]
}),
el("div", {
Expand Down
4 changes: 2 additions & 2 deletions assets/js/builder.min.js

Large diffs are not rendered by default.

26 changes: 15 additions & 11 deletions src/theme-studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,22 +584,26 @@ export function mountThemeControls( options: ThemeControlsOptions ): HTMLElement
el( 'div', {
class: 'atf-studio__top',
children: [
// The actions share the heading's line, not the strip's.
// Beside the strip they took the width the last chip needed
// and cut it down the middle, which reads as a broken card
// rather than as "there is more, scroll".
el( 'div', {
class: 'atf-studio__picker',
class: 'atf-studio__topbar',
children: [
el( 'h2', { class: 'atf-studio__heading', text: 'Theme' } ),
swatches,
],
} ),
el( 'div', {
class: 'atfs__actions',
children: [
button( 'Save as a theme', () => void saveAsTheme(), 'primary' ),
button( 'Export', () => void exportTheme() ),
button( 'Import', importTheme ),
deleteButton,
el( 'div', {
class: 'atfs__actions',
children: [
button( 'Save as a theme', () => void saveAsTheme(), 'primary' ),
button( 'Export', () => void exportTheme() ),
button( 'Import', importTheme ),
deleteButton,
],
} ),
],
} ),
swatches,
],
} ),
el( 'div', {
Expand Down
76 changes: 76 additions & 0 deletions tests/vitest/theme-studio-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Where the Theme tab puts its picker and its buttons.
*
* A layout assertion, which is unusual and deliberate. The picker strip scrolls
* horizontally, so anything sharing its line silently takes width away from it
* — and the symptom is not a broken build or a failing render, it is the last
* theme card cut down the middle in a screenshot somebody has to notice. This
* pins the one structural fact that prevents it: the actions live on the
* heading's row, and the strip has the whole width of its own.
*/

import { describe, expect, it } from 'vitest';
import { mountThemeControls, type ThemeControlsOptions } from '../../src/theme-studio';
import type { Theme, ThemeToken } from '../../src/types';

/** The two tokens the quick dials need to render at all. */
const TOKENS: ThemeToken[] = [
{ key: 'color-accent', label: 'Accent', group: 'Colour', type: 'color', default: '#2271b1' },
{ key: 'radius-field', label: 'Field radius', group: 'Shape', type: 'text', default: '6px' },
] as unknown as ThemeToken[];

const THEMES: Theme[] = [ 'clean', 'midnight', 'glass' ].map( ( slug, index ) => ( {
slug,
label: slug,
description: '',
custom: false,
dark: false,
id: index + 1,
tokens: { 'color-accent': '#2271b1' },
// The chip is painted from these, so a theme without them cannot render.
resolved: {
surface: '#fff',
border: '#ccc',
'radius-field': '6px',
'color-accent': '#2271b1',
},
} ) );

/** Mounts a studio with every callback stubbed. */
function mount( extra: Partial< ThemeControlsOptions > = {} ): HTMLElement {
return mountThemeControls( {
themes: THEMES,
tokens: TOKENS,
activeSlug: 'clean',
overrides: {},
onTheme: () => undefined,
onOverride: () => undefined,
previewFor: () => Promise.resolve( '<form></form>' ),
onThemesChanged: () => undefined,
...extra,
} );
}

describe( 'the Theme tab’s top region', () => {
it( 'puts the actions on the heading’s row', () => {
const topbar = mount().querySelector( '.atf-studio__topbar' );

expect( topbar ).not.toBeNull();
expect( topbar?.querySelector( '.atf-studio__heading' ) ).not.toBeNull();
expect( topbar?.querySelector( '.atfs__actions' ) ).not.toBeNull();
} );

it( 'gives the picker strip a row to itself', () => {
// Not "the strip exists" — the strip must not share a parent with the
// buttons, because that parent is a flex row and sharing it is exactly
// what clipped the last chip.
const strip = mount().querySelector( '.atfs-themes' );

expect( strip?.parentElement?.className ).toBe( 'atf-studio__top' );
expect( strip?.parentElement?.querySelector( ':scope > .atfs__actions' ) ).toBeNull();
} );

it( 'keeps every theme in the strip rather than dropping the overflow', () => {
expect( mount().querySelectorAll( '.atfs-theme' ) ).toHaveLength( THEMES.length );
} );
} );
Loading