style: format the six files the formatting check names

They are outside scripts/format-baseline.json, so new code there must be
formatted; Prettier only, no change in behaviour.
This commit is contained in:
Maurice
2026-11-08 00:10:57 +00:00
parent 2a2dfec247
commit b9d0d3397e
6 changed files with 121 additions and 89 deletions
@@ -103,7 +103,10 @@ describe('localizedParams', () => {
});
it('FE-NOTIF-MODEL-009: anything that is not a bare date passes through, and no params stay none', () => {
expect(localizedParams({ trip: '2026-11-05 at noon', n: '3' }, 'de', NOW)).toEqual({ trip: '2026-11-05 at noon', n: '3' });
expect(localizedParams({ trip: '2026-11-05 at noon', n: '3' }, 'de', NOW)).toEqual({
trip: '2026-11-05 at noon',
n: '3',
});
expect(localizedParams(undefined, 'de', NOW)).toEqual({});
});
});
@@ -137,12 +137,19 @@ function FeedCard({ entry, color, active, onTap, showMood, showWeather, t }: Fee
) : (
<MapPin size={11} strokeWidth={2.2} className="flex-none" />
)}
<span dir="auto" className="truncate font-geist text-[length:calc(11px*var(--fs-scale-caption,1))] font-semibold">
<span
dir="auto"
className="truncate font-geist text-[length:calc(11px*var(--fs-scale-caption,1))] font-semibold"
>
{place}
</span>
</span>
)}
{story && <span dir="auto" className="line-clamp-3 text-caption leading-[1.45] text-m-ink opacity-80">{story}</span>}
{story && (
<span dir="auto" className="line-clamp-3 text-caption leading-[1.45] text-m-ink opacity-80">
{story}
</span>
)}
</span>
</button>
);
@@ -44,7 +44,10 @@ export function CardTitleButton({
onClick={onOpen}
className="flex min-w-0 flex-1 flex-wrap items-center gap-x-[7px] gap-y-1 text-start"
>
<span dir="auto" className="line-clamp-2 min-w-0 flex-[1_1_auto] break-words text-[0.78125rem] font-bold text-m-ink wide:basis-full">
<span
dir="auto"
className="line-clamp-2 min-w-0 flex-[1_1_auto] break-words text-[0.78125rem] font-bold text-m-ink wide:basis-full"
>
{res.title}
</span>
<span className="inline-flex flex-none items-center gap-1 rounded-full border border-[color:var(--m-rowbr)] bg-m-card px-2 py-[2px] font-geist text-[0.5625rem] font-bold uppercase tracking-[.06em] text-m-muted">
@@ -116,7 +119,9 @@ export function CardWhenAndCode({
<Field label={t('reservations.date')} className="flex-[1.4]">
{dayDate ? (
<span className="flex min-w-0 items-center justify-center gap-1.5">
<span dir="auto" className="min-w-0 truncate">{dayValue}</span>
<span dir="auto" className="min-w-0 truncate">
{dayValue}
</span>
<span className="flex-none font-medium text-m-faint">{dayDate}</span>
</span>
) : (
@@ -178,7 +183,9 @@ export function CardFiles({ files, card, t }: { files: ReturnType<typeof filesFo
className="flex items-center gap-[6px] rounded-[10px] border border-[color:var(--m-rowbr)] bg-m-card px-[10px] py-[7px]"
>
<FileText size={12} strokeWidth={2} className="flex-none text-m-muted" />
<span dir="auto" className="truncate font-geist text-[0.65625rem] font-semibold text-m-muted">{f.original_name}</span>
<span dir="auto" className="truncate font-geist text-[0.65625rem] font-semibold text-m-muted">
{f.original_name}
</span>
</span>
))}
</div>
+5 -4
View File
@@ -15,9 +15,9 @@
* Zero specificity, so an explicit text-center or text-end on the element wins.
*/
:where(
:root[dir='rtl'] [dir='auto']:dir(ltr):is(.truncate, [class*='line-clamp-'], input, textarea),
:root[dir='ltr'] [dir='auto']:dir(rtl):is(.truncate, [class*='line-clamp-'], input, textarea)
) {
:root[dir='rtl'] [dir='auto']:dir(ltr):is(.truncate, [class*='line-clamp-'], input, textarea),
:root[dir='ltr'] [dir='auto']:dir(rtl):is(.truncate, [class*='line-clamp-'], input, textarea)
) {
text-align: end;
}
@@ -32,7 +32,8 @@
* own property, so it composes with a rotation the icon carries (an open
* disclosure pointing down) instead of replacing it.
*/
:root[dir='rtl'] .m-root
:root[dir='rtl']
.m-root
:is(
.lucide-chevron-left,
.lucide-chevron-right,
+37 -25
View File
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
// FE-RTL-CSS-001 to FE-RTL-CSS-004
//
@@ -8,43 +8,55 @@ import { describe, expect, it } from 'vitest'
// text that lines up with the UI in its own direction, arrows that turn round
// in the touch shell, and Arabic's caption labels. jsdom applies no
// stylesheet, so these read the real file.
const css = readFileSync(resolve(process.cwd(), 'src/styles/rtl.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
const css = readFileSync(resolve(process.cwd(), 'src/styles/rtl.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
/** The declarations of the rule whose selector text contains every given part. */
function ruleBody(...parts: string[]): string {
for (const chunk of css.split('}')) {
const open = chunk.indexOf('{')
if (open < 0) continue
const head = chunk.slice(0, open)
if (parts.every(part => head.includes(part))) return chunk.slice(open + 1)
const open = chunk.indexOf('{');
if (open < 0) continue;
const head = chunk.slice(0, open);
if (parts.every((part) => head.includes(part))) return chunk.slice(open + 1);
}
return ''
return '';
}
describe('rtl.css', () => {
it('FE-RTL-CSS-001: the stylesheet is loaded with the app', () => {
const main = readFileSync(resolve(process.cwd(), 'src/main.tsx'), 'utf8')
expect(main).toMatch(/import '\.\/styles\/rtl\.css'/)
})
const main = readFileSync(resolve(process.cwd(), 'src/main.tsx'), 'utf8');
expect(main).toMatch(/import '\.\/styles\/rtl\.css'/);
});
it('FE-RTL-CSS-002: cut, clamped or typed user text in its own direction sits on the UI start edge', () => {
const body = ruleBody(":root[dir='rtl'] [dir='auto']:dir(ltr)", '.truncate', "[class*='line-clamp-']", 'input', 'textarea')
expect(body).toMatch(/text-align:\s*end/)
const body = ruleBody(
":root[dir='rtl'] [dir='auto']:dir(ltr)",
'.truncate',
"[class*='line-clamp-']",
'input',
'textarea'
);
expect(body).toMatch(/text-align:\s*end/);
// The left-to-right UI gets the mirror case: Arabic text in an English row.
expect(css).toContain(":root[dir='ltr'] [dir='auto']:dir(rtl)")
expect(css).toContain(":root[dir='ltr'] [dir='auto']:dir(rtl)");
// Zero specificity: an explicit alignment class on the element still wins.
expect(css).toMatch(/:where\(\s*:root\[dir='rtl'\]/)
})
expect(css).toMatch(/:where\(\s*:root\[dir='rtl'\]/);
});
it('FE-RTL-CSS-003: in the touch shell every back, forward and flow arrow turns round in a right-to-left language', () => {
const body = ruleBody(":root[dir='rtl'] .m-root", '.lucide-chevron-left', '.lucide-chevron-right', '.lucide-arrow-left', '.lucide-arrow-right')
expect(body).toMatch(/scale:\s*-1 1/)
const body = ruleBody(
":root[dir='rtl'] .m-root",
'.lucide-chevron-left',
'.lucide-chevron-right',
'.lucide-arrow-left',
'.lucide-arrow-right'
);
expect(body).toMatch(/scale:\s*-1 1/);
// An icon that names its own treatment keeps it.
expect(css).toContain(":not([class*='rtl:'])")
})
expect(css).toContain(":not([class*='rtl:'])");
});
it('FE-RTL-CSS-004: Arabic and Hebrew caption labels grow, drop the tracking and take the muted ink', () => {
expect(ruleBody(':lang(ar)', ':lang(he)', '.m-root .uppercase')).toMatch(/zoom:\s*1\.2[\s\S]*letter-spacing:\s*0/)
expect(ruleBody('.uppercase.text-m-faint')).toMatch(/color:\s*var\(--m-muted\)/)
})
})
expect(ruleBody(':lang(ar)', ':lang(he)', '.m-root .uppercase')).toMatch(/zoom:\s*1\.2[\s\S]*letter-spacing:\s*0/);
expect(ruleBody('.uppercase.text-m-faint')).toMatch(/color:\s*var\(--m-muted\)/);
});
});
+56 -54
View File
@@ -1,8 +1,8 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { directionOf, pickLocale, translator } from '../../../native-shell/i18n'
import { directionOf, pickLocale, translator } from '../../../native-shell/i18n';
// FE-NATIVE-SHELL-001 to FE-NATIVE-SHELL-006
//
@@ -12,83 +12,85 @@ import { directionOf, pickLocale, translator } from '../../../native-shell/i18n'
/** Every rule of a stylesheet as its selector list, its declarations and the media query around it, comments dropped. */
function rules(css: string): { selectors: string[]; body: string; media: string }[] {
const out: { selectors: string[]; body: string; media: string }[] = []
const stack: string[] = []
let buf = ''
const out: { selectors: string[]; body: string; media: string }[] = [];
const stack: string[] = [];
let buf = '';
for (const ch of css.replace(/\/\*[\s\S]*?\*\//g, '')) {
if (ch === '{') {
stack.push(buf.trim())
buf = ''
stack.push(buf.trim());
buf = '';
} else if (ch === '}') {
const head = stack.pop() ?? ''
const head = stack.pop() ?? '';
if (!head.startsWith('@')) {
const media = stack.filter(h => h.startsWith('@media')).join(' ')
out.push({ selectors: head.split(',').map(sel => sel.trim()), body: buf, media })
const media = stack.filter((h) => h.startsWith('@media')).join(' ');
out.push({ selectors: head.split(',').map((sel) => sel.trim()), body: buf, media });
}
buf = ''
buf = '';
} else {
buf += ch
buf += ch;
}
}
return out
return out;
}
const css = readFileSync(resolve(process.cwd(), 'native-shell/shell.css'), 'utf8')
const all = rules(css)
const css = readFileSync(resolve(process.cwd(), 'native-shell/shell.css'), 'utf8');
const all = rules(css);
const find = (selector: string, media?: RegExp) =>
all.filter(r => r.selectors.includes(selector) && (!media || media.test(r.media)))
all.filter((r) => r.selectors.includes(selector) && (!media || media.test(r.media)));
describe('native shell', () => {
it('FE-NATIVE-SHELL-001: Arabic and Hebrew start right to left, every other language left to right', () => {
expect(pickLocale(['ar-SA'])).toBe('ar')
expect(pickLocale(['he-IL'])).toBe('he')
expect(directionOf('ar')).toBe('rtl')
expect(directionOf('he')).toBe('rtl')
expect(directionOf('ru')).toBe('ltr')
expect(directionOf('de')).toBe('ltr')
})
expect(pickLocale(['ar-SA'])).toBe('ar');
expect(pickLocale(['he-IL'])).toBe('he');
expect(directionOf('ar')).toBe('rtl');
expect(directionOf('he')).toBe('rtl');
expect(directionOf('ru')).toBe('ltr');
expect(directionOf('de')).toBe('ltr');
});
it('FE-NATIVE-SHELL-002: the first screens speak without dashes as punctuation', () => {
for (const code of ['en', 'de']) {
const t = translator(code)
const t = translator(code);
for (const key of ['native.welcome.sub', 'native.cloud.sub', 'native.cloud.f3']) {
expect(t(key)).not.toMatch(/[\u2013\u2014]| - /)
expect(t(key)).not.toMatch(/[\u2013\u2014]| - /);
}
}
})
});
it('FE-NATIVE-SHELL-003: Russian keeps TREK\'s faces through Cyrillic stand-ins under the same family names', () => {
const faces = css.match(/@font-face\s*\{[^}]*\}/g) ?? []
const cyrillic = faces.filter(f => /unicode-range:\s*U\+0301, U\+0400-045F/.test(f))
expect(cyrillic.filter(f => /font-family:\s*'Poppins'/.test(f))).toHaveLength(3)
expect(cyrillic.filter(f => /font-family:\s*'MuseoModerno'/.test(f))).toHaveLength(1)
it("FE-NATIVE-SHELL-003: Russian keeps TREK's faces through Cyrillic stand-ins under the same family names", () => {
const faces = css.match(/@font-face\s*\{[^}]*\}/g) ?? [];
const cyrillic = faces.filter((f) => /unicode-range:\s*U\+0301, U\+0400-045F/.test(f));
expect(cyrillic.filter((f) => /font-family:\s*'Poppins'/.test(f))).toHaveLength(3);
expect(cyrillic.filter((f) => /font-family:\s*'MuseoModerno'/.test(f))).toHaveLength(1);
for (const face of cyrillic) {
const file = /url\('\.\/fonts\/([^']+)'\)/.exec(face)?.[1]
expect(file).toBeTruthy()
expect(() => readFileSync(resolve(process.cwd(), 'native-shell/fonts', file as string))).not.toThrow()
const file = /url\('\.\/fonts\/([^']+)'\)/.exec(face)?.[1];
expect(file).toBeTruthy();
expect(() => readFileSync(resolve(process.cwd(), 'native-shell/fonts', file as string))).not.toThrow();
}
})
});
it('FE-NATIVE-SHELL-004: a short screen on its side puts the two halves of a screen side by side', () => {
const compact = /orientation: landscape\) and \(max-height: 500px\)/
expect(find('.screen:has(> .col)', compact)[0]?.body).toMatch(/grid-template-columns:\s*minmax\(0, 1fr\) minmax\(0, 1fr\)/)
expect(find('.col-about', compact)[0]?.body).toMatch(/position:\s*sticky/)
const compact = /orientation: landscape\) and \(max-height: 500px\)/;
expect(find('.screen:has(> .col)', compact)[0]?.body).toMatch(
/grid-template-columns:\s*minmax\(0, 1fr\) minmax\(0, 1fr\)/
);
expect(find('.col-about', compact)[0]?.body).toMatch(/position:\s*sticky/);
// Everywhere else the halves are no boxes of their own: one column, as before.
expect(find('.col')[0]?.body).toMatch(/display:\s*contents/)
})
expect(find('.col')[0]?.body).toMatch(/display:\s*contents/);
});
it('FE-NATIVE-SHELL-005: a screen taller than the window scrolls instead of overflowing both edges', () => {
expect(find('.screen')[0]?.body).toMatch(/margin-block:\s*auto/)
const shell = find('.shell')[0]?.body ?? ''
expect(shell).toMatch(/overflow-x:\s*clip/)
expect(shell).not.toMatch(/justify-content:\s*center/)
})
expect(find('.screen')[0]?.body).toMatch(/margin-block:\s*auto/);
const shell = find('.shell')[0]?.body ?? '';
expect(shell).toMatch(/overflow-x:\s*clip/);
expect(shell).not.toMatch(/justify-content:\s*center/);
});
it('FE-NATIVE-SHELL-006: in Arabic the chevrons sit at the far edge and point along the reading direction, the price stands apart', () => {
expect(find('.btn .chev')[0]?.body).toMatch(/margin-inline-start:\s*auto/)
expect(find(":root[dir='rtl'] .chev")[0]?.body).toMatch(/transform:\s*scaleX\(-1\)/)
expect(find('.plan-price small')[0]?.body).toMatch(/margin-inline-start:\s*7px/)
expect(find('.btn-row')[0]?.body).toMatch(/text-align:\s*start/)
expect(css).not.toMatch(/margin-left|margin-right|padding-left|padding-right|text-align:\s*left/)
})
})
expect(find('.btn .chev')[0]?.body).toMatch(/margin-inline-start:\s*auto/);
expect(find(":root[dir='rtl'] .chev")[0]?.body).toMatch(/transform:\s*scaleX\(-1\)/);
expect(find('.plan-price small')[0]?.body).toMatch(/margin-inline-start:\s*7px/);
expect(find('.btn-row')[0]?.body).toMatch(/text-align:\s*start/);
expect(css).not.toMatch(/margin-left|margin-right|padding-left|padding-right|text-align:\s*left/);
});
});