From 131a809ff48d2fe3d9117596f6d9063c733d0f50 Mon Sep 17 00:00:00 2001 From: XDan <109234186+XDanfr@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:32:10 +0100 Subject: [PATCH 1/7] Refine Fontify's M3E interface and verify accessible extension workflows --- anti-slop/delivery-2026-10-12.md | 120 ++++++++++ public/ui.css | 172 +++++++++----- src/ui.js | 263 +++++++++++---------- tests/browser.mjs | 377 ++++++++++++++++++++++++++++++- 4 files changed, 756 insertions(+), 176 deletions(-) create mode 100644 anti-slop/delivery-2026-10-12.md diff --git a/anti-slop/delivery-2026-10-12.md b/anti-slop/delivery-2026-10-12.md new file mode 100644 index 0000000..53ff7b9 --- /dev/null +++ b/anti-slop/delivery-2026-10-12.md @@ -0,0 +1,120 @@ +# Fontify Anti Slop delivery + +Applied the Anti Slop plugin in during mode. Direction comes from Fontify's existing Outfit typography, lavender M3E tonal palette, expressive selected shapes, and browser-extension workflow. ENERGY 2 / RHYTHM 2 / MOTION 1. + +## Changes and reasons + +- Retain Outfit, the existing icon, and M3E components: these are Fontify's established identity. +- Retain lavender as the primary accent: it marks selected controls and the working font specimen. +- Remove repeated slogan eyebrows and decorative hero blobs: settings titles should identify the task, and the specimen should demonstrate the fonts. +- Keep the typography page's existing pun: it gives Fontify a recognisable voice without obscuring the functional navigation labels. +- Replace vague library, override, upload, and preference copy with action names: users can identify where to change a setting. +- Use a pangram, numerals, bold, and italic in the specimen: users can compare actual letterforms and styles. +- Apply selected families to the profile-card glyphs: those samples previously showed the interface font rather than the selected profile. +- Place preview controls beside the specimen in reading order: changes have an immediate, nearby result. +- Use 18px profile-card corners, larger grouping surfaces, and the existing asymmetric specimen/selected-navigation shapes: radius communicates hierarchy while preserving M3E character. +- Use plain empty-state text rather than repeated decorative icons and dashed cards: the next useful action is easier to read. +- Wrap navigation at narrow widths and constrain grid tracks: settings remain accessible without horizontal page scrolling. +- Give buttons, filters, icons, and the size slider at least 44px target height: the compact extension remains usable by touch. +- Keep native control borders at the outline token and remove text opacity reductions: boundaries and text remain distinguishable in both themes. +- Make the main shell inert while dialogs are open, focus the editor, and restore its trigger after closing: keyboard users stay in the task and return to their place. +- Resolve the system theme before choosing the corner toggle's action: clicking it now switches to the opposite visible theme. +- Add a persistent startup error and retry action: a failed catalogue load no longer leaves a loading screen with a disappearing toast. +- Ignore outdated asynchronous preview results and clear recovered errors: a slow previous selection cannot overwrite a later specimen. + +No new visual assets, navigation destinations, dependencies, or settings-schema changes. + +## Validation + +- `npm test`: 8 tests passed. +- `npm run build`: Chrome and Firefox builds passed. +- `npm run lint:firefox`: 0 errors, 0 notices, 1 existing `UNSAFE_VAR_ASSIGNMENT` warning from Lit's bundled template parser, already documented in README.md. +- `npm run test:browser`: passed using Playwright 1.55.1 and Chromium 140 with the actual unpacked extension. Google font responses use a deterministic bundled-font fixture, so these tests validate loading and application behaviour rather than the visual appearance of each remote family. +- Every settings section checked in light and dark at 1240, 768, 390, and 320px. Document overflow, rendered text contrast using the WCAG luminance formula, and control target height assertions passed. The popup also passed light/dark checks at 320px. +- Screenshots recorded for all 40 settings/theme/width combinations and the popup. Desktop typography, narrow typography/preferences, and popup captures were visually inspected. +- Keyboard tests verify initial editor focus, nested picker/editor Escape handling, restored trigger focus, and focus remaining in the popup dialog while tabbing. Source review confirms native focus outlines and M3E strong-focus remain enabled. +- Firefox interactive behaviour and physical mobile keyboards were not tested. Firefox validation is build/lint only. The builds remain unsigned development packages. + +## Recorded interaction checks + +- Regular, serif, sans-serif, bold, italic, and monospace font choices open a picker and select a profile; live page typography changes. Regular/bold/italic card glyphs load the selected face. +- Style weights render their defaults; override weight changes persist after Save changes. +- Global enable switches pause and restore page typography, including original inline priorities. Code replacement changes fixed-width text while protecting icons. +- Preview text updates the specimen; size changes update the specimen and its px output. +- Picker search filters results; all category filters change their selected state; Show more expands the list; saving and unsaving a favourite changes Saved results; unmatched searches show an empty state. +- Picker Close, Done, and Escape close it. Closing a nested picker returns focus inside the override editor. Closing the editor returns focus to Edit. +- Each navigation button opens its real section; narrow navigation wraps visibly. +- Upload accepts the bundled TTF; Use applies it; Remove confirms deletion and restores the empty library state. +- Exception type selection works; an invalid hostname shows validation; selector exceptions preserve page text; font exceptions can be added and removed. +- A context-menu-equivalent block request opens the composer; Save override persists across reloads; Edit, Save changes, Cancel, and Remove work. +- Theme controls change light/dark mode. System light and system dark each make the corner button switch to the opposite visible mode. +- Download permission toggles; Clear cache confirms completion while keeping custom fonts. +- Export produces a JSON backup; Import restores it after confirmation; Reset clears settings while retaining uploaded fonts. +- Popup Pause here and Resume here update the site's exception. All settings opens the options page. +- Failed catalogue startup shows a persistent error; Try again recovers the interface. +- Runtime page-error collection is empty. Existing external Source & feedback and M3E links retain their real destinations and explicit new-tab arrows; no new links were introduced. + +## Delivery gate + +Each PASS below is limited to the Chromium runtime, source review, and build/lint evidence above. + +### Hard gate + +- R-02 PASS: UI source scan has no em dashes in authored copy. +- R-03 PASS: overflow and 44px target assertions pass at the four recorded widths in both themes. +- R-17 PASS: catalogue totals, result counts, and file sizes derive from real stored data. +- R-18 PASS: no testimonial or avatar content exists in the interface. +- R-23 PASS: existing assets and navigation reused; no new visual asset created. +- R-24 PASS: all five navigation controls open existing settings views. +- R-25 PASS: rendered text passes computed WCAG AA contrast in the recorded views/themes and picker filters. +- R-26 PASS: shared control actions exercised in the interaction record; no placeholder controls added. +- R-27 PASS: loading status, collection/search empty states, font errors, and persistent startup error/retry exist; failure recovery exercised. +- R-28 PASS: no FAQ exists or was added. +- R-32 PASS: native outlines and M3E strong focus retained; dialog keyboard/focus assertions pass. +- R-33 PASS: edits made directly in UI source and CSS; no source-rewriting helper scripts added. +- R-34 PASS: light, dark, and system-toggle behaviour verified; both rendered themes pass layout and contrast assertions. +- R-35 PASS: both browser builds run; real Chromium extension interaction suite passes with no page errors. +- R-36 PASS: no new security, compliance, performance, or customer claims. +- R-37 PASS: existing brand direction and explicit dials declared before edits. +- R-38 PASS: product copy describes existing implemented behaviour; no invented features or content. + +### Purpose gate + +- R-01 PASS: existing lavender tonal tokens retained for state and specimen hierarchy; no gradients/glows added. +- R-04 PASS: Aa/Ff denote typography; exception, override, settings, theme, and favourite marks denote real actions. +- R-06 PASS: Outfit retained for established identity; labels now use normal case without wide tracking. +- R-07 PASS: no background grid or ornamental texture. +- R-08 PASS: existing external/new-tab arrows retained for destination behaviour; no new decorative arrows. +- R-09 PASS: author badge removed; remaining counts reflect actual exception/result data. +- R-10 PASS: no glassmorphism. +- R-12 PASS: shadows limited to the modal layer and transient status message. +- R-13 PASS: no glow treatment. +- R-14 PASS: equivalent profile cards remain comparable; category controls, specimen, settings rows, and library lists use distinct layouts for their tasks. +- R-19 PASS: no new animation; existing state transitions and reduced-motion handling retained. +- R-22 PASS: no illustrations created. + +### Liveliness + +- Dials PASS: ENERGY 2 / RHYTHM 2 / MOTION 1 declared and maintained. +- Focal point PASS: the working specimen anchors typography; other screens use task titles and the relevant action/list. +- Whitespace PASS: settings groups, profile controls, specimen, and explanation use distinct spacing levels. +- Accent PASS: lavender identifies selection, primary actions, and the specimen. +- Motif PASS: Outfit letterforms and existing asymmetric M3E selected/specimen shapes remain. +- Design Read PASS: browser typography utility with Fontify's existing M3E language declared before generation. + +### Craftsmanship and quality locks + +- C-1 PASS: major visual decisions have written reasons above. +- C-2 PASS: shared interactive control actions verified in the recorded suite. +- C-3 PASS: all sections serve real extension functions; repeated decorative headings removed. +- C-4 PASS: layout, theme, error recovery, and keyboard dialog tests pass within the stated coverage. +- C-5 PASS: no fabricated testimonials, statistics, or claims. +- R-05 PASS: task-oriented settings structure retained; specimen, profile grids, rows, and lists reflect their content. +- R-11 PASS: profile cards, grouping panels, controls, and asymmetric selected shapes have distinct radii. +- R-15 PASS: library action now says Choose regular font; other actions name their behaviour. +- R-16 PASS: vague promotional UI copy replaced by specific setting descriptions. +- R-20 PASS: existing Fontify icon, Outfit, lavender M3E palette, and typography specimen retain the product identity. +- R-21 PASS: existing light/dark/system controls remain functional; system toggle corrected. +- R-29 PASS: existing coherent M3E tonal palette retained without additional core colours. +- R-30 PASS: no unrelated product's visual design copied. +- R-31 PASS: palette, layout, typography, spacing, surfaces, and icon purposes documented above. diff --git a/public/ui.css b/public/ui.css index 6421a6c..c877e69 100644 --- a/public/ui.css +++ b/public/ui.css @@ -27,6 +27,9 @@ button { cursor: pointer; } m3e-theme { + --m3e-button-container-height: 44px; + --m3e-icon-button-container-height: 44px; + --m3e-icon-button-container-width: 44px; display: block; min-height: 100vh; background: var(--md-sys-color-surface); @@ -76,11 +79,9 @@ h3 { line-height: 1.55; } .eyebrow { - font-size: 11px; - font-weight: 650; - letter-spacing: 1.7px; - text-transform: uppercase; - color: var(--md-sys-color-primary); + font-size: 13px; + font-weight: 600; + color: var(--md-sys-color-on-surface-variant); } .shell { max-width: 1240px; @@ -95,6 +96,7 @@ h3 { margin-bottom: 38px; } .brand { + min-height: 44px; display: flex; align-items: center; gap: 11px; @@ -108,14 +110,6 @@ h3 { width: 40px; height: 40px; } -.badge { - font-size: 10px; - border: 1px solid var(--md-sys-color-outline-variant); - padding: 5px 9px; - border-radius: 99px; - letter-spacing: 1px; - font-weight: 500; -} .top-actions, .row { display: flex; @@ -130,12 +124,28 @@ h3 { grid-template-columns: 195px 1fr; gap: 38px; } +.layout > *, +.two-col > *, +.hero > div, +.settings-row > div { + min-width: 0; +} +main, +.settings-row, +.list-item { + overflow-wrap: anywhere; +} +m3e-switch, +m3e-button { + flex-shrink: 0; +} .nav { display: flex; flex-direction: column; gap: 7px; } .nav button { + min-height: 44px; display: flex; align-items: center; gap: 13px; @@ -174,19 +184,6 @@ h3 { margin-top: 12px; max-width: 480px; } -.hero-mark { - border-radius: 45% 50% 28% 48%; - background: var(--md-sys-color-primary-container); - color: var(--md-sys-color-on-primary-container); - width: 105px; - height: 105px; - display: grid; - place-items: center; - flex: none; - font-size: 56px; - font-weight: 600; - transform: rotate(-7deg); -} .panel { background: var(--md-sys-color-surface-container-low); padding: 24px; @@ -204,7 +201,7 @@ h3 { .font-card { padding: 22px 18px; background: var(--md-sys-color-surface-container); - border-radius: 24px; + border-radius: 18px; min-width: 0; } .font-card .glyph { @@ -214,6 +211,7 @@ h3 { margin: 14px 0; } .font-choice { + min-height: 44px; display: flex; justify-content: space-between; align-items: center; @@ -252,7 +250,7 @@ textarea { width: 100%; min-height: 45px; background: var(--md-sys-color-surface-container-high); - border: 1px solid var(--md-sys-color-outline-variant); + border: 1px solid var(--md-sys-color-outline); border-radius: 13px; padding: 11px 13px; color: var(--md-sys-color-on-surface); @@ -260,7 +258,7 @@ textarea { input[type="range"] { accent-color: var(--md-sys-color-primary); padding: 0; - min-height: 24px; + min-height: 44px; border: 0; } textarea { @@ -276,7 +274,6 @@ textarea { } .preview .eyebrow { color: inherit; - opacity: 0.7; } .preview h2 { font-size: 35px; @@ -292,10 +289,16 @@ textarea { display: block; margin-top: 12px; } +.preview strong { + display: block; + margin-top: 12px; +} +.preview-controls { + margin-top: 12px; +} .preview small { display: block; margin-top: 20px; - opacity: 0.7; font: 12px Outfit, sans-serif; @@ -356,18 +359,13 @@ textarea { margin-top: 4px; } .empty { - padding: 36px 24px; - border: 1px dashed var(--md-sys-color-outline-variant); - border-radius: 20px; - text-align: center; + padding: 24px 0; } .empty .symbol { - font-size: 35px; - color: var(--md-sys-color-primary); - margin-bottom: 8px; + display: none; } .empty p { - margin: 8px auto 0; + margin: 8px 0 0; max-width: 400px; font-size: 14px; } @@ -414,7 +412,6 @@ textarea { } .override-panel .muted { color: inherit; - opacity: 0.8; } .override-panel h2 { margin: 8px 0; @@ -423,7 +420,6 @@ textarea { margin: 16px 0; padding-left: 14px; border-left: 3px solid currentColor; - opacity: 0.8; overflow-wrap: anywhere; } .override-panel .row { @@ -485,6 +481,7 @@ textarea { color: inherit; padding: 8px 13px; font-size: 12px; + min-height: 44px; } .chip[aria-pressed="true"] { background: var(--md-sys-color-secondary-container); @@ -519,6 +516,7 @@ textarea { display: block; font-size: 18px; font-weight: 500; + overflow-wrap: anywhere; } .font-result .choose small { display: block; @@ -533,8 +531,8 @@ textarea { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); border-radius: 50%; - width: 40px; - height: 40px; + width: 44px; + height: 44px; font-size: 19px; } .icon-btn.active { @@ -551,7 +549,7 @@ textarea { gap: 12px; } .popup { - width: 390px; + width: min(390px, 100vw); min-height: 540px; } .popup m3e-theme { @@ -606,7 +604,7 @@ textarea { padding: 10px; } .popup .picker { - max-height: 560px; + max-height: min(560px, calc(100dvh - 20px)); border-radius: 24px; } .popup .picker-head { @@ -636,7 +634,10 @@ textarea { } .range-row output { font-size: 13px; - min-width: 28px; + min-width: 36px; +} +.range-row input { + min-width: 0; } .preview-error { font-size: 12px; @@ -673,7 +674,7 @@ textarea { } .font-card { display: grid; - grid-template-columns: 90px 1fr; + grid-template-columns: 70px minmax(0, 1fr); column-gap: 15px; } .font-card .eyebrow { @@ -694,15 +695,82 @@ textarea { .form-grid { grid-template-columns: 1fr; } - .hero-mark { - width: 75px; - height: 75px; - font-size: 40px; - } .topbar { margin-bottom: 25px; } } +@media (max-width: 480px) { + .nav { + flex-wrap: wrap; + overflow: visible; + } + .nav button { + flex: 1 1 120px; + gap: 8px; + padding: 12px 10px; + font-size: 13px; + } + .shell { + padding: 20px 16px; + } + h1 { + font-size: 36px; + letter-spacing: -1px; + } + .panel { + padding: 18px; + border-radius: 22px; + } + .row { + flex-wrap: wrap; + } + .list-item { + align-items: flex-start; + flex-wrap: wrap; + } + .settings-row { + gap: 12px; + flex-wrap: wrap; + } + .settings-row > div { + flex: 1 1 180px; + } + .picker-head { + padding: 18px 16px 12px; + } + .picker-head .row { + flex-wrap: nowrap; + align-items: flex-start; + } + .picker-head h2 { + min-width: 0; + font-size: 21px; + } + .dialog { + padding: 10px; + } + .picker { + max-height: calc(100dvh - 20px); + border-radius: 24px; + } + .picker-head { + flex-shrink: 0; + } + .picker-footer { + flex-shrink: 0; + flex-wrap: wrap; + padding: 12px 16px; + } + .preview { + padding: 24px; + } + .preview h2 { + font-size: 29px; + } + .popup h1 { + font-size: 32px; + } +} @media (prefers-reduced-motion: reduce) { * { transition: none !important; diff --git a/src/ui.js b/src/ui.js index 4508429..5bf1557 100644 --- a/src/ui.js +++ b/src/ui.js @@ -28,15 +28,17 @@ let s = normalise(), ruleScope = "site", ruleKind = "font", previewSize = 19, - previewText = "The web looks better your way.", + previewText = "Your web, your type.", previewError = "", + initError = "", ready = false; let queue = Promise.resolve(); const loaded = new Set(); let pickerTrigger; +let editTrigger; const nav = [ ["typography", "Aa", "Typography"], - ["library", "◈", "Font library"], + ["library", "Ff", "Font library"], ["exceptions", "⊘", "Exceptions"], ["rules", "↳", "Overrides"], ["preferences", "⚙", "Preferences"], @@ -50,7 +52,7 @@ function tell(message) { draw(); }, 4200); } -function change(mutator, message = "Saved — applied to your open pages") { +function change(mutator, message = "Saved. Applied to your open pages.") { queue = queue .then(async () => { const latest = await getSettings(); @@ -142,7 +144,20 @@ function openPicker(which, event) { function closePicker() { picker = null; draw(); - pickerTrigger?.focus(); + if (editing) focusDialog(); + else pickerTrigger?.focus(); +} +function focusDialog() { + requestAnimationFrame(() => + document + .querySelector('[role="dialog"] button, [role="dialog"] input') + ?.focus(), + ); +} +function closeEditor() { + editing = null; + draw(); + editTrigger?.focus(); } async function choose(family) { if (picker === "rule") { @@ -183,6 +198,9 @@ function fontButton(which, value) { `; } function header() { + const light = + s.theme === "light" || + (s.theme === "auto" && matchMedia("(prefers-color-scheme: light)").matches); return html`
${!popup - ? html`BY XDAN` + ? html`By XDan` : nothing} change((v) => { - v.theme = s.theme === "light" ? "dark" : "light"; + v.theme = light ? "dark" : "light"; }, "Theme updated")} - >
`; } -function hero(title, description, mark = "Aa") { +function hero(title, description) { return html`
-

YOUR WEB, YOUR TYPE

${title}

${description}

- ${!popup - ? html`` - : nothing}
`; } function specimen() { return html`
-

A LITTLE PREVIEW

+

Font preview

${previewText}

- A fresh perspective, one letter at a time. - Make a statement.And let the details speak. + The quick brown fox jumps over the lazy dog. 0123456789 + Bold text for emphasis.Italic text for a different voice.

${s.monospaceEnabled ? `Code uses ${fontName(s.monospace.family)}. Icons stay themselves.` - : "Code stays code. Your icons stay themselves."}${previewError ? html`
${previewError}
` : nothing} @@ -251,10 +261,10 @@ function specimen() { function categoryControls() { return html`
-

A font for every kind of text.

+

Serif & sans-serif

- Give serif and sans-serif text their own character. Leave either on - the default to use your regular typeface. + Choose by the page’s original text category, or use your regular + typeface.

@@ -323,7 +333,7 @@ function categoryControls() {
 const font = "${fontName(s.monospace.family)}";
-// Make code feel like home.

Replacement is off by default. Icons and exceptions stay protected, and @@ -334,15 +344,13 @@ const font = "${fontName(s.monospace.family)}"; function typography() { return html`${hero( "Make yourself at font.", - "Give the whole web a typeface that feels like you. Keep the structure. Change the character.", + "Choose the fonts you read on the web. Changes apply to open pages automatically.", )}

-

Fontify the web

-

- Apply your fonts on every site, except your exceptions. -

+

Apply your fonts

+

Pause to restore each page’s original typography.

${switchControl("Enable Fontify", s.enabled, (v) => change((x) => { @@ -355,11 +363,10 @@ function typography() { ${["regular", "bold", "italic"].map( (slot, i) => html`
-

- ${["The everyday", "The emphasis", "The expression"][i]} -

+

${["Regular", "Bold", "Italic"][i]}

- ${specimen()} ${categoryControls()} -
+ ${specimen()} +
@@ -418,14 +426,15 @@ function typography() { uses the serif or sans-serif family selected above, with its selected style weight. Bold italics use the italic profile.

-
`; +
+ ${categoryControls()}`; } function popupView() { const excluded = activeSite && s.exceptions.sites.some((p) => matchesSite(activeSite, p)); return html`${hero( - "Your web.\nYour type.", - "A small change. A whole new feeling.", + "Your fonts, on this page.", + "Choose a typeface or pause Fontify below.", )}
@@ -435,7 +444,7 @@ function popupView() { ${status?.count ? `${status.count} text elements restyled` : status?.failures - ? "A font could not load — see settings" + ? "A font could not load. Check settings." : activeSite ? excluded ? "This site is an exception" @@ -476,7 +485,7 @@ function popupView() { Browser settings, stores and other protected pages cannot be restyled.

`}
@@ -987,9 +987,8 @@ async function clearCache() { } function preferences() { return html`${hero( - "Just how you like it.", - "A quieter web, a brighter interface, or a fresh start. You’re in control.", - "✳", + "Preferences", + "Set the theme, manage font downloads, and back up your settings.", )}

Appearance & downloads

@@ -1039,7 +1038,7 @@ function preferences() {
-

Your settings, to go.

+

Backup & reset

Back up your settings, overrides, exceptions and custom font files. Downloads from Google are excluded. @@ -1078,7 +1077,7 @@ function preferences() {

-

Made for your eyes. By XDan.

+

About Fontify

Fontify 1.1.0 · MIT licensed · Built with matraic’s M3E components.

@@ -1155,7 +1154,8 @@ function pickerDialog() {

- ${catalogue.length.toLocaleString()} Google families. Yours included. + ${catalogue.length.toLocaleString()} Google Fonts families, plus your + uploaded fonts.