mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4051af58a7 |
@@ -19,7 +19,8 @@ import type { StudioUploader } from './studioUpload'
|
||||
* has a handful of completely different kinds of thing you reach for (which
|
||||
* page, what from the journey, what arrangement, what to draw), and putting
|
||||
* them behind tabs in a single 200px column would make each of them cramped.
|
||||
* An icon rail costs 64px and gives the panel the room a photo browser needs.
|
||||
* An icon rail costs 64px, a little more where a section's name holds a longer
|
||||
* word, and gives the panel the room a photo browser needs.
|
||||
*
|
||||
* Which section is open is the shell's when it says so (`section` and
|
||||
* `onSection`), so the inspector on the other side can open the Theme or the
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BookDocument } from '@trek/shared';
|
||||
import { normalizeBookDocument } from '@trek/shared';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { fireEvent, render, screen, within } from '../../../../tests/helpers/render';
|
||||
@@ -9,11 +11,11 @@ import type { StudioUploader } from '../studioUpload';
|
||||
import { RAIL_SECTIONS, type RailSection } from './railSections';
|
||||
|
||||
/**
|
||||
* FE-STUDIO-RAIL-001 to FE-STUDIO-RAIL-006: the rail down the side of Studio
|
||||
* FE-STUDIO-RAIL-001 to FE-STUDIO-RAIL-007: the rail down the side of Studio
|
||||
* and the panel it opens. Every section is a tab that opens its own panel
|
||||
* under one head, the keyboard reaches every tab, a long name wraps inside
|
||||
* the rail, and the shell can hold the open section so the inspector can open
|
||||
* one from the other side.
|
||||
* the rail and only between words, and the shell can hold the open section so
|
||||
* the inspector can open one from the other side.
|
||||
*/
|
||||
|
||||
const SOURCE: JourneySource = { entries: [], photos: [], photoEntries: {} };
|
||||
@@ -54,6 +56,16 @@ function draw(over: { section?: RailSection; onSection?: (s: RailSection) => voi
|
||||
);
|
||||
}
|
||||
|
||||
/** Studio's stylesheet, which jsdom does not apply: its rules are the assertion. */
|
||||
const STUDIO_CSS = readFileSync(resolve(process.cwd(), 'src/styles/studio.css'), 'utf8');
|
||||
|
||||
/** The rule written for exactly this selector, searched in `source` (one media query, say). */
|
||||
function rule(selector: string, source = STUDIO_CSS): string {
|
||||
const at = source.indexOf(`${selector} {`);
|
||||
expect(at, `${selector} missing from studio.css`).toBeGreaterThan(-1);
|
||||
return source.slice(at, source.indexOf('}', at) + 1);
|
||||
}
|
||||
|
||||
/** The head of the panel that is open: the name every panel opens on. */
|
||||
const panelTitle = () => must(document.querySelector('.st-side .st-panel-head .st-what-label')).textContent;
|
||||
|
||||
@@ -168,4 +180,42 @@ describe('the rail', () => {
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'journey.studio.stickers' }));
|
||||
expect(screen.getByRole('textbox', { name: 'journey.studio.searchStickers' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('FE-STUDIO-RAIL-007: wraps a name only between words, the rail growing to its longest word instead', () => {
|
||||
// "Elements" in the label's 9.5px Poppins is 50.4px wide and a 52px tab left
|
||||
// it 48px, so overflow-wrap: anywhere cut it into "Element" and "s" wherever
|
||||
// no hyphenation dictionary was at hand. Catalan's "Maquetacions" is 77px and
|
||||
// a larger text size widens every word, so no fixed width holds them all.
|
||||
draw();
|
||||
const selector = '.trek-dash .st-rail-btn .st-rail-label';
|
||||
const names = [...document.querySelectorAll('.st-rail-label')];
|
||||
expect(names).toHaveLength(RAIL_SECTIONS.length);
|
||||
// Every name the rail renders is one the rule reaches; .trek-dash is the shell's root.
|
||||
for (const name of names) expect(name.matches(selector.replace('.trek-dash ', ''))).toBe(true);
|
||||
const label = rule(selector);
|
||||
expect(label).toMatch(/overflow-wrap:\s*normal;/);
|
||||
expect(label).toMatch(/word-break:\s*keep-all;/);
|
||||
expect(label).toMatch(/hyphens:\s*manual;/);
|
||||
expect(label).not.toMatch(/anywhere|break-all|break-word|hyphens:\s*auto/);
|
||||
// The type stays on the caption step the user's text size scales.
|
||||
expect(label).toMatch(/font-size:\s*calc\([\d.]+px \* var\(--fs-scale-caption, 1\)\)/);
|
||||
|
||||
// A tab takes the rail's width, and the rail's column is 64px or its
|
||||
// min-content, which is the longest word, in each of the three layouts.
|
||||
expect(rule('.trek-dash .st-rail-btn')).not.toMatch(/[\s;{]width:/);
|
||||
const columns = [...STUDIO_CSS.matchAll(/\.st-body \{[^}]*?grid-template-columns:\s*([^;]+);/g)].map((m) => m[1]);
|
||||
expect(columns).toEqual([
|
||||
'minmax(64px, min-content) 236px minmax(0, 1fr) 300px',
|
||||
'minmax(64px, min-content) 236px minmax(0, 1fr)',
|
||||
'minmax(64px, min-content) minmax(0, 1fr)',
|
||||
]);
|
||||
|
||||
// Below 1080 the panel floats over the workbench's own cell, so it starts
|
||||
// where the rail ends, not at a fixed 64px from the edge.
|
||||
const at = STUDIO_CSS.indexOf('@media (max-width: 1079px) {');
|
||||
expect(at, 'the narrow layout missing from studio.css').toBeGreaterThan(-1);
|
||||
const narrow = STUDIO_CSS.slice(at, STUDIO_CSS.indexOf('\n}', at));
|
||||
expect(narrow).toContain('.trek-dash .st-side, .trek-dash .st-work-wrap { grid-area: 1 / 2; }');
|
||||
expect(rule('.trek-dash .st-side', narrow)).not.toMatch(/position:\s*absolute|inset-inline-start/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,9 +11,9 @@ import { RAIL_IDS, RAIL_SECTIONS, type RailSection } from './railSections';
|
||||
* A list of tabs to a screen reader and to the keyboard, as it is to the eye:
|
||||
* Tab reaches the open section, the arrows move up and down the rail, Home and
|
||||
* End jump to its ends, and Enter or Space opens a section (studioRoving.ts).
|
||||
* A name longer than the rail is wide wraps onto a second line and breaks
|
||||
* inside a word only when the word alone is wider than the rail; the title
|
||||
* carries it whole either way.
|
||||
* A name longer than the rail is wide wraps onto a second line, only ever
|
||||
* between words: where one word alone is wider, the rail grows to fit it
|
||||
* (studio.css) instead of cutting it. The title carries the name whole.
|
||||
*/
|
||||
export function StudioRail({
|
||||
section,
|
||||
|
||||
@@ -211,16 +211,17 @@
|
||||
.trek-dash .st-sep { width: 1px; height: 20px; background: var(--line-2); margin: 0 2px; flex: none; }
|
||||
|
||||
/* ----------------- body: rail | workbench | inspector ----------------- */
|
||||
/* The rail is 64px, or as wide as the longest word in a section's name. */
|
||||
.trek-dash .st-body {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 236px minmax(0, 1fr) 300px;
|
||||
grid-template-columns: minmax(64px, min-content) 236px minmax(0, 1fr) 300px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Under 1280 the inspector floats over the workbench instead of narrowing it —
|
||||
below roughly 700px of canvas a spread stops being legible at any zoom. */
|
||||
@media (max-width: 1279px) {
|
||||
.trek-dash .st-body { grid-template-columns: 64px 236px minmax(0, 1fr); }
|
||||
.trek-dash .st-body { grid-template-columns: minmax(64px, min-content) 236px minmax(0, 1fr); }
|
||||
.trek-dash .st-inspector {
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
@@ -231,12 +232,11 @@
|
||||
}
|
||||
}
|
||||
@media (max-width: 1079px) {
|
||||
.trek-dash .st-body { grid-template-columns: 64px minmax(0, 1fr); }
|
||||
.trek-dash .st-body { grid-template-columns: minmax(64px, min-content) minmax(0, 1fr); }
|
||||
/* The panel floats over the workbench's cell, so it starts wherever the rail ends. */
|
||||
.trek-dash .st-side, .trek-dash .st-work-wrap { grid-area: 1 / 2; }
|
||||
.trek-dash .st-side {
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
inset-inline-start: 64px;
|
||||
bottom: 0;
|
||||
justify-self: start;
|
||||
width: 236px;
|
||||
box-shadow: var(--sh-lg);
|
||||
}
|
||||
@@ -457,7 +457,6 @@
|
||||
}
|
||||
.trek-dash .st-rail-btn {
|
||||
flex: none;
|
||||
width: 52px;
|
||||
min-height: 52px;
|
||||
padding: 6px 2px;
|
||||
display: flex;
|
||||
@@ -469,9 +468,9 @@
|
||||
color: var(--ink-3);
|
||||
transition: background .16s, color .16s;
|
||||
}
|
||||
/* Two lines at most, broken inside a word that is longer than the rail is wide
|
||||
("Путешествие", "Tərtibatlar"); the button's title carries the whole name. */
|
||||
.trek-dash .st-rail-btn span {
|
||||
/* Wrapped between words, never inside one: the rail grows to the longest word
|
||||
("Maquetacions", "Путешествие"), and keep-all holds "コンテンツ" whole too. */
|
||||
.trek-dash .st-rail-btn .st-rail-label {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
@@ -481,8 +480,9 @@
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
hyphens: auto;
|
||||
overflow-wrap: normal;
|
||||
word-break: keep-all;
|
||||
hyphens: manual;
|
||||
}
|
||||
.trek-dash .st-rail-btn:focus { outline: none; }
|
||||
.trek-dash .st-rail-btn:focus-visible { box-shadow: 0 0 0 2px var(--accent-soft), 0 0 0 3px var(--accent); }
|
||||
|
||||
Reference in New Issue
Block a user