diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55e7d07..90904dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,12 @@ jobs: node-version: "22" cache: npm - run: npm ci + - name: Validate release tag and notes + if: startsWith(github.ref, 'refs/tags/v') + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + node --input-type=module -e 'import { readFileSync, accessSync } from "node:fs"; const { version } = JSON.parse(readFileSync("package.json")); if (process.env.RELEASE_TAG !== `v${version}`) throw new Error("Tag does not match package version"); accessSync(`docs/releases/${process.env.RELEASE_TAG}.md`);' - run: npm test - run: npm run build - run: npm run lint:firefox @@ -34,6 +40,11 @@ jobs: echo "CHROME_KEY_PATH=$RUNNER_TEMP/fontify.pem" >> "$GITHUB_ENV" fi - run: npm run package + - name: Package human-readable review source + if: startsWith(github.ref, 'refs/tags/v') + run: | + VERSION=$(node -p "require('./package.json').version") + git archive --format=zip --output="artifacts/fontify-$VERSION-source.zip" HEAD src public scripts data tests docs package.json package-lock.json README.md LICENSE THIRD_PARTY_NOTICES.md - name: Sign Firefox XPI for unlisted distribution if: startsWith(github.ref, 'refs/tags/v') env: @@ -42,7 +53,8 @@ jobs: shell: bash run: | if [ -n "$AMO_JWT_ISSUER" ] && [ -n "$AMO_JWT_SECRET" ]; then - npx web-ext sign --source-dir dist/firefox --artifacts-dir artifacts/signed --channel unlisted --api-key "$AMO_JWT_ISSUER" --api-secret "$AMO_JWT_SECRET" + VERSION=$(node -p "require('./package.json').version") + npx web-ext sign --source-dir dist/firefox --artifacts-dir artifacts/signed --channel unlisted --upload-source-code "artifacts/fontify-$VERSION-source.zip" --api-key "$AMO_JWT_ISSUER" --api-secret "$AMO_JWT_SECRET" else echo 'AMO credentials are not configured. The unsigned XPI is available for development.' fi @@ -68,6 +80,7 @@ jobs: permissions: contents: write steps: + - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: fontify-packages @@ -75,6 +88,7 @@ jobs: - uses: softprops/action-gh-release@v2 with: generate_release_notes: true + body_path: docs/releases/${{ github.ref_name }}.md files: | packages/*.zip packages/*.crx diff --git a/README.md b/README.md index 505ce4a..4e8c882 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Download the latest **fontify-packages** artifact from [Actions](https://github. ### Chrome / Chromium -1. Extract `fontify-1.1.0-chrome.zip`. +1. Extract `fontify-1.1.1-chrome.zip`. 2. Open `chrome://extensions` and enable **Developer mode**. 3. Choose **Load unpacked** and select the folder containing `manifest.json`. 4. Refresh any already-open websites once after installation. Later settings changes apply live. @@ -42,7 +42,7 @@ A CRX3 package is also built. A self-signed CRX is useful for compatible develop ### Firefox -1. Extract `fontify-1.1.0-firefox.zip`. +1. Extract `fontify-1.1.1-firefox.zip`. 2. Open `about:debugging#/runtime/this-firefox`. 3. Choose **Load Temporary Add-on** and select `manifest.json`. 4. Refresh already-open websites once after installation. @@ -69,10 +69,11 @@ Output: | -------------------------------------- | ------------------------------------------------------ | | `dist/chrome/` | Unpacked Chromium MV3 build; service worker background | | `dist/firefox/` | Firefox MV3 build; event-page background | -| `artifacts/fontify-1.1.0-chrome.zip` | Chrome development/store upload package | -| `artifacts/fontify-1.1.0-firefox.zip` | Firefox source package | -| `artifacts/fontify-1.1.0.crx` | Self-signed CRX3 | -| `artifacts/fontify-1.1.0-unsigned.xpi` | Unsigned Firefox development package | +| `artifacts/fontify-1.1.1-chrome.zip` | Chrome development/store upload package | +| `artifacts/fontify-1.1.1-firefox.zip` | Firefox extension package | +| `artifacts/fontify-1.1.1.crx` | Self-signed CRX3 | +| `artifacts/fontify-1.1.1-unsigned.xpi` | Unsigned Firefox development package | +| `artifacts/fontify-1.1.1-source.zip` | Human-readable source for release review | `npm run catalogue:update` refreshes the Google Fonts metadata snapshot. Catalogue access is never needed during an ordinary build. Fonts themselves download on demand through the background extension context and are cached in IndexedDB. All executable code and UI assets are bundled locally. @@ -82,7 +83,7 @@ Mozilla's linter currently reports one `UNSAFE_VAR_ASSIGNMENT` warning in the bu ## Automated builds and signing -Every push to `main` and every pull request builds, tests, validates, packages, and uploads artifacts. Tags matching `v*` also create GitHub Releases. +Every push to `main` and every pull request builds, tests, validates, packages, and uploads artifacts. Tags matching `v*` also sign an unlisted Firefox XPI when credentials are configured and publish GitHub release assets. Unlisted signing does not publish a Mozilla Add-ons store listing. See the [1.1.1 release notes](docs/releases/v1.1.1.md). Optional repository **Actions secrets**: diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 0b35ada..a18c264 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -5,3 +5,5 @@ Fontify is MIT licensed. It uses @m3e/web (MIT, copyright 2025–2026 matraic), The Google Fonts catalogue contains family names and metadata retrieved from Google's public metadata endpoint. Individual font files retain their own licences; selected Google fonts are downloaded at runtime, not redistributed in this repository. Outfit is bundled for the interface under SIL Open Font License 1.1; see public/fonts/OFL.txt. Custom fonts are supplied by the user and stored only on their device. Users should have permission to use their uploaded fonts. + +Sidebar symbols are Google's Material Symbols Rounded, weight 700, in outlined and filled variants, from https://github.com/google/material-design-icons. They are bundled under Apache License 2.0; see public/symbols/LICENSE. diff --git a/anti-slop/delivery-2026-10-12.md b/anti-slop/delivery-2026-10-12.md new file mode 100644 index 0000000..556584f --- /dev/null +++ b/anti-slop/delivery-2026-10-12.md @@ -0,0 +1,130 @@ +# Fontify Anti Slop delivery + +Applied the Anti Slop plugin in during mode. Direction comes from Fontify's existing lavender M3E tonal palette, expressive selected shapes, and browser-extension workflow, plus the owner's request for interface-wide font application, a wider popup, a larger sidebar, thick rounded icons, and micro-interaction morphs. Outfit remains the loading/error fallback. ENERGY 2 / RHYTHM 2 / MOTION 2. + +## Changes and reasons + +- Retain Outfit as the fallback, the existing app icon, and M3E components: these are Fontify's established identity. +- Apply the selected sans-serif/default profile across the interface, including native inputs and M3E shadow-root labels; apply the bold and italic profiles to their text styles: the extension itself now demonstrates the saved choices. Fonts apply after loading, with Outfit as a fallback on failure. +- Increase the desktop sidebar from 195px to 260px, with 76px navigation rows and 30px symbols: the controls are easier to read and target. +- Bundle Google's Material Symbols Rounded at weight 700 in outlined/filled pairs: typography, library, exclusion, tuning, and settings symbols describe each section, keep their shape when the interface font changes, and meet the owner's explicit icon direction. +- Morph navigation/icon corners and crossfade outlined/filled selection states; soften button/filter/icon press shapes and animate the picker entrance: short motion communicates actions and hierarchy. M3E expressive motion is enabled; reduced-motion preferences remove the added motion. Background colours switch immediately to avoid low-contrast transitional pairings. +- Set native toolbar popup width explicitly to 520px, while ordinary-tab previews remain responsive: viewport-relative width caused the native Chrome popup to shrink to 25px in a reproduced test. The actual toolbar popup now measures 520px, with quick settings visible before the specimen. +- 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. +- Put popup site actions and All settings above the specimen: quick controls stay reachable before the longer preview. +- 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 logo, navigation destinations, dependencies, or settings-schema changes. Requested sidebar symbols are bundled with Google's Apache-2.0 licence and attribution. + +## 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 tab preview also passed light/dark checks at 320px and was captured at 520px. +- Actual native Chromium toolbar popup opened through `chrome.action.openPopup`; its DevTools target measured 520px body/content width, no horizontal overflow, and All settings within the visible height. Native viewport width can also include platform scrollbar space. This validates native sizing separately from opening popup.html in a tab. +- Selecting independent Bungee/Lora/Roboto regular/bold/italic profiles changes computed interface font families, including inputs, section headings, and M3E shadow-root labels. This checks propagation while keeping deterministic font data in the integration suite. +- All ten local outlined/filled SVG variants fetch successfully. Reduced-motion emulation reports no picker animation and zero navigation-icon transition duration. +- 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. +- Interface-wide fonts change with the profiles; real native toolbar sizing is 520px; icon states load locally; added animations are disabled with reduced motion. +- 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 app assets and navigation reused; owner's requested rounded symbols bundled with their licence. +- 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: Material Symbols Rounded text_fields, collections_bookmark, block, tune, and settings identify their sections; 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: short selection/press morphs and picker entrance communicate actual interaction; no looping animation; reduced-motion assertions pass. +- R-22 PASS: no illustrations created. + +### Liveliness + +- Dials PASS: ENERGY 2 / RHYTHM 2 / MOTION 2 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 fallback, lavender M3E palette, and typography specimen retain identity while the interface demonstrates the user's font choices. +- 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/docs/RELEASING.md b/docs/RELEASING.md index c75697d..dac7b18 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -18,18 +18,22 @@ The key signs a CRX3; it does not grant a Chrome Web Store signature. For public 1. Create Mozilla Add-ons API credentials in your AMO developer account. 2. Add the key as `AMO_JWT_ISSUER` and the secret as `AMO_JWT_SECRET` in repository Actions secrets. 3. The Firefox manifest uses the extension ID `fontify@xdan.cc`. The domain migration changes the add-on identity: sign and distribute it as a new add-on, since it cannot update installations using the previous ID. Keep the new ID stable for subsequent releases. -4. On a version tag, the workflow runs `web-ext sign --channel unlisted`. A signed XPI is included in the artifact and Release. +4. On a version tag, the workflow runs `web-ext sign --channel unlisted` with a human-readable review-source ZIP. A signed XPI is included in the artifact and Release. This signs for distribution outside the Mozilla Add-ons store and does not create a public store listing. Without credentials, the workflow succeeds with an explicitly named **unsigned** XPI. A signing failure with configured credentials fails the build so an unsigned package is never mistaken for a signed one. Mozilla may require source review; provide this repository and its locked dependency/build instructions. Automated unlisted signing does not create a public AMO listing. ## Cut a version 1. Update `package.json` and `package-lock.json` using `npm version --no-git-tag-version`. -2. Update visible version text in `src/ui.js` and any versioned examples in the README. +2. Update versioned examples in the README and add `docs/releases/v.md`. The interface version is injected automatically from package.json during the build. 3. Run unit tests, both builds, Firefox lint and Chromium integration tests. 4. Commit the version update. -5. Tag the corresponding commit, for example `git tag v1.1.0`, and push that tag. -6. Verify that the build and Release contain the expected version and signing results. +5. Merge the verified changes to main, then tag that commit, for example `git tag v1.1.1`, and push the tag. Creating a GitHub release with a new tag on that commit also triggers the tag workflow. +6. Verify that the build and Release contain the expected version and signing results. The workflow rejects a tag that differs from the package version or has no matching release-notes file. + +Tags trigger signing and release uploads automatically; a normal workflow_dispatch on main only produces development packages. To retry a tagged signing run, rerun that run after inspecting its failure rather than dispatching the workflow on main. Never publish a listed Mozilla version as part of this workflow. + +The review-source archive uses an explicit list of application directories and metadata files. Local notes, unrelated root files, dependency installations, artifacts, and private keys are excluded. Do not reuse a Firefox version after it has been submitted to AMO. Do not expose signing secrets to forked pull requests; the workflow does not read credentials in PR runs. diff --git a/docs/releases/v1.1.1.md b/docs/releases/v1.1.1.md new file mode 100644 index 0000000..6b3ba29 --- /dev/null +++ b/docs/releases/v1.1.1.md @@ -0,0 +1,30 @@ +# Fontify 1.1.1 + +Fontify now shows your font choices throughout its own interface, with a wider popup and clearer Material 3 Expressive navigation. + +## Changes + +- Apply the selected default or sans-serif font throughout settings and the popup. Bold and italic text use their saved profiles; fonts that cannot load fall back to Outfit. +- Widen the native toolbar popup to 520px and keep site pause/resume and All settings above the preview. Fix the viewport-relative sizing that could shrink the popup. +- Enlarge the sidebar and use thick Material Symbols Rounded with outlined and filled selection states. +- Add short selection and press shape changes, icon transitions, and a picker entrance. Respect reduced-motion preferences. +- Replace vague settings copy with clear action names, and use a pangram, numerals, bold, and italic text in the preview. Profile-card glyphs now show their selected fonts. +- Improve narrow layouts, touch targets, and text contrast in light and dark themes. +- Keep keyboard focus inside dialogs and restore it to the opening control after closing. Correct the theme button when following the system theme. +- Show a persistent retry action if startup fails, and prevent older font requests from overwriting newer previews. + +Existing settings, uploaded fonts, exceptions, and overrides remain compatible. No new extension permissions or runtime dependencies. + +## Downloads + +Chrome: extract the Chrome ZIP and load its folder from chrome://extensions. The CRX3 is signed with the configured repository key; it is not a Chrome Web Store signature. + +Firefox: use the signed XPI from this release when available. The explicitly named unsigned XPI is for temporary development installation. Signing uses Mozilla's unlisted channel; this release does not create a public Mozilla Add-ons listing. + +The source ZIP contains the human-readable application, tests, locked dependencies, and build instructions. Signing keys are excluded from downloads. + +## Validation + +Eight unit tests, Chrome and Firefox builds, Firefox lint, and the real Chromium extension integration suite. The suite covers font propagation into the extension interface, uploads, exceptions, overrides, pause/restoration, backups, reset, keyboard focus, theme controls, startup recovery, local symbols, reduced motion, and native popup sizing. + +Layout, text contrast, and control targets are checked at 1240, 768, 390, and 320px in both themes. Firefox lint retains the previously documented Lit dependency warning; Firefox interactive behaviour and physical mobile keyboards were not tested. diff --git a/package-lock.json b/package-lock.json index 1a6b8d3..dbe41ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fontify", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fontify", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@m3e/web": "2.8.2", diff --git a/package.json b/package.json index 7b811e7..59e6fbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fontify", - "version": "1.1.0", + "version": "1.1.1", "private": true, "type": "module", "description": "Your web, your type. Expressive font control for Chrome and Firefox.", diff --git a/public/symbols/LICENSE b/public/symbols/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/public/symbols/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/public/symbols/block-0.svg b/public/symbols/block-0.svg new file mode 100644 index 0000000..a5e0afb --- /dev/null +++ b/public/symbols/block-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/block-1.svg b/public/symbols/block-1.svg new file mode 100644 index 0000000..a5e0afb --- /dev/null +++ b/public/symbols/block-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/collections_bookmark-0.svg b/public/symbols/collections_bookmark-0.svg new file mode 100644 index 0000000..d31dcaa --- /dev/null +++ b/public/symbols/collections_bookmark-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/collections_bookmark-1.svg b/public/symbols/collections_bookmark-1.svg new file mode 100644 index 0000000..b3d5979 --- /dev/null +++ b/public/symbols/collections_bookmark-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/settings-0.svg b/public/symbols/settings-0.svg new file mode 100644 index 0000000..5f611c5 --- /dev/null +++ b/public/symbols/settings-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/settings-1.svg b/public/symbols/settings-1.svg new file mode 100644 index 0000000..0a59084 --- /dev/null +++ b/public/symbols/settings-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/text_fields-0.svg b/public/symbols/text_fields-0.svg new file mode 100644 index 0000000..5cd2463 --- /dev/null +++ b/public/symbols/text_fields-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/text_fields-1.svg b/public/symbols/text_fields-1.svg new file mode 100644 index 0000000..5cd2463 --- /dev/null +++ b/public/symbols/text_fields-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/tune-0.svg b/public/symbols/tune-0.svg new file mode 100644 index 0000000..4b10702 --- /dev/null +++ b/public/symbols/tune-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/symbols/tune-1.svg b/public/symbols/tune-1.svg new file mode 100644 index 0000000..4b10702 --- /dev/null +++ b/public/symbols/tune-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ui.css b/public/ui.css index 6421a6c..f9ce77e 100644 --- a/public/ui.css +++ b/public/ui.css @@ -5,8 +5,9 @@ font-display: swap; } :root { - font-family: Outfit, system-ui, sans-serif; - font-synthesis: none; + font-family: var(--fontify-regular-family, Outfit, system-ui, sans-serif); + font-weight: var(--fontify-regular-weight, 400); + font-synthesis: weight style; color-scheme: dark; background: #141217; color: #ece6f0; @@ -26,7 +27,26 @@ textarea { button { cursor: pointer; } +h1, +h2, +h3, +strong, +b { + font-family: var(--fontify-bold-family, inherit); + font-weight: var(--fontify-bold-weight, 700); +} +em, +i { + font-family: var(--fontify-italic-family, inherit); + font-weight: var(--fontify-italic-weight, 400); +} m3e-theme { + font-family: var(--fontify-regular-family, Outfit, system-ui, sans-serif); + font-weight: var(--fontify-regular-weight, 400); + --m3e-button-label-text-font-weight: var(--fontify-regular-weight, 400); + --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); @@ -58,32 +78,30 @@ p { } h1 { font-size: clamp(38px, 5vw, 58px); - font-weight: 650; + font-weight: var(--fontify-bold-weight, 700); letter-spacing: -2px; line-height: 1.08; } h2 { font-size: 24px; letter-spacing: -0.6px; - font-weight: 600; + font-weight: var(--fontify-bold-weight, 700); } h3 { font-size: 19px; - font-weight: 550; + font-weight: var(--fontify-bold-weight, 700); } .muted { color: var(--md-sys-color-on-surface-variant); 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; + max-width: 1360px; margin: auto; padding: 30px 40px; } @@ -95,6 +113,7 @@ h3 { margin-bottom: 38px; } .brand { + min-height: 44px; display: flex; align-items: center; gap: 11px; @@ -108,14 +127,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; @@ -127,37 +138,100 @@ h3 { } .layout { display: grid; - grid-template-columns: 195px 1fr; - gap: 38px; + grid-template-columns: 260px minmax(0, 1fr); + gap: 40px; +} +.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; + gap: 10px; } .nav button { + min-height: 76px; display: flex; align-items: center; - gap: 13px; - padding: 16px 18px; + gap: 14px; + padding: 12px 16px; + font-size: 17px; text-align: left; background: transparent; border: 0; color: var(--md-sys-color-on-surface-variant); border-radius: 24px; transition: - background 150ms, - border-radius 150ms; + border-radius 260ms cubic-bezier(0.2, 0.8, 0.2, 1), + translate 260ms cubic-bezier(0.2, 0.8, 0.2, 1); } .nav button[aria-current="page"] { background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); - font-weight: 600; - border-radius: 28px 12px 28px 28px; + font-family: var(--fontify-bold-family, inherit); + font-weight: var(--fontify-bold-weight, 700); + border-radius: 32px 16px 32px 32px; } -.nav button:hover { +.nav button:hover:not([aria-current="page"]) { background: var(--md-sys-color-surface-container-high); } +.nav button:active { + border-radius: 18px; +} +.nav-icon { + width: 48px; + height: 48px; + display: grid; + place-items: center; + flex: none; + border-radius: 16px; + transition: + border-radius 280ms cubic-bezier(0.2, 0.8, 0.2, 1), + transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1); +} +.symbol { + grid-area: 1 / 1; + display: block; + width: 30px; + height: 30px; + background: currentColor; + mask: var(--symbol) center / contain no-repeat; + transition: opacity 180ms; +} +.symbol-filled { + opacity: 0; +} +.nav button[aria-current="page"] .nav-icon { + border-radius: 24px; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); +} +.nav button[aria-current="page"] .symbol-filled { + opacity: 1; +} +.nav button[aria-current="page"] .symbol-outline { + opacity: 0; +} +.nav button:hover .nav-icon { + transform: scale(1.08); + border-radius: 24px; +} +.nav button:active .nav-icon { + transform: scale(0.94); + border-radius: 14px; +} .nav-note { padding: 22px 16px; margin-top: 30px; @@ -174,19 +248,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 +265,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 +275,7 @@ h3 { margin: 14px 0; } .font-choice { + min-height: 44px; display: flex; justify-content: space-between; align-items: center; @@ -225,6 +287,7 @@ h3 { color: var(--md-sys-color-on-secondary-container); text-align: left; gap: 8px; + transition: border-radius 220ms cubic-bezier(0.2, 0.8, 0.2, 1); } .font-choice span { overflow: hidden; @@ -232,7 +295,12 @@ h3 { white-space: nowrap; } .font-choice:hover { - filter: brightness(1.07); + border-radius: 24px; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); +} +.font-choice:active { + border-radius: 12px; } label { display: block; @@ -252,7 +320,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 +328,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 +344,6 @@ textarea { } .preview .eyebrow { color: inherit; - opacity: 0.7; } .preview h2 { font-size: 35px; @@ -292,13 +359,17 @@ 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; + font-size: 12px; } .settings-row { display: flex; @@ -356,18 +427,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 +480,6 @@ textarea { } .override-panel .muted { color: inherit; - opacity: 0.8; } .override-panel h2 { margin: 8px 0; @@ -423,7 +488,6 @@ textarea { margin: 16px 0; padding-left: 14px; border-left: 3px solid currentColor; - opacity: 0.8; overflow-wrap: anywhere; } .override-panel .row { @@ -464,6 +528,7 @@ textarea { flex-direction: column; overflow: hidden; box-shadow: 0 12px 50px #0003; + animation: picker-enter 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } .picker-head { padding: 24px 24px 16px; @@ -485,6 +550,11 @@ textarea { color: inherit; padding: 8px 13px; font-size: 12px; + min-height: 44px; + transition: border-radius 220ms; +} +.chip:active { + border-radius: 12px; } .chip[aria-pressed="true"] { background: var(--md-sys-color-secondary-container); @@ -519,6 +589,7 @@ textarea { display: block; font-size: 18px; font-weight: 500; + overflow-wrap: anywhere; } .font-result .choose small { display: block; @@ -533,9 +604,16 @@ 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; + transition: border-radius 220ms; +} +.icon-btn:hover { + border-radius: 15px; +} +.icon-btn:active { + border-radius: 10px; } .icon-btn.active { background: var(--md-sys-color-primary-container); @@ -551,14 +629,17 @@ textarea { gap: 12px; } .popup { - width: 390px; + width: min(520px, 100vw); min-height: 540px; } +.toolbar-popup { + width: 520px; +} .popup m3e-theme { min-height: 540px; } .popup .shell { - padding: 22px; + padding: 24px; } .popup .topbar { margin-bottom: 22px; @@ -567,14 +648,14 @@ textarea { font-size: 26px; } .popup h1 { - font-size: 39px; - letter-spacing: -1.5px; + font-size: 30px; + letter-spacing: -0.8px; } .popup .hero { - margin-bottom: 20px; + margin-bottom: 16px; } .popup .hero .muted { - font-size: 13px; + font-size: 14px; margin-top: 8px; } .popup .panel { @@ -582,7 +663,7 @@ textarea { border-radius: 22px; } .popup .preview { - padding: 22px; + padding: 20px; margin: 15px 0; } .popup .preview h2 { @@ -592,6 +673,10 @@ textarea { .popup .preview p { font-size: 14px; } +.popup .preview strong, +.popup .preview em { + margin-top: 8px; +} .popup .preview small { margin-top: 12px; } @@ -606,7 +691,7 @@ textarea { padding: 10px; } .popup .picker { - max-height: 560px; + max-height: min(560px, calc(100dvh - 20px)); border-radius: 24px; } .popup .picker-head { @@ -624,6 +709,9 @@ textarea { .popup .settings-row { padding: 12px 0; } +.popup .settings-row > div { + flex-basis: 120px; +} .footer-credit { font-size: 11px; letter-spacing: 0.6px; @@ -636,7 +724,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; @@ -664,6 +755,7 @@ textarea { .nav button { flex: none; padding: 12px 16px; + min-height: 64px; } .nav-note { display: none; @@ -673,7 +765,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 +786,101 @@ 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; + } + .nav-icon { + width: 36px; + height: 36px; + border-radius: 14px; + } + .symbol { + width: 26px; + height: 26px; + } + .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: 28px; + } +} +@keyframes picker-enter { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} @media (prefers-reduced-motion: reduce) { * { transition: none !important; diff --git a/scripts/build.mjs b/scripts/build.mjs index ef22708..7f573a9 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -44,7 +44,10 @@ for (const browser of ["chrome", "firefox"]) { await build({ entryPoints: ["src/background.js", "src/content.js", "src/ui.js"], bundle: true, - define: { GOOGLE_FONT_CATEGORIES: JSON.stringify(categories) }, + define: { + GOOGLE_FONT_CATEGORIES: JSON.stringify(categories), + FONTIFY_VERSION: JSON.stringify(version), + }, outdir, format: "iife", target: ["chrome120", "firefox140"], diff --git a/src/ui.js b/src/ui.js index 4508429..6dce70e 100644 --- a/src/ui.js +++ b/src/ui.js @@ -8,6 +8,10 @@ import { normalise, DEFAULTS, matchesSite, alias, fontKey } from "./model.js"; import { customList, importFont, fontDB, loadFace } from "./fonts.js"; const popup = document.body.dataset.page === "popup"; document.body.classList.toggle("popup", popup); +document.body.classList.toggle( + "toolbar-popup", + popup && api.extension.getViews({ type: "popup" }).includes(window), +); const app = document.getElementById("app"); let s = normalise(), catalogue = [], @@ -28,19 +32,33 @@ 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"], - ["exceptions", "⊘", "Exceptions"], - ["rules", "↳", "Overrides"], - ["preferences", "⚙", "Preferences"], + ["typography", "text_fields", "Typography"], + ["library", "collections_bookmark", "Font library"], + ["exceptions", "block", "Exceptions"], + ["rules", "tune", "Overrides"], + ["preferences", "settings", "Preferences"], ]; +function navIcon(name) { + return html``; +} function tell(message) { toast = message; clearTimeout(toastTimer); @@ -50,7 +68,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 +160,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 +214,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 +277,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 +349,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 +360,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 +379,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 +442,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 +460,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" @@ -453,7 +478,7 @@ function popupView() { Regular typeface${fontButton("regular", s.regular.family)}
- ${specimen()}${activeSite + ${activeSite ? html`
THIS WEBSITE @@ -476,12 +501,13 @@ function popupView() { Browser settings, stores and other protected pages cannot be restyled.

`} `; +
+ ${specimen()}`; } async function upload(event) { const files = [...event.target.files]; @@ -531,19 +557,18 @@ async function removeCustom(font) { } function library() { return html`${hero( - "Find your type.", - "An entire world of letterforms. All Google Fonts, and the ones you bring along.", - "Fg", + "Font library", + "Browse Google Fonts or add font files from your device.", )}
-

${catalogue.length.toLocaleString()} families. One you.

+

${catalogue.length.toLocaleString()} Google Fonts families

- Search by name, explore a category, or keep your favourites close. + Search by name, filter by category, and save favourites.

- ${btn("Explore fonts", (e) => openPicker("regular", e), "filled")} + ${btn("Choose regular font", (e) => openPicker("regular", e), "filled")}

Choosing a font here sets your regular typeface. Google fonts download @@ -554,7 +579,7 @@ function library() {

Your own collection

-

Drop in a little personality.

+

Add font files

TTF or OTF · Up to 15 MB per file · Multiple files welcome

@@ -593,9 +618,9 @@ function library() {
`, ) : empty( - "A collection waiting to happen.", + "No uploaded fonts yet", "Add a font file to make it available everywhere in Fontify.", - "◈", + "Aa", )}
`; @@ -626,9 +651,8 @@ async function addException() { } function exceptions() { return html`${hero( - "Some things stay.", + "Exceptions", "Leave favourite sites, specific fonts, and selected page elements exactly as they are.", - "Ø", )}

Add an exception

@@ -717,7 +741,7 @@ async function saveRule() { weight: ruleWeight, label: request.text, }); - }, "Override saved — return to your page to see it"); + }, "Override saved. Return to your page to see it."); await api.storage.local.remove(`request:${requestId}`); request = null; history.replaceState(null, "", "options.html"); @@ -727,7 +751,7 @@ async function saveRule() { function composer() { return request ? html`
-

MAKE IT YOURS

+

Selected text

Override ${request.family}

Selected on ${request.site}

${request.text}
@@ -802,9 +826,8 @@ function composer() { } function rules() { return html`${hero( - "A few personal touches.", + "Overrides", "Fine-tune a font family or a single block. Your latest matching override takes priority.", - "↳", )}${composer()}

Saved overrides

@@ -845,7 +868,7 @@ function rules() {
`, ) : empty( - "The details are yours to decide.", + "No saved overrides yet", "Select some text, right-click, and choose whether to replace its font family or just that text or code block.", "↳", )} @@ -854,10 +877,12 @@ function rules() { } let editing = null; function editingRule(rule) { + editTrigger = document.activeElement; editing = rule; ruleFamily = rule.family; ruleWeight = rule.weight; draw(); + focusDialog(); } function editDialog() { return editing @@ -898,18 +923,10 @@ function editDialog() { rule.weight = ruleWeight; } }, "Override updated"); - editing = null; - draw(); + closeEditor(); }, "filled", - )}${btn( - "Cancel", - () => { - editing = null; - draw(); - }, - "text", - )} + )}${btn("Cancel", closeEditor, "text")}
@@ -987,9 +1004,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 +1055,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,9 +1094,10 @@ function preferences() {

-

Made for your eyes. By XDan.

+

About Fontify

- Fontify 1.1.0 · MIT licensed · Built with matraic’s M3E components. + Fontify ${FONTIFY_VERSION} · MIT licensed · Built with matraic’s M3E + components.

No analytics. No accounts. No browsing history stored. Your settings and @@ -1155,7 +1172,8 @@ function pickerDialog() {

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