From 2ffd41accc0d0125b6dde0f901fba874bf4f30ff Mon Sep 17 00:00:00 2001 From: "noz.am" Date: Sat, 26 Sep 2026 16:29:23 +0200 Subject: [PATCH] 2.0 update --- .gitignore | 5 +- PRIVACY.md | 102 + README.md | 49 +- branding/README.md | 33 + branding/extension-icons/icon128.png | Bin 0 -> 9046 bytes branding/extension-icons/icon16.png | Bin 0 -> 6300 bytes branding/extension-icons/icon32.png | Bin 0 -> 6765 bytes branding/extension-icons/icon48.png | Bin 0 -> 7105 bytes branding/png/bskyplus-lockup-white.png | Bin 0 -> 40424 bytes branding/png/bskyplus-lockup.png | Bin 0 -> 46657 bytes branding/png/bskyplus-symbol-1024.png | Bin 0 -> 38612 bytes branding/png/bskyplus-symbol-128.png | Bin 0 -> 9046 bytes branding/png/bskyplus-symbol-16.png | Bin 0 -> 6300 bytes branding/png/bskyplus-symbol-24.png | Bin 0 -> 6537 bytes branding/png/bskyplus-symbol-256.png | Bin 0 -> 12092 bytes branding/png/bskyplus-symbol-32.png | Bin 0 -> 6765 bytes branding/png/bskyplus-symbol-48.png | Bin 0 -> 7105 bytes branding/png/bskyplus-symbol-512.png | Bin 0 -> 20625 bytes branding/png/bskyplus-symbol-64.png | Bin 0 -> 7498 bytes branding/png/bskyplus-wordmark-white.png | Bin 0 -> 29084 bytes branding/png/bskyplus-wordmark.png | Bin 0 -> 35243 bytes branding/svg/bskyplus-lockup-white.svg | 1 + branding/svg/bskyplus-lockup.svg | 1 + branding/svg/bskyplus-symbol-small.svg | 1 + branding/svg/bskyplus-symbol.svg | 1 + branding/svg/bskyplus-wordmark-white.svg | 1 + branding/svg/bskyplus-wordmark.svg | 1 + extension/chrome/background.js | 6 +- extension/chrome/content.css | 6 +- extension/chrome/content.js | 228 +- extension/chrome/icons/icon128.png | Bin 7567 -> 9046 bytes extension/chrome/icons/icon16.png | Bin 732 -> 6300 bytes extension/chrome/icons/icon48.png | Bin 2741 -> 7105 bytes extension/chrome/manifest.json | 31 +- extension/chrome/popup/popup.css | 443 +++- extension/chrome/popup/popup.html | 87 +- extension/chrome/popup/popup.js | 157 +- extension/chrome/shared/account-age.js | 22 +- extension/chrome/shared/cdn-background.js | 10 +- extension/chrome/shared/cdn-config.js | 3 +- extension/chrome/shared/cdn-settings.js | 123 +- extension/chrome/shared/composer.js | 29 +- extension/chrome/shared/dom-utils.js | 100 +- extension/chrome/shared/downloader.js | 48 +- extension/chrome/shared/emoji-picker.js | 12 +- extension/chrome/shared/feature-flags.js | 14 +- extension/chrome/shared/features.css | 366 +-- extension/chrome/shared/feed-width.js | 166 +- .../shared/integrated-settings-rules.json | 24 + .../chrome/shared/integrated-settings.css | 49 + .../chrome/shared/integrated-settings.js | 194 +- extension/chrome/shared/llm-settings.css | 164 +- extension/chrome/shared/llm-settings.js | 332 ++- extension/chrome/shared/llm.js | 126 +- extension/chrome/shared/media-viewer.js | 352 ++- extension/chrome/shared/settings-frame.css | 24 +- extension/chrome/shared/settings-frame.html | 1 + extension/chrome/shared/settings-frame.js | 29 +- extension/chrome/shared/thread-unroll.js | 769 ++----- extension/firefox/background.js | 6 +- extension/firefox/content.js | 117 +- extension/firefox/icons/icon128.png | Bin 7567 -> 9046 bytes extension/firefox/icons/icon16.png | Bin 732 -> 6300 bytes extension/firefox/icons/icon48.png | Bin 2741 -> 7105 bytes .../firefox/integrated-settings-route.js | 6 + extension/firefox/manifest.json | 42 +- extension/firefox/popup/popup.css | 443 +++- extension/firefox/popup/popup.html | 87 +- extension/firefox/popup/popup.js | 157 +- extension/firefox/shared/account-age.js | 22 +- extension/firefox/shared/cdn-background.js | 10 +- extension/firefox/shared/cdn-config.js | 3 +- extension/firefox/shared/cdn-settings.js | 123 +- extension/firefox/shared/composer.js | 29 +- extension/firefox/shared/dom-utils.js | 100 +- extension/firefox/shared/downloader.js | 48 +- extension/firefox/shared/emoji-picker.js | 12 +- extension/firefox/shared/feature-flags.js | 14 +- extension/firefox/shared/features.css | 366 +-- extension/firefox/shared/feed-width.js | 166 +- .../shared/integrated-settings-rules.json | 24 + .../firefox/shared/integrated-settings.css | 49 + .../firefox/shared/integrated-settings.js | 194 +- extension/firefox/shared/llm-settings.css | 164 +- extension/firefox/shared/llm-settings.js | 332 ++- extension/firefox/shared/llm.js | 126 +- extension/firefox/shared/media-viewer.js | 352 ++- extension/firefox/shared/settings-frame.css | 24 +- extension/firefox/shared/settings-frame.html | 1 + extension/firefox/shared/settings-frame.js | 29 +- extension/firefox/shared/thread-unroll.js | 769 ++----- extension/safari/background.js | 6 +- extension/safari/content.js | 133 +- extension/safari/icons/icon128.png | Bin 7567 -> 9046 bytes extension/safari/icons/icon16.png | Bin 732 -> 6300 bytes extension/safari/icons/icon48.png | Bin 2741 -> 7105 bytes extension/safari/manifest.json | 31 +- extension/safari/popup/popup.css | 443 +++- extension/safari/popup/popup.html | 87 +- extension/safari/popup/popup.js | 157 +- extension/safari/shared/account-age.js | 22 +- extension/safari/shared/cdn-background.js | 10 +- extension/safari/shared/cdn-config.js | 3 +- extension/safari/shared/cdn-settings.js | 123 +- extension/safari/shared/composer.js | 29 +- extension/safari/shared/dom-utils.js | 100 +- extension/safari/shared/downloader.js | 48 +- extension/safari/shared/emoji-picker.js | 12 +- extension/safari/shared/feature-flags.js | 14 +- extension/safari/shared/features.css | 366 +-- extension/safari/shared/feed-width.js | 166 +- .../shared/integrated-settings-rules.json | 24 + .../safari/shared/integrated-settings.css | 49 + .../safari/shared/integrated-settings.js | 194 +- extension/safari/shared/llm-settings.css | 164 +- extension/safari/shared/llm-settings.js | 332 ++- extension/safari/shared/llm.js | 126 +- extension/safari/shared/media-viewer.js | 352 ++- extension/safari/shared/settings-frame.css | 24 +- extension/safari/shared/settings-frame.html | 1 + extension/safari/shared/settings-frame.js | 29 +- extension/safari/shared/thread-unroll.js | 769 ++----- extension/shared/account-age.js | 22 +- extension/shared/appview-config.js | 42 + extension/shared/appview-page.js | 130 ++ extension/shared/appview-settings.js | 168 ++ extension/shared/appview.js | 88 + extension/shared/bookmarks-page.js | 1197 ++++++++++ extension/shared/bookmarks-settings.js | 134 ++ extension/shared/bookmarks.css | 355 +++ extension/shared/bookmarks.js | 45 + extension/shared/cdn-background.js | 10 +- extension/shared/cdn-config.js | 3 +- extension/shared/cdn-permissions.html | 28 + extension/shared/cdn-permissions.js | 101 + extension/shared/cdn-settings.js | 123 +- extension/shared/composer.js | 29 +- extension/shared/dom-utils.js | 100 +- extension/shared/downloader.js | 48 +- extension/shared/edit-post-page.js | 497 ++++ extension/shared/edit-post.css | 221 ++ extension/shared/edit-post.js | 30 + extension/shared/emoji-picker.js | 12 +- extension/shared/feature-flags.js | 14 +- extension/shared/features.css | 366 +-- extension/shared/feed-width.js | 166 +- extension/shared/hide-liked-by.js | 39 + .../shared/integrated-settings-rules.json | 24 + extension/shared/integrated-settings.css | 49 + extension/shared/integrated-settings.js | 194 +- extension/shared/llm-permissions.html | 27 + extension/shared/llm-permissions.js | 119 + extension/shared/llm-settings.css | 164 +- extension/shared/llm-settings.js | 332 ++- extension/shared/llm.js | 126 +- extension/shared/media-viewer.js | 352 ++- extension/shared/popup-theme.js | 249 ++ extension/shared/settings-frame.css | 24 +- extension/shared/settings-frame.html | 1 + extension/shared/settings-frame.js | 29 +- extension/shared/theme-editor.css | 450 ++++ extension/shared/theme-editor.js | 805 +++++++ extension/shared/theme-model.js | 540 +++++ extension/shared/theme-runtime.js | 407 ++++ extension/shared/thread-unroll.css | 137 ++ extension/shared/thread-unroll.js | 769 ++----- package-lock.json | 4 +- package.json | 13 +- pnpm-lock.yaml | 2043 +++++++++++++++++ pnpm-workspace.yaml | 3 + scripts/package_firefox.mjs | 1 + scripts/sync_shared.mjs | 1 + site/assets/bskyplus-symbol-512.png | Bin 0 -> 20625 bytes site/assets/bskyplus-symbol-small.svg | 1 + site/assets/og-card.png | Bin 0 -> 29209 bytes site/assets/shot-emoji.webp | Bin 0 -> 30138 bytes site/index.html | 225 ++ test/test_appview.mjs | 208 ++ test/test_bookmarks.mjs | 603 +++++ test/test_bookmarks_session_fallback.mjs | 116 + test/test_cdn.mjs | 24 +- test/test_cdn_permissions.mjs | 116 + test/test_cdn_settings.mjs | 62 +- test/test_default_feed_width.mjs | 16 +- test/test_dom_utils.mjs | 99 + test/test_e2e_all_features.mjs | 276 +++ test/test_edit_post.mjs | 427 ++++ test/test_extension_composer.mjs | 34 +- test/test_extension_llm_visibility.mjs | 18 +- test/test_extension_settings.mjs | 247 +- test/test_fetch_interceptors.mjs | 96 + test/test_firefox_accent.mjs | 169 -- test/test_firefox_build.mjs | 3 +- test/test_integrated_settings.mjs | 77 +- test/test_llm.mjs | 211 +- test/test_llm_permissions.mjs | 155 ++ test/test_llm_settings_save.mjs | 185 +- test/test_modal_ocr.mjs | 42 + test/test_new_features.mjs | 139 +- test/test_notification_width.mjs | 124 + test/test_popup_bookmarks.mjs | 66 + test/test_popup_flags.mjs | 19 +- test/test_popup_themes.mjs | 186 ++ test/test_settings_frame.mjs | 200 ++ test/test_settings_theme.mjs | 193 ++ test/test_theme_editor.mjs | 919 ++++++++ test/test_theme_model.mjs | 375 +++ test/test_theme_runtime.mjs | 317 +++ test/test_thread_unroll.mjs | 232 ++ test/test_video_controls.mjs | 278 ++- 210 files changed, 22575 insertions(+), 6453 deletions(-) create mode 100644 PRIVACY.md create mode 100644 branding/README.md create mode 100644 branding/extension-icons/icon128.png create mode 100644 branding/extension-icons/icon16.png create mode 100644 branding/extension-icons/icon32.png create mode 100644 branding/extension-icons/icon48.png create mode 100644 branding/png/bskyplus-lockup-white.png create mode 100644 branding/png/bskyplus-lockup.png create mode 100644 branding/png/bskyplus-symbol-1024.png create mode 100644 branding/png/bskyplus-symbol-128.png create mode 100644 branding/png/bskyplus-symbol-16.png create mode 100644 branding/png/bskyplus-symbol-24.png create mode 100644 branding/png/bskyplus-symbol-256.png create mode 100644 branding/png/bskyplus-symbol-32.png create mode 100644 branding/png/bskyplus-symbol-48.png create mode 100644 branding/png/bskyplus-symbol-512.png create mode 100644 branding/png/bskyplus-symbol-64.png create mode 100644 branding/png/bskyplus-wordmark-white.png create mode 100644 branding/png/bskyplus-wordmark.png create mode 100644 branding/svg/bskyplus-lockup-white.svg create mode 100644 branding/svg/bskyplus-lockup.svg create mode 100644 branding/svg/bskyplus-symbol-small.svg create mode 100644 branding/svg/bskyplus-symbol.svg create mode 100644 branding/svg/bskyplus-wordmark-white.svg create mode 100644 branding/svg/bskyplus-wordmark.svg create mode 100644 extension/shared/appview-config.js create mode 100644 extension/shared/appview-page.js create mode 100644 extension/shared/appview-settings.js create mode 100644 extension/shared/appview.js create mode 100644 extension/shared/bookmarks-page.js create mode 100644 extension/shared/bookmarks-settings.js create mode 100644 extension/shared/bookmarks.css create mode 100644 extension/shared/bookmarks.js create mode 100644 extension/shared/cdn-permissions.html create mode 100644 extension/shared/cdn-permissions.js create mode 100644 extension/shared/edit-post-page.js create mode 100644 extension/shared/edit-post.css create mode 100644 extension/shared/edit-post.js create mode 100644 extension/shared/hide-liked-by.js create mode 100644 extension/shared/llm-permissions.html create mode 100644 extension/shared/llm-permissions.js create mode 100644 extension/shared/popup-theme.js create mode 100644 extension/shared/theme-editor.css create mode 100644 extension/shared/theme-editor.js create mode 100644 extension/shared/theme-model.js create mode 100644 extension/shared/theme-runtime.js create mode 100644 extension/shared/thread-unroll.css create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 site/assets/bskyplus-symbol-512.png create mode 100644 site/assets/bskyplus-symbol-small.svg create mode 100644 site/assets/og-card.png create mode 100644 site/assets/shot-emoji.webp create mode 100644 site/index.html create mode 100644 test/test_appview.mjs create mode 100644 test/test_bookmarks.mjs create mode 100644 test/test_bookmarks_session_fallback.mjs create mode 100644 test/test_cdn_permissions.mjs create mode 100644 test/test_dom_utils.mjs create mode 100644 test/test_edit_post.mjs create mode 100644 test/test_fetch_interceptors.mjs delete mode 100644 test/test_firefox_accent.mjs create mode 100644 test/test_llm_permissions.mjs create mode 100644 test/test_modal_ocr.mjs create mode 100644 test/test_popup_bookmarks.mjs create mode 100644 test/test_popup_themes.mjs create mode 100644 test/test_settings_frame.mjs create mode 100644 test/test_settings_theme.mjs create mode 100644 test/test_theme_editor.mjs create mode 100644 test/test_theme_model.mjs create mode 100644 test/test_theme_runtime.mjs create mode 100644 test/test_thread_unroll.mjs diff --git a/.gitignore b/.gitignore index 22acae2..e1b5ea4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ test/artifacts/* test/fixtures/test_ocr_blank.png dist/ -PRIVACY.md + +extension/chrome +extension/firefox +extension/safari diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..4d318ba --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,102 @@ +# Bsky+ — Privacy Practices & Permission Justifications + +Bsky+ adds browser-side features to Bluesky. Its preferences are kept in the +browser profile, except local bookmark snapshots, which are kept in the +`bsky.app` site database. Some optional features make network requests to +Bluesky, a destination the user selects, or a public repository the user asks +to import. Those requests expose ordinary connection and request metadata to +the contacted service; Bsky+ does not anonymize or proxy general network +traffic. + +## Data stored and sent + +- **Extension settings and themes:** Feature preferences, display settings, + selected AppView/CDN, theme library, and LLM endpoint settings are stored with + the browser extension's `storage.local` API. This storage is local to the + browser profile and is not browser-synced. LLM settings can include an + endpoint URL, model name, API key, and custom prompt. Clearing the endpoint + and saving disables +LLM, but retains the model, key, and prompt. Clear each + field separately and save to remove it. Clearing the endpoint also does not + revoke a previously granted site permission; remove that in browser settings + if desired. +- **Local OCR:** OCR uses the bundled Tesseract.js worker and WebAssembly in the + browser. Bsky+ does not send the image to an OCR provider. The browser may + still retrieve the image from its existing Bluesky image URL. +- **Optional +LLM:** After an endpoint is saved and the user clicks +LLM for an + image, Bsky+ sends that selected image and the active system or custom prompt + to the saved OpenAI-compatible chat/completions URL. If configured, the model + name is included in the request and the API key is sent in an Authorization + header. The request does not add composer post text or other attached images. + HTTP and HTTPS endpoints are accepted; use HTTPS for non-local endpoints. + The endpoint operator may process or retain the request under its own + practices. A localhost endpoint can keep model processing local. +- **Local bookmarks:** When enabled, Bsky+ downloads saved-post views through + the signed-in Bluesky session and stores them in the `bskyplus-bookmarks` + IndexedDB database on the `bsky.app` origin. Snapshots are keyed by account + DID and bookmark AppView. They can include post text, author and embed data, + image descriptions, and media URLs; media files and session credentials are + not persisted. The data is local to that browser profile, is visible to code + running on the `bsky.app` origin, and is not browser-synced. Disabling local + bookmarks disables local caching and clears all account and AppView snapshots + in that browser profile, including caches created with previously selected providers. + Clearing `bsky.app` site data removes the database. +- **AppView and image CDN:** An alternative AppView receives the feed and social + API traffic routed to it. Authenticated requests remain addressed to the + account's PDS with an `atproto-proxy` header naming the selected AppView; + public GET requests to a known AppView may be sent directly to that host + without browser credentials. Bsky+ does not redirect account settings, + posting, chat, or video-upload requests to an alternative AppView. The + optional image CDN redirects only Bluesky feed-thumbnail, full-size, and + avatar image URLs to the selected HTTPS hostname. Operators of the PDS, + AppView, original CDN, or selected CDN receive ordinary metadata for requests + they handle. +- **WitchSky theme import and post editing:** Importing a public WitchSky theme + resolves the DID (`plc.directory` for `did:plc`, or the named host for + `did:web`) and fetches its public theme record from the repository's PDS. + These requests omit browser credentials and the referrer. The imported theme + is saved locally. When the user saves an edited post, Bsky+ uses the current + Bluesky page session to read the record and submit the changed record to the + signed-in account's PDS. It does not store a copy of the session token. +- **Page features:** Features such as theme rendering and thread unrolling + change the Bluesky page in the browser. Bsky+ has no analytics endpoint or + developer-operated data collection server. Its JavaScript and OCR code are + packaged with the extension; responses from LLMs and public theme services + are handled as data, not executed as remote code. + +## Chrome permission justifications + +The Chrome manifest explicitly declares `https://bsky.app/*` in +`host_permissions`. Its content scripts separately match `*://bsky.app/*` and +`*://*.bsky.app/*` so the page features can run on Bluesky hosts. These scripts +add composer and settings controls, apply page preferences, and support +bookmarks, post editing, AppView routing, and display features. + +- **`storage`:** Saves extension settings and preferences in `storage.local`. +- **`offscreen`:** Hosts the bundled Tesseract OCR worker when the user runs + local OCR; it does not provide a remote OCR service. +- **`declarativeNetRequestWithHostAccess`:** Applies packaged settings-page + redirects and the optional image-CDN redirect rule. The CDN rule is limited + to the listed Bluesky image URL paths and runs only while the user-selected + source and destination host permissions are granted. +- **Optional HTTP/HTTPS hosts:** The manifest allows Bsky+ to ask for access to + a user-configured LLM endpoint or image CDN when needed. The LLM request goes + to the saved URL; the CDN feature asks for the Bluesky image host and chosen + CDN host. Public theme import can also contact the domains named by the + imported DID and PDS record. + +## Firefox permission justifications + +The Firefox manifest has the same explicit `https://bsky.app/*` host permission +and the same separate content-script match patterns as Chrome. It declares +`storage`, `webRequest`, and `webRequestBlocking`. The two `webRequest` +permissions support the optional image-CDN redirect; its listener is limited to +the feed-thumbnail, full-size, and avatar paths on `cdn.bsky.app`. The manifest +also declares optional HTTP/HTTPS host permissions for user-selected network +destinations such as an LLM endpoint or image CDN. + +Firefox declares no required data-collection types and the optional types +`websiteContent` and `authenticationInfo`. On Firefox desktop 140+ and Android +142+, saving an LLM endpoint requests `websiteContent` consent, plus +`authenticationInfo` if an API key is saved. Bsky+ checks the applicable +consents and endpoint host access before sending each request. Older Firefox +versions request endpoint host access without the built-in data-consent prompt. diff --git a/README.md b/README.md index 452a3c7..8aa0d0a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bsky+ Browser Extension -> Enhanced features for Bluesky (bsky.app) including Local OCR Alt Text, Custom Accent Colors, AI Image Descriptions, Media Downloader, Video Controls, Quick Emoji, Feed Width Slider, Fresh Account Badges, and Thread Reader View. +> Enhanced features for Bluesky (bsky.app) including Local OCR Alt Text, AI Image Descriptions, Media Downloader, Video Controls, Quick Emoji, Feed Width Slider, and Fresh Account Badges. Supports **Google Chrome**, **Mozilla Firefox**, and **Apple Safari**. @@ -16,6 +16,7 @@ Supports **Google Chrome**, **Mozilla Firefox**, and **Apple Safari**. ### 2. Enhanced Media Viewer - Video playback speed controller toolbar (`0.5x`, `1x`, `1.25x`, `1.5x`, `2x`). +- Each new video starts at `1x`; speed choices apply only to the current video. - Native Picture-in-Picture (PiP) toggle for watching videos while scrolling feeds. - Persistent volume and mute memory saved in browser storage across video plays. @@ -35,21 +36,13 @@ Supports **Google Chrome**, **Mozilla Firefox**, and **Apple Safari**. - Efficiently cached in browser **IndexedDB** to avoid repetitive network requests. - Paced and batched background requests (up to 25 accounts per request) with exponential backoff to respect Bluesky rate limits. -### 6. Thread Unroll & Reader View -- **3-Dot Post Menu Integration**: Opens directly from the standard "•••" more options menu on any post in feeds, searches, profile timelines, or thread pages via **📖 Unroll Thread**. -- Also available as a direct **📖 Unroll** action button in thread headers. -- Concatenates the author's connected skeets into a distraction-free reader modal with reading time estimates, full-resolution media, and post numbers. -- One-click **Export to Markdown (`.md`)** with full post hierarchy, links, and alt-text citations. -- Clean **Export to PDF** triggered via `window.print()` with dedicated `@media print` styling that hides distraction elements. -- **Copy Markdown** directly to clipboard. - -### 7. In-Browser Local OCR for Alt Text +### 6. In-Browser Local OCR for Alt Text - Adds **+OCR** beside **+ALT** on each image preview in the post composer. It reads the selected image and saves its alt text directly, without opening the editor. - The existing **"Add with OCR"** action is also available inside the Alt Text modal. - **100% Client-Side / Local**: Runs offline in the browser using Tesseract.js WebAssembly with zero external server calls. - **Toast Notifications**: If an uploaded image contains no text (e.g. blank image or graphic), it immediately displays a floating toast: `"There was no text in the image"`. -### 8. Case-Preserving Skeet Mode +### 7. Case-Preserving Skeet Mode - Automatically replaces all occurrences of the word `"post"` / `"posts"` with `"skeet"` / `"skeets"`. - Strictly maintains character casing: - `POST` → `SKEET` @@ -60,20 +53,28 @@ Supports **Google Chrome**, **Mozilla Firefox**, and **Apple Safari**. - `POSTS` → `SKEETS` - When toggled off, reverts seamlessly without page reload. -### 9. Custom Accent Color & Pride Flag Palette -- Allows overriding Bluesky's official blue accent color (`#1083fe` / `rgb(16, 131, 254)`) across buttons, links, icons, borders, and highlights. -- **7 Predefined Pride Flag Colors** (in exact rainbow order): - 1. `#FF69B4` (Pink) - 2. `#FF0018` (Red) - 3. `#FFA52C` (Orange) - 4. `#FFFF41` (Yellow) - 5. `#008018` (Green) - 6. `#004CFF` (Blue) - 7. `#750787` (Violet) -- Full custom color support via HTML5 color picker and hex input field. +### 8. Theme editor +- Open **Bsky+ settings → Theme editor** (`/bskyplus/theme`) to create a theme from a light, dim, or dark starting palette. +- Customize surfaces, text and icons, borders and focus, links and primary controls, status and reactions, overlays and shadows. +- Changes appear in the preview. Save themes locally, use **Save & apply** to apply one across Bluesky tabs, or restore **Use Bluesky default**. +- Export or import a theme as versioned JSON. Imports are validated and loaded into the preview for review before saving or applying. Theme files contain only the name, ID, base, and colors. +- Import public `app.witchsky.theme.colors` AT records by URI. The base palette and every named variant are saved as separate themes. +- Enable **Preview changes on this page** to try the current draft across the open Bluesky page without saving or applying it. +- Palette mapping covers recognized Bluesky CSS, inline styles, SVGs, and single-color fades. Images, multicolor artwork gradients, and unrecognized colors retain their originals. +- See [the source audit and color groups](docs/theme-color-audit.md) for coverage and limitations. + +### 9. Inline thread unroll +- On a numbered thread beginning with **Post 1 of X**, select **Unroll thread** beside **Thread options** to read its posts together. +- Repeated author details and extra post controls are hidden. Each post keeps its native reply, repost, and like controls in a compact row, along with its text and attachments. +- Select **Restore thread** to return to the normal layout. Unrolling applies only to the current thread. + +### 10. Edit recent posts +- **Edit post** appears automatically in the options menu for your eligible posts; no settings toggle is required. +- Posts can be edited once, within 10 minutes of posting. Eligibility is checked again when saving. +- Existing attachments and the original text are preserved in the replacement record. -### 10. Feature Flags & Release Configuration -All features are governed by a centralized feature flag system in [`extension/shared/feature-flags.js`](file:///Users/david/bskyplus/extension/shared/feature-flags.js): +### 11. Feature Flags & Release Configuration +Configurable features use a centralized feature flag system in [`extension/shared/feature-flags.js`](extension/shared/feature-flags.js): - **Feature Stages**: - `'release'`: Production-ready, visible in settings, enabled by default. - `'alpha'`: In active development. Hidden from regular users on release builds. Accessible only when developer/alpha mode is enabled. diff --git a/branding/README.md b/branding/README.md new file mode 100644 index 0000000..5ccf887 --- /dev/null +++ b/branding/README.md @@ -0,0 +1,33 @@ +# Bsky+ logo + +Symbol: a white cloud with a red plus, on a blue rounded tile ("Cloud+"). + +## Files + +- `svg/bskyplus-symbol.svg`: the main symbol. Use it at 48 px and above. +- `svg/bskyplus-symbol-small.svg`: a variant with a bigger plus and a tighter cloud, for 32 px and below (toolbar, favicon). +- `svg/bskyplus-lockup.svg` / `-white.svg`: symbol + wordmark, for light / dark backgrounds. +- `svg/bskyplus-wordmark.svg` / `-white.svg`: the wordmark on its own. +- `png/`: the same, rendered (symbol from 16 to 1024 px, lockups and wordmarks at 2x). +- `extension-icons/icon16.png`, `icon32.png`, `icon48.png`, `icon128.png`: the same names as `extension/*/icons/`, so they can replace the current icons directly. + +All text in the SVGs is outlined, so no font is needed to display them. + +`npm run sync:shared` and the browser packaging commands copy `extension-icons/` into each browser build. The popup and settings use the 32 px icon. + +## Colours + +| Name | Hex | Use | +|--------|-----------|-----------------------------| +| Blue | `#1185FE` | Tile | +| Red | `#FF2727` | The plus | +| Ink | `#0B1220` | Wordmark on light surfaces | +| White | `#FFFFFF` | Cloud; wordmark on dark | + +## Type + +Wordmark: Inter Black (900), tracking −1.2%. The "+" is always red. + +## Spacing + +In the lockup the symbol is as tall as the font size, the gap is 0.29× the symbol height, and the wordmark's cap height is centred on the symbol. diff --git a/branding/extension-icons/icon128.png b/branding/extension-icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..573fb1f64740f9e84b837668ecf231df4050d9fb GIT binary patch literal 9046 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 literal 0 HcmV?d00001 diff --git a/branding/extension-icons/icon16.png b/branding/extension-icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..a645a730ce364dbc328125dbf879c93d751b2b83 GIT binary patch literal 6300 zcme7R@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!u9>XH+kB0zLjX>Jk&1A|;v zX>LlgQ9&YuAcH^y1H&o?i`j+DU;zdOu?VOtcZjN7R8^%#c~&5mR>sCj#^#A;iMpnS z78bfDX32@VmIfx4x|T^sX2~WgX~~90CJYP=%#)$UX+n%kEXJ@@FR{2dwWuUBKd+d9 zfq`KMRD%mxLsEVbvIb$0-FlgM=|!n2nW=dtdS%8SJrc=A#Tl70&Mv3?r=-tR%3rb1 zV(CK$28NvEr2L}AnJLLuY57IDi6xntxryni`UQFES($moC5d^-sh%zsIu*GER+*V8 zRwm{~=85Ks#=0q~W~RC(MybiVmX@YQx@MM^iG~IyMuw@$X*oryIf-D$W@Hv*6eJd< z=9T!TF)%Pbg1Xxgx4RRQ!5a0-jNpzw>i(3;#=g^KgY)iX|K#r}GcYjD1UY(9I#hAf zqBICA8|?M`g4Dd!l&pfpqQu|DGIrXl?oY&WvL3uMX8A;sVNHiMGCq3DVb@RsVQJ>U?Y(gdHvd3ni&`v8bA@eG&wORJ*_w+(a6*+xgasc z7o-D@p>Z!@LH+`FlWC(uRhYQA)3z>aS*(BVZcnxF!N6pGz%~OhSto)g%-nnYav^nAi-O7<(5mGchtTi8wr6!1eBs<%NAant!o5 zvD&7G3NsjRv2kd%d7QIlVP-b)G!!=wWn&Iy;S%P9yH`gcz{%NA&p-zx!7Qu_mr!tq z>ULy+w3j;GlOEA~K z1-Dh22I>&26p*ZflvFSqgEQ0fGV{_6A`OJu*ul=Z^SQbUnZu_Xk4CB{`bx)8&~EMji-86wYBzLVkV*Up4IO6 z65ngSee&W6gD?X=Hs(-Sen!UsEG*1S>m<$YyWku#O zR$V$B+IPc6&yfB*1l>Ss&Y=%acG!NPy$n0P(X%9yltcgVfV)e)zd1K(dxeP%rW(L>Rk#am5k zE4CLFCjOq4&cMLHmgMd3!tfsi7wla=nSp`vxTlL_h(&MjL|>1PM3J`n#d|ZISH`j* zE!cE%k$_;J(-O1D?v9osWe+p%@W9zluD3cAoZEKSZ9AJF;IuoSU;$%cs)%6Fg#8O5 z9`mOKT3VL9&r}SGd(O9S_2-YD?C$@b>;K|n&gqgW!JUjUsr69~Pfq@_{dJVnf!}{>)*G|31xmI^89&b5oUv100;6Qu21Yifs7HqY0 zZa$^--oRlRT*WY2&M$UL6&eNw2thicRFI-?~WJr!L7Sw*D z!*G0$!twqCJA*1>zOXqdu2`*n&DP<|+)r73=i64;oVk+pPugaFLyJ8}o&A+-MkXtd zq#fAyb>;%CYqeH^dwHi+HJ+Pt;O^!_%R^sCUp_KzG4tzax9{;9J9?}2%$?qYlBuVw KpUXO@geCyX$PEnu literal 0 HcmV?d00001 diff --git a/branding/extension-icons/icon32.png b/branding/extension-icons/icon32.png new file mode 100644 index 0000000000000000000000000000000000000000..d0da83c54f135f8fc37609fe6e8b99579fce536c GIT binary patch literal 6765 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 literal 0 HcmV?d00001 diff --git a/branding/extension-icons/icon48.png b/branding/extension-icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..27d455650f13c3d6465d75a7fe1dd8774dca2849 GIT binary patch literal 7105 zcme7R@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?Ft%ilwY(sGbPz7Ex#x?u_QAyH!(d`zaTF?D>JXSBrz{J)zhUyry{q&Dl;?1 zDk(L^GR?#=QPu$;2pC*W5JOJjo!{B+bwwIj1N!ClTz}jLd?Jg2bZK zyb}L31_s7QP)K)QDSatNorB?!fZrn6gMtPE-lKb&@E2QNmB;7C(RbD3lYNl zU_L^NUYT)9Mq+VBguX<(KDWr=%7q7iAWJf~O+Q8Em2g%tVEfjLc$%w9K4Tg~Sqtl8jV^qSV6D)Z!9_ z{4|9`h0@~GB89}f6ouTxN`;KXvQ&lSqSVBa)D(sMB8A-il+3iu)D*Bbuo21mc_pcN zCB=HV#rbI^<%vb9j_Ij+B@0vX5_40bRu?7a5mL$R>nCZp$TX$Dl+V^HoKj1Gpe-#4*Ljx#+mnJ9Xq^A{UBpR8TB^M;7 z_=0r6F*HtI%hYcA@+16xn?tX;mGj|-P5hf91&`ZafAq#ODSYiSaj>KFK+Z~2$W6@4 zOiL{;$x5wA&M7ShWv9kPsl_FUMI{o7X*sER=_MJGVqFXjOm-j*3=9nFV1F1T=OE`W za5(^q)XZE+I%VQVcW!2JW_n&?Noi4Pg)dS{11Bwgk_rg07CpFeSTkj+v4Mp}l39wb zQF4m8u1ShXs&108sfDhoadM)8QKEUWfuThaG|J-<3B8$ySpj0S7Lwn=xe8%9#9xqn zTdY@R1dYywmtUqTtn1;G^WR>*J5zh@nVLm2T@UIhafm2&u5{sahM22K$Xs|LgBn}2 z-tx!c6Uw%)-M*VX)>eFGcBB5q!3&wZOXZKAt<9^MQ<9vMn3)@&o|>0hlvt8q6rY)w zmcJOBcJduz`B=d%Gbgn;y)3l|lyp)J^$he3vWp8+lf$4QMtUZC1`G@W7SPnA0Zl!{ zn5hRIp$rTR0vw>^bEzegNoWzHnuKB`6PG~~6PrO3WA6fHCPpSE5r?M>xZWMIys&Ra z^Dj0hR@?MYVFm*(HV&;ek8`#x%*+O!hT;aIY|No7T*7>C_v$DFI5`{Y8R&o{n1xm0 z5(>_c9IoJ8l$w&7SCW~SQ>+k_pI@Tj>}Vh-&TC|5U}0cxXklV(Y8EBVYh+NBKpkS00+Lmbk_u*HaAta5W?s5Mq=7ISJJ|V5j8K0tGqN)~F|f?2;>tc9 zAi*kf)nu>JQp;$UmpATS-}KY#jrfJ_%fz%9jmuNY|K2%e3Os zK+1$c%9soo4CFyPWflnou?CTIfsa@k{ZgWpe>^^NewN*wgV$pYA%{3~I)gz9lYwEe ztjIjZs!OLs`_8#Nw4K*AJBQ6Lt^O9%)=&P74ym73+uOL_`usxW`q!ThbDKktF&PxZ z$K3szxGKqOnf{d4)w}^GHC|sgieEC>_IK1>ewKcn!~Ir9>pCl51pf*Ne7M)0b0MhY zi{zaLFZV_nqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Uh0t0^cGWx-U-F6+Uu3 z`9g!lO^-R*T=QSM6#Us&^x-<^e$z6E{T3e2&Y1~YPtd)`RXPvaiib2FRfhYQdKK?>aY1FEfdeXVUkMn0=5Sm zcj&lzrMU*HM<+l~_%V}-H}7bYd{O%(W)q^lCbGV?i`kZYZ3$L52U4u-mn*<&M6}Z`K9tZF5Q5qNwKes?7WF z+of9sPKk3A=2h>mYqd>Jh+cb1{@aQ;CLYNd=i`O0oheIuEmTpj_ry==p52-+t_o{> zEPB1U3w7!&^q(KP_S}80va?bO`=&_?YZ6~5c|ALDxxeL3O~(Q!jYJupDxOWj53brM zo)i6bfT`xknhmcKDkdM?$gEoY;PLa8CqFhElRmg;jQ{B%YP*5k9YwFY#)x);Ey+t(pyOQ>Grw#AkU3_Y0=LQA#W!4o9+xTSmi}Y&R%xcmsSQ7DL ztILnd-U)Lb-M?nG(6g`o^6Tb!$()s|Ki8Ec+<29hk+&~=f8F1`MP+6>@#o^#w*9QG zea^qFedq1i4ZkK7Wh^zwbYeMj{rcfur3p7vj6y?|*IQf7*jT1py!pYEsTZFynt$4S zJ@H>$YP@Ypi?zC5%m(rI-EC|PYU)4M@mZ<+b$#Af$-$;v@JUv2^LpN}H4Ur7=bFwj zU|hB8!k-$hePMd{rE-Nz{A6CO5BN9v)7ecSKBcB6rjGIRwpr#G9F)CLFrLdx!PU znU5P6Z{ytcTOseV8ekc@t>4OS?2&Ak)!lb}s-NfGJX3+i_U2*sobh{0}Yeag~Gf#+g=v4MWSZmjvU&};FMw^zikUV5CYUd$7C zQ(=YIExy0LiuTv4nwDH{_A%{P(VC-Xbu;-%z}?!-F?kXV--=eU-WAODbu75|)Nran zj(o;z(`W%(0c+pB%$Me$qE5RPvA5-?h$e0QIwQ$(f6vS<2j;vt|K#I7Ge&C9!(x5; zh?(CQE-zL)JI^nqe7DWAq7NteAN@~=S@m esb^jz`H%g|tAZJV-<>vs%5qOvKbLh*2~7YFL7U6~ literal 0 HcmV?d00001 diff --git a/branding/png/bskyplus-lockup-white.png b/branding/png/bskyplus-lockup-white.png new file mode 100644 index 0000000000000000000000000000000000000000..6a44e0c4da923964f8b53f3ad5b257bb5eaf30fc GIT binary patch literal 40424 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 PDu_H?{an^LB{Ts5TMC>H literal 0 HcmV?d00001 diff --git a/branding/png/bskyplus-symbol-128.png b/branding/png/bskyplus-symbol-128.png new file mode 100644 index 0000000000000000000000000000000000000000..19f43ed6ad3de88f47420960a1af1a04982cc1aa GIT binary patch literal 9046 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 literal 0 HcmV?d00001 diff --git a/branding/png/bskyplus-symbol-16.png b/branding/png/bskyplus-symbol-16.png new file mode 100644 index 0000000000000000000000000000000000000000..e5a21da80be319ca9577fc378c7ec7fbc9ec659f GIT binary patch literal 6300 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 I>zopr0DLkE1poj5 literal 0 HcmV?d00001 diff --git a/branding/png/bskyplus-symbol-24.png b/branding/png/bskyplus-symbol-24.png new file mode 100644 index 0000000000000000000000000000000000000000..304a958a68e39833eb1f939cb8716d7b32ec24c4 GIT binary patch literal 6537 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 literal 0 HcmV?d00001 diff --git a/branding/png/bskyplus-symbol-48.png b/branding/png/bskyplus-symbol-48.png new file mode 100644 index 0000000000000000000000000000000000000000..495369a38e0953645d62d167e5ab5497a958dd37 GIT binary patch literal 7105 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 literal 0 HcmV?d00001 diff --git a/branding/png/bskyplus-symbol-512.png b/branding/png/bskyplus-symbol-512.png new file mode 100644 index 0000000000000000000000000000000000000000..9003e53db0ddc49d39f66638a9e199a9bc91aa51 GIT binary patch literal 20625 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 iXH+kB0zLjX>Jk&1A|;v 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 dNF{K2J!5O}1;GwmN;32F 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/CZI1ByruCV4tXZzVukZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaDWmD2jvUUV4nFbI62KOt6wAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCB63j1FH5WvljwDgDTjqxfLP+v3F40z5RdkyRra++Po6WRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjEZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmY1OWI4MDdmLTcyZGEtNGNkYi1hYzMyLTlkMDNiZGNiMzBhYy9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmNhYmEyNDgyLTQzMTAtNDdkMy05YzYzLWJkMTRmNjYwZDI2YnJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCB8pSLTL10cZpuM8DObXDfmwbF38JkjUHKu4JXi1dnNW6JjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggFeSg4ckY+MUPhmJEVgtTxgfe/OgWWnAP7197Se9Z52SiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggJYPEqAtQhXMWqtN/QPaHqknOgOR7Ym77P/n5pjfwhe90Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQG51Su1xcaIt3WHx4uKTON9ZfD0tewogXOHIKuavgCqyZZyKbuEZOk5lUsrKZhrS0A9aRdedjub+YRKafeYh6HI=Bsky+ diff --git a/branding/svg/bskyplus-lockup.svg b/branding/svg/bskyplus-lockup.svg new file mode 100644 index 0000000..3636b90 --- /dev/null +++ b/branding/svg/bskyplus-lockup.svg @@ -0,0 +1 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo0ZWJhNzc1OC1iZTc0LTQwOTEtODVmYS1kYjQ0Y2NlMWFmOWYAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaNahYPIkcSJI33aKK+KhxyQAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDoyNDRmMTI3Yi1lNjlkLTQzYzMtYmMyNi02YWI5OTg2YzkwMWZscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoEOnfYPOgNrpWjzKxow62swAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggwvG3MtzpbCzV33f9kOF6ovyGqoQoumNXl1sEDM7hI02kZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaK3rIWOAwB39CvXfxU6FlIoAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBZQOMuzVfGogdcGnq/UyA0ni2fJ1XMml4MYcdgDOpadmRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjEZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjRlYmE3NzU4LWJlNzQtNDA5MS04NWZhLWRiNDRjY2UxYWY5Zi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmM3NWY5NTJmLTkxNmEtNDkwNi1hOTE2LTY2MTM5NDNmODUwNnJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCDC8bcy3OlsLNXfd/2Q4Xqi/IaqhCi6Y1eXWwQMzuEjTaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggoQzDNNpNys4h8/b0iKL/MqKNic3BTEMCPVeWmUIJyxqiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggv3LiN6efcn0JM0KwlFHuvSvYI0qdMPCMIunjDXG0rWN0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQPzqiHle8yzyAMESKZDP/B7IL0BLV+QKQeEj3uXocRq00Gg9xGs/++YK8a/7UwZyEFpaZlEWQgbY1+m20ks9p90=Bsky+ diff --git a/branding/svg/bskyplus-symbol-small.svg b/branding/svg/bskyplus-symbol-small.svg new file mode 100644 index 0000000..08f8bf1 --- /dev/null +++ b/branding/svg/bskyplus-symbol-small.svg @@ -0,0 +1 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTowOTQyYmJlMi0xMGMzLTQ3ZmYtYWQ3ZS1jYjIxZDFlMzg1MTQAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaHwQ8aigPnZQ0C09scibkekAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplMzE0ODQ5Mi01ODI3LTRmMzktOWNjMS0xN2IxZTE0ZDUyZWJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoqxBuWA98IZJAnGA2gXT5VwAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggtqonwEIXOPv4aQchUzc7EVW42GOTqqZUqVrw1xK53USkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaBFYNeh02+V5hZYq/XfbD+wAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCi6LK17yFbk9BmSvGQ9ULC094/0cprDY/RNbwsmLdkaWRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjJZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjA5NDJiYmUyLTEwYzMtNDdmZi1hZDdlLWNiMjFkMWUzODUxNC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmY4MzI3YWU2LWQ0MGItNGY5Ny05OTNhLTA3MTg2M2ExMTZlYXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCC2qifAQhc4+/hpByFTNzsRVbjYY5OqplSpWvDXErndRKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggvmZ+TYtoi3pAaP5J2AJj4qNEAy8A8PJOuowtfAb5q+miY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggPG9yd0QPakgE0puo7H8togOACsYrLhpYVCWFKh5UGwt0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQJeGfH4rjl2PIDA3jitUY4TLQe69goa1HPEn3v/wQhASqYIIu5jZkIbCoNFhKh95VdfIKIHBgpWPayEpCK/PHgc=Bsky+ symbol (small sizes) diff --git a/branding/svg/bskyplus-symbol.svg b/branding/svg/bskyplus-symbol.svg new file mode 100644 index 0000000..ff4c508 --- /dev/null +++ b/branding/svg/bskyplus-symbol.svg @@ -0,0 +1 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozMjVkMzJjZS0xNjExLTRiOTgtOTgzYi1iMjUyMjg2YjdmZWYAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaGBTH63ceIm/EEib/2bXiwgAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDoxODQ0MjZiZi0wYjY1LTRkMTYtYjdmZC04ZmMzNzkxNTkzMjVscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoSe75mcG/+B+QW7F+a5HLXwAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggdyFw3JS0L+aQ3/1BfSOdsheaU70yomhyiCOhZVuX4PykZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaMu9jTA/g4BFyx8euJCgwIsAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCB7NNLUf/Kccm3yF6A9i1ut7gqJssokWVYxFG23N54022RuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi7Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjMyNWQzMmNlLTE2MTEtNGI5OC05ODNiLWIyNTIyODZiN2ZlZi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmYxMzU0MDg3LTFjODMtNDFkNy1iZWNkLWNiYmE4NjcwMzZmOHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCB3IXDclLQv5pDf/UF9I52yF5pTvTKiaHKII6FlW5fg/KJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgg8LgnvylVq6Sbcc7CoIYfKgIajBzSRkClFBfTw2pnUNqiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggOsY8RR/P+IvYbisEYUKZI07X4wbxcxnTR32a9z1HFfB0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQNgM6NPuOUYTZ3lbGyZUmY6une5dB8/pUJdjaEdqH342CJrM18cA5L8IoKUuRrfgSHgoaMkzxUgexRCX4QZo+MY=Bsky+ symbol diff --git a/branding/svg/bskyplus-wordmark-white.svg b/branding/svg/bskyplus-wordmark-white.svg new file mode 100644 index 0000000..30d8bb2 --- /dev/null +++ b/branding/svg/bskyplus-wordmark-white.svg @@ -0,0 +1 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyZjI5NzVlMi0zNzIwLTRjMTMtOWY5OS1iZTIyYjJhYThkODkAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaEwYbwHfTfcDtr5/abMGF7EAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpjOWZkYjA1YS01MmY1LTRjMzAtOTJjYy0yOGMzZGQ1ODk2ZTRscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNosVbb6m9F+k0hF3jZMnD56gAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggavZUtTzGm8KqcxMHaH6Wxu7TTSZSPwr+hFQodl8zccmkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaIR3OhhPb22yzP+6N2QJ9LsAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCAQ4Z4Hm/U4VTfJHo8f+03KcLFm5y41V8Gfl/cKPXbN4GRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjEZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjJmMjk3NWUyLTM3MjAtNGMxMy05Zjk5LWJlMjJiMmFhOGQ4OS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmUxYzIwYjY4LWU3NmMtNDZjOS04ZDdiLTg4OTBiMmRlMjQzZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBq9lS1PMabwqpzEwdofpbG7tNNJlI/Cv6EVCh2XzNxyaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggXw8lguc1uq/OiS3OxlSzWY7esrRzKd6/rmes5Cj1+tGiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggKD64OrGTYTJLQPiEAJ1mfcmzhYgF+yr0va23hAH/J5t0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQMdDaEZEuVtyIJJ7XOlhl9pdchpzX/+XcoeU3tRYfaehRGShgdRFBpZW//BWduoxdoQqsZXn5Ik+PaqUk639QEE=Bsky+ diff --git a/branding/svg/bskyplus-wordmark.svg b/branding/svg/bskyplus-wordmark.svg new file mode 100644 index 0000000..c4d1a1f --- /dev/null +++ b/branding/svg/bskyplus-wordmark.svg @@ -0,0 +1 @@ +AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozNTc3M2NiZi1mNTJiLTQxMWItYWI2Ny00OTY5ZDJhMGJiYjgAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaDL49PIwOJiheaBhhCBZfJ8AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo3MGEzZmNmZi04NDhmLTQ3MDEtODQwNy0zNmMzYTE4NGYwNjlscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo/QHrFNHySuF5yBtW2VVetgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggpz7fZvzs9ZVJ/GI1kBBnfpqQbi2CorBUjdBUXaAUvVykZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaBzzCrPJvl8F4ZdTQLQnyn0AAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCu/5dQ1VzNHrn0gW2TLupGC8wp5VUYzt/TQoVLgYFqUWRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBjEZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjM1NzczY2JmLWY1MmItNDExYi1hYjY3LTQ5NjlkMmEwYmJiOC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmI1NGRjYjkyLTMzMTktNDc1MS1hYmM5LTIyY2JkMjFiZGFhOHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCnPt9m/Oz1lUn8YjWQEGd+mpBuLYKisFSN0FRdoBS9XKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggEXLqHGpdRxMpsW4sSNsg4eb5rrb9ih1K0Du1wYH932uiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggYFr78xuVpJeKahRHKcIYUDcovdBOBrUOdwUPpXSF67l0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQCrQv5zPvKsGO5zOf5W4j7q3Gk6Q9cD80BUaZVO81tipRXj2YjAHhQMvo/1s1DjNYGYUZIR3/ItPptLVjiH57Pw=Bsky+ diff --git a/extension/chrome/background.js b/extension/chrome/background.js index 19a62af..83df076 100644 --- a/extension/chrome/background.js +++ b/extension/chrome/background.js @@ -12,10 +12,12 @@ chrome.action.onClicked.addListener(tab => { }); const DEFAULT_SETTINGS = { - skeetMode: false, - accentColor: '#1083fe' + skeetMode: false }; +// Remove the obsolete accent-color setting when the background starts. +void chrome.storage.local.remove('accentColor'); + // Initialize default settings on install chrome.runtime.onInstalled.addListener(async () => { const data = await chrome.storage.local.get(null); diff --git a/extension/chrome/content.css b/extension/chrome/content.css index d2527bd..633f3fe 100644 --- a/extension/chrome/content.css +++ b/extension/chrome/content.css @@ -17,7 +17,7 @@ align-items: center !important; justify-content: center !important; gap: 8px !important; - background-color: var(--bskyplus-accent, #1083fe) !important; + background-color: #1083fe !important; color: #ffffff !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important; font-size: 14px !important; @@ -55,7 +55,7 @@ align-items: center !important; justify-content: center !important; gap: 6px !important; - background-color: var(--bskyplus-accent, #1083fe) !important; + background-color: #1083fe !important; color: #ffffff !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important; font-size: 13px !important; @@ -100,7 +100,7 @@ } .bskyplus-composer-ocr-btn:hover { - background-color: var(--bskyplus-accent, #1083fe) !important; + background-color: #1083fe !important; transform: translateY(-1px) !important; } diff --git a/extension/chrome/content.js b/extension/chrome/content.js index b5920c2..c22b5b3 100644 --- a/extension/chrome/content.js +++ b/extension/chrome/content.js @@ -3,11 +3,8 @@ (function () { 'use strict'; - const DEFAULT_BLUE = '#1083fe'; - let settings = { skeetMode: false, - accentColor: DEFAULT_BLUE, localOcrEnabled: true }; @@ -176,22 +173,54 @@ } } + const altTextDialogSelector = '[role="dialog"], [aria-modal="true"], :is([aria-label="Add alt text" i], [aria-label="Edit alt text" i], [aria-label="Alt text" i]):not(input):not(textarea):not(button):not(img):not([role="button"])'; + + function ownedDialogElements(dialog, selector) { + return Array.from(dialog.querySelectorAll(selector)).filter(element => + element.closest(altTextDialogSelector) === dialog + ); + } + + function altTextDialogFields(dialog) { + if (dialog.closest('.bskyplus-edit-dialog')) return null; + + const normalizeTitle = value => (value || '').replace(/\s+/g, ' ').trim(); + let title = normalizeTitle(dialog.getAttribute('aria-label')); + if (!title) { + const labelledBy = (dialog.getAttribute('aria-labelledby') || '').split(/\s+/).filter(Boolean); + title = normalizeTitle(labelledBy.map(id => { + const label = document.getElementById(id); + return label && (!dialog.contains(label) || label.closest(altTextDialogSelector) === dialog) + ? label.textContent : ''; + }).join(' ')); + } + if (!title) { + title = normalizeTitle(ownedDialogElements(dialog, 'h1, h2, h3, h4, h5, h6, [role="heading"]')[0]?.textContent); + } + if (!/^(?:Add alt text|Edit alt text|Alt text)$/i.test(title)) return null; + + const textarea = ownedDialogElements(dialog, 'textarea, input[type="text"]')[0]; + const img = ownedDialogElements(dialog, 'img')[0]; + return textarea && img ? { textarea, img } : null; + } + function checkAltTextModal() { if (settings.localOcrEnabled === false) { document.querySelectorAll('#bskyplus-ocr-btn, #bskyplus-ocr-header-btn, .bskyplus-composer-ocr-btn').forEach(btn => btn.remove()); return; } + document.querySelectorAll('#bskyplus-ocr-btn, #bskyplus-ocr-header-btn').forEach(button => { + const dialog = button.closest(altTextDialogSelector); + if (!dialog || !altTextDialogFields(dialog)) button.remove(); + }); + // 1. Check for the Alt text modal - const dialogs = Array.from(document.querySelectorAll('div[role="dialog"], [aria-modal="true"], [aria-label*="alt" i]')); + const dialogs = Array.from(document.querySelectorAll(altTextDialogSelector)); for (const dialog of dialogs) { - const textarea = dialog.querySelector('textarea, input[type="text"]'); - const img = dialog.querySelector('img'); - const buttons = Array.from(dialog.querySelectorAll('button')); - const hasSave = buttons.some(b => b.innerText?.trim() === 'Save' || b.getAttribute('aria-label') === 'Save'); - const ariaLabel = (dialog.getAttribute('aria-label') || '').toLowerCase(); - const isAltModal = ariaLabel.includes('alt') || hasSave || dialog.innerText?.includes('Add alt text') || dialog.innerText?.includes('Alt text'); - - if (textarea && isAltModal) { + const fields = altTextDialogFields(dialog); + if (fields) { + const { textarea } = fields; + const buttons = ownedDialogElements(dialog, 'button'); // A. Inject prominent button above textarea /* if (!dialog.querySelector('#bskyplus-ocr-btn')) { const container = document.createElement('div'); @@ -211,9 +240,8 @@ ocrBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); - const currentImg = dialog.querySelector('img') || img; - const currentTextarea = dialog.querySelector('textarea, input[type="text"]') || textarea; - runOcrOnImage(currentImg, currentTextarea, ocrBtn); + const currentFields = dialog.isConnected && altTextDialogFields(dialog); + if (currentFields) runOcrOnImage(currentFields.img, currentFields.textarea, ocrBtn); }); container.appendChild(ocrBtn); @@ -240,9 +268,8 @@ headerBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); - const currentImg = dialog.querySelector('img') || img; - const currentTextarea = dialog.querySelector('textarea, input[type="text"]') || textarea; - runOcrOnImage(currentImg, currentTextarea, headerBtn); + const currentFields = dialog.isConnected && altTextDialogFields(dialog); + if (currentFields) runOcrOnImage(currentFields.img, currentFields.textarea, headerBtn); }); saveBtn.parentElement.insertAdjacentElement('beforebegin', headerBtn); @@ -365,170 +392,15 @@ } } - // ------------------------------------------------------------- - // Feature 3: Accent Color Customizer - // Comprehensive recoloring for Bluesky official blue across the site - // ------------------------------------------------------------- - const ACCENT_STYLE_ID = 'bskyplus-accent-style'; - - function isBlueskyBlue(str) { - if (!str) return false; - const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (!m) return false; - const r = parseInt(m[1], 10); - const g = parseInt(m[2], 10); - const b = parseInt(m[3], 10); - return b > 140 && b > r + 30 && b > g + 20; - } - - function scanAndRecolorBluesky(root = document.body) { - if (!root) return; - if (!settings.accentColor || settings.accentColor.toLowerCase() === DEFAULT_BLUE.toLowerCase()) { - return; - } - - const elements = root.querySelectorAll ? root.querySelectorAll('*') : []; - for (let i = 0; i < elements.length; i++) { - const el = elements[i]; - if (el.classList && el.classList.contains('bskyplus-toast')) continue; - - const cs = window.getComputedStyle(el); - - if (isBlueskyBlue(cs.backgroundColor)) { - el.dataset.bskyplusAccentBg = 'true'; - } - if (isBlueskyBlue(cs.color) && el.children.length === 0) { - el.dataset.bskyplusAccentColor = 'true'; - } - if (isBlueskyBlue(cs.borderColor)) { - el.dataset.bskyplusAccentBorder = 'true'; - } - if (isBlueskyBlue(cs.fill)) { - el.dataset.bskyplusAccentFill = 'true'; - } - if (isBlueskyBlue(cs.stroke)) { - el.dataset.bskyplusAccentStroke = 'true'; - } - } - } - - function cleanAccentAttributes() { - const tagged = document.querySelectorAll('[data-bskyplus-accent-bg], [data-bskyplus-accent-color], [data-bskyplus-accent-border], [data-bskyplus-accent-fill], [data-bskyplus-accent-stroke]'); - for (const el of tagged) { - delete el.dataset.bskyplusAccentBg; - delete el.dataset.bskyplusAccentColor; - delete el.dataset.bskyplusAccentBorder; - delete el.dataset.bskyplusAccentFill; - delete el.dataset.bskyplusAccentStroke; - } - } - - function applyAccentColor(color) { - let style = document.getElementById(ACCENT_STYLE_ID); - if (!color || color.toLowerCase() === DEFAULT_BLUE.toLowerCase()) { - if (style) style.remove(); - cleanAccentAttributes(); - return; - } - - if (!style) { - style = document.createElement('style'); - style.id = ACCENT_STYLE_ID; - document.head.appendChild(style); - } - - style.textContent = ` - :root { - --bskyplus-accent: ${color} !important; - } - /* Backgrounds for Bluesky official blue variants */ - [style*="background-color: rgb(15, 115, 255)"], - [style*="background-color: rgb(16, 131, 254)"], - [style*="background-color: rgb(0, 106, 255)"], - [style*="background-color: rgb(0, 133, 255)"], - [style*="background-color: rgb(18, 126, 241)"], - [style*="background-color: rgb(32, 139, 254)"], - [style*="background-color: rgb(46, 147, 254)"], - [style*="background-color: rgb(25, 118, 210)"], - [style*="background-color: #1083fe"], - [style*="background-color: #0f73ff"], - [style*="background-color: #006aff"], - [data-bskyplus-accent-bg] { - background-color: ${color} !important; - } - - /* Text and Links */ - [style*="color: rgb(77, 151, 255)"], - [style*="color: rgb(15, 115, 255)"], - [style*="color: rgb(16, 131, 254)"], - [style*="color: rgb(0, 106, 255)"], - [style*="color: rgb(0, 133, 255)"], - [style*="color: rgb(18, 126, 241)"], - [style*="color: #1083fe"], - [style*="color: #0f73ff"], - [style*="color: #4d97ff"], - a[href^="/hashtag/"], - [data-bskyplus-accent-color] { - color: ${color} !important; - } - - /* Borders */ - [style*="border-color: rgb(15, 115, 255)"], - [style*="border-color: rgb(16, 131, 254)"], - [style*="border-color: rgb(0, 106, 255)"], - [style*="border-color: rgb(77, 151, 255)"], - [style*="border-color: #1083fe"], - [style*="border-color: #0f73ff"], - [data-bskyplus-accent-border] { - border-color: ${color} !important; - } - - /* SVGs (Icons, Butterfly logo, loaders, strokes) */ - svg [fill="rgb(15, 115, 255)"], - svg [fill="rgb(16, 131, 254)"], - svg [fill="rgb(0, 106, 255)"], - svg [fill="rgb(77, 151, 255)"], - svg [fill="#1083fe"], - svg [fill="#0f73ff"], - svg [fill="#006aff"], - svg [fill="#4d97ff"], - [data-bskyplus-accent-fill] { - fill: ${color} !important; - } - svg [stroke="rgb(15, 115, 255)"], - svg [stroke="rgb(16, 131, 254)"], - svg [stroke="rgb(0, 106, 255)"], - svg [stroke="rgb(25, 118, 210)"], - svg [stroke="#1083fe"], - svg [stroke="#0f73ff"], - [data-bskyplus-accent-stroke] { - stroke: ${color} !important; - } - `; - - scanAndRecolorBluesky(document.body); - } - // ------------------------------------------------------------- // Mutation Observer & Interaction Loop // ------------------------------------------------------------- let isWalking = false; - let accentScanDebounce = null; - const observer = new MutationObserver((mutations) => { if (isWalking) return; checkAltTextModal(); - if (settings.accentColor && settings.accentColor.toLowerCase() !== DEFAULT_BLUE.toLowerCase()) { - if (!accentScanDebounce) { - accentScanDebounce = setTimeout(() => { - accentScanDebounce = null; - scanAndRecolorBluesky(document.body); - }, 80); - } - } - if (settings.skeetMode) { isWalking = true; for (const mutation of mutations) { @@ -551,9 +423,6 @@ setInterval(() => { checkAltTextModal(); - if (settings.accentColor && settings.accentColor.toLowerCase() !== DEFAULT_BLUE.toLowerCase()) { - scanAndRecolorBluesky(document.body); - } }, 500); document.addEventListener('click', () => { @@ -570,10 +439,6 @@ document.querySelectorAll('#bskyplus-ocr-btn, #bskyplus-ocr-header-btn, .bskyplus-composer-ocr-btn').forEach(btn => btn.remove()); } - if (newSettings.accentColor !== undefined) { - applyAccentColor(settings.accentColor); - } - if (settings.skeetMode && !prevSkeet) { walkTextNodes(document.body); } else if (!settings.skeetMode && prevSkeet) { @@ -582,11 +447,10 @@ } // Initial load from storage - chrome.storage.local.get(['skeetMode', 'accentColor', 'localOcrEnabled'], (res) => { + chrome.storage.local.get(['skeetMode', 'localOcrEnabled'], (res) => { if (res) { updateLocalSettings({ skeetMode: !!res.skeetMode, - accentColor: res.accentColor || DEFAULT_BLUE, localOcrEnabled: res.localOcrEnabled !== false }); } diff --git a/extension/chrome/icons/icon128.png b/extension/chrome/icons/icon128.png index fa4e27a5429ec4e59b67d77503cd6eaf8cbbe140..573fb1f64740f9e84b837668ecf231df4050d9fb 100644 GIT binary patch literal 9046 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 literal 7567 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 KpUXO@geCyX$PEnu literal 732 zcme7R@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!to7JIrlhE&Yyo$T!qk|=Wg z|MNT5&#kh2AE$7p*&RByX+e|R4y{G5SJVV$v15mOc+~u9%St^@8as=W2T7f>O>g3_eZBCa(FDbrt;Q;*$3K7F zwc!z0cPyjxMS{3p4wS|O`RusWD6gEYIt;qYj;2^ zi_@My&&7orpAsfKTk0Ygb#*%X%DC7M(M6xm^t1f@G{GkCV$+ld4IC$CDwrSiD=T6; zpt9`$r!5+a$9LYHvMBqhh>?nv+PdO7w{y1iMETdI4jfw9iM%eNHKHm@f&(<} zY$|9~l%K~Yu}SClKC8qEuP!4eyXt_eo4O`+$i%sw`P$Lp;9~ad#~V*aJC{ST(^3UK z*am+zi+*6nd)9D=6;lxNrdBa#!=zdz!#9APIG(T9x9-*NJM*kP4^K;7;-Pr9B7JMObjUo=%4>8?IqhV} z>B4qm#=#X#mn?p|e&4%a+j^6;w%^xSyYP(6IJ<4jrb$Hl_|N{Nd(!#jQwb5lqXz}v z^?APZ+0an=_bP0l+XkKEUsHz diff --git a/extension/chrome/icons/icon48.png b/extension/chrome/icons/icon48.png index 3a4e837fcfd43b4620fb0be29e017431b3d50093..27d455650f13c3d6465d75a7fe1dd8774dca2849 100644 GIT binary patch literal 7105 zcme7R@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?Ft%ilwY(sGbPz7Ex#x?u_QAyH!(d`zaTF?D>JXSBrz{J)zhUyry{q&Dl;?1 zDk(L^GR?#=QPu$;2pC*W5JOJjo!{B+bwwIj1N!ClTz}jLd?Jg2bZK zyb}L31_s7QP)K)QDSatNorB?!fZrn6gMtPE-lKb&@E2QNmB;7C(RbD3lYNl zU_L^NUYT)9Mq+VBguX<(KDWr=%7q7iAWJf~O+Q8Em2g%tVEfjLc$%w9K4Tg~Sqtl8jV^qSV6D)Z!9_ z{4|9`h0@~GB89}f6ouTxN`;KXvQ&lSqSVBa)D(sMB8A-il+3iu)D*Bbuo21mc_pcN zCB=HV#rbI^<%vb9j_Ij+B@0vX5_40bRu?7a5mL$R>nCZp$TX$Dl+V^HoKj1Gpe-#4*Ljx#+mnJ9Xq^A{UBpR8TB^M;7 z_=0r6F*HtI%hYcA@+16xn?tX;mGj|-P5hf91&`ZafAq#ODSYiSaj>KFK+Z~2$W6@4 zOiL{;$x5wA&M7ShWv9kPsl_FUMI{o7X*sER=_MJGVqFXjOm-j*3=9nFV1F1T=OE`W za5(^q)XZE+I%VQVcW!2JW_n&?Noi4Pg)dS{11Bwgk_rg07CpFeSTkj+v4Mp}l39wb zQF4m8u1ShXs&108sfDhoadM)8QKEUWfuThaG|J-<3B8$ySpj0S7Lwn=xe8%9#9xqn zTdY@R1dYywmtUqTtn1;G^WR>*J5zh@nVLm2T@UIhafm2&u5{sahM22K$Xs|LgBn}2 z-tx!c6Uw%)-M*VX)>eFGcBB5q!3&wZOXZKAt<9^MQ<9vMn3)@&o|>0hlvt8q6rY)w zmcJOBcJduz`B=d%Gbgn;y)3l|lyp)J^$he3vWp8+lf$4QMtUZC1`G@W7SPnA0Zl!{ zn5hRIp$rTR0vw>^bEzegNoWzHnuKB`6PG~~6PrO3WA6fHCPpSE5r?M>xZWMIys&Ra z^Dj0hR@?MYVFm*(HV&;ek8`#x%*+O!hT;aIY|No7T*7>C_v$DFI5`{Y8R&o{n1xm0 z5(>_c9IoJ8l$w&7SCW~SQ>+k_pI@Tj>}Vh-&TC|5U}0cxXklV(Y8EBVYh+NBKpkS00+Lmbk_u*HaAta5W?s5Mq=7ISJJ|V5j8K0tGqN)~F|f?2;>tc9 zAi*kf)nu>JQp;$UmpATS-}KY#jrfJ_%fz%9jmuNY|K2%e3Os zK+1$c%9soo4CFyPWflnou?CTIfsa@k{ZgWpe>^^NewN*wgV$pYA%{3~I)gz9lYwEe ztjIjZs!OLs`_8#Nw4K*AJBQ6Lt^O9%)=&P74ym73+uOL_`usxW`q!ThbDKktF&PxZ z$K3szxGKqOnf{d4)w}^GHC|sgieEC>_IK1>ewKcn!~Ir9>pCl51pf*Ne7M)0b0MhY zi{zaLFZV_nqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Uh0t0^cGWx-U-F6+Uu3 z`9g!lO^-R*T=QSM6#Us&^x-<^e$z6E{T3e2&Y1~YPtd)`RXPvaiib2FRfhYQdKK?>aY1FEfdeXVUkMn0=5Sm zcj&lzrMU*HM<+l~_%V}-H}7bYd{O%(W)q^lCbGV?i`kZYZ3$L52U4u-mn*<&M6}Z`K9tZF5Q5qNwKes?7WF z+of9sPKk3A=2h>mYqd>Jh+cb1{@aQ;CLYNd=i`O0oheIuEmTpj_ry==p52-+t_o{> zEPB1U3w7!&^q(KP_S}80va?bO`=&_?YZ6~5c|ALDxxeL3O~(Q!jYJupDxOWj53brM zo)i6bfT`xknhmcKDkdM?$gEoY;PLa8CqFhElRmg;jQ{B%YP*5k9YwFY#)x);Ey+t(pyOQ>Grw#AkU3_Y0=LQA#W!4o9+xTSmi}Y&R%xcmsSQ7DL ztILnd-U)Lb-M?nG(6g`o^6Tb!$()s|Ki8Ec+<29hk+&~=f8F1`MP+6>@#o^#w*9QG zea^qFedq1i4ZkK7Wh^zwbYeMj{rcfur3p7vj6y?|*IQf7*jT1py!pYEsTZFynt$4S zJ@H>$YP@Ypi?zC5%m(rI-EC|PYU)4M@mZ<+b$#Af$-$;v@JUv2^LpN}H4Ur7=bFwj zU|hB8!k-$hePMd{rE-Nz{A6CO5BN9v)7ecSKBcB6rjGIRwpr#G9F)CLFrLdx!PU znU5P6Z{ytcTOseV8ekc@t>4OS?2&Ak)!lb}s-NfGJX3+i_U2*sobh{0}Yeag~Gf#+g=v4MWSZmjvU&};FMw^zikUV5CYUd$7C zQ(=YIExy0LiuTv4nwDH{_A%{P(VC-Xbu;-%z}?!-F?kXV--=eU-WAODbu75|)Nran zj(o;z(`W%(0c+pB%$Me$qE5RPvA5-?h$e0QIwQ$(f6vS<2j;vt|K#I7Ge&C9!(x5; zh?(CQE-zL)JI^nqe7DWAq7NteAN@~=S@m esb^jz`H%g|tAZJV-<>vs%5qOvKbLh*2~7YFL7U6~ literal 2741 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 .section-title .section-collapse-arrow { + transform: rotate(-45deg); +} + +.section-collapsed > :not(.section-title) { + display: none !important; +} + +@media (prefers-reduced-motion: reduce) { + .theme-select-chevron { transition: none; } + + .section-collapse-arrow { + transition: none; + } +} + .section-subtitle { font-size: 11px; color: var(--text-secondary); @@ -151,7 +409,7 @@ body { } .switch:checked { - background-color: var(--accent-default); + background-color: #1083fe; } .switch:checked::before { @@ -203,7 +461,7 @@ body { width: 16px; height: 16px; border-radius: 50%; - background: var(--accent-default); + background: #1083fe; cursor: pointer; box-shadow: 0 0 4px rgba(0,0,0,0.4); } @@ -212,7 +470,7 @@ body { width: 16px; height: 16px; border-radius: 50%; - background: var(--accent-default); + background: #1083fe; cursor: pointer; border: none; } @@ -245,86 +503,6 @@ body { cursor: not-allowed; } -/* Palette */ -.palette-container { - display: flex; - gap: 7px; - justify-content: space-between; - margin-bottom: 12px; -} - -.color-swatch { - width: 32px; - height: 32px; - border-radius: 50%; - border: 2px solid transparent; - cursor: pointer; - transition: transform 0.15s ease, border-color 0.15s ease; - padding: 0; - outline: none; -} - -.color-swatch:hover { - transform: scale(1.15); -} - -.color-swatch.active { - border-color: #ffffff; - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4); - transform: scale(1.1); -} - -.custom-color-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; -} - -.color-input-wrapper { - display: flex; - align-items: center; - gap: 8px; - background-color: var(--bg-secondary); - padding: 4px 8px; - border-radius: 8px; - border: 1px solid var(--border-color); - flex: 1; -} - -#colorPicker { - appearance: none; - -webkit-appearance: none; - border: none; - width: 26px; - height: 26px; - border-radius: 6px; - cursor: pointer; - background: none; - padding: 0; -} - -#colorPicker::-webkit-color-swatch-wrapper { - padding: 0; -} - -#colorPicker::-webkit-color-swatch { - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 6px; -} - -#hexInput { - background: transparent; - border: none; - color: var(--text-primary); - font-family: monospace; - font-size: 13px; - font-weight: 600; - width: 75px; - outline: none; - text-transform: uppercase; -} - .secondary-btn { background-color: var(--bg-secondary); color: var(--text-secondary); @@ -360,3 +538,98 @@ body { font-size: 10px; color: var(--text-secondary); } +.header-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +#moreSettingsButton { + width: auto; + margin: 0; + white-space: nowrap; +} +#featuresSection .bskyplus-popup-feature, +#altTextSection .bskyplus-popup-feature, +#infrastructureSection .bskyplus-popup-feature { + border-radius: 10px; + background: var(--bg-secondary); + margin-bottom: 8px; + overflow-anchor: none; +} + +.bskyplus-popup-feature > .toggle-row { + margin-bottom: 0; + transition: background-color 0.15s ease, padding-bottom 300ms ease; +} + +.bskyplus-controls-reveal { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transition: grid-template-rows 300ms ease, opacity 180ms ease; +} + +.bskyplus-feature-expanded > .bskyplus-controls-reveal { + grid-template-rows: 1fr; + opacity: 1; +} + +.bskyplus-controls-clip { + min-height: 0; + min-width: 0; + overflow: hidden; +} + +@media (prefers-reduced-motion: reduce) { + .bskyplus-controls-reveal, + .bskyplus-popup-feature > .toggle-row { + transition: none; + } +} + +.bskyplus-feature-expanded > .toggle-row { + padding-bottom: 9px; +} + +.bskyplus-popup-controls { + padding: 0 12px 12px; +} + +.bskyplus-popup-controls .bskyplus-llm-field { + gap: 4px; + margin-bottom: 8px; +} + +.bskyplus-popup-controls .bskyplus-llm-label { + padding-inline: 10px; + line-height: 1.4; +} + +.bskyplus-popup-controls [role="status"]:empty { + display: none; +} + +.bskyplus-popup-controls [role="status"], +.bskyplus-popup-controls .bskyplus-llm-notice { + min-height: 0; + margin-top: 4px; +} + +.bskyplus-popup-controls[hidden] { + display: none; +} + +#bookmarksSection .bskyplus-popup-controls { + padding-bottom: 4px; +} + +#bookmarksSection .bskyplus-llm-field { + margin-bottom: 4px; +} + +.bskyplus-popup-controls .bskyplus-llm-actions { + flex-wrap: wrap; + gap: 6px; +} diff --git a/extension/chrome/popup/popup.html b/extension/chrome/popup/popup.html index 046da08..4149d34 100644 --- a/extension/chrome/popup/popup.html +++ b/extension/chrome/popup/popup.html @@ -10,13 +10,16 @@
- Bsky+ Logo + Bsky+ Logo

Bsky+

Power tools for Bluesky
- v1.0.2 +
+ + v2.0.0 +
@@ -35,6 +38,20 @@
+
+
Theme
+
+ Color theme + +
+
+ + +
+

+
+
Features
@@ -49,7 +66,7 @@ @@ -57,7 +74,7 @@ @@ -70,20 +87,12 @@ -
-
', ''); +function fixture({ count = 3, children = true } = {}) { + return `

Thread

+
+
${avatar(1)}Alice
@alice.test
+
${text(1, count)}${media(1)}
+
Everybody can reply
+ + +
${controls(1)}
+
+ ${children ? childMarkup(2, count) + childMarkup(3, count) : ''} +
Unrelated reply
+
`; +} + +async function setup(t, options) { + const browser = await puppeteer.launch({ executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome', headless: true }); + t.after(() => browser.close()); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', request => request.respond({ contentType: 'text/html', body: fixture(options) })); + await page.goto('https://bsky.app/profile/alice.test/post/p1'); + // React creates nested native links programmatically; HTML parsing alone + // would repair those anchors into a different tree. + await page.evaluate(() => { + for (const node of document.querySelectorAll('[role="link"][data-testid]')) { + const link = document.createElement('a'); + for (const attr of node.attributes) link.setAttribute(attr.name, attr.value); + link.append(...node.childNodes); + node.replaceWith(link); + } + }); + await page.addStyleTag({ content: await readFile('extension/shared/thread-unroll.css', 'utf8') }); + await page.addScriptTag({ content: await readFile('extension/shared/thread-unroll.js', 'utf8') }); + return page; +} + +const toggle = '.bskyplus-thread-unroll-toggle'; +const visible = (page, selector) => page.$eval(selector, node => { + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0 && getComputedStyle(node).visibility !== 'hidden'; +}); + +test('inline unroll preserves native interactions and media, then restores the thread', async t => { + const page = await setup(t); + await page.waitForSelector(toggle); + assert.equal(await page.$eval(toggle, node => node.previousElementSibling?.id === 'thread-options' || node.nextElementSibling?.id === 'thread-options'), true); + await page.evaluate(() => { + window.savedLike = document.getElementById('like-2'); + window.clicks = 0; + window.savedLike.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); window.clicks++; }); + }); + await page.click(toggle); + await page.waitForFunction(() => document.getElementById('author-2').getClientRects().length === 0); + assert.deepEqual(await page.$$eval('[data-bskyplus-unroll-post] [data-word-wrap="1"], [data-bskyplus-unroll-post] [data-word-wrap="1"] a', nodes => nodes.map(node => getComputedStyle(node).fontSize)), Array(6).fill('16.9px')); + for (const selector of ['#author-1', '#text-1', '#text-2', '#text-3', '#media-2', '#alt-2', '#reply-2', '#repost-2', '#like-2', '#unrelated']) assert.equal(await visible(page, selector), true, selector); + for (const selector of ['#avatar-2', '#author-3', '#menu-1', '#menu-2', '#date-1', '#stats-1', '#known-likers', '#reply-prompt']) assert.equal(await visible(page, selector), false, selector); + assert.equal(await page.evaluate(() => document.getElementById('like-2') === window.savedLike), true); + await page.click('#like-2'); + assert.equal(await page.evaluate(() => window.clicks), 1); + assert.equal(await page.$eval('#like-2', node => node.textContent.trim()), '5'); + await page.setViewport({ width: 390, height: 844 }); + await page.evaluate(() => { + const prompt = document.getElementById('reply-prompt').cloneNode(true); + prompt.id = 'mobile-reply-prompt'; + prompt.removeAttribute('data-bskyplus-unroll-chrome'); + const button = document.createElement('div'); + button.setAttribute('role', 'button'); + button.setAttribute('aria-label', 'Compose reply'); + button.textContent = 'Write your reply'; + prompt.replaceChildren(button); + document.querySelector('main').append(prompt); + }); + await page.waitForFunction(() => document.getElementById('mobile-reply-prompt').getClientRects().length === 0); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); + if (process.env.UNROLL_SCREENSHOT) await page.screenshot({ path: process.env.UNROLL_SCREENSHOT, fullPage: true }); + await page.click(toggle); + await page.waitForFunction(() => document.getElementById('author-2').getClientRects().length > 0); + assert.equal(await page.$eval('#text-2', node => getComputedStyle(node).fontSize), '15px'); + for (const selector of ['#avatar-2', '#menu-1', '#stats-1', '#known-likers', '#reply-prompt', '#mobile-reply-prompt']) assert.equal(await visible(page, selector), true, selector); +}); + +test('unroll requires a first-post marker and resets when leaving the thread', async t => { + const page = await setup(t, { count: 1, children: false }); + assert.equal(await page.$(toggle), null); + await page.$eval('span[aria-label="Post 1 of 1"]', node => node.setAttribute('aria-label', 'Post 1 of 3')); + await page.waitForSelector(toggle); + await page.click(toggle); + await page.evaluate(markup => document.getElementById('unrelated').insertAdjacentHTML('beforebegin', markup), childMarkup(2)); + await page.waitForFunction(() => document.getElementById('author-2').getClientRects().length === 0); + await page.evaluate(() => { history.pushState({}, '', '/'); window.dispatchEvent(new PopStateEvent('popstate')); }); + await page.waitForFunction(() => !document.querySelector('.bskyplus-thread-unroll-toggle')); + assert.equal(await visible(page, '#author-2'), true); +}); + +test('numbered bounds exclude later replies and nested quoted posts', async t => { + const page = await setup(t); + await page.waitForSelector(toggle); + await page.evaluate(markup => { + document.getElementById('thread-list').insertAdjacentHTML('beforeend', markup); + const quote = document.createElement('div'); + quote.dataset.testid = 'postThreadItem-by-quoted.test'; + quote.id = 'quoted-post'; + const badge = document.createElement('span'); + badge.setAttribute('aria-label', 'Post 1 of 9'); + badge.textContent = 'Quoted thread'; + quote.append(badge); + for (const id of ['replyBtn', 'repostBtn', 'likeBtn', 'postDropdownBtn']) { + const control = document.createElement('button'); + control.dataset.testid = id; + control.textContent = 'Quoted control'; + quote.append(control); + } + document.getElementById('media-1').append(quote); + }, childMarkup(4, 4)); + await page.click(toggle); + await page.waitForFunction(() => document.getElementById('post-3').hasAttribute('data-bskyplus-unroll-post')); + assert.deepEqual(await page.$$eval('[data-bskyplus-unroll-post]', nodes => nodes.map(node => node.id)), ['post-1', 'post-2', 'post-3']); + assert.equal(await visible(page, '#author-4'), true); + assert.equal(await visible(page, '#quoted-post'), true); + assert.equal(await visible(page, '#menu-1'), false); + await page.$eval('#text-1 span[aria-label]', node => node.remove()); + await page.waitForFunction(() => !document.querySelector('.bskyplus-thread-unroll-toggle')); + assert.equal(await page.$$eval('[data-bskyplus-unroll-post]', nodes => nodes.length), 0); +}); + +test('opening a middle post keeps its expanded body while hiding its repeated author', async t => { + const page = await setup(t); + await page.waitForSelector(toggle); + await page.evaluate(markup => { + document.getElementById('post-2').parentElement.remove(); + const anchor = document.getElementById('post-1'); + for (const node of [anchor, ...anchor.querySelectorAll('[id]')]) node.id = node.id.replace(/-1$/, '-2'); + anchor.querySelector('span[aria-label="Post 1 of 3"]').setAttribute('aria-label', 'Post 2 of 3'); + anchor.parentElement.insertAdjacentHTML('beforebegin', markup); + history.replaceState({}, '', '/profile/alice.test/post/p2'); + window.dispatchEvent(new PopStateEvent('popstate')); + }, childMarkup(1)); + await page.waitForFunction(() => document.querySelector('.bskyplus-thread-unroll-toggle')?.getAttribute('aria-pressed') === 'false'); + // Let the navigation scan run before making a selection on the new route. + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await page.click(toggle); + await page.waitForFunction(() => document.getElementById('author-2').getClientRects().length === 0); + for (const selector of ['#text-2', '#media-2', '#reply-2', '#like-2', '#author-1']) assert.equal(await visible(page, selector), true, selector); + assert.deepEqual(await page.$$eval('[data-bskyplus-unroll-post]', nodes => nodes.map(node => node.id)), ['post-1', 'post-2', 'post-3']); +}); + +test('default avatars and role-based header buttons use the same compact layout', async t => { + const page = await setup(t); + await page.waitForSelector(toggle); + await page.evaluate(() => { + for (const avatar of document.querySelectorAll('[id^="avatar-"] img')) { + const placeholder = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + placeholder.setAttribute('width', '40'); + placeholder.setAttribute('height', '40'); + avatar.replaceWith(placeholder); + } + const button = document.getElementById('thread-options'); + const roleButton = document.createElement('div'); + roleButton.setAttribute('role', 'button'); + roleButton.setAttribute('aria-label', 'Thread options'); + roleButton.textContent = 'Thread options'; + button.replaceWith(roleButton); + }); + await page.waitForFunction(() => document.querySelector('.bskyplus-thread-unroll-toggle')?.nextElementSibling?.getAttribute('role') === 'button'); + await page.click(toggle); + await page.waitForFunction(() => document.getElementById('author-2').getClientRects().length === 0); + assert.equal(await visible(page, '#avatar-2'), false); + assert.equal(await visible(page, '#author-1'), true); +}); + +test('unrolled post bodies do not navigate but nested links and controls still work', async t => { + const page = await setup(t); + await page.waitForSelector(toggle); + await page.evaluate(() => { + window.interactions = { posts: 0, links: 0, likes: 0, media: 0 }; + const post = document.getElementById('post-2'); + post.addEventListener('click', event => { + event.preventDefault(); + window.interactions.posts++; + }); + for (const [selector, key] of [['#text-2 a', 'links'], ['#like-2', 'likes'], ['#alt-2', 'media']]) { + document.querySelector(selector).addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + window.interactions[key]++; + }); + } + }); + await page.click(toggle); + await page.click('#text-2', { offset: { x: 10, y: 5 } }); + await page.focus('#post-2'); + await page.keyboard.press('Enter'); + assert.equal(await page.$eval('#post-2', post => post.dispatchEvent(new MouseEvent('auxclick', { bubbles: true, cancelable: true, button: 1 }))), false); + await page.click('#text-2 a'); + await page.click('#like-2'); + await page.click('#alt-2'); + assert.deepEqual(await page.evaluate(() => window.interactions), { posts: 0, links: 1, likes: 1, media: 1 }); + await page.click(toggle); + await page.click('#text-2', { offset: { x: 10, y: 5 } }); + assert.equal(await page.evaluate(() => window.interactions.posts), 1); +}); diff --git a/test/test_video_controls.mjs b/test/test_video_controls.mjs index b361835..6577137 100644 --- a/test/test_video_controls.mjs +++ b/test/test_video_controls.mjs @@ -23,6 +23,7 @@ async function loadMediaViewer(page, { enabled = true } = {}) {
+
+
player overlay
@@ -39,10 +41,14 @@ async function loadMediaViewer(page, { enabled = true } = {}) { await page.evaluate((isEnabled) => { const changedListeners = []; const storageWrites = []; + const storageReads = []; window.chrome = { storage: { local: { - get: (keys, callback) => callback(isEnabled ? {} : { mediaViewerEnabled: false }), + get: (keys, callback) => { + storageReads.push(keys); + callback({ ...(isEnabled ? {} : { mediaViewerEnabled: false }), videoSpeed: 1.5 }); + }, set: values => storageWrites.push(values) }, onChanged: { @@ -54,6 +60,7 @@ async function loadMediaViewer(page, { enabled = true } = {}) { changedListeners.forEach(listener => listener(changes, 'local')); }; window.__storageWrites = storageWrites; + window.__storageReads = storageReads; }, enabled); await page.addScriptTag({ content: scripts[0] }); await page.addScriptTag({ content: scripts[1] }); @@ -72,9 +79,12 @@ test('post figure videos get controls in the Bluesky toolbar while preserving ov figureControls: document.querySelector('#figure-video').hasAttribute('controls'), figureOverlayPresent: !!document.querySelector('#figure-overlay'), galleryControls: document.querySelector('#gallery-video').hasAttribute('controls'), + galleryAltButton: !!document.querySelector('#gallery-alt-button'), postToolbar: !!document.querySelector('#post-controls-row .bskyplus-video-toolbar'), galleryToolbar: !!document.querySelector('#gallery-controls-row .bskyplus-video-toolbar'), postSpeed: !!document.querySelector('#post-controls-row .bskyplus-speed-select'), + postRate: document.querySelector('#post-video').playbackRate, + galleryRate: document.querySelector('#gallery-video').playbackRate, postPip: !!document.querySelector('#post-controls-row .bskyplus-pip-btn'), postToolbarIsThirdControl: document.querySelector('#post-controls-row').children[3] ?.classList.contains('bskyplus-video-toolbar'), @@ -86,9 +96,16 @@ test('post figure videos get controls in the Bluesky toolbar while preserving ov assert.equal(initial.figureControls, false, 'figure video should preserve custom controls'); assert.equal(initial.figureOverlayPresent, true, 'production player overlay should be preserved'); assert.equal(initial.galleryControls, false, 'gallery video should preserve custom controls'); + assert.equal(initial.galleryAltButton, true, 'the ALT action after the figure should remain in place'); + assert.equal(initial.postRate, 1, 'legacy stored speed should not affect a new video'); + assert.equal(initial.galleryRate, 1, 'each new video should start at 1x'); + assert.equal(await page.evaluate(() => window.__storageReads.flat().includes('videoSpeed')), false, + 'legacy videoSpeed storage is ignored'); assert.equal(initial.postToolbar, true, 'toolbar should be added to the Bluesky controls row'); assert.equal(initial.galleryToolbar, true, 'gallery toolbar should be added to the Bluesky controls row'); - assert.equal(initial.postSpeed, true, 'speed select should be added to the Bluesky controls row'); + assert.equal(await page.$eval('#gallery-controls-row .bskyplus-speed-select', trigger => + trigger.value), '1', 'video with an intervening ALT button should still get a speed trigger'); + assert.equal(initial.postSpeed, true, 'speed menu trigger should be added to the Bluesky controls row'); assert.equal(initial.postPip, true, 'PiP button should be added to the Bluesky controls row'); assert.equal(initial.postToolbarIsThirdControl, true, 'toolbar should be the fourth control in the row'); assert.equal(initial.outsideControls, false, 'videos outside posts should be unchanged'); @@ -117,6 +134,18 @@ test('post figure videos get controls in the Bluesky toolbar while preserving ov assert.equal(audioState.galleryMuted, true, 'applyToAllVideos should preserve gallery mute state'); assert.equal(audioState.writes.some(values => 'videoVolume' in values || 'videoMuted' in values), false, 'volume changes should not be persisted'); + const alternateLayoutRate = await page.evaluate(() => { + const video = document.querySelector('#gallery-video'); + video.playbackRate = 1.25; + video.dispatchEvent(new Event('ratechange')); + return { + altButtonPresent: !!document.querySelector('#gallery-alt-button'), + triggerValue: document.querySelector('#gallery-controls-row .bskyplus-speed-select').value, + }; + }); + assert.equal(alternateLayoutRate.altButtonPresent, true, 'speed updates should preserve the intervening ALT action'); + assert.equal(alternateLayoutRate.triggerValue, '1.25', + 'rate changes should update controls after an intervening ALT button'); await page.evaluate(() => { const media = document.querySelector('#post-figure'); @@ -128,83 +157,186 @@ test('post figure videos get controls in the Bluesky toolbar while preserving ov assert.equal(await page.$eval('#unrelated-sibling', element => element.isConnected), true, 'a sibling added after the first scan must survive repeated scans'); - await page.select('#post-controls-row .bskyplus-speed-select', '1.5'); - assert.equal(await page.$eval('#post-video', video => video.playbackRate), 1.5); - const speedWidths = await page.evaluate(() => { - const select = document.querySelector('#post-controls-row .bskyplus-speed-select'); - const widthFor = value => { - select.value = value; - select.dispatchEvent(new Event('change')); - return select.getBoundingClientRect().width; + const speedTrigger = '#post-controls-row .bskyplus-speed-select'; + const menuSelector = await page.$eval(speedTrigger, trigger => `#${trigger.getAttribute('aria-controls')}`); + const openSpeedMenu = async () => { + await page.click(speedTrigger); + await page.waitForFunction((trigger, menu) => + document.querySelector(trigger)?.getAttribute('aria-expanded') === 'true' && + document.querySelector(menu)?.getAttribute('role') === 'menu', {}, speedTrigger, menuSelector); + await page.waitForSelector(`${menuSelector}:not([hidden])`, { visible: true }); + }; + const closeSpeedMenu = async () => { + await page.waitForFunction((trigger, menu) => + document.querySelector(trigger)?.getAttribute('aria-expanded') === 'false' && + !document.querySelector(menu)?.checkVisibility(), {}, speedTrigger, menuSelector); + }; + + await openSpeedMenu(); + if (process.env.SCREENSHOT_PATH) { + await page.screenshot({ path: process.env.SCREENSHOT_PATH, fullPage: true }); + } + const menuAppearance = await page.$eval(menuSelector, menu => { + const style = getComputedStyle(menu); + const rect = menu.getBoundingClientRect(); + const triggerRect = document.querySelector('.bskyplus-speed-select').getBoundingClientRect(); + return { + borderRadius: parseFloat(style.borderTopLeftRadius), + background: style.backgroundColor, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + gapBelowTrigger: rect.top - triggerRect.bottom, + gapAboveTrigger: triggerRect.top - rect.bottom, + viewportWidth: innerWidth, + viewportHeight: innerHeight, }; - return { one: widthFor('1'), onePointTwentyFive: widthFor('1.25') }; }); - assert.ok(speedWidths.onePointTwentyFive > speedWidths.one, - 'speed select should expand to fit longer labels'); - await page.select('#post-controls-row .bskyplus-speed-select', '1.5'); - const closedPickerClick = await page.$eval('#post-controls-row .bskyplus-speed-select', select => { + assert.ok(menuAppearance.borderRadius >= 6, + `speed menu has rounded corners (${menuAppearance.borderRadius}px)`); + const menuRgb = menuAppearance.background.match(/\d+/g)?.slice(0, 3).map(Number); + assert.ok(menuRgb && menuRgb.every(channel => channel < 100), + `speed menu uses a dark surface (${menuAppearance.background})`); + assert.ok(menuAppearance.top >= 0 && menuAppearance.left >= 0 && + menuAppearance.bottom <= menuAppearance.viewportHeight && + menuAppearance.right <= menuAppearance.viewportWidth, + 'speed menu stays inside the viewport'); + assert.ok((menuAppearance.gapBelowTrigger >= -1 && menuAppearance.gapBelowTrigger <= 20) || + (menuAppearance.gapAboveTrigger >= -1 && menuAppearance.gapAboveTrigger <= 20), + 'speed menu stays anchored beside its trigger'); + + await page.click(`${menuSelector} [data-speed="1.5"]`); + await closeSpeedMenu(); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), 1.5); + assert.equal(await page.$eval(speedTrigger, trigger => trigger.value), '1.5'); + assert.equal(await page.$eval(`${menuSelector} [data-speed="1.5"]`, option => + option.getAttribute('aria-checked')), 'true'); + assert.equal(await page.evaluate(() => window.__storageWrites.some(values => 'videoSpeed' in values)), false, + 'choosing a playback speed does not persist videoSpeed'); + + await openSpeedMenu(); + await page.keyboard.press('ArrowDown'); + assert.equal(await page.evaluate(() => document.activeElement?.getAttribute('data-speed')), '2', + 'ArrowDown moves focus to the next speed option'); + await page.keyboard.press('Enter'); + await closeSpeedMenu(); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), 2, + 'Enter selects the focused speed'); + assert.equal(await page.$eval(speedTrigger, trigger => + trigger.querySelector('.bskyplus-speed-value')?.textContent), '2x'); + + await openSpeedMenu(); + const rateBeforeEscape = await page.$eval('#post-video', video => video.playbackRate); + await page.keyboard.press('ArrowUp'); + assert.equal(await page.evaluate(() => document.activeElement?.getAttribute('data-speed')), '1.5', + 'ArrowUp moves focus to the previous speed option'); + await page.keyboard.press('Escape'); + await closeSpeedMenu(); + assert.equal(await page.$eval(speedTrigger, trigger => document.activeElement === trigger), true, + 'Escape closes the menu and restores focus to the trigger'); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), rateBeforeEscape, + 'Escape without selection preserves playback speed'); + + await openSpeedMenu(); + await page.click(`${menuSelector} [data-speed="1.5"]`); + await closeSpeedMenu(); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), 1.5, + 'mouse selection changes playback speed'); + const rateBeforeReselect = await page.$eval('#post-video', video => video.playbackRate); + await openSpeedMenu(); + await page.click(`${menuSelector} [data-speed="1.5"]`); + await closeSpeedMenu(); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), rateBeforeReselect, + 'reselecting the current speed preserves playback rate'); + + await page.evaluate(() => { const video = document.querySelector('#post-video'); - select.focus(); - const rateBefore = video.playbackRate; - select.dispatchEvent(new MouseEvent('click', { bubbles: true })); + video.setAttribute('src', 'data:video/mp4;base64,AAAA'); + video.dispatchEvent(new Event('loadstart')); + }); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), 1, + 'reusing a video element for a different source resets playback to 1x'); + assert.equal(await page.$eval(speedTrigger, trigger => trigger.value), '1', + 'the speed control reflects the reset after a source change'); + + await openSpeedMenu(); + await page.click(`${menuSelector} [data-speed="1.5"]`); + await closeSpeedMenu(); + + await openSpeedMenu(); + await page.click('#outside-click-target'); + await closeSpeedMenu(); + assert.equal(await page.$eval(speedTrigger, trigger => trigger.getAttribute('aria-expanded')), 'false', + 'clicking outside closes the speed menu'); + + await page.evaluate(() => { + const dialog = document.createElement('dialog'); + dialog.id = 'native-video-dialog'; + const player = document.querySelector('#post-with-controls'); + dialog.appendChild(player); + document.body.appendChild(dialog); + dialog.showModal(); + }); + await page.waitForFunction(() => document.querySelector('#native-video-dialog')?.open); + await openSpeedMenu(); + const dialogMenuState = await page.$eval(menuSelector, menu => { + const option = menu.querySelector('[data-speed="0.5"]'); + const rect = option.getBoundingClientRect(); return { - blurred: document.activeElement !== select, - rate: video.playbackRate, - rateBefore + inDialog: menu.parentElement?.id === 'native-video-dialog', + topElementIsOption: option.contains(document.elementFromPoint( + rect.left + rect.width / 2, rect.top + rect.height / 2 + )) }; }); - await page.waitForFunction(() => { - const select = document.querySelector('#post-controls-row .bskyplus-speed-select'); - return document.activeElement !== select; - }); - assert.equal(await page.$eval('#post-controls-row .bskyplus-speed-select', - select => document.activeElement !== select), true, - 'clicking the already selected speed should clear focus when the picker is closed'); - assert.equal(closedPickerClick.rate, closedPickerClick.rateBefore, - 'reselecting the current speed should preserve the playback rate'); + assert.equal(dialogMenuState.inDialog, true, + 'speed menu mounts inside its native dialog scope'); + assert.equal(dialogMenuState.topElementIsOption, true, + 'speed option remains the hit target above the modal dialog'); + await page.click(`${menuSelector} [data-speed="0.5"]`); + await closeSpeedMenu(); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), 0.5, + 'choosing an option inside a native dialog changes playback speed'); - await page.click('#post-controls-row .bskyplus-speed-select'); - await page.keyboard.press('Enter'); - await page.waitForFunction(() => { - const select = document.querySelector('#post-controls-row .bskyplus-speed-select'); - return document.activeElement !== select; + await openSpeedMenu(); + await page.evaluate(() => document.querySelector('#native-video-dialog').close()); + await page.waitForFunction(selector => + document.querySelector('.bskyplus-speed-select')?.getAttribute('aria-expanded') === 'false' && + document.querySelector(selector)?.parentElement === document.body, {}, menuSelector); + await page.evaluate(() => { + const player = document.querySelector('#post-with-controls'); + document.body.appendChild(player); + document.querySelector('#native-video-dialog').remove(); + const fullscreenButton = document.querySelector('#post-controls-row button[aria-label="Fullscreen"]'); + fullscreenButton.addEventListener('click', () => player.requestFullscreen()); }); - const actualPickerSelection = await page.$eval('#post-controls-row .bskyplus-speed-select', select => ({ - blurred: document.activeElement !== select, - value: select.value, - rate: document.querySelector('#post-video').playbackRate - })); - assert.equal(actualPickerSelection.blurred, true, - 'choosing the already selected speed through the browser picker should clear focus'); - assert.equal(actualPickerSelection.value, '1.5', - 'choosing the already selected speed should preserve the selected value'); - assert.equal(actualPickerSelection.rate, 1.5, - 'choosing the already selected speed should preserve the playback rate'); - await page.keyboard.press('Escape'); - for (const key of ['Enter', 'Escape']) { - const closedPickerKey = await page.$eval('#post-controls-row .bskyplus-speed-select', - (select, keyName) => { - const video = document.querySelector('#post-video'); - select.focus(); - const rateBefore = video.playbackRate; - select.dispatchEvent(new KeyboardEvent('keyup', { key: keyName, bubbles: true })); - return { - blurred: document.activeElement !== select, - rate: video.playbackRate, - rateBefore - }; - }, key); - await page.waitForFunction(() => { - const select = document.querySelector('#post-controls-row .bskyplus-speed-select'); - return document.activeElement !== select; - }); - assert.equal(await page.$eval('#post-controls-row .bskyplus-speed-select', - select => document.activeElement !== select), true, - `${key} should clear focus when the speed picker is closed`); - assert.equal(closedPickerKey.rate, closedPickerKey.rateBefore, - `${key} should preserve the playback rate`); - } + await page.click('#post-controls-row button[aria-label="Fullscreen"]'); + await page.waitForFunction(() => document.fullscreenElement?.id === 'post-with-controls'); + await openSpeedMenu(); + assert.equal(await page.$eval(menuSelector, menu => + menu.parentElement?.id === 'post-with-controls'), true, + 'speed menu mounts inside the active fullscreen root'); + const fullscreenOptionState = await page.$eval(menuSelector, menu => { + const option = menu.querySelector('[data-speed="1.25"]'); + const rect = option.getBoundingClientRect(); + return option.contains(document.elementFromPoint( + rect.left + rect.width / 2, rect.top + rect.height / 2 + )); + }); + assert.equal(fullscreenOptionState, true, + 'speed option remains the hit target in fullscreen'); + await page.click(`${menuSelector} [data-speed="1.25"]`); + await closeSpeedMenu(); + assert.equal(await page.$eval('#post-video', video => video.playbackRate), 1.25, + 'choosing an option in fullscreen changes playback speed'); + await openSpeedMenu(); + await page.evaluate(() => document.exitFullscreen()); + await page.waitForFunction(() => document.fullscreenElement === null); + await closeSpeedMenu(); + await page.waitForFunction(selector => + document.querySelector(selector)?.parentElement === document.body, {}, menuSelector); await page.evaluate(() => { const row = document.querySelector('#post-controls-row'); @@ -219,10 +351,10 @@ test('post figure videos get controls in the Bluesky toolbar while preserving ov }); await page.waitForSelector('#delayed-controls-row .bskyplus-speed-select'); await page.waitForSelector('#post-controls-row .bskyplus-speed-select'); - assert.equal(await page.$eval('#post-controls-row .bskyplus-speed-select', select => select.value), '1.5', + assert.equal(await page.$eval('#post-controls-row .bskyplus-speed-select', select => select.value), '1.25', 'replacing the controls row should retain the current speed'); - assert.equal(await page.$eval('#figure-video', video => video.playbackRate), 1.5, - 'a player whose controls arrive later should inherit the remembered speed'); + assert.equal(await page.$eval('#figure-video', video => video.playbackRate), 1, + 'a new player whose controls arrive later should start at 1x'); await page.evaluate(() => window.BskyPlusMediaViewer.scanVideos()); assert.equal(await page.$$eval('#post-controls-row .bskyplus-video-toolbar', toolbars => toolbars.length), 1); -- knotmirror 0.0.1
-
Accent Color
-
Pride palette & custom colors
- -
- - - - - - - - -
- -
-
- - +
+
ALT Text
+
+ +
- +
+
Theme
+
+ Color theme + +
+
+ + +
+

+
+
Features
@@ -49,7 +66,7 @@ @@ -57,7 +74,7 @@ @@ -70,20 +87,12 @@ -
-
-
Accent Color
-
Pride palette & custom colors
- -
- - - - - - - - -
- -
-
- - +
+
ALT Text
+
+ +
- +
+
Theme
+
+ Color theme + +
+
+ + +
+

+
+
Features
@@ -49,7 +66,7 @@ @@ -57,7 +74,7 @@ @@ -70,20 +87,12 @@ -
-
-
Accent Color
-
Pride palette & custom colors
- -
- - - - - - - - -
- -
-
- - +
+
ALT Text
+
+ +
-
+ `; } @@ -63,8 +66,10 @@ function documentFor(pathname, search = '') { async function startFixtureServer() { const server = createServer((request, response) => { const url = new URL(request.url, 'http://127.0.0.1'); - if (url.pathname === '/bskyplus') { - response.writeHead(302, { location: '/settings?bskyplus=1' }); + if (url.pathname === '/bskyplus' || url.pathname === '/bskyplus/theme') { + response.writeHead(302, { + location: url.pathname === '/bskyplus/theme' ? '/settings?bskyplus=theme' : '/settings?bskyplus=1', + }); response.end(); return; } @@ -80,6 +85,7 @@ async function openFixture() { const fixture = await startFixtureServer(); const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); const page = await browser.newPage(); + page.setDefaultTimeout(5000); return { ...fixture, browser, page }; } @@ -119,6 +125,25 @@ test('settings link uses the about entry and client-side navigation mounts Bsky+ { tag: 'SECTION', hidden: false }, ]); + assert.equal(await fixture.page.$eval('[data-bskyplus-theme-link]', link => link.getAttribute('href')), '/bskyplus/theme'); + await fixture.page.click('[data-bskyplus-theme-link]'); + await fixture.page.waitForFunction(() => location.pathname === '/bskyplus/theme'); + await fixture.page.waitForSelector('section[data-bskyplus-settings-page] h1'); + assert.equal(await fixture.page.$eval('section[data-bskyplus-settings-page] h1', heading => heading.textContent), 'Theme editor'); + assert.equal(await fixture.page.$eval('section[data-bskyplus-settings-page] a', link => link.getAttribute('href')), '/bskyplus'); + + await fixture.page.click('section[data-bskyplus-settings-page] a[href="/bskyplus"]'); + await fixture.page.waitForFunction(() => location.pathname === '/bskyplus'); + await fixture.page.waitForSelector('section[data-bskyplus-settings-page] h1'); + assert.equal(await fixture.page.$eval('section[data-bskyplus-settings-page] h1', heading => heading.textContent), 'Bsky+'); + await fixture.page.goBack(); + await fixture.page.waitForFunction(() => location.pathname === '/bskyplus/theme'); + await fixture.page.waitForSelector('section[data-bskyplus-settings-page] h1'); + assert.equal(await fixture.page.$eval('section[data-bskyplus-settings-page] h1', heading => heading.textContent), 'Theme editor'); + await fixture.page.goBack(); + await fixture.page.waitForFunction(() => location.pathname === '/bskyplus'); + await fixture.page.waitForSelector('section[data-bskyplus-settings-page] h1'); + assert.equal(await fixture.page.$eval('section[data-bskyplus-settings-page] h1', heading => heading.textContent), 'Bsky+'); await fixture.page.goBack(); await fixture.page.waitForFunction(() => location.pathname === '/settings'); await fixture.page.waitForSelector('section[data-bskyplus-settings-page]', { hidden: true }); @@ -144,23 +169,32 @@ test('fallback account link, direct refresh, back/forward, and mutation remain i assert.equal(await fixture.page.evaluate(() => location.pathname), '/bskyplus'); assert.equal(await fixture.page.$$eval('section[data-bskyplus-settings-page]', sections => sections.length), 1); + await fixture.page.goto(`${fixture.origin}/bskyplus/theme`, { waitUntil: 'networkidle0' }); + await fixture.page.waitForSelector('section[data-bskyplus-settings-page] h1'); + assert.equal(await fixture.page.evaluate(() => location.pathname), '/bskyplus/theme'); + assert.equal(await fixture.page.$eval('section[data-bskyplus-settings-page] h1', heading => heading.textContent), 'Theme editor'); + await fixture.page.reload({ waitUntil: 'networkidle0' }); + await fixture.page.waitForSelector('section[data-bskyplus-settings-page] h1'); + assert.equal(await fixture.page.evaluate(() => location.pathname), '/bskyplus/theme'); + await fixture.page.reload({ waitUntil: 'networkidle0' }); assert.equal(await fixture.page.$$eval('section[data-bskyplus-settings-page]', sections => sections.length), 1); - await fixture.page.click('section[data-bskyplus-settings-page] a[href="/settings"]'); - await fixture.page.waitForFunction(() => location.pathname === '/settings'); + await fixture.page.click('section[data-bskyplus-settings-page] a[href="/bskyplus"]'); + await fixture.page.waitForFunction(() => location.pathname === '/bskyplus'); await fixture.page.goBack(); await fixture.page.waitForSelector('section[data-bskyplus-settings-page]'); + assert.equal(await fixture.page.evaluate(() => location.pathname), '/bskyplus/theme'); await fixture.page.goForward(); - await fixture.page.waitForFunction(() => location.pathname === '/settings'); + await fixture.page.waitForFunction(() => location.pathname === '/bskyplus'); await fixture.page.evaluate(() => { const extra = document.createElement('div'); extra.textContent = 'native mutation'; document.querySelector('main').append(extra); }); - await fixture.page.waitForFunction(() => document.querySelectorAll('a[data-bskyplus-settings-link]').length === 1); - assert.equal(await fixture.page.$$eval('a[data-bskyplus-settings-link]', links => links.length), 1); + await fixture.page.waitForFunction(() => document.querySelectorAll('[data-bskyplus-theme-link]').length === 1); + assert.equal(await fixture.page.$$eval('[data-bskyplus-theme-link]', links => links.length), 1); } finally { await closeFixture(fixture); } @@ -187,6 +221,23 @@ test('Chrome route rule redirects only the exact Bsky+ main-frame path', () => { assert.equal(matcher.test('https://bsky.app/bskyplus-extra'), false); assert.equal(matcher.test('https://bsky.app/settings'), false); assert.equal(rule.action.redirect.url, 'https://bsky.app/settings?bskyplus=1'); + + const themeRule = routeRules.find(candidate => candidate.id === 2); + const themeMatcher = new RegExp(themeRule.condition.regexFilter); + assert.deepEqual(themeRule.condition.resourceTypes, ['main_frame']); + assert.equal(themeMatcher.test('https://bsky.app/bskyplus/theme'), true); + assert.equal(themeMatcher.test('https://bsky.app/bskyplus/theme/'), true); + assert.equal(themeMatcher.test('https://bsky.app/bskyplus/theme?from=settings'), true); + assert.equal(themeMatcher.test('https://bsky.app/bskyplus/theme-extra'), false); + assert.equal(themeRule.action.redirect.url, 'https://bsky.app/settings?bskyplus=theme'); + const infrastructureRule = routeRules.find(candidate => candidate.id === 3); + const infrastructureMatcher = new RegExp(infrastructureRule.condition.regexFilter); + assert.deepEqual(infrastructureRule.condition.resourceTypes, ['main_frame']); + for (const suffix of ['', '/', '?from=settings']) { + assert.equal(infrastructureMatcher.test('https://bsky.app/bskyplus/infrastructure' + suffix), true); + } + assert.equal(infrastructureMatcher.test('https://bsky.app/bskyplus/infrastructure-extra'), false); + assert.equal(infrastructureRule.action.redirect.url, 'https://bsky.app/settings?bskyplus=infrastructure'); }); test('integrated settings follow the native center through compact sidebar breakpoints', async () => { @@ -226,5 +277,15 @@ test('Firefox redirects only the custom route', () => { assert.equal(listener({ url: `https://bsky.app${route}` }).redirectUrl, 'https://bsky.app/settings?bskyplus=1'); } + for (const route of ['/bskyplus/theme', '/bskyplus/theme/', '/bskyplus/theme?from=settings']) { + assert.equal(listener({ url: `https://bsky.app${route}` }).redirectUrl, + 'https://bsky.app/settings?bskyplus=theme'); + } assert.equal(listener({ url: 'https://bsky.app/bskyplus-extra' }).redirectUrl, undefined); + assert.equal(listener({ url: 'https://bsky.app/bskyplus/theme-extra' }).redirectUrl, undefined); + for (const suffix of ['', '/', '?from=settings']) { + assert.equal(listener({ url: 'https://bsky.app/bskyplus/infrastructure' + suffix }).redirectUrl, + 'https://bsky.app/settings?bskyplus=infrastructure'); + } + assert.equal(listener({ url: 'https://bsky.app/bskyplus/infrastructure-extra' }).redirectUrl, undefined); }); diff --git a/test/test_llm.mjs b/test/test_llm.mjs index 52272e1..be7d621 100644 --- a/test/test_llm.mjs +++ b/test/test_llm.mjs @@ -64,13 +64,27 @@ function makeBridge(overrides = {}) { return { bridge: llm.createBridge(env), calls, env }; } +function requestLlmAccess(bridge, sender, message = { action: 'REQUEST_LLM_ACCESS' }) { + return new Promise((resolve, reject) => { + try { + assert.equal(bridge.onMessage(message, sender, resolve), true); + } catch (error) { + reject(error); + } + }); +} + test('normalizeSettings trims strings and defaults missing fields', () => { assert.deepEqual(llm.normalizeSettings({ endpoint: ' https://x/y ', model: ' m ', apiKey: 'k' }), { endpoint: 'https://x/y', model: 'm', apiKey: 'k', + useCustomPrompt: false, + customPrompt: '', + }); + assert.deepEqual(llm.normalizeSettings(null), { + endpoint: '', model: '', apiKey: '', useCustomPrompt: false, customPrompt: '', }); - assert.deepEqual(llm.normalizeSettings(null), { endpoint: '', model: '', apiKey: '' }); }); test('validateEndpoint accepts full http(s) chat URLs and local endpoints', () => { @@ -150,6 +164,17 @@ test('validateEndpoint rejects blank, non-http, credentialed, fragmented, and ba assert.equal(llm.validateEndpoint('https://api.example.com/').ok, false); }); +test('disabled LLM retains configuration but refuses generation', async () => { + const disabled = { ...ENABLED, useCustomPrompt: false, customPrompt: '', enabled: false }; + assert.equal(llm.isEnabled(disabled), false); + assert.deepEqual(llm.normalizeSettings(disabled), disabled); + const { bridge, calls } = makeBridge({ getSettings: async () => disabled }); + const result = await bridge.runLlm({ image: VALID_IMAGE }, SENDER); + assert.equal(result.success, false); + assert.equal(result.error, 'LLM is disabled'); + assert.equal(calls.length, 0); +}); + test('isEnabled is derived from a valid endpoint', () => { assert.equal(llm.isEnabled(ENABLED), true); assert.equal(llm.isEnabled({ endpoint: '', model: 'x', apiKey: 'k' }), false); @@ -194,10 +219,12 @@ test('buildRequestBody sends prompt plus data:image and optional model', () => { const body = llm.buildRequestBody({ model: 'vision-1' }, { system: 'S', user: 'U' }, VALID_IMAGE); assert.equal(body.stream, false); assert.equal(body.model, 'vision-1'); - assert.equal(body.messages[0].role, 'system'); - assert.equal(body.messages[0].content, 'S'); - const parts = body.messages[1].content; - assert.equal(parts[0].text, 'U'); + assert.equal(body.messages.length, 1); + assert.equal(body.messages[0].role, 'user'); + const parts = body.messages[0].content; + assert.equal(parts.length, 2); + assert.equal(parts[0].type, 'text'); + assert.equal(parts[0].text, 'S'); assert.equal(parts[1].image_url.url, VALID_IMAGE); const noModel = llm.buildRequestBody({ model: '' }, null, VALID_IMAGE); @@ -219,8 +246,48 @@ test('RUN_LLM success sends a POST with bearer key and returns trimmed text', as const body = JSON.parse(calls[0].opts.body); assert.equal(body.model, 'test-model'); assert.equal(body.stream, false); - assert.equal(body.messages[0].content, 'sys prompt'); - assert.equal(body.messages[1].content[1].image_url.url, VALID_IMAGE); + assert.equal(body.messages.length, 1); + assert.equal(body.messages[0].role, 'user'); + assert.equal(body.messages[0].content.length, 2); + assert.equal(body.messages[0].content[0].text, 'sys prompt'); + assert.equal(body.messages[0].content[1].image_url.url, VALID_IMAGE); +}); + +test('RUN_LLM uses only the saved custom prompt and image', async () => { + const customPrompt = 'Describe the image in one sentence.'; + const { bridge, calls } = makeBridge({ + getSettings: async () => ({ ...ENABLED, useCustomPrompt: true, customPrompt }), + }); + await bridge.runLlm({ image: VALID_IMAGE }, SENDER); + + const body = JSON.parse(calls[0].opts.body); + assert.equal(body.messages.length, 1); + assert.equal(body.messages[0].content.length, 2); + assert.equal(body.messages[0].content[0].text, customPrompt); + assert.equal(body.messages[0].content[1].image_url.url, VALID_IMAGE); +}); + +test('RUN_LLM keeps the bundled prompt when custom prompt is disabled but its draft remains saved', async () => { + const { bridge, calls } = makeBridge({ + getSettings: async () => ({ ...ENABLED, useCustomPrompt: false, customPrompt: 'Saved draft' }), + }); + await bridge.runLlm({ image: VALID_IMAGE }, SENDER); + + const body = JSON.parse(calls[0].opts.body); + assert.equal(body.messages.length, 1); + assert.equal(body.messages[0].content[0].text, 'sys prompt'); +}); + +test('RUN_LLM falls back to the bundled prompt when the enabled custom prompt is blank', async () => { + const { bridge, calls } = makeBridge({ + getSettings: async () => ({ ...ENABLED, useCustomPrompt: true, customPrompt: ' ' }), + }); + await bridge.runLlm({ image: VALID_IMAGE }, SENDER); + + const body = JSON.parse(calls[0].opts.body); + assert.equal(body.messages.length, 1); + assert.equal(body.messages[0].content[0].text, 'sys prompt'); + assert.equal(body.messages[0].content.length, 2); }); test('RUN_LLM omits Authorization when no API key is saved', async () => { @@ -303,6 +370,28 @@ test('RUN_LLM never throws into the message channel', async () => { assert.equal(typeof result.error, 'string'); }); +test('LLM completion clears its timeout on success, parse errors, and synchronous failures', async t => { + t.mock.timers.enable({ apis: ['setTimeout'] }); + for (const response of ['valid', 'invalid', 'empty', 'throw']) { + let aborted = false; + const { bridge } = makeBridge({ + fetch: (url, options) => { + options.signal.addEventListener('abort', () => { aborted = true; }); + if (response === 'throw') throw new TypeError('failed synchronously'); + return Promise.resolve({ ok: true, text: async () => response === 'valid' + ? JSON.stringify({ choices: [{ message: { content: 'Alt text' } }] }) + : response === 'invalid' ? 'not JSON' : '' }); + }, + }); + const result = await bridge.runLlm({ image: VALID_IMAGE }, SENDER); + assert.equal(result.success, response === 'valid'); + t.mock.timers.tick(2000); + assert.equal(aborted, false, `${response}: completed request must not leave an abort timer running`); + const retry = await bridge.runLlm({ image: VALID_IMAGE }, SENDER); + assert.notEqual(retry.error, 'An LLM request is already in progress'); + } +}); + test('RUN_LLM rejects bad images before any network call', async () => { const { bridge, calls } = makeBridge(); const result = await bridge.runLlm({ image: 'not-a-data-url' }, SENDER); @@ -479,6 +568,95 @@ test('sender validation rejects other ids, sub-frames, and non-bsky origins', as } }); +test('REQUEST_LLM_ACCESS accepts this extension settings page and rejects malicious senders', async () => { + let settingsReads = 0; + let permissionChecks = 0; + const opened = []; + const extensionRoot = `moz-extension://${OWN_ID}/`; + const bridge = llm.createBridge({ + api: { + runtime: { id: OWN_ID, getURL: (p) => extensionRoot + p }, + permissions: { contains: async () => { permissionChecks++; return true; } }, + tabs: { create: async options => { opened.push(options); } }, + }, + getSettings: async () => { settingsReads++; return ENABLED; }, + supportsDataConsent: false, + }); + + const accepted = await requestLlmAccess(bridge, { + id: OWN_ID, frameId: 0, url: extensionRoot + 'shared/settings-frame.html', + }); + assert.deepEqual(accepted, { success: true, granted: true }); + assert.equal(permissionChecks, 1); + + for (const sender of [ + { id: 'other-extension', url: extensionRoot + 'settings.html' }, + { id: OWN_ID, url: 'https://evil.example/settings.html' }, + { id: OWN_ID, url: `moz-extension://${OWN_ID}.evil/settings.html` }, + ]) { + const rejected = await requestLlmAccess(bridge, sender); + assert.deepEqual(rejected, { success: false, error: 'Open Bsky+ settings to grant LLM access.' }); + } + assert.equal(settingsReads, 1, 'rejected senders do not read settings'); + assert.equal(permissionChecks, 1, 'rejected senders do not probe permissions'); + assert.deepEqual(opened, [], 'already-granted access does not open a page'); +}); + +test('REQUEST_LLM_ACCESS rejects an invalid endpoint without opening a page', async () => { + const opened = []; + let permissionChecks = 0; + const extensionRoot = `moz-extension://${OWN_ID}/`; + const bridge = llm.createBridge({ + api: { + runtime: { id: OWN_ID, getURL: (p) => extensionRoot + p }, + permissions: { contains: async () => { permissionChecks++; return false; } }, + tabs: { create: async options => { opened.push(options); } }, + }, + getSettings: async () => ({ ...ENABLED, endpoint: 'not-an-endpoint' }), + supportsDataConsent: true, + }); + + const result = await requestLlmAccess(bridge, { + id: OWN_ID, url: extensionRoot + 'settings.html', + }); + assert.deepEqual(result, { success: false, error: 'Save a valid LLM endpoint before granting access.' }); + assert.equal(permissionChecks, 0); + assert.deepEqual(opened, []); +}); + +test('REQUEST_LLM_ACCESS opens the fixed extension permission page when host or consent is missing', async () => { + const extensionRoot = `moz-extension://${OWN_ID}/`; + const fixedPageUrl = extensionRoot + 'shared/llm-permissions.html'; + for (const { missing, hostGranted, consentGranted } of [ + { missing: 'host', hostGranted: false, consentGranted: true }, + { missing: 'consent', hostGranted: true, consentGranted: false }, + ]) { + const probes = []; + const opened = []; + const bridge = llm.createBridge({ + api: { + runtime: { id: OWN_ID, getURL: (p) => extensionRoot + p }, + permissions: { contains: async permission => { + probes.push(permission); + return permission.origins ? hostGranted : consentGranted; + } }, + tabs: { create: async options => { opened.push(options); } }, + }, + getSettings: async () => ENABLED, + supportsDataConsent: true, + }); + + const result = await requestLlmAccess(bridge, { + id: OWN_ID, url: extensionRoot + 'settings.html', + }, { action: 'REQUEST_LLM_ACCESS', url: 'https://attacker.example/permission-page' }); + assert.deepEqual(result, { success: true, opened: true }, `missing ${missing} opens the permission page`); + assert.equal(probes.length, 2); + assert.ok(probes.some(permission => permission.origins?.[0] === 'http://127.0.0.1/*')); + assert.ok(probes.some(permission => permission.data_collection?.join(',') === 'websiteContent,authenticationInfo')); + assert.deepEqual(opened, [{ url: fixedPageUrl }], 'the broker opens its own fixed page, not the message URL'); + } +}); + test('concurrent RUN_LLM from the same tab is deduplicated without blocking other tabs', async () => { let release; const gate = new Promise((resolve) => { release = resolve; }); @@ -542,8 +720,11 @@ test('prompt is loaded once from runtime.getURL(shared/alt-text-prompt.json)', a const endpointCall = calls.find((call) => call.url === ENABLED.endpoint); const body = JSON.parse(endpointCall.opts.body); - assert.equal(body.messages[0].content, 'PROMPT_SYS'); - assert.equal(body.messages[1].content[0].text, 'PROMPT_USR'); + assert.equal(body.messages.length, 1); + assert.equal(body.messages[0].content.length, 2); + assert.equal(body.messages[0].role, 'user'); + assert.equal(body.messages[0].content[0].text, 'PROMPT_SYS'); + assert.equal(body.messages[0].content[1].image_url.url, VALID_IMAGE); }); test('settings validateEndpoint matches backend rules and strips ports', () => { @@ -586,7 +767,10 @@ test('storageGet resolves stored values and defaults to empty on failure', async test('settings module never broadcasts the endpoint or API key', async () => { const source = await readFile(path.join(repoRoot, 'extension/shared/llm-settings.js'), 'utf8'); assert.equal(source.includes('SETTINGS_UPDATED'), false); - assert.equal(source.includes('sendMessage'), false); + const brokerPayload = source.match(/sendMessage\(\s*(\{[^}]*\})\s*\)/)?.[1]; + assert.ok(brokerPayload, 'settings asks the background broker to check permissions'); + assert.match(brokerPayload, /action:\s*'REQUEST_LLM_ACCESS'/); + assert.doesNotMatch(brokerPayload, /\b(?:endpoint|apiKey)\b/i); }); test('onMessage keeps the channel open for RUN_LLM and always responds', async () => { @@ -601,14 +785,15 @@ test('onMessage keeps the channel open for RUN_LLM and always responds', async ( assert.equal(ignored, undefined); }); -test('shared prompt JSON targets blind/low-vision readers, forbids invention, and caps at 2000', async () => { +test('shared prompt targets blind/low-vision readers and requests output within the 2000-character limit', async () => { const prompt = JSON.parse(await readFile(path.join(repoRoot, 'extension/shared/alt-text-prompt.json'), 'utf8')); assert.equal(typeof prompt.system, 'string'); - assert.match(prompt.system, /2000/); + const limit = prompt.system.match(/under (\d+) characters/i); + assert.ok(limit, 'Prompt must specify an output length limit'); + assert.ok(Number(limit[1]) > 0 && Number(limit[1]) <= 2000); assert.match(prompt.system, /alt text/i); assert.match(prompt.system, /blind|low-vision/i); assert.match(prompt.system, /only/i); - assert.match(prompt.system, /2000/); assert.match(prompt.system, /do not invent|not invent/i); }); diff --git a/test/test_llm_permissions.mjs b/test/test_llm_permissions.mjs new file mode 100644 index 0000000..8386c8b --- /dev/null +++ b/test/test_llm_permissions.mjs @@ -0,0 +1,155 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import puppeteer from 'puppeteer-core'; + +const chromePath = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const llmSource = await readFile('extension/shared/llm.js', 'utf8'); +const pageSource = await readFile('extension/shared/llm-permissions.js', 'utf8'); +const manifest = JSON.parse(await readFile('extension/firefox/manifest.json', 'utf8')); +const SETTINGS = { + endpoint: 'https://llm.example.test/v1/chat/completions', + model: 'vision-test', + apiKey: 'test-key-do-not-send', + useCustomPrompt: false, + customPrompt: '', +}; + +async function openPermissionPage(browser, { + stored = SETTINGS, + version = '140.0', + request = ['grant'], +} = {}) { + const page = await browser.newPage(); + await page.setContent('

'); + await page.evaluate(({ initial, firefoxVersion, outcomes }) => { + const calls = { request: [], contains: [], listeners: 0 }; + const state = { + settings: structuredClone(initial), + originGranted: false, + dataGranted: false, + outcomes: outcomes.slice(), + }; + window.__llmPermissionCalls = calls; + window.__llmPermissionState = state; + window.browser = { + runtime: { + id: 'test-extension', + getURL: path => `moz-extension://test-extension/${path}`, + getBrowserInfo: async () => ({ name: 'Firefox', version: firefoxVersion }), + onMessage: { addListener: () => { calls.listeners += 1; } }, + }, + storage: { + local: { + get: async () => ({ llm: structuredClone(state.settings) }), + }, + }, + permissions: { + contains: async value => { + calls.contains.push(structuredClone(value)); + if (value.origins) return state.originGranted; + if (value.data_collection) return state.dataGranted; + return false; + }, + request: async value => { + calls.request.push(structuredClone(value)); + const outcome = state.outcomes.shift() || 'grant'; + if (outcome === 'deny') return false; + if (outcome === 'reject') throw new Error('permission API rejected'); + state.originGranted = true; + if (value.data_collection) state.dataGranted = true; + return true; + }, + }, + }; + }, { initial: stored, firefoxVersion: version, outcomes: request }); + await page.evaluate(source => { + const script = document.createElement('script'); + script.setAttribute('data-no-register', ''); + script.textContent = source; + document.head.appendChild(script); + script.remove(); + }, llmSource); + await page.addScriptTag({ content: pageSource }); + await page.waitForFunction(() => !document.querySelector('#grant').disabled || document.querySelector('#grant').hidden); + return page; +} + +async function clickGrant(page) { + await page.waitForFunction(() => !document.querySelector('#grant').hidden && !document.querySelector('#grant').disabled); + await page.click('#grant'); +} + +async function calls(page) { + return page.evaluate(() => structuredClone(window.__llmPermissionCalls)); +} + +test('Firefox manifest declares data categories in gecko settings, not optional_permissions', () => { + assert.equal(manifest.optional_permissions.includes('data_collection'), false); + assert.deepEqual(manifest.browser_specific_settings.gecko.data_collection_permissions, { + required: ['none'], + optional: ['websiteContent', 'authenticationInfo'], + }); +}); + +test('permission page grants saved origin and Firefox data consent without a duplicate bridge listener', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openPermissionPage(browser); + await clickGrant(page); + await page.waitForFunction(() => document.querySelector('#status').textContent.includes('active')); + const recorded = await calls(page); + assert.deepEqual(recorded.request, [{ + origins: ['https://llm.example.test/*'], + data_collection: ['websiteContent', 'authenticationInfo'], + }]); + assert.equal(recorded.listeners, 0); + } finally { + await browser.close(); + } +}); + +test('denied permission can be retried from the privileged page', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openPermissionPage(browser, { request: ['deny', 'grant'] }); + await clickGrant(page); + await page.waitForFunction(() => document.querySelector('#status').textContent.includes('not granted')); + await clickGrant(page); + await page.waitForFunction(() => document.querySelector('#status').textContent.includes('active')); + assert.equal((await calls(page)).request.length, 2); + } finally { + await browser.close(); + } +}); + +test('changed saved endpoint is detected before the old grant is reported as active', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openPermissionPage(browser); + await page.evaluate(() => { + window.__llmPermissionState.settings.endpoint = 'https://other.example.test/v1/chat/completions'; + }); + await clickGrant(page); + await page.waitForFunction(() => document.querySelector('#status').textContent.includes('settings changed')); + assert.equal(documentContainsActive(await page.$eval('#status', status => status.textContent)), false); + } finally { + await browser.close(); + } +}); + +test('Firefox versions before built-in data consent request only the saved endpoint origin', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openPermissionPage(browser, { version: '109.0' }); + await clickGrant(page); + await page.waitForFunction(() => document.querySelector('#status').textContent.includes('active')); + assert.deepEqual((await calls(page)).request, [{ origins: ['https://llm.example.test/*'] }]); + } finally { + await browser.close(); + } +}); + +function documentContainsActive(statusText) { + return statusText.includes('active'); +} diff --git a/test/test_llm_settings_save.mjs b/test/test_llm_settings_save.mjs index 005e1bf..491e6f6 100644 --- a/test/test_llm_settings_save.mjs +++ b/test/test_llm_settings_save.mjs @@ -10,32 +10,44 @@ const SAVED = { endpoint: 'https://llm.example.test/v1/chat/completions', model: 'vision-test', apiKey: 'test-key-do-not-send', + useCustomPrompt: false, + customPrompt: '', }; -async function openSettings(browser, { stored = {}, permission, storageSet } = {}) { +async function openSettings(browser, { + stored = {}, permission, storageSet, missingPermissions = false, + brokerResponse = { success: true, opened: true }, +} = {}) { const page = await browser.newPage(); await page.setContent('
'); - await page.evaluate(({ initial, permissionResults, setResult }) => { - const calls = { get: 0, set: [], request: [] }; + await page.evaluate(({ initial, permissionResults, setResult, missingPermissions, brokerResponse }) => { + const calls = { get: 0, set: [], request: [], messages: [] }; window.__llmTestCalls = calls; + let storedSettings = structuredClone(initial); const outcomes = permissionResults.slice(); - window.chrome = { + const api = { runtime: { getBrowserInfo: async () => ({ name: 'Chrome', version: '140.0' }), + sendMessage: async message => { + calls.messages.push(structuredClone(message)); + return brokerResponse; + }, }, storage: { local: { get: async () => { calls.get += 1; - return { llm: initial }; + return { llm: structuredClone(storedSettings) }; }, set: async value => { calls.set.push(structuredClone(value)); if (setResult === 'reject') throw new Error('storage unavailable'); + storedSettings = structuredClone(value.llm); }, }, }, - permissions: { + }; + if (!missingPermissions) api.permissions = { request: async request => { calls.request.push({ request: structuredClone(request), @@ -45,12 +57,14 @@ async function openSettings(browser, { stored = {}, permission, storageSet } = { if (outcome === 'reject') throw new Error('permission request failed'); return outcome !== 'deny'; }, - }, - }; + }; + window.chrome = api; }, { initial: stored, permissionResults: Array.isArray(permission) ? permission : [permission || 'grant'], setResult: storageSet || 'resolve', + missingPermissions, + brokerResponse, }); await page.addScriptTag({ content: settingsScript }); await page.waitForSelector('.bskyplus-llm-section'); @@ -88,6 +102,37 @@ test('Save persists LLM settings before requesting host access', async () => { } }); +test('missing embedded permissions API opens the broker page and remains retryable', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openSettings(browser, { missingPermissions: true }); + await fillAndSave(page); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent.includes('permissions page')); + assert.equal(await page.$eval('.bskyplus-llm-grant', button => button.hidden), false); + assert.notEqual(await page.$eval('.bskyplus-llm-status', status => status.textContent), 'LLM endpoint saved.'); + await page.click('.bskyplus-llm-grant'); + await page.waitForFunction(() => window.__llmTestCalls.messages.length === 2); + assert.deepEqual((await calls(page)).messages, [ + { action: 'REQUEST_LLM_ACCESS' }, + { action: 'REQUEST_LLM_ACCESS' }, + ]); + } finally { + await browser.close(); + } +}); + +test('missing permissions API does not treat an unexpected broker response as a grant', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openSettings(browser, { missingPermissions: true, brokerResponse: { success: true } }); + await fillAndSave(page); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent.includes('finish enabling')); + assert.equal(await page.$eval('.bskyplus-llm-grant', button => button.hidden), false); + } finally { + await browser.close(); + } +}); + test('denied or rejected permission keeps saved settings and exposes retry', async () => { for (const permission of ['deny', 'reject']) { const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); @@ -120,6 +165,128 @@ test('storage failure never requests host access', async () => { } }); +test('custom prompt draft is saved even when it is incomplete, without permission', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openSettings(browser); + await page.evaluate(endpoint => { + document.querySelector('#bskyplusLlmEndpoint').value = endpoint; + document.querySelector('#bskyplusLlmUseCustomPrompt').click(); + }, SAVED.endpoint); + await page.click('.bskyplus-llm-save'); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent.includes('Enter a custom prompt')); + const recorded = await calls(page); + assert.ok(recorded.set.length >= 1, 'the toggle and current settings draft are saved'); + assert.equal(recorded.set.at(-1).llm.useCustomPrompt, true); + assert.equal(recorded.request.length, 0); + } finally { + await browser.close(); + } +}); + +test('custom prompt stays saved while its disabled field is empty and can be restored', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const prompt = 'Describe colors and visible text.'; + const page = await openSettings(browser, { + stored: { ...SAVED, useCustomPrompt: true, customPrompt: prompt }, + }); + assert.deepEqual(await page.$eval('#bskyplusLlmCustomPrompt', input => ({ + value: input.value, + disabled: input.disabled, + })), { value: prompt, disabled: false }); + assert.equal(await page.$$eval('.bskyplus-llm-label', labels => + labels.filter(label => label.textContent === 'Custom prompt').length), 1); + assert.equal(await page.$eval('.bskyplus-llm-section', section => section.textContent.includes('Use custom prompt')), false); + + await page.click('#bskyplusLlmUseCustomPrompt'); + await page.waitForFunction(() => window.__llmTestCalls.set.some(({ llm }) => llm.useCustomPrompt === false)); + assert.deepEqual(await page.$eval('#bskyplusLlmCustomPrompt', input => ({ + value: input.value, + disabled: input.disabled, + placeholder: input.placeholder, + })), { value: '', disabled: true, placeholder: '' }); + + await page.click('.bskyplus-llm-save'); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent === 'LLM endpoint saved.'); + const persisted = (await calls(page)).set.at(-1).llm; + assert.equal(persisted.useCustomPrompt, false); + assert.equal(persisted.customPrompt, prompt); + await page.close(); + + const reopened = await openSettings(browser, { stored: persisted }); + assert.deepEqual(await reopened.$eval('#bskyplusLlmCustomPrompt', input => ({ + value: input.value, + disabled: input.disabled, + })), { value: '', disabled: true }); + await reopened.click('#bskyplusLlmUseCustomPrompt'); + assert.deepEqual(await reopened.$eval('#bskyplusLlmCustomPrompt', input => ({ + value: input.value, + disabled: input.disabled, + })), { value: prompt, disabled: false }); + } finally { + await browser.close(); + } +}); + +test('each independent setting is saved when its field loses focus', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openSettings(browser, { stored: { model: 'old-model', apiKey: 'old-key' } }); + await page.focus('#bskyplusLlmEndpoint'); + await page.$eval('#bskyplusLlmEndpoint', (input, value) => { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); }, SAVED.endpoint); + await page.click('#bskyplusLlmModel'); + await page.$eval('#bskyplusLlmModel', input => { input.value = 'new-model'; input.dispatchEvent(new Event('input', { bubbles: true })); }); + await page.click('#bskyplusLlmApiKey'); + await page.$eval('#bskyplusLlmApiKey', input => { input.value = 'new-key'; input.dispatchEvent(new Event('input', { bubbles: true })); }); + await page.click('.bskyplus-llm-save'); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent.includes('saved')); + + const recorded = await calls(page); + assert.deepEqual(recorded.set.slice(0, 3).map(value => value.llm), [ + { endpoint: SAVED.endpoint, model: 'old-model', apiKey: 'old-key', useCustomPrompt: false, customPrompt: '' }, + { endpoint: SAVED.endpoint, model: 'new-model', apiKey: 'old-key', useCustomPrompt: false, customPrompt: '' }, + { endpoint: SAVED.endpoint, model: 'new-model', apiKey: 'new-key', useCustomPrompt: false, customPrompt: '' }, + ]); + assert.equal(recorded.request.length, 1); + } finally { + await browser.close(); + } +}); + +test('window blur starts saving a focused draft and drafts never request permission', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openSettings(browser, { stored: { model: 'old-model' } }); + await page.focus('#bskyplusLlmModel'); + await page.$eval('#bskyplusLlmModel', input => { input.value = 'draft-model'; input.dispatchEvent(new Event('input', { bubbles: true })); }); + await page.evaluate(() => window.dispatchEvent(new Event('blur'))); + await page.waitForFunction(() => window.__llmTestCalls.set.length > 0); + const recorded = await calls(page); + assert.equal(recorded.set.at(-1).llm.model, 'draft-model'); + assert.equal(recorded.request.length, 0); + } finally { + await browser.close(); + } +}); + +test('clearing only the endpoint retains other saved settings', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const existing = { ...SAVED, enabled: false }; + const page = await openSettings(browser, { stored: existing }); + await page.focus('#bskyplusLlmEndpoint'); + await page.$eval('#bskyplusLlmEndpoint', input => { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); }); + await page.click('.bskyplus-llm-save'); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent.includes('cleared')); + const recorded = await calls(page); + assert.deepEqual(recorded.set.at(-1).llm, { ...existing, endpoint: '' }); + assert.equal(recorded.request.length, 0); + } finally { + await browser.close(); + } +}); + test('reopened settings popup reads the persisted LLM values', async () => { const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); try { @@ -133,7 +300,7 @@ test('reopened settings popup reads the persisted LLM values', async () => { endpoint: document.querySelector('#bskyplusLlmEndpoint').value, model: document.querySelector('#bskyplusLlmModel').value, apiKey: document.querySelector('#bskyplusLlmApiKey').value, - })), SAVED); + })), { endpoint: SAVED.endpoint, model: SAVED.model, apiKey: SAVED.apiKey }); } finally { await browser.close(); } diff --git a/test/test_modal_ocr.mjs b/test/test_modal_ocr.mjs new file mode 100644 index 0000000..8dd747a --- /dev/null +++ b/test/test_modal_ocr.mjs @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import puppeteer from 'puppeteer-core'; + +const ocrButtons = '#bskyplus-ocr-btn, #bskyplus-ocr-header-btn'; +const image = 'Uploaded image'; +const fields = `${image}
`; + +for (const browserName of ['chrome', 'firefox', 'safari']) { + test(`${browserName}: modal OCR belongs only to the alt-text editor`, async t => { + const browser = await puppeteer.launch({ executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome', headless: true }); + t.after(() => browser.close()); + const page = await browser.newPage(); + await page.setContent(` + + + + `); + await page.evaluate(() => { + window.chrome = { + storage: { local: { get: (_, callback) => callback({ localOcrEnabled: true }) }, onChanged: { addListener() {} } }, + runtime: { onMessage: { addListener() {} }, sendMessage: async () => ({ success: true, text: 'Recognized text' }) }, + }; + }); + await page.addScriptTag({ content: await readFile(`extension/${browserName}/content.js`, 'utf8') }); + await page.waitForSelector(`#alt :is(${ocrButtons})`); + assert.equal(await page.$$eval(ocrButtons, nodes => nodes.length), 1); + assert.equal(await page.$(`#profile :is(${ocrButtons})`), null); + assert.equal(await page.$(`#ocr-title :is(${ocrButtons})`), null); + await page.click(`#alt :is(${ocrButtons})`); + await page.waitForFunction(() => document.querySelector('#alt textarea').value === 'Recognized text'); + assert.equal(await page.$eval('#profile textarea', node => node.value), ''); + await page.$eval('#alt', modal => { + modal.setAttribute('aria-label', 'Edit profile'); + modal.querySelector('h2').textContent = 'Edit profile'; + }); + await page.waitForFunction(selector => !document.querySelector(selector), {}, ocrButtons); + }); +} diff --git a/test/test_new_features.mjs b/test/test_new_features.mjs index 9b45f37..29ed70d 100644 --- a/test/test_new_features.mjs +++ b/test/test_new_features.mjs @@ -212,6 +212,13 @@ class MockElement { return null; } + contains(node) { + for (let current = node; current; current = current.parentElement) { + if (current === this) return true; + } + return false; + } + addEventListener(event, fn) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(fn); @@ -478,17 +485,21 @@ test('2. Enhanced Media Viewer: toolbar placement, playback speed, and PiP', () assert.equal(video1.getAttribute('controls'), null, 'Video must retain custom controls'); const speedSelect = toolbar.querySelector('.bskyplus-speed-select'); - assert.ok(speedSelect, 'Speed select must be attached'); - assert.deepEqual(speedSelect.querySelectorAll('option').map(option => option.value), - ['0.5', '1', '1.25', '1.5', '2']); + assert.ok(speedSelect, 'Speed menu trigger must be attached'); + assert.equal(speedSelect.tagName, 'BUTTON', 'Speed menu trigger should be a custom button'); + assert.equal(speedSelect.getAttribute('aria-haspopup'), 'menu'); // Selecting 1.5x sets video playbackRate and is remembered. - speedSelect.value = '1.5'; - speedSelect.focus(); - speedSelect.dispatchEvent(new window.Event('change')); + const speedMenu = document.querySelector('.bskyplus-speed-menu'); + assert.ok(speedMenu, 'Custom speed menu should be attached to the document'); + assert.deepEqual(speedMenu.querySelectorAll('[role="menuitemradio"]') + .map(option => option.getAttribute('data-speed')), + ['0.5', '1', '1.25', '1.5', '2']); + speedMenu.querySelector('[data-speed="1.5"]').click(); assert.equal(video1.playbackRate, 1.5); - assert.equal(document.activeElement, document.body, - 'changing speed should blur the selector'); + assert.equal(speedSelect.value, '1.5'); + assert.equal(speedSelect.querySelector('.bskyplus-speed-value').textContent, '1.5x'); + assert.equal(speedMenu.querySelector('[data-speed="1.5"]').getAttribute('aria-checked'), 'true'); assert.equal(controlsRow.childNodes[3], toolbar, 'toolbar should be inserted as the fourth direct control in the row'); @@ -682,87 +693,7 @@ test('5. Account Age: fresh account detection and badge calculation', () => { assert.ok(olderAge.days > 30, 'Should not qualify as fresh account'); }); -test('6. Thread Unroll: linear author thread extraction, 3-dot dropdown menu injection, and Markdown generation', () => { - const { context, window, document } = createEnvironment(); - runScript('extension/shared/feature-flags.js', context); - window.BskyPlusFeatureFlags?.setAllowAlpha(true); - runScript('extension/shared/dom-utils.js', context); - runScript('extension/shared/thread-unroll.js', context); - - const { extractLinearAuthorThread, generateMarkdown, scanForDropdownMenus } = window.BskyPlusThreadUnroll; - - const authorDid = 'did:plc:author123'; - const threadNode = { - post: { - uri: 'at://author123/app.bsky.feed.post/post1', - cid: 'cid1', - author: { did: authorDid, handle: 'alice.bsky.social', displayName: 'Alice' }, - record: { text: '1/3 First skeet in thread!', createdAt: '2026-09-14T09:00:00Z' }, - embed: { - images: [{ thumb: 'thumb1.jpg', fullsize: 'full1.jpg', alt: 'A sunset photo' }] - } - }, - replies: [ - { - post: { - uri: 'at://author123/app.bsky.feed.post/post2', - author: { did: authorDid, handle: 'alice.bsky.social', displayName: 'Alice' }, - record: { text: '2/3 Second skeet in thread with more details.', createdAt: '2026-09-14T09:01:00Z' } - }, - replies: [ - { - post: { - uri: 'at://author123/app.bsky.feed.post/post3', - author: { did: authorDid, handle: 'alice.bsky.social', displayName: 'Alice' }, - record: { text: '3/3 Final concluding skeet.', createdAt: '2026-09-14T09:02:00Z' } - } - } - ] - }, - { - // Unrelated third-party reply that should NOT be part of author thread - post: { - uri: 'at://rando/app.bsky.feed.post/rando1', - author: { did: 'did:plc:other', handle: 'bob.bsky.social', displayName: 'Bob' }, - record: { text: 'Cool thread!', createdAt: '2026-09-14T09:05:00Z' } - } - } - ] - }; - - const linearPosts = extractLinearAuthorThread(threadNode); - assert.equal(linearPosts.length, 3, 'Should extract only the 3 author skeets'); - assert.equal(linearPosts[0].text, '1/3 First skeet in thread!'); - assert.equal(linearPosts[1].text, '2/3 Second skeet in thread with more details.'); - assert.equal(linearPosts[2].text, '3/3 Final concluding skeet.'); - assert.equal(linearPosts[0].images.length, 1); - assert.equal(linearPosts[0].images[0].alt, 'A sunset photo'); - - // Test Markdown export generation - const md = generateMarkdown(linearPosts); - assert.ok(md.includes('# Thread by Alice (@alice.bsky.social)')); - assert.ok(md.includes('### 1/3')); - assert.ok(md.includes('1/3 First skeet in thread!')); - assert.ok(md.includes('![A sunset photo](full1.jpg)')); - assert.ok(md.includes('### 2/3')); - assert.ok(md.includes('### 3/3')); - - // Test 3-dot dropdown menu injection - const menu = document.createElement('div'); - menu.setAttribute('role', 'menu'); - const translateItem = document.createElement('div'); - translateItem.setAttribute('role', 'menuitem'); - translateItem.textContent = 'Translate'; - menu.appendChild(translateItem); - document.body.appendChild(menu); - - scanForDropdownMenus(); - const unrollMenuItem = menu.querySelector('.bskyplus-unroll-menu-item'); - assert.ok(unrollMenuItem, 'Should inject Unroll Thread item into 3-dot post dropdown menu'); - assert.ok(unrollMenuItem.textContent.includes('Unroll Thread'), 'Menu item should say Unroll Thread'); -}); - -test('7. Feature Flags: stage gating, alpha toggle, and runtime feature suppression', () => { +test('6. Feature Flags: stage gating, alpha toggle, and runtime feature suppression', () => { const { context, window, document } = createEnvironment(); runScript('extension/shared/feature-flags.js', context); runScript('extension/shared/dom-utils.js', context); @@ -776,41 +707,21 @@ test('7. Feature Flags: stage gating, alpha toggle, and runtime feature suppress assert.ok(flags.FEATURE_FLAGS.quickEmoji, 'quickEmoji flag exists'); assert.ok(flags.FEATURE_FLAGS.feedWidth, 'feedWidth flag exists'); assert.ok(flags.FEATURE_FLAGS.accountAge, 'accountAge flag exists'); - assert.ok(flags.FEATURE_FLAGS.threadUnroll, 'threadUnroll flag exists'); - // Test release stage behavior flags.setFeatureStage('quickEmoji', 'release'); assert.equal(flags.isFeatureEnabled('quickEmoji'), true, 'Release feature should be enabled'); // Test Alpha stage gating - flags.setFeatureStage('threadUnroll', 'alpha'); + flags.setFeatureStage('accountAge', 'alpha'); flags.setAllowAlpha(false); - assert.equal(flags.isFeatureEnabled('threadUnroll'), false, 'Alpha feature should be disabled when alpha mode is off'); + assert.equal(flags.isFeatureEnabled('accountAge'), false, 'Alpha feature should be disabled when alpha mode is off'); flags.setAllowAlpha(true); - assert.equal(flags.isFeatureEnabled('threadUnroll'), true, 'Alpha feature should be enabled when alpha mode is on'); + assert.equal(flags.isFeatureEnabled('accountAge'), true, 'Alpha feature should be enabled when alpha mode is on'); // Test Disabled stage - flags.setFeatureStage('threadUnroll', 'disabled'); - assert.equal(flags.isFeatureEnabled('threadUnroll'), false, 'Disabled feature should stay disabled even with alpha mode on'); - - // Test runtime feature suppression in thread-unroll.js - flags.setFeatureStage('threadUnroll', 'disabled'); - runScript('extension/shared/thread-unroll.js', context); - const menu = document.createElement('div'); - menu.setAttribute('role', 'menu'); - const randoItem = document.createElement('div'); - randoItem.textContent = 'Translate'; - menu.appendChild(randoItem); - document.body.appendChild(menu); - - window.BskyPlusThreadUnroll.scanForDropdownMenus(); - assert.equal(menu.querySelector('.bskyplus-unroll-menu-item'), null, 'Should NOT inject unroll menu item when feature is disabled'); - - // Reset to release stage and re-test - flags.setFeatureStage('threadUnroll', 'release'); - window.BskyPlusThreadUnroll.scanForDropdownMenus(); - assert.ok(menu.querySelector('.bskyplus-unroll-menu-item'), 'Should inject unroll menu item once re-enabled'); + flags.setFeatureStage('accountAge', 'disabled'); + assert.equal(flags.isFeatureEnabled('accountAge'), false, 'Disabled feature should stay disabled even with alpha mode on'); // Test runtime suppression in media downloader flags.setFeatureStage('mediaDownloader', 'alpha'); diff --git a/test/test_notification_width.mjs b/test/test_notification_width.mjs index ea764e7..2975510 100644 --- a/test/test_notification_width.mjs +++ b/test/test_notification_width.mjs @@ -23,6 +23,9 @@ test('custom width leaves notification avatar columns narrow', async () => {
`); await page.addScriptTag({ path: 'extension/shared/feed-width.js' }); await page.evaluate(() => window.BskyPlusFeedWidth.setWidth(1000)); + await page.waitForFunction(() => + document.getElementById('feed').getBoundingClientRect().width === 1000 && + !document.documentElement.hasAttribute('data-bskyplus-feed-width-animating')); const layout = await page.evaluate(() => Object.fromEntries( ['feed', 'avatar', 'text'].map(id => [id, document.getElementById(id).getBoundingClientRect().width]))); assert.equal(layout.feed, 1000); @@ -32,3 +35,124 @@ test('custom width leaves notification avatar columns narrow', async () => { await browser.close(); } }); + +test('widened text-only posts use the full column while embeds stay contained', async () => { + const browser = await puppeteer.launch({ + executablePath: process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: true, + }); + try { + const page = await browser.newPage(); + await page.setViewport({ width: 1600, height: 1000 }); + await page.setContent(` +
+
+
+
A text-only post that should fill the feed.
+
+
+
+
A post with media.
+
Sample image
+
+
+
+
+
+
+
Quoted author
+ A text-only quote card. +
+
+
+ `); + await page.addStyleTag({ path: 'extension/shared/features.css' }); + await page.addScriptTag({ path: 'extension/shared/feed-width.js' }); + await page.evaluate(() => window.BskyPlusFeedWidth.setWidth(1000)); + await page.waitForFunction(() => + document.getElementById('feed').getBoundingClientRect().width === 1000 && + !document.documentElement.hasAttribute('data-bskyplus-feed-width-animating')); + + const widths = await page.evaluate(() => { + const post = document.createElement('div'); + post.setAttribute('data-testid', 'contentHider-post'); + post.style.cssText = 'display:flex;flex-direction:column'; + const wrapper = document.createElement('div'); + wrapper.id = 'newTextWrapper'; + wrapper.style.marginBottom = '2px'; + const text = document.createElement('div'); + text.setAttribute('data-testid', 'postText'); + text.textContent = 'A newly loaded text-only post.'; + wrapper.appendChild(text); + post.appendChild(wrapper); + document.getElementById('feed').appendChild(post); + + return Object.fromEntries(['textWrapper', 'newTextWrapper', 'media', 'mediaImage', 'videoWrapper', 'postVideo', 'quote'].map(id => [ + id, document.getElementById(id).getBoundingClientRect().width + ])); + }); + assert.deepEqual(widths, { + textWrapper: 1000, + newTextWrapper: 1000, + media: 600, + mediaImage: 600, + videoWrapper: 600, + postVideo: 600, + quote: 1000 + }); + } finally { + await browser.close(); + } +}); + +test('live width changes animate the feed, borders, and sidebars unless motion is reduced', async () => { + const browser = await puppeteer.launch({ + executablePath: process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: true, + }); + try { + const page = await browser.newPage(); + await page.setViewport({ width: 1600, height: 1000 }); + await page.setContent(` + + +
+
+ `); + await page.addScriptTag({ path: 'extension/shared/feed-width.js' }); + await page.evaluate(() => window.BskyPlusFeedWidth.setWidth(900)); + await page.waitForFunction(() => document.getElementById('feed').getAnimations() + .some(animation => animation.transitionProperty === 'max-width')); + + const active = await page.evaluate(() => { + const isTransitioning = (id, property) => document.getElementById(id).getAnimations() + .some(animation => animation.transitionProperty === property); + return { + feed: isTransitioning('feed', 'max-width'), + borders: isTransitioning('borders', 'width'), + left: isTransitioning('left', 'margin-left'), + right: isTransitioning('right', 'margin-left') + }; + }); + assert.deepEqual(active, { feed: true, borders: true, left: true, right: true }); + const duration = await page.$eval('#feed', feed => feed.getAnimations() + .find(animation => animation.transitionProperty === 'max-width').effect.getTiming().duration); + assert.equal(duration, 800); + + await page.waitForFunction(() => + Math.round(document.getElementById('feed').getBoundingClientRect().width) === 900 && + !document.documentElement.hasAttribute('data-bskyplus-feed-width-animating')); + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); + const reducedMotion = await page.evaluate(() => { + window.BskyPlusFeedWidth.setWidth(700); + const feed = document.getElementById('feed'); + return { + width: feed.getBoundingClientRect().width, + animations: feed.getAnimations().length + }; + }); + assert.deepEqual(reducedMotion, { width: 700, animations: 0 }); + } finally { + await browser.close(); + } +}); diff --git a/test/test_popup_bookmarks.mjs b/test/test_popup_bookmarks.mjs new file mode 100644 index 0000000..016dc50 --- /dev/null +++ b/test/test_popup_bookmarks.mjs @@ -0,0 +1,66 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import puppeteer from 'puppeteer-core'; + +test('popup enable toggle ignores state polls that began before the click', async () => { + const browser = await puppeteer.launch({ + executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome', + headless: true, + args: ['--no-sandbox'] + }); + try { + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', request => request.respond({ + status: 200, + contentType: 'text/html', + body: '

Details

' + })); + await page.goto('https://extension.test/popup/popup.html'); + await page.evaluate(() => { + const pending = []; + window.chrome = { + tabs: { + query: async () => [{ id: 1 }], + sendMessage: async (id, message) => new Promise(resolve => pending.push({ action: message.action, resolve })) + }, + storage: { + local: { get: async () => ({ appviewHostname: 'api.bsky.app' }) }, + onChanged: { addListener() {} } + } + }; + window.__pendingBookmarkRequests = pending; + window.__intervalCallback = null; + window.setInterval = callback => { window.__intervalCallback = callback; return 1; }; + }); + await page.addScriptTag({ content: await readFile('extension/chrome/popup/feature-row.js', 'utf8') }); + await page.addScriptTag({ content: await readFile('extension/chrome/shared/bookmarks-settings.js', 'utf8') }); + await page.waitForFunction(() => window.__pendingBookmarkRequests.length === 1); + assert.equal(await page.$$eval('#bookmarksSection button', buttons => + buttons.some(button => button.textContent === 'Sync now')), false); + assert.equal(await page.$('#bookmarksSection [role="status"]'), null); + assert.equal(await page.$eval('#bookmarksAppview', select => select.parentElement.hidden), false); + await page.evaluate(() => window.__pendingBookmarkRequests[0].resolve({ available: true, enabled: false })); + await page.waitForFunction(() => { + const toggle = document.getElementById('bookmarksEnabledToggle'); + return toggle && !toggle.disabled; + }); + + // Begin a periodic state request, then complete enable before that poll. + await page.evaluate(() => { window.__intervalCallback(); }); + await page.waitForFunction(() => window.__pendingBookmarkRequests.length === 2); + await page.click('#bookmarksEnabledToggle'); + await page.waitForFunction(() => window.__pendingBookmarkRequests.length === 3); + assert.equal(await page.evaluate(() => window.__pendingBookmarkRequests[2].action), 'enable'); + await page.evaluate(() => window.__pendingBookmarkRequests[2].resolve({ available: true, enabled: true })); + await page.waitForFunction(() => document.getElementById('bookmarksEnabledToggle').checked); + + await page.evaluate(() => window.__pendingBookmarkRequests[1].resolve({ available: true, enabled: false })); + await new Promise(resolve => setTimeout(resolve, 0)); + assert.equal(await page.$eval('#bookmarksEnabledToggle', toggle => toggle.checked), true, + 'a stale poll must not turn off the successful enable state'); + } finally { + await browser.close(); + } +}); diff --git a/test/test_popup_flags.mjs b/test/test_popup_flags.mjs index f484fae..df1cc96 100644 --- a/test/test_popup_flags.mjs +++ b/test/test_popup_flags.mjs @@ -17,15 +17,18 @@ async function loadPopup(page, browserName, { enableAlphaFeatures = false } = {} const stored = { enableAlphaFeatures: initialAlpha }; const listeners = []; const get = (keys, callback) => { - setTimeout(() => { + const result = new Promise(resolve => setTimeout(() => { + if (typeof keys === 'string') keys = [keys]; if (Array.isArray(keys)) { - callback(Object.fromEntries(keys + resolve(Object.fromEntries(keys .filter(key => Object.prototype.hasOwnProperty.call(stored, key)) .map(key => [key, stored[key]]))); } else { - callback({ ...keys, ...stored }); + resolve({ ...keys, ...stored }); } - }, 0); + }, 0)); + if (callback) result.then(callback); + else return result; }; window.chrome = { runtime: { getManifest: () => ({ version: 'test' }) }, @@ -79,11 +82,11 @@ for (const browserName of browsers) { await loadPopup(page, browserName); const state = await visibleState(page); - for (const feature of ['mediaDownloader', 'accountAge', 'threadUnroll']) { + for (const feature of ['mediaDownloader', 'accountAge']) { assert.equal(state[feature].visible, false, `${feature} should be hidden by default`); assert.equal(state[feature].badgeCount, 0, `${feature} should have no hidden badge`); } - for (const feature of ['feedWidth', 'mediaViewer', 'quickEmoji', 'skeetMode', 'accentColor', 'localOcr']) { + for (const feature of ['feedWidth', 'mediaViewer', 'quickEmoji', 'skeetMode', 'localOcr']) { assert.equal(state[feature].visible, true, `${feature} should be visible at release stage`); } assert.equal(await page.$eval('#localOcrToggle', input => input.checked), true); @@ -100,7 +103,7 @@ for (const browserName of browsers) { const page = await browser.newPage(); await loadPopup(page, browserName, { enableAlphaFeatures: true }); let state = await visibleState(page); - for (const feature of ['mediaDownloader', 'accountAge', 'threadUnroll']) { + for (const feature of ['mediaDownloader', 'accountAge']) { assert.equal(state[feature].visible, true, `${feature} should show when alpha is enabled`); assert.equal(state[feature].badgeCount, 1, `${feature} should have one alpha badge`); } @@ -116,7 +119,7 @@ for (const browserName of browsers) { window.eval('currentSettings.enableAlphaFeatures = false; updateUI()'); }); state = await visibleState(page); - for (const feature of ['mediaDownloader', 'accountAge', 'threadUnroll']) { + for (const feature of ['mediaDownloader', 'accountAge']) { assert.equal(state[feature].visible, false, `${feature} should hide after alpha is disabled`); assert.equal(state[feature].badgeCount, 0, `${feature} badge should be removed on refresh`); } diff --git a/test/test_popup_themes.mjs b/test/test_popup_themes.mjs new file mode 100644 index 0000000..54b4d6a --- /dev/null +++ b/test/test_popup_themes.mjs @@ -0,0 +1,186 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import puppeteer from 'puppeteer-core'; + +const chromePath = process.env.CHROME_PATH || '/usr/bin/google-chrome'; + +async function openPopup(browser, browserName) { + const page = await browser.newPage(); + const html = await readFile(`extension/${browserName}/popup/popup.html`, 'utf8'); + const popupScript = await readFile(`extension/${browserName}/popup/popup.js`, 'utf8'); + const themeModel = await readFile(`extension/${browserName}/shared/theme-model.js`, 'utf8'); + const themePopup = await readFile(`extension/${browserName}/shared/popup-theme.js`, 'utf8'); + const featureFlags = await readFile('extension/shared/feature-flags.js', 'utf8'); + const popupCss = await readFile(`extension/${browserName}/popup/popup.css`, 'utf8'); + + await page.setContent(html.replace(//gi, '')); + await page.addStyleTag({ content: popupCss }); + await page.evaluate(() => { + const stored = {}; + const listeners = []; + const state = { navigation: null, failThemeWrite: false }; + window.close = () => {}; + function resultFor(keys) { + if (typeof keys === 'string') return keys in stored ? { [keys]: stored[keys] } : {}; + if (Array.isArray(keys)) return Object.fromEntries(keys.filter(key => key in stored).map(key => [key, stored[key]])); + if (keys && typeof keys === 'object') return { ...keys, ...stored }; + return { ...stored }; + } + window.__popupThemeState = state; + window.chrome = { + runtime: { getManifest: () => ({ version: 'test' }) }, + storage: { + local: { + get: (keys, callback) => { + const promise = Promise.resolve(resultFor(keys)); + if (callback) promise.then(callback); + return promise; + }, + set: values => { + if (state.failThemeWrite && Object.hasOwn(values, 'bskyplusThemes')) { + return Promise.reject(new Error('Storage unavailable')); + } + const changes = {}; + for (const [key, newValue] of Object.entries(values)) { + changes[key] = { oldValue: stored[key], newValue }; + stored[key] = newValue; + } + listeners.forEach(listener => listener(changes, 'local')); + return Promise.resolve(); + }, + }, + onChanged: { addListener: listener => listeners.push(listener) }, + }, + tabs: { + query: (query, callback) => { + const promise = Promise.resolve([{ id: 7, url: 'https://bsky.app/bskyplus' }]); + if (callback) promise.then(callback); + return promise; + }, + update: async (id, properties) => { state.navigation = { id, ...properties }; }, + create: async properties => { state.navigation = properties; }, + sendMessage: async () => ({ maxWidth: 1200 }), + }, + }; + }); + + await page.addScriptTag({ content: featureFlags }); + await page.addScriptTag({ content: themeModel }); + const initialTheme = await page.evaluate(() => { + const theme = { ...BskyPlusThemes.BUILTIN_THEMES['bluesky-dark'], id: 'saved-ocean', name: 'Saved ocean' }; + const second = { ...BskyPlusThemes.BUILTIN_THEMES['bluesky-light'], id: 'saved-forest', name: 'Saved forest' }; + const value = { schemaVersion: 1, activeId: theme.id, themes: [theme, second] }; + chrome.storage.local.set({ bskyplusThemes: value }); + return { id: theme.id, secondId: second.id }; + }); + await page.addScriptTag({ content: popupScript }); + await page.addScriptTag({ content: themePopup }); + await page.waitForFunction(() => document.querySelector('#themeSelect')?.disabled === false); + return { page, initialTheme }; +} + +for (const browserName of ['chrome', 'firefox', 'safari']) { + test(`${browserName} popup selects complete themes and follows storage changes`, async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const { page, initialTheme } = await openPopup(browser, browserName); + const openOptions = async () => { + if (await page.$eval('#themeOptions', options => options.hidden)) await page.click('#themeSelect'); + await page.waitForFunction(() => !document.querySelector('#themeOptions').hidden); + }; + const chooseTheme = async (id, keyboard = false) => { + await openOptions(); + const option = `[data-theme-option="${id}"]`; + if (keyboard) { + await page.focus(option); + await page.keyboard.press('Enter'); + } else await page.click(option); + }; + + assert.equal(await page.$eval('#themeSelect', button => button.disabled), false); + await page.click('#themeSelect'); + await page.waitForFunction(() => !document.querySelector('#themeOptions').hidden); + if (process.env.SCREENSHOT_PATH && browserName === 'chrome') { + await page.screenshot({ path: process.env.SCREENSHOT_PATH, fullPage: true }); + } + const preview = await page.$eval('[data-theme-option="bluesky-light"]', option => { + const row = option; + return { + primary: getComputedStyle(row.querySelector('.theme-option-preview-bar')).backgroundColor, + rowHeight: row.getBoundingClientRect().height, + }; + }); + const expectedPrimary = await page.evaluate(() => { + const hex = BskyPlusThemes.BUILTIN_THEMES['bluesky-light'].colors.primary.slice(1); + const [red, green, blue] = hex.match(/../g).map(channel => parseInt(channel, 16)); + return `rgb(${red}, ${green}, ${blue})`; + }); + assert.equal(preview.primary, expectedPrimary, 'preview swatch uses the actual theme palette'); + assert.ok(preview.rowHeight < 60, `theme rows stay compact (${preview.rowHeight}px)`); + assert.equal(await page.$eval('#themeOptions', panel => { + const rows = [...panel.querySelectorAll('.theme-option')]; + return panel.scrollWidth <= panel.clientWidth + 1 && + rows.every(row => row.scrollWidth <= row.clientWidth + 1); + }), true, 'theme picker has no horizontal overflow'); + + await page.keyboard.press('Escape'); + await page.waitForFunction(() => document.querySelector('#themeOptions').hidden); + assert.equal(await page.$eval('#themeSelect', trigger => document.activeElement === trigger), true, + 'Escape restores focus to the picker trigger'); + + await chooseTheme('bluesky-light', true); + await page.waitForFunction(() => document.querySelector('#themeStatus').textContent === 'Theme applied.'); + await page.waitForFunction(() => document.activeElement?.id === 'themeSelect'); + assert.equal(await page.$eval('#themeSelect', trigger => document.activeElement === trigger), true, + 'successful keyboard selection restores focus to the trigger'); + let saved = await page.evaluate(async () => (await chrome.storage.local.get('bskyplusThemes')).bskyplusThemes); + assert.equal(saved.activeId, 'bluesky-light'); + assert.deepEqual(saved.themes.map(theme => theme.id), [initialTheme.id, initialTheme.secondId]); + assert.equal(await page.$eval('[data-theme-option="bluesky-light"]', option => option.getAttribute('aria-pressed')), 'true'); + + await page.evaluate(async id => { + const library = (await chrome.storage.local.get('bskyplusThemes')).bskyplusThemes; + await chrome.storage.local.set({ bskyplusThemes: { ...library, activeId: id } }); + }, initialTheme.secondId); + await page.waitForFunction(() => document.querySelector('[data-theme-option="saved-forest"]')?.getAttribute('aria-pressed') === 'true'); + + await chooseTheme(''); + await page.waitForFunction(() => document.querySelector('#themeStatus').textContent === 'Theme applied.'); + saved = await page.evaluate(async () => (await chrome.storage.local.get('bskyplusThemes')).bskyplusThemes); + assert.equal(saved.activeId, null); + assert.equal(saved.themes.length, 2); + + await page.evaluate(() => { window.__popupThemeState.failThemeWrite = true; }); + await chooseTheme('bluesky-dark'); + await page.waitForFunction(() => document.querySelector('#themeStatus').textContent === 'Could not apply that theme. Try again.'); + assert.equal(await page.$eval('[data-theme-option=""]', option => option.getAttribute('aria-pressed')), 'true'); + await page.waitForFunction(() => document.activeElement?.getAttribute('data-theme-option') === 'bluesky-dark'); + assert.equal(await page.$eval('[data-theme-option="bluesky-dark"]', option => document.activeElement === option), true, + 'failed selection restores focus to the attempted option for retry'); + + await page.evaluate(async () => { + window.__popupThemeState.failThemeWrite = false; + const { bskyplusThemes } = await chrome.storage.local.get('bskyplusThemes'); + await chrome.storage.local.set({ bskyplusThemes: { + ...bskyplusThemes, activeId: 'bluesky-dark', deletedBuiltinIds: ['bluesky-dark'], + } }); + }); + await page.waitForFunction(() => !document.querySelector('[data-theme-option="bluesky-dark"]')); + assert.equal(await page.$eval('[data-theme-option=""]', option => option.getAttribute('aria-pressed')), 'true', + 'removing the active included theme restores native selection'); + await chooseTheme('bluesky-light'); + await page.waitForFunction(() => document.querySelector('#themeStatus').textContent === 'Theme applied.'); + saved = await page.evaluate(async () => (await chrome.storage.local.get('bskyplusThemes')).bskyplusThemes); + assert.deepEqual(saved.deletedBuiltinIds, ['bluesky-dark'], + 'changing themes preserves deleted suggestions'); + assert.equal(await page.$('[data-theme-option="bluesky-dark"]'), null); + + await page.click('#editThemeButton'); + await page.waitForFunction(() => window.__popupThemeState.navigation?.url === 'https://bsky.app/settings?bskyplus=theme'); + assert.equal(await page.$eval('#editThemeButton', button => button.textContent), 'Edit theme'); + } finally { + await browser.close(); + } + }); +} diff --git a/test/test_settings_frame.mjs b/test/test_settings_frame.mjs new file mode 100644 index 0000000..fa1fc12 --- /dev/null +++ b/test/test_settings_frame.mjs @@ -0,0 +1,200 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; +import puppeteer from 'puppeteer-core'; + +test('canonical settings scripts mount feature rows and retain their layout and saves', async () => { + const browser = await puppeteer.launch({ + executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome', + headless: true, + }); + try { + const served = new Set(); + const page = await browser.newPage(); + await page.evaluateOnNewDocument(() => { + const listeners = []; + const stored = () => JSON.parse(localStorage.getItem('settings') || '{}'); + const reply = (value, callback) => { callback?.(value); return Promise.resolve(value); }; + window.chrome = { + runtime: { + id: 'test-extension', + getURL: filename => location.origin + '/' + filename, + getManifest: () => ({ version: '1.0' }), + onMessage: { addListener() {} }, + sendMessage: () => Promise.resolve({ success: true, hostname: stored().imageCdnHostname || '', + enabled: !!stored().imageCdnHostname }), + }, + storage: { + local: { + get: (keys, callback) => reply({ ...(typeof keys === 'object' && !Array.isArray(keys) ? keys : {}), ...stored() }, callback), + set: (value, callback) => { + const old = stored(); + localStorage.setItem('settings', JSON.stringify({ ...old, ...value })); + const changes = Object.fromEntries(Object.entries(value).map(([key, newValue]) => + [key, { oldValue: old[key], newValue }])); + listeners.forEach(listener => listener(changes, 'local')); + return reply(undefined, callback); + }, + }, + onChanged: { addListener: listener => listeners.push(listener) }, + }, + tabs: { + query: (query, callback) => reply([{ id: 1, url: 'https://bsky.app' }], callback), + sendMessage: (id, message, callback) => reply({ success: true, available: false, enabled: false, + hostname: '', pds: 'https://pds.example.com', allowed: {}, maxWidth: 1200 }, callback), + }, + permissions: { request: () => Promise.resolve(true) }, + }; + }); + await page.setRequestInterception(true); + page.on('request', async request => { + const url = new URL(request.url()); + if (!/^\/(shared|popup)\//.test(url.pathname)) return request.respond({ status: 404 }); + const canonical = url.pathname.startsWith('/shared/'); + const filename = path.basename(url.pathname); + const body = await readFile(path.join(canonical ? 'extension/shared' : 'extension/chrome/popup', filename)); + if (canonical) served.add(filename); + const contentType = filename.endsWith('.css') ? 'text/css' : + filename.endsWith('.html') ? 'text/html' : 'text/javascript'; + return request.respond({ contentType, body }); + }); + + const frameUrl = 'https://extension.test/shared/settings-frame.html'; + await page.goto(frameUrl); + await page.waitForFunction(() => document.querySelector('#appviewProvider')?.disabled === false); + assert.deepEqual(await page.$$eval('body > .section', sections => + sections.map(section => section.id || section.dataset.feature)), + ['feedWidth', 'themeSection', 'featuresSection', 'altTextSection', 'infrastructureSection']); + for (const selector of ['#appviewSection', '.bskyplus-cdn-section']) { + assert.equal(await page.$eval(selector, row => row.parentElement.id), 'infrastructureSection'); + } + assert.equal(await page.$eval('.bskyplus-llm-section', row => row.parentElement.id), 'altTextSection'); + assert.equal(await page.$eval('#bookmarksSection', row => row.parentElement.id), 'featuresSection'); + assert.equal(await page.$$eval('#bookmarksSection button', buttons => + buttons.some(button => button.textContent === 'Sync now')), true); + assert.notEqual(await page.$('#bookmarksSection [role="status"]'), null); + assert.equal(await page.$('#editPostToggle'), null); + assert.equal(await page.$('.bskyplus-llm-section summary'), null); + assert.equal(await page.$('#moreSettingsButton'), null); + + for (const selector of ['#llmEnabledToggle', '#appviewEnabledToggle', '#cdnEnabledToggle']) { + await page.waitForFunction(selector => !document.querySelector(selector).disabled, {}, selector); + await page.focus(selector); + await page.keyboard.press('Space'); + } + await page.waitForSelector('#bskyplusLlmEndpoint', { visible: true }); + await page.waitForSelector('#appviewProvider', { visible: true }); + await page.waitForSelector('#bskyplusCdnProvider', { visible: true }); + for (const [selector, expectedRatio] of [ + ['.bskyplus-llm-key-row', 4], ['.bskyplus-appview-provider-row', 7 / 3], + ]) { + const layout = await page.$eval(selector, row => { + const style = getComputedStyle(row); + const columns = style.gridTemplateColumns.split(' ').map(parseFloat); + return { display: style.display, gap: style.gap, ratio: columns[0] / columns[1], + radius: getComputedStyle(row.lastElementChild).borderRadius }; + }); + assert.equal(layout.display, 'grid'); + assert.equal(layout.gap, '8px'); + assert.equal(layout.radius, '8px'); + assert.ok(Math.abs(layout.ratio - expectedRatio) < 0.01); + } + + await page.evaluate(() => { + document.querySelector('#bskyplusLlmEndpoint').value = 'https://bsky.app/v1/chat/completions'; + document.querySelector('#bskyplusLlmModel').value = 'vision-test'; + document.querySelector('#bskyplusLlmApiKey').value = 'test-key'; + document.querySelector('#bskyplusLlmUseCustomPrompt').checked = true; + document.querySelector('#bskyplusLlmUseCustomPrompt').dispatchEvent(new Event('change', { bubbles: true })); + document.querySelector('#bskyplusLlmCustomPrompt').value = 'Describe colors and visible text.'; + document.querySelector('#bskyplusLlmCustomPrompt').dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.focus('.bskyplus-llm-save'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent === 'LLM endpoint saved.'); + assert.deepEqual(await page.evaluate(async () => (await chrome.storage.local.get('llm')).llm), { + endpoint: 'https://bsky.app/v1/chat/completions', model: 'vision-test', apiKey: 'test-key', + useCustomPrompt: true, customPrompt: 'Describe colors and visible text.', enabled: true, + }); + await page.focus('#bskyplusLlmUseCustomPrompt'); + await page.keyboard.press('Space'); + await page.focus('.bskyplus-llm-save'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => document.querySelector('.bskyplus-llm-status').textContent === 'LLM endpoint saved.'); + assert.deepEqual(await page.evaluate(async () => (await chrome.storage.local.get('llm')).llm), { + endpoint: 'https://bsky.app/v1/chat/completions', model: 'vision-test', apiKey: 'test-key', + useCustomPrompt: false, customPrompt: 'Describe colors and visible text.', enabled: true, + }); + await page.select('#bskyplusCdnProvider', ''); + await page.focus('.bskyplus-cdn-save'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => document.querySelector('.bskyplus-cdn-status').textContent === 'Image CDN disabled.'); + await page.select('#appviewProvider', ''); + await page.focus('.bskyplus-appview-provider-row button'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => !document.querySelector('#appviewEnabledToggle').checked); + + await page.reload(); + await page.waitForFunction(() => document.querySelector('#bskyplusLlmApiKey')?.value === 'test-key'); + assert.equal(await page.$eval('#llmEnabledToggle', toggle => toggle.checked), true); + assert.equal(await page.$eval('#bskyplusLlmUseCustomPrompt', toggle => toggle.checked), false); + assert.deepEqual(await page.$eval('#bskyplusLlmCustomPrompt', input => ({ value: input.value, disabled: input.disabled })), + { value: '', disabled: true }); + assert.equal(await page.$$eval('.bskyplus-llm-label', labels => + labels.filter(label => label.textContent === 'Custom prompt').length), 1); + assert.equal(await page.$eval('.bskyplus-llm-section', section => section.textContent.includes('Use custom prompt')), false); + await page.focus('#bskyplusLlmUseCustomPrompt'); + await page.keyboard.press('Space'); + await page.waitForFunction(() => document.querySelector('#bskyplusLlmCustomPrompt').value === 'Describe colors and visible text.'); + assert.equal(await page.$eval('#bskyplusLlmCustomPrompt', input => input.disabled), false); + await page.focus('#bskyplusLlmUseCustomPrompt'); + await page.keyboard.press('Space'); + await page.waitForFunction(() => document.querySelector('#bskyplusLlmCustomPrompt').value === '' && + document.querySelector('#bskyplusLlmCustomPrompt').disabled); + await page.goto('https://extension.test/popup/popup.html'); + await page.waitForFunction(() => document.querySelector('#appviewProvider')?.disabled === false); + assert.deepEqual(await page.$$eval('body > .section', sections => + sections.map(section => section.id || section.dataset.feature)), + ['feedWidth', 'themeSection', 'featuresSection', 'altTextSection', 'infrastructureSection']); + assert.equal(await page.$$eval('#infrastructureSection', sections => sections.length), 1); + assert.equal(await page.$$eval('#bookmarksSection button', buttons => + buttons.some(button => button.textContent === 'Sync now')), false); + assert.equal(await page.$('#bookmarksSection [role="status"]'), null); + assert.equal(await page.$eval('.bskyplus-llm-section', row => row.parentElement.id), 'altTextSection'); + for (const selector of ['#appviewSection', '.bskyplus-cdn-section']) { + assert.equal(await page.$eval(selector, row => row.parentElement.id), 'infrastructureSection'); + } + assert.equal(await page.$eval('#bookmarksSection', row => row.parentElement.id), 'featuresSection'); + assert.equal(await page.$eval('#bskyplusLlmApiKey', input => input.value), 'test-key'); + assert.deepEqual(await page.$eval('#bskyplusLlmCustomPrompt', input => ({ value: input.value, disabled: input.disabled })), + { value: '', disabled: true }); + assert.equal(await page.$('#editPostToggle'), null); + assert.equal(await page.$eval('body', body => body.getBoundingClientRect().width), 400); + assert.ok(await page.$eval('body', body => body.scrollWidth <= 400)); + await page.waitForFunction(() => + document.querySelector('#infrastructureSection .section-collapse-button')?.getAttribute('aria-expanded') === 'true'); + await page.click('#infrastructureSection .section-collapse-button'); + await page.waitForFunction(() => + document.querySelector('#infrastructureSection .section-collapse-button')?.getAttribute('aria-expanded') === 'false'); + assert.equal((await page.evaluate(() => chrome.storage.local.get('settingsSectionCollapsed_infrastructure'))) + .settingsSectionCollapsed_infrastructure, true); + await page.reload(); + await page.waitForFunction(() => document.querySelector('#appviewProvider')?.disabled === false); + await page.waitForFunction(() => + document.querySelector('#infrastructureSection .section-collapse-button')?.getAttribute('aria-expanded') === 'false'); + await page.click('#llmEnabledToggle'); + await page.waitForFunction(async () => (await chrome.storage.local.get('llm')).llm.enabled === false); + assert.deepEqual(await page.evaluate(async () => { + const { useCustomPrompt, customPrompt } = (await chrome.storage.local.get('llm')).llm; + return { useCustomPrompt, customPrompt }; + }), { useCustomPrompt: false, customPrompt: 'Describe colors and visible text.' }); + for (const filename of ['settings-frame.html', 'settings-frame.js', 'settings-frame.css', + 'llm-settings.js', 'llm-settings.css', 'cdn-config.js', 'cdn-settings.js', + 'appview-config.js', 'appview-settings.js', 'bookmarks-settings.js']) { + assert.ok(served.has(filename), `Expected canonical ${filename} to be loaded`); + } + } finally { + await browser.close(); + } +}); diff --git a/test/test_settings_theme.mjs b/test/test_settings_theme.mjs new file mode 100644 index 0000000..47f31ee --- /dev/null +++ b/test/test_settings_theme.mjs @@ -0,0 +1,193 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import test from 'node:test'; +import puppeteer from 'puppeteer-core'; + +const executablePath = process.env.CHROME_PATH || '/usr/bin/google-chrome'; + +test('opening settings with an active theme keeps the page and iframe readable', async () => { + const browser = await puppeteer.launch({ executablePath, headless: true, + enableExtensions: [path.resolve('extension/chrome')] }); + try { + const worker = await browser.waitForTarget(target => target.type() === 'service_worker'); + const seed = await browser.newPage(); + await seed.goto(new URL('shared/settings-frame.html?section=theme', worker.url()).href); + await seed.waitForSelector('[data-theme-library]'); + const page = await browser.newPage(); + let nativeMode = 'light'; + await page.setRequestInterception(true); + page.on('request', request => { + const url = new URL(request.url()); + if (url.origin !== 'https://bsky.app') return request.continue(); + const settings = url.pathname === '/settings'; + return request.respond({ contentType: 'text/html', body: ` + + +

${settings ? 'Settings' : 'Home'}

+ ${settings ? 'About' : 'Home feed'} +
` }); + }); + + async function assertReadable(text, background) { + await page.waitForSelector('[data-bskyplus-settings-frame]'); + const element = await page.$('[data-bskyplus-settings-frame]'); + const frame = await element.contentFrame(); + await frame.waitForSelector('#featuresSection'); + await frame.waitForFunction(expected => { + const label = document.querySelector('#featuresSection .toggle-label'); + return label && getComputedStyle(label).color === expected; + }, {}, text); + assert.equal(await page.$eval('[data-bskyplus-settings-page]', node => getComputedStyle(node).color), text); + const variables = await frame.evaluate(() => ({ + text: document.documentElement.style.getPropertyValue('--text-primary'), + background: document.documentElement.style.getPropertyValue('--bg-primary'), + })); + assert.equal(variables.text, text); + assert.equal(variables.background, background); + assert.notEqual(variables.text, variables.background); + } + + for (nativeMode of ['light', 'dim']) { + for (const base of ['light', 'dim', 'dark']) { + const colors = await seed.evaluate(async base => { + const theme = BskyPlusThemes.createTheme('Copied default ' + base, base); + await chrome.storage.local.set({ bskyplusThemes: { schemaVersion: 1, activeId: theme.id, themes: [theme] } }); + return theme.colors; + }, base); + const rgb = hex => 'rgb(' + hex.slice(1).match(/../g).map(channel => parseInt(channel, 16)).join(', ') + ')'; + await page.goto('https://bsky.app/'); + await page.waitForSelector('[data-bskyplus-theme-active]'); + await page.click('nav a'); + await page.waitForSelector('[data-bskyplus-settings-link]'); + await page.click('[data-bskyplus-settings-link]'); + await assertReadable(rgb(colors.text), rgb(colors.background)); + await page.reload(); + await assertReadable(rgb(colors.text), rgb(colors.background)); + } + } + + // Removing the override must refresh the sampled native foreground too. + await seed.evaluate(async () => { + const stored = await chrome.storage.local.get('bskyplusThemes'); + await chrome.storage.local.set({ bskyplusThemes: { ...stored.bskyplusThemes, activeId: null } }); + }); + await page.waitForFunction(() => !document.documentElement.hasAttribute('data-bskyplus-theme-active')); + await assertReadable('rgb(255, 255, 255)', 'rgb(21, 29, 40)'); + } finally { + await browser.close(); + } +}); + +test('theme editor preserves native styling until selection, then crossfades the host page over 1000ms', async () => { + const browser = await puppeteer.launch({ executablePath, headless: true, + enableExtensions: [path.resolve('extension/chrome')] }); + try { + const worker = await browser.waitForTarget(target => target.type() === 'service_worker'); + const seed = await browser.newPage(); + await seed.goto(new URL('shared/settings-frame.html?section=theme', worker.url()).href); + await seed.waitForSelector('[data-theme-library]'); + const preview = await seed.evaluate(async () => { + const first = BskyPlusThemes.createTheme('First preview', 'light'); + const second = BskyPlusThemes.createTheme('Second preview', 'dark'); + first.colors.background = '#FFFFFF'; + second.colors.background = '#123456'; + second.colors.primary = '#AA3300'; + second.colors.like = '#FF55AA'; + await chrome.storage.local.set({ bskyplusThemes: { + schemaVersion: 1, activeId: null, themes: [first, second], + } }); + return { id: second.id, background: second.colors.background }; + }); + + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', request => { + const url = new URL(request.url()); + if (url.origin !== 'https://bsky.app') return request.continue(); + const settings = url.pathname === '/settings'; + return request.respond({ contentType: 'text/html', body: ` + + +

${settings ? 'Settings' : 'Home'}

+ ${settings ? 'About' : 'Home feed'} +
Handle + +
` }); + }); + + await page.goto('https://bsky.app/'); + await page.click('nav a'); + await page.waitForSelector('[data-bskyplus-settings-link]'); + await page.click('[data-bskyplus-settings-link]'); + await page.waitForSelector('[data-bskyplus-theme-link]'); + await page.click('[data-bskyplus-theme-link]'); + await page.waitForFunction(() => [...document.querySelectorAll('iframe')] + .some(frame => frame.src.includes('section=theme'))); + const frame = page.frames().find(candidate => candidate.url().includes('section=theme')); + await frame.waitForSelector(`[data-theme-card="${preview.id}"]`); + const styleProperties = { + body: ['backgroundColor', 'color'], + '.post': ['backgroundColor', 'color', 'borderTopColor'], + '.muted': ['color'], + '.primary': ['backgroundColor'], + '.like': ['fill'], + }; + const readStyles = () => page.evaluate(properties => Object.fromEntries( + Object.entries(properties).map(([selector, names]) => [selector, Object.fromEntries(names.map(name => + [name, getComputedStyle(document.querySelector(selector))[name]]))])), styleProperties); + const initial = await readStyles(); + assert.equal(await page.$('[data-bskyplus-theme-active]'), null, + 'opening the editor with native styling must not apply a draft or saved theme'); + assert.equal(initial.body.backgroundColor, 'rgb(255, 255, 255)'); + assert.equal(initial.body.color, 'rgb(0, 0, 0)'); + + await frame.$eval(`[data-theme-card="${preview.id}"] .bskyplus-theme-preview-post`, element => element.click()); + await page.waitForFunction(id => document.documentElement.dataset.bskyplusThemeActive === id, {}, preview.id); + const start = await page.evaluate(() => { + const rootAnimations = () => document.getAnimations().filter(animation => + animation.effect.pseudoElement === '::view-transition-old(root)' || + animation.effect.pseudoElement === '::view-transition-new(root)'); + return { + background: getComputedStyle(document.body).backgroundColor, + elementAnimations: document.getAnimations().filter(animation => + !animation.effect.pseudoElement?.startsWith('::view-transition')).length, + rootAnimations: rootAnimations().map(animation => ({ + duration: animation.effect.getTiming().duration, playState: animation.playState, + })), + }; + }); + const startStyles = await readStyles(); + assert.notEqual(start.background, initial.body.backgroundColor); + assert.equal(start.background, 'rgb(18, 52, 86)'); + for (const selector of Object.keys(styleProperties)) { + assert.notDeepEqual(startStyles[selector], initial[selector], `${selector} switches with the preview`); + } + assert.equal(start.elementAnimations, 0); + assert.ok(start.rootAnimations.length > 0); + assert.ok(start.rootAnimations.every(animation => animation.duration === 1000)); + assert.ok(start.rootAnimations.every(animation => animation.playState === 'running')); + await page.evaluate(() => { + for (const animation of document.getAnimations()) { + if (animation.effect.pseudoElement === '::view-transition-old(root)' || + animation.effect.pseudoElement === '::view-transition-new(root)') animation.currentTime = 500; + } + }); + assert.ok(await page.evaluate(() => document.getAnimations().some(animation => + animation.effect.pseudoElement === '::view-transition-old(root)' && animation.playState === 'running'))); + await page.waitForFunction(() => !document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + assert.deepEqual(await readStyles(), startStyles); + } finally { + await browser.close(); + } +}); diff --git a/test/test_theme_editor.mjs b/test/test_theme_editor.mjs new file mode 100644 index 0000000..ee79d1d --- /dev/null +++ b/test/test_theme_editor.mjs @@ -0,0 +1,919 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import puppeteer from 'puppeteer-core'; + +const chromePath = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const modelScript = await readFile('extension/shared/theme-model.js', 'utf8'); +const editorScript = await readFile('extension/shared/theme-editor.js', 'utf8'); + +async function openEditor(browser, { seed = null, rejectSet = false, rejectGet = false, permissionGranted = true } = {}) { + const page = await browser.newPage(); + page.setDefaultTimeout(5_000); + await page.setContent('
Existing settings
'); + await page.evaluate(({ failSet, failGet, permissionGranted }) => { + let themeNumber = 0; + if (!crypto.randomUUID) Object.defineProperty(crypto, 'randomUUID', { + value: () => `theme-${++themeNumber}`, + }); + const listeners = []; + window.__themeStorage = { value: null, sets: [], listeners }; + window.__themeMessages = []; + window.__themePermissionRequests = []; + window.__themePermissionOrder = []; + window.postMessage = message => window.__themeMessages.push(structuredClone(message)); + window.confirm = () => { throw new Error('Native confirmation should not be used.'); }; + window.chrome = { + storage: { + local: { + get: async key => { + if (failGet) throw new Error('storage unavailable'); + return { [key]: structuredClone(window.__themeStorage.value) }; + }, + set: async values => { + if (failSet) throw new Error('storage unavailable'); + for (const [key, value] of Object.entries(values)) { + const oldValue = structuredClone(window.__themeStorage.value); + window.__themeStorage.value = structuredClone(value); + window.__themeStorage.sets.push(structuredClone(value)); + listeners.forEach(listener => listener({ [key]: { oldValue, newValue: structuredClone(value) } }, 'local')); + } + }, + }, + onChanged: { addListener: listener => listeners.push(listener) }, + }, + permissions: { + request: async permissions => { + window.__themePermissionRequests.push(structuredClone(permissions)); + window.__themePermissionOrder.push('permission'); + return permissionGranted; + }, + }, + }; + URL.createObjectURL = blob => { window.__themeExport = { blob, revoked: false }; return 'blob:bskyplus-theme'; }; + URL.revokeObjectURL = () => { window.__themeExport.revoked = true; }; + HTMLAnchorElement.prototype.click = function click() { + window.__themeDownload = { href: this.href, download: this.download }; + }; + }, { failSet: rejectSet, failGet: rejectGet, permissionGranted }); + await page.addScriptTag({ content: modelScript }); + if (seed) { + await page.evaluate(kind => { + const first = BskyPlusThemes.createTheme('Ocean', 'dark'); + const second = BskyPlusThemes.createTheme('Sunrise', 'light'); + window.__themeStorage.value = { + schemaVersion: 1, + activeId: kind === 'active' ? first.id : null, + themes: kind === 'one' ? [first] : [first, second], + }; + }, seed); + } + await page.addScriptTag({ content: editorScript }); + await page.waitForSelector('[data-bskyplus-theme-editor]'); + await page.waitForFunction(() => document.querySelector('.bskyplus-theme-name')); + return page; +} + +async function setName(page, value) { + await page.$eval('.bskyplus-theme-name', (input, next) => { + input.value = next; + input.dispatchEvent(new Event('input', { bubbles: true })); + }, value); +} + +async function stored(page) { + return page.evaluate(() => structuredClone(window.__themeStorage.value)); +} + +test('theme disclosures animate in both directions, reverse smoothly, and respect reduced motion', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + await page.setViewport({ width: 640, height: 1100 }); + for (const file of ['extension/chrome/popup/popup.css', 'extension/shared/settings-frame.css', 'extension/shared/theme-editor.css']) { + await page.addStyleTag({ content: await readFile(file, 'utf8') }); + } + await page.$eval('[data-theme-hex="onPrimary"]', input => { + input.value = '#006AFF'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + const selectors = ['[data-theme-library]', '[data-theme-contrast-warning]', + ...await page.$$eval('[data-theme-group]', groups => groups.map(group => `[data-theme-group="${group.dataset.themeGroup}"]`))]; + for (const selector of selectors) { + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); + await page.waitForFunction(() => matchMedia('(prefers-reduced-motion: reduce)').matches); + const collapsed = await page.$eval(selector, details => { + if (details.open) details.querySelector('summary').click(); + return details.getBoundingClientRect().height; + }); + await page.waitForFunction(selector => !document.querySelector(selector).open, {}, selector); + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'no-preference' }]); + await page.waitForFunction(() => !matchMedia('(prefers-reduced-motion: reduce)').matches); + const middle = await page.$eval(selector, async details => { + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + details.querySelector('summary').click(); + await new Promise(resolve => setTimeout(resolve, 90)); + return { height: details.getBoundingClientRect().height, + animating: details.querySelector('.bskyplus-theme-section-reveal').getAnimations().length > 0 }; + }); + await page.waitForFunction(selector => { + const details = document.querySelector(selector); + return details.open && !details.querySelector('.bskyplus-theme-section-reveal').getAnimations().length; + }, {}, selector); + const expanded = await page.$eval(selector, details => details.getBoundingClientRect().height); + assert.ok(middle.animating && middle.height > collapsed && middle.height < expanded, `${selector} expands gradually: ${JSON.stringify({ collapsed, middle, expanded })}`); + const closing = await page.$eval(selector, async details => { + details.querySelector('summary').click(); + await new Promise(resolve => setTimeout(resolve, 90)); + return { height: details.getBoundingClientRect().height, + inert: details.querySelector('.bskyplus-theme-section-content').inert }; + }); + assert.ok(closing.inert && closing.height > collapsed && closing.height < expanded, `${selector} collapses gradually and prevents hidden controls receiving focus`); + await page.waitForFunction(selector => !document.querySelector(selector).open, {}, selector); + } + await page.$eval('[data-theme-library]', async details => { + const summary = details.querySelector('summary'); + summary.click(); + await new Promise(resolve => setTimeout(resolve, 70)); + summary.click(); + await new Promise(resolve => setTimeout(resolve, 30)); + summary.click(); + }); + await page.waitForFunction(() => { + const details = document.querySelector('[data-theme-library]'); + return details.open && details.dataset.themeExpanded === 'true' && + !details.querySelector('.bskyplus-theme-section-reveal').getAnimations().length; + }); + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); + for (const selector of selectors) { + assert.equal(await page.$eval(selector, details => { + const expected = !details.open; + details.querySelector('summary').click(); + return details.open === expected && !details.querySelector('.bskyplus-theme-section-reveal').getAnimations().length; + }), true, `${selector} toggles immediately with reduced motion`); + } + if (process.env.THEME_SECTIONS_SCREENSHOT) console.log('THEME_SECTIONS_SCREENSHOT=' + await page.screenshot({ encoding: 'base64' })); + } finally { await browser.close(); } +}); + +test('saved theme gallery shows independent palettes in two columns and preserves collapse state', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + assert.equal(await page.$('.bskyplus-theme-bases'), null); + assert.deepEqual(await page.$$eval('button', buttons => buttons.filter(button => /^New (light|dim|dark)$/.test(button.textContent)).map(button => button.textContent)), []); + for (const file of ['extension/chrome/popup/popup.css', 'extension/shared/settings-frame.css', 'extension/shared/theme-editor.css']) { + await page.addStyleTag({ content: await readFile(file, 'utf8') }); + } + const original = await stored(page); + assert.equal(await page.$('.bskyplus-theme-select'), null); + assert.equal(await page.$('[data-theme-preview]'), null); + assert.equal(await page.$eval('[data-theme-library]', node => + node.previousElementSibling.classList.contains('bskyplus-theme-preview-toggle')), true); + const includedCount = await page.evaluate(() => Object.keys(BskyPlusThemes.BUILTIN_THEMES).length); + assert.equal(await page.$$eval('[data-theme-card]', cards => cards.length), includedCount + original.themes.length); + for (const theme of original.themes) { + assert.equal(await page.$eval(`[data-theme-card="${theme.id}"] .bskyplus-theme-preview`, card => + card.style.getPropertyValue('--bskyplus-preview-background')), theme.colors.background); + } + for (const width of [640, 390]) { + await page.setViewport({ width, height: 900 }); + const cards = await page.$$eval('[data-theme-card]', cards => cards.map(card => { + const { x, y, width } = card.getBoundingClientRect(); + return { x, y, width }; + })); + assert.equal(cards[0].width, cards[1].width); + assert.equal(cards[0].y, cards[1].y); + assert.ok(cards[1].x > cards[0].x + cards[0].width); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); + } + await page.click(`[data-theme-card="${original.themes[1].id}"] .bskyplus-theme-preview-post`); + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Sunrise'); + assert.equal(await page.$eval('[data-theme-selected="true"]', card => card.dataset.themeCard), original.themes[1].id); + assert.equal(await page.$eval('[data-theme-selected="true"] .bskyplus-theme-card-preview', preview => + getComputedStyle(preview).outlineWidth), '2px'); + assert.deepEqual(await stored(page), original); + await page.click(`[data-theme-select="${original.themes[0].id}"]`); + await page.$eval('[data-theme-hue]', input => { + input.value = '60'; input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$eval(`[data-theme-card="${original.activeId}"] .bskyplus-theme-preview`, card => + card.style.getPropertyValue('--bskyplus-preview-primary')), original.themes[0].colors.primary); + await page.click('.bskyplus-theme-hue button'); + await page.click('[data-theme-library] > summary'); + await setName(page, 'Ocean edited'); + await page.click('[data-theme-save]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Saved and applied'); + assert.equal(await page.$eval('[data-theme-library]', details => details.open), false); + await page.click('[data-theme-library] > summary'); + assert.equal(await page.$eval(`[data-theme-card="${original.activeId}"] .bskyplus-theme-card-name`, node => node.textContent), 'Ocean edited'); + } finally { await browser.close(); } +}); + +test('hue shift updates the complete draft, resets losslessly, and saves only on request', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + const original = await stored(page); + const readColors = () => page.$$eval('[data-theme-hex]', inputs => Object.fromEntries( + inputs.map(input => [input.dataset.themeHex, input.value.toUpperCase()]))); + const shift = value => page.$eval('[data-theme-hue]', (input, value) => { + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + }, value); + assert.equal(await page.$eval('.bskyplus-theme-preview-toggle input', input => input.checked), true); + await shift(120); + const shifted = await readColors(); + assert.notEqual(shifted.primary, original.themes[0].colors.primary); + assert.deepEqual(shifted, await page.evaluate(colors => BskyPlusThemes.shiftHue(colors, 120), original.themes[0].colors)); + assert.deepEqual(await stored(page), original); + assert.deepEqual(await page.evaluate(() => window.__themeMessages.at(-1).theme.colors), shifted); + await shift(-47); + await page.click('.bskyplus-theme-hue button'); + assert.deepEqual(await readColors(), original.themes[0].colors); + await shift(73); + await page.$eval('[data-theme-hex="background"]', input => { + input.value = '#804040'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$eval('[data-theme-hue]', input => input.value), '0'); + await shift(120); + assert.equal((await readColors()).background, '#408040'); + const finalColors = await readColors(); + await page.click('.bskyplus-theme-apply'); + if (await page.$('dialog[open]')) await page.click('dialog button[type="submit"]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Saved and applied'); + assert.deepEqual((await stored(page)).themes.find(theme => theme.id === original.activeId).colors, finalColors); + assert.equal(await page.$eval('[data-theme-hue]', input => input.value), '0'); + await page.click(`[data-theme-select="${original.themes[1].id}"]`); + assert.deepEqual(await readColors(), original.themes[1].colors); + assert.equal(await page.$eval('[data-theme-hue-value]', output => output.textContent), '0°'); + } finally { + await browser.close(); + } +}); + +test('color picker coalesces rapid drag input before a change or save', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + const original = await stored(page); + const choose = (value, type) => page.$eval('[data-theme-picker="primary"]', (picker, { value, type }) => { + picker.value = value; + picker.dispatchEvent(new Event(type, { bubbles: true })); + }, { value, type }); + + await choose('#123456', 'input'); + await page.waitForFunction(() => window.__themeMessages.at(-1)?.theme?.colors.primary === '#123456'); + assert.equal(await page.$eval('[data-theme-hex="primary"]', input => input.value), '#123456'); + assert.equal(await page.$eval('[data-theme-save-as]', button => button.disabled), false); + assert.deepEqual(await stored(page), original); + + await choose('#654321', 'input'); + await page.waitForFunction(() => window.__themeMessages.at(-1)?.theme?.colors.primary === '#654321'); + assert.deepEqual(await stored(page), original); + + const messageCount = await page.evaluate(() => window.__themeMessages.length); + await page.$eval('[data-theme-picker="primary"]', picker => { + for (const color of ['#112233', '#223344', '#334455']) { + picker.value = color; + picker.dispatchEvent(new Event('input', { bubbles: true })); + } + }); + assert.equal(await page.evaluate(() => window.__themeMessages.length), messageCount); + await page.waitForFunction(() => window.__themeMessages.at(-1)?.theme?.colors.primary === '#334455'); + assert.equal(await page.evaluate(() => window.__themeMessages.length), messageCount + 1); + assert.deepEqual(await stored(page), original); + + await choose('#AABBCC', 'change'); + assert.equal(await page.evaluate(() => window.__themeMessages.at(-1).theme.colors.primary), '#AABBCC'); + assert.deepEqual(await stored(page), original); + } finally { await browser.close(); } +}); + +test('low contrast warns before saving and allows cancelling or saving anyway', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.$eval('[data-theme-hex="surfaceRaised"]', input => { + input.value = '#FFFFFF'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$eval('[data-theme-contrast-warning]', node => node.hidden), false); + assert.match(await page.$eval('[data-theme-contrast-warning]', node => node.textContent), /Main text on Raised surface: 1.0:1/); + await page.click('[data-theme-save]'); + await page.waitForSelector('dialog[open]'); + assert.equal(await stored(page), null); + await page.keyboard.press('Escape'); + await page.waitForSelector('dialog', { hidden: true }); + assert.equal(await stored(page), null); + await page.click('[data-theme-save]'); + assert.equal(await page.$eval('dialog button[type="submit"]', button => button.textContent), 'Save anyway'); + await page.click('dialog button[type="submit"]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 1); + assert.equal((await stored(page)).themes[0].colors.surfaceRaised, '#FFFFFF'); + assert.equal((await stored(page)).activeId, (await stored(page)).themes[0].id); + } finally { await browser.close(); } +}); + +test('Save as requires edits and creates a named copy without changing the original', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + const original = await stored(page); + assert.equal(await page.$eval('[data-theme-save-as]', button => button.disabled), true); + await page.$eval('[data-theme-hex="background"]', input => { + input.value = '#10231E'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$eval('[data-theme-save-as]', button => button.disabled), false); + await page.click('[data-theme-save-as]'); + await page.waitForSelector('dialog[open] #bskyplusThemeCopyName'); + await page.keyboard.press('Escape'); + await page.waitForSelector('dialog', { hidden: true }); + assert.deepEqual(await stored(page), original); + await page.click('[data-theme-save-as]'); + await page.$eval('#bskyplusThemeCopyName', input => { input.value = 'Forest copy'; }); + await page.click('dialog button[type="submit"]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 3); + const saved = await stored(page); + assert.deepEqual(saved.themes.find(theme => theme.id === original.activeId), original.themes[0]); + const copy = saved.themes.find(theme => theme.id === saved.activeId); + assert.equal(copy.name, 'Forest copy'); + assert.equal(copy.colors.background, '#10231E'); + assert.notEqual(copy.id, original.activeId); + assert.equal(await page.$eval('[data-theme-save-as]', button => button.disabled), true); + } finally { await browser.close(); } +}); + +test('contrast recommendations reveal both settings and clear both highlights when either color fixes the pair', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.$eval('[data-theme-hex="onPrimary"]', input => { + input.value = '#006AFF'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.click('[data-theme-contrast-warning] summary'); + assert.match(await page.$eval('[data-theme-contrast-warning]', node => node.textContent), /Darken “Text on colored controls” in Links and primary controls/); + await page.click('[data-theme-contrast-warning] [data-theme-contrast-edit="onPrimary"]'); + await page.waitForFunction(() => document.activeElement?.dataset.themeHex === 'onPrimary'); + assert.equal(await page.$eval('[data-theme-field="onPrimary"]', field => + field.classList.contains('bskyplus-theme-field-highlight') && field.closest('details').open), true); + assert.deepEqual(await page.$$eval('.bskyplus-theme-field-highlight', fields => fields.map(field => field.dataset.themeField).sort()), ['onPrimary', 'primary']); + await page.$eval('[data-theme-field="onPrimary"]', field => { field.closest('details').open = false; }); + await page.click('[data-theme-save]'); + await page.click('dialog [data-theme-contrast-edit="onPrimary"]'); + await page.waitForSelector('dialog', { hidden: true }); + await page.waitForFunction(() => document.activeElement?.dataset.themeHex === 'onPrimary'); + assert.equal(await stored(page), null); + assert.equal(await page.$$eval('.bskyplus-theme-field-highlight', fields => fields.length), 2); + await page.$eval('[data-theme-hex="onPrimary"]', input => { + input.value = '#000000'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$('.bskyplus-theme-field-highlight'), null); + assert.equal(await page.$eval('[data-theme-contrast-warning]', node => node.hidden), true); + await page.$eval('[data-theme-hex="surfaceRaised"]', input => { + input.value = '#FFFFFF'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.$$eval('[data-theme-group]', groups => groups.forEach(group => { group.open = false; })); + await page.click('[data-theme-contrast-warning] [data-theme-contrast-edit="text"][data-theme-contrast-background="surfaceRaised"]'); + await page.waitForFunction(() => document.activeElement?.dataset.themeHex === 'text'); + assert.deepEqual(await page.$$eval('.bskyplus-theme-field-highlight', fields => fields.map(field => ({ key: field.dataset.themeField, open: field.closest('details').open }))), [ + { key: 'surfaceRaised', open: true }, { key: 'text', open: true }, + ]); + await page.$eval('[data-theme-hex="surfaceRaised"]', input => { + input.value = '#19222E'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$('.bskyplus-theme-field-highlight'), null); + } finally { await browser.close(); } +}); + +test('contrast highlights expire after three seconds without changing the colors or closing sections', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.$eval('[data-theme-hex="onPrimary"]', input => { + input.value = '#006AFF'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.click('[data-theme-contrast-warning] summary'); + await page.click('[data-theme-contrast-edit="onPrimary"]'); + assert.equal(await page.$$eval('.bskyplus-theme-field-highlight', fields => fields.length), 2); + await page.waitForFunction(() => !document.querySelector('.bskyplus-theme-field-highlight'), { timeout: 4500 }); + assert.equal(await page.$eval('[data-theme-hex="onPrimary"]', input => input.value), '#006AFF'); + assert.equal(await page.$eval('[data-theme-field="onPrimary"]', field => field.closest('details').open), true); + } finally { await browser.close(); } +}); + +test('saving and applying preserves open and closed theme groups', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await setName(page, 'Keep editing'); + assert.equal(await page.$eval('[data-theme-save]', button => button.textContent), 'Save and apply'); + const states = [false, true, false, true, true, false]; + await page.$$eval('[data-theme-group]', (groups, states) => { + groups.forEach((group, index) => { group.open = states[index]; }); + }, states); + await page.click('.bskyplus-theme-apply'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Saved and applied'); + assert.equal(await page.$$eval('.bskyplus-theme-actions button', buttons => + buttons.filter(button => button.textContent === 'Apply').length), 1); + assert.equal(await page.$eval('[data-theme-save-as]', button => button.disabled), true); + assert.deepEqual(await page.$$eval('[data-theme-group]', groups => groups.map(group => group.open)), states); + await setName(page, 'Keep editing again'); + assert.equal(await page.$eval('[data-theme-save]', button => button.textContent), 'Save and apply'); + await page.click('.bskyplus-theme-apply'); + await page.waitForFunction(() => window.__themeStorage.value?.themes[0].name === 'Keep editing again'); + assert.deepEqual(await page.$$eval('[data-theme-group]', groups => groups.map(group => group.open)), states); + } finally { await browser.close(); } +}); + +test('saves multiple local themes and reloads the active library', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await setName(page, 'Ocean'); + await page.click('[data-theme-save]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 1); + await page.click('[data-theme-select="bluesky-light"]'); + await setName(page, 'Sunrise'); + await page.click('.bskyplus-theme-apply'); + await page.click('dialog button[type="submit"]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 2); + const library = await stored(page); + assert.deepEqual(library.themes.map(theme => theme.name), ['Ocean', 'Sunrise']); + assert.equal(library.activeId, library.themes[1].id); + + const reload = await browser.newPage(); + await reload.setContent('
'); + await reload.evaluate(value => { + const listeners = []; + window.chrome = { storage: { local: { get: async key => ({ [key]: value }), set: async () => {} }, onChanged: { addListener: listener => listeners.push(listener) } } }; + }, library); + await reload.addScriptTag({ content: modelScript }); + await reload.addScriptTag({ content: editorScript }); + await reload.waitForFunction(savedCount => document.querySelectorAll('[data-theme-card]').length === + Object.keys(BskyPlusThemes.BUILTIN_THEMES).length + savedCount, {}, library.themes.length); + assert.equal(await reload.$eval('.bskyplus-theme-name', input => input.value), 'Sunrise'); + } finally { + await browser.close(); + } +}); + +test('apply, Bluesky default, and delete change only the active saved theme', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + const initial = await stored(page); + await page.click('.bskyplus-theme-apply'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Saved and applied'); + assert.equal((await stored(page)).activeId, initial.themes[0].id); + + await page.click('[data-theme-default]'); + await page.waitForFunction(() => window.__themeStorage.value.activeId === null); + assert.equal((await stored(page)).themes.length, 2); + + await page.click('.bskyplus-theme-delete'); + await page.waitForSelector('[data-theme-confirm][open]'); + await page.click('[data-theme-confirm-accept]'); + await page.waitForFunction(() => window.__themeStorage.value.themes.length === 1); + const afterDelete = await stored(page); + assert.equal(afterDelete.themes[0].id, initial.themes[1].id); + assert.equal(afterDelete.activeId, null); + } finally { + await browser.close(); + } +}); + +test('invalid hex stays out of storage and a valid draft exports JSON', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.$eval('[data-theme-hex="background"]', input => { + input.value = '#bad'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.$eval('[data-theme-hex="background"]', input => input.getAttribute('aria-invalid')), 'true'); + assert.equal(await stored(page), null); + + await setName(page, 'Export me'); + await page.click('.bskyplus-theme-actions button:last-child'); + await page.waitForFunction(() => !!window.__themeExport && !!window.__themeDownload); + const exported = await page.evaluate(async () => ({ + json: await window.__themeExport.blob.text(), + download: window.__themeDownload.download, + })); + const theme = JSON.parse(exported.json); + assert.equal(theme.name, 'Export me'); + assert.equal(theme.schemaVersion, 1); + assert.equal(exported.download, 'Export-me.json'); + } finally { + await browser.close(); + } +}); + +test('imports a valid JSON theme as an unsaved draft', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + const imported = await page.evaluate(() => ({ + ...BskyPlusThemes.createTheme('Imported ocean', 'light'), + id: 'imported-ocean', + colors: { ...BskyPlusThemes.PRESETS.light, primary: '#123456' }, + })); + await page.$eval('[data-theme-import-input]', (input, theme) => { + const file = new File([JSON.stringify(theme)], 'ocean.json', { type: 'application/json' }); + Object.defineProperty(input, 'files', { configurable: true, value: [file] }); + input.dispatchEvent(new Event('change', { bubbles: true })); + }, imported); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent.startsWith('Imported theme')); + + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Imported ocean'); + assert.equal(await page.$eval('[data-theme-hex="primary"]', input => input.value), '#123456'); + assert.equal(await page.$('[data-theme-select][aria-pressed="true"]'), null); + assert.equal(await stored(page), null); + + await page.click('[data-theme-save]'); + await page.click('dialog button[type="submit"]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes[0]?.name === 'Imported ocean'); + assert.equal((await stored(page)).themes[0].id, 'imported-ocean'); + } finally { + await browser.close(); + } +}); + +test('rejects invalid JSON without replacing the current draft', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await setName(page, 'Keep this draft'); + await page.$eval('[data-theme-import-input]', input => { + const file = new File(['{not JSON'], 'broken.json', { type: 'application/json' }); + Object.defineProperty(input, 'files', { configurable: true, value: [file] }); + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'The selected file is not valid JSON.'); + + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Keep this draft'); + assert.equal(await stored(page), null); + } finally { + await browser.close(); + } +}); + +test('imports a theme with an existing ID as a copy', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'one' }); + const original = (await stored(page)).themes[0]; + await page.$eval('[data-theme-import-input]', (input, theme) => { + const file = new File([JSON.stringify(theme)], 'copy.json', { type: 'application/json' }); + Object.defineProperty(input, 'files', { configurable: true, value: [file] }); + input.dispatchEvent(new Event('change', { bubbles: true })); + }, { ...original, name: 'Ocean copy' }); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent.startsWith('Imported as a copy')); + await page.click('[data-theme-save]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 2); + + const library = await stored(page); + assert.equal(library.themes[0].id, original.id); + assert.notEqual(library.themes[1].id, original.id); + assert.equal(library.themes[1].name, 'Ocean copy'); + } finally { + await browser.close(); + } +}); + +test('imports a WitchSky base palette and its variants as separate saved themes', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.evaluate(() => { + const colors = { + canvas: '#1c1310', surface: '#2e2018', surfaceRaised: '#251a15', text: '#f4deb8', + textMuted: '#e8c28a', border: '#3d2b1f', accent: '#d4975c', accentSoft: '#4d3828', + onAccent: '#2c221a', positive: '#4caf7d', warning: '#e2b76f', critical: '#e06c6c', favorite: '#cc5577', + }; + const uri = 'at://did:plc:l37td5yhxl2irrzrgvei4qay/app.witchsky.theme.colors/3mvpzxkrbms25'; + window.fetch = async url => { + if (String(url).startsWith('https://plc.directory/')) { + return new Response(JSON.stringify({ service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.test' }] })); + } + return new Response(JSON.stringify({ uri, value: { + $type: 'app.witchsky.theme.colors', name: 'Coffee', base: { name: 'Default', colors }, + variants: [ + { name: 'Light', colors: { canvas: '#f7f2e9', surface: '#efe3d0', surfaceRaised: '#fdf8f0', text: '#1c1310' } }, + { name: 'Dark', colors: { canvas: '#050505', surface: '#171717', surfaceRaised: '#111111', text: '#f7f1e8' } }, + ], + } })); + }; + }); + await page.type('[data-theme-witchsky-uri]', 'at://did:plc:l37td5yhxl2irrzrgvei4qay/app.witchsky.theme.colors/3mvpzxkrbms25'); + await page.click('[data-theme-witchsky-import]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 3); + + const library = await stored(page); + assert.deepEqual(library.themes.map(theme => theme.name), ['Coffee — Default', 'Coffee — Light', 'Coffee — Dark']); + assert.equal(library.activeId, null); + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Coffee — Default'); + assert.equal(await page.$eval('[data-theme-status]', node => node.textContent), 'Imported 3 WitchSky themes'); + } finally { + await browser.close(); + } +}); + +test('imports the provided did:web WitchSky URI after verifying its DID document and PDS record', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.evaluate(() => { + const uri = 'at://did:web:vessel.darkworld.download/app.witchsky.theme.colors/3mv3ih7bfgs2m'; + const colors = { + canvas: '#1c1310', surface: '#2e2018', surfaceRaised: '#251a15', text: '#f4deb8', + textMuted: '#e8c28a', border: '#3d2b1f', accent: '#d4975c', accentSoft: '#4d3828', + onAccent: '#2c221a', positive: '#4caf7d', warning: '#e2b76f', critical: '#e06c6c', favorite: '#cc5577', + }; + const variants = Array.from({ length: 11 }, (_, index) => ({ + name: 'Variant ' + (index + 1), + colors: { canvas: index % 2 ? '#f7f2e9' : '#050505' }, + })); + const didDocument = { + id: 'did:web:vessel.darkworld.download', + service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pushingbuddies.kralsei.stream' }], + }; + const record = { uri, value: { + $type: 'app.witchsky.theme.colors', name: 'Vessel palette', + base: { name: 'Default', colors }, variants, + } }; + window.__witchSkyRequests = []; + window.fetch = async url => { + const requestUrl = new URL(url); + window.__themePermissionOrder.push('fetch ' + requestUrl.pathname); + window.__witchSkyRequests.push(requestUrl.href); + if (requestUrl.href === 'https://vessel.darkworld.download/.well-known/did.json') { + return new Response(JSON.stringify(didDocument)); + } + if (requestUrl.origin === 'https://pushingbuddies.kralsei.stream') { + window.__witchSkyRecordQuery = Object.fromEntries(requestUrl.searchParams.entries()); + return new Response(JSON.stringify(record)); + } + throw new Error('Unexpected WitchSky request: ' + requestUrl.href); + }; + }); + const uri = 'at://did:web:vessel.darkworld.download/app.witchsky.theme.colors/3mv3ih7bfgs2m'; + await page.type('[data-theme-witchsky-uri]', uri); + await page.click('[data-theme-witchsky-import]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 12); + + const library = await stored(page); + assert.equal(library.themes.length, 12); + assert.equal(library.themes[0].name, 'Vessel palette — Default'); + assert.equal(library.themes.at(-1).name, 'Vessel palette — Variant 11'); + assert.deepEqual(await page.evaluate(() => window.__themePermissionRequests), [ + { origins: ['https://vessel.darkworld.download/*'] }, + ]); + assert.deepEqual(await page.evaluate(() => window.__themePermissionOrder), [ + 'permission', 'fetch /.well-known/did.json', 'fetch /xrpc/com.atproto.repo.getRecord', + ]); + assert.deepEqual(await page.evaluate(() => window.__themeStorage.value && window.__witchSkyRecordQuery), { + repo: 'did:web:vessel.darkworld.download', + collection: 'app.witchsky.theme.colors', + rkey: '3mv3ih7bfgs2m', + }); + assert.equal(await page.$eval('[data-theme-status]', node => node.textContent), 'Imported 12 WitchSky themes'); + } finally { + await browser.close(); + } +}); + +test('denied did:web host permission prevents network access and storage changes', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { permissionGranted: false }); + await page.evaluate(() => { + window.__witchSkyRequests = []; + window.fetch = async url => { window.__witchSkyRequests.push(String(url)); return new Response('{}'); }; + }); + await page.type('[data-theme-witchsky-uri]', 'at://did:web:vessel.darkworld.download/app.witchsky.theme.colors/3mv3ih7bfgs2m'); + await page.click('[data-theme-witchsky-import]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Access to the did:web repository was not granted.'); + + assert.deepEqual(await page.evaluate(() => window.__themePermissionRequests), [ + { origins: ['https://vessel.darkworld.download/*'] }, + ]); + assert.deepEqual(await page.evaluate(() => window.__witchSkyRequests), []); + assert.equal(await stored(page), null); + } finally { + await browser.close(); + } +}); + +test('did:web importer rejects a DID document whose id does not match the URI', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + await page.evaluate(() => { + window.__witchSkyRequests = []; + window.fetch = async url => { + const requestUrl = new URL(url); + window.__witchSkyRequests.push(requestUrl.href); + return new Response(JSON.stringify({ + id: 'did:web:another.example', + service: [{ id: '#atproto_pds', serviceEndpoint: 'https://pds.example.test' }], + })); + }; + }); + await page.type('[data-theme-witchsky-uri]', 'at://did:web:vessel.darkworld.download/app.witchsky.theme.colors/3mv3ih7bfgs2m'); + await page.click('[data-theme-witchsky-import]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'The did:web document does not match the WitchSky repository.'); + + assert.deepEqual(await page.evaluate(() => window.__witchSkyRequests), [ + 'https://vessel.darkworld.download/.well-known/did.json', + ]); + assert.equal(await stored(page), null); + } finally { + await browser.close(); + } +}); + +test('page preview checkbox sends validated drafts without saving them', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + assert.equal(await page.$eval('.bskyplus-theme-preview-toggle input', input => input.checked), true); + await page.click('[data-theme-select="bluesky-dark"]'); + await setName(page, 'Live preview'); + const preview = await page.evaluate(() => window.__themeMessages.filter(message => message.type === 'BSKYPLUS_THEME_PREVIEW').at(-1)); + + assert.equal(preview.theme.name, 'Live preview'); + assert.equal(await stored(page), null); + + await page.click('.bskyplus-theme-preview-toggle input'); + const cleared = await page.evaluate(() => window.__themeMessages.filter(message => message.type === 'BSKYPLUS_THEME_PREVIEW').at(-1)); + assert.equal(cleared.theme, null); + } finally { + await browser.close(); + } +}); + +test('native theme preview stays off on initial load until a theme is chosen', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + for (const seed of [null, 'two']) { + const page = await openEditor(browser, { seed }); + const initialPreviews = await page.evaluate(() => window.__themeMessages + .filter(message => message.type === 'BSKYPLUS_THEME_PREVIEW')); + assert.ok(initialPreviews.length > 0); + assert.ok(initialPreviews.every(message => message.theme === null), `seed ${seed}: initial preview messages should clear rather than choose a theme`); + + await page.click('[data-theme-select="bluesky-light"]'); + await page.waitForFunction(() => window.__themeMessages + .filter(message => message.type === 'BSKYPLUS_THEME_PREVIEW').at(-1)?.theme?.id === 'bluesky-light'); + const chosen = await page.evaluate(() => window.__themeMessages + .filter(message => message.type === 'BSKYPLUS_THEME_PREVIEW').at(-1)); + assert.equal(chosen.theme.id, 'bluesky-light'); + await page.close(); + } + } finally { + await browser.close(); + } +}); + +test('dirty edits survive a cancelled selection and an external library change', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'two' }); + await setName(page, 'Unsaved Ocean'); + await page.click(`[data-theme-select="${(await stored(page)).themes[1].id}"]`); + await page.waitForSelector('[data-theme-confirm][open]'); + assert.match(await page.$eval('[data-theme-confirm]', dialog => dialog.textContent), /Discard unsaved theme changes\?/); + await page.click('[data-theme-confirm-cancel]'); + await page.waitForSelector('[data-theme-confirm]', { hidden: true }); + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Unsaved Ocean'); + assert.equal(await page.$eval('[data-theme-select][aria-pressed="true"]', button => button.dataset.themeSelect), (await stored(page)).themes[0].id); + + await page.evaluate(() => { + const changed = BskyPlusThemes.createTheme('Elsewhere', 'dim'); + window.__themeStorage.listeners.forEach(listener => listener({ + bskyplusThemes: { oldValue: null, newValue: { schemaVersion: 1, activeId: changed.id, themes: [changed] } }, + }, 'local')); + }); + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Unsaved Ocean'); + assert.match(await page.$eval('[data-theme-status]', node => node.textContent), /another Bsky\+ window/); + } finally { + await browser.close(); + } +}); + +test('discard modal confirms theme replacement and delete modal stays in the page', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'active' }); + const original = await stored(page); + await setName(page, 'Unsaved Ocean'); + await page.click('[data-theme-select="bluesky-dim"]'); + await page.waitForSelector('[data-theme-confirm][open]'); + await page.keyboard.press('Escape'); + await page.waitForSelector('[data-theme-confirm]', { hidden: true }); + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Unsaved Ocean'); + + await page.click(`[data-theme-select="${original.themes[1].id}"]`); + await page.waitForSelector('[data-theme-confirm][open]'); + await page.click('[data-theme-confirm-accept]'); + await page.waitForFunction(() => document.querySelector('.bskyplus-theme-name')?.value === 'Sunrise'); + assert.equal(await page.$eval('.bskyplus-theme-name', input => input.value), 'Sunrise'); + assert.deepEqual(await stored(page), original); + + await page.click('.bskyplus-theme-delete'); + await page.waitForSelector('[data-theme-confirm][open]'); + assert.match(await page.$eval('[data-theme-confirm]', dialog => dialog.textContent), /Delete “Sunrise”\?/); + await page.click('[data-theme-confirm-cancel]'); + assert.deepEqual(await stored(page), original); + await page.click('.bskyplus-theme-delete'); + await page.waitForSelector('[data-theme-confirm][open]'); + await page.click('[data-theme-confirm-accept]'); + await page.waitForFunction(() => window.__themeStorage.value?.themes.length === 1); + assert.equal((await stored(page)).themes[0].name, 'Ocean'); + } finally { await browser.close(); } +}); + +test('built-in themes apply without saving, edits save as custom copies, and saved themes remain deletable', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { seed: 'one' }); + const before = await stored(page); + const includedCount = await page.evaluate(() => Object.keys(BskyPlusThemes.BUILTIN_THEMES).length); + assert.equal(await page.$$eval('[data-theme-card]', cards => cards.length), includedCount + before.themes.length); + const darkId = 'bluesky-dark'; + await page.click(`[data-theme-select="${darkId}"]`); + assert.equal(await page.$eval('.bskyplus-theme-delete', button => button.disabled), false); + await page.click('[data-theme-save]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Saved and applied'); + const applied = await stored(page); + assert.equal(applied.activeId, darkId); + assert.deepEqual(applied.themes, before.themes); + + await setName(page, 'My Dark Copy'); + await page.click('[data-theme-save]'); + if (await page.$('dialog[open]')) await page.click('dialog button[type="submit"]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent === 'Saved and applied'); + const copied = await stored(page); + assert.equal(copied.themes.length, before.themes.length + 1); + assert.notEqual(copied.activeId, darkId); + assert.equal(copied.themes.at(-1).name, 'My Dark Copy'); + await page.click('.bskyplus-theme-delete'); + await page.click('dialog [data-theme-confirm-accept]'); + await page.waitForFunction(id => !document.querySelector(`[data-theme-card="${id}"]`), {}, copied.activeId); + assert.equal((await stored(page)).activeId, null); + assert.equal(await page.$$eval('[data-theme-card]', cards => cards.filter(card => card.dataset.themeCard.startsWith('bluesky-')).length), 3); + } finally { await browser.close(); } +}); + +test('all included themes can be removed per user and stay removed after storage refresh', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser); + const builtinIds = await page.evaluate(() => Object.keys(BskyPlusThemes.BUILTIN_THEMES)); + for (const id of builtinIds) { + await page.click(`[data-theme-select="${id}"]`); + assert.equal(await page.$eval('.bskyplus-theme-delete', button => button.disabled), false, `${id} has a delete action`); + await page.click('[data-theme-save]'); + await page.waitForFunction(id => window.__themeStorage.value?.activeId === id, {}, id); + + await page.click('.bskyplus-theme-delete'); + await page.waitForSelector('[data-theme-confirm][open]'); + await page.click('[data-theme-confirm-accept]'); + await page.waitForFunction(id => !document.querySelector(`[data-theme-card="${id}"]`) && + window.__themeStorage.value?.deletedBuiltinIds?.includes(id) && + window.__themeStorage.value.activeId === null, {}, id); + const latestPreview = await page.evaluate(() => window.__themeMessages + .filter(message => message.type === 'BSKYPLUS_THEME_PREVIEW').at(-1)); + assert.equal(latestPreview.theme, null, `${id} deletion clears the page preview`); + } + const library = await stored(page); + assert.deepEqual(library.deletedBuiltinIds, builtinIds); + assert.equal(library.themes.length, 0); + assert.equal(library.activeId, null); + assert.deepEqual(await page.$$eval('[data-theme-card]', cards => cards.map(card => card.dataset.themeCard)), []); + assert.deepEqual(await page.evaluate(() => BskyPlusThemes.availableBuiltinThemes(window.__themeStorage.value)), []); + } finally { await browser.close(); } +}); + +test('a storage write failure leaves the draft dirty and reports the failure', async () => { + const browser = await puppeteer.launch({ executablePath: chromePath, headless: true }); + try { + const page = await openEditor(browser, { rejectSet: true }); + await setName(page, 'Cannot save'); + await page.click('[data-theme-save]'); + await page.waitForFunction(() => document.querySelector('[data-theme-status]').textContent.includes('Could not save')); + assert.equal(await stored(page), null); + assert.equal(await page.$eval('[data-theme-save]', button => button.disabled), false); + } finally { + await browser.close(); + } +}); diff --git a/test/test_theme_model.mjs b/test/test_theme_model.mjs new file mode 100644 index 0000000..25a5721 --- /dev/null +++ b/test/test_theme_model.mjs @@ -0,0 +1,375 @@ +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import test from 'node:test'; +import vm from 'node:vm'; + +const source = await readFile(new URL('../extension/shared/theme-model.js', import.meta.url), 'utf8'); +const context = vm.createContext({crypto: {randomUUID: () => 'generated-theme-id'}, URL}); +vm.runInContext(source, context, {filename: 'theme-model.js'}); +const themes = context.BskyPlusThemes; +const plain = value => JSON.parse(JSON.stringify(value)); + +function validTheme(overrides = {}) { + const base = overrides.base || 'light'; + return { + schemaVersion: 1, + id: 'night-owl_01', + name: 'Night owl', + base, + colors: {...themes.PRESETS[base]}, + ...overrides, + }; +} + +test('hue shifts rotate colors in either direction and leave neutral colors unchanged', () => { + const colors = { red: '#FF0000', muted: '#804040', black: '#000000', white: '#FFFFFF', gray: '#777777' }; + assert.deepEqual(plain(themes.shiftHue(colors, 120)), { + red: '#00FF00', muted: '#408040', black: '#000000', white: '#FFFFFF', gray: '#777777', + }); + assert.equal(themes.shiftHue(colors, -120).red, '#0000FF'); + assert.equal(themes.shiftHue(colors, 480).red, '#00FF00'); + assert.deepEqual(plain(themes.shiftHue(colors, 360)), colors); + assert.equal(colors.red, '#FF0000'); + assert.throws(() => themes.shiftHue(colors, NaN), /finite hue shift/); +}); + +test('hue shifts cover complete palettes and preserve channel extrema', () => { + for (const palette of Object.values(themes.PRESETS)) { + const shifted = themes.shiftHue(palette, 73); + assert.deepEqual(Object.keys(shifted), Object.keys(palette)); + for (const key of themes.KEYS) { + const channels = hex => hex.slice(1).match(/../g).map(value => parseInt(value, 16)); + assert.equal(Math.min(...channels(shifted[key])), Math.min(...channels(palette[key]))); + assert.equal(Math.max(...channels(shifted[key])), Math.max(...channels(palette[key]))); + } + assert.deepEqual(plain(themes.shiftHue(palette, 0)), plain(palette)); + } +}); + +test('contrast checker catches unreadable text on secondary surfaces and colored buttons', () => { + const colors = { ...themes.PRESETS.dark, surfaceRaised: '#FFFFFF', primary: '#FFFFFF' }; + const issues = themes.contrastIssues(colors); + assert.ok(issues.some(issue => issue.foreground === 'text' && issue.background === 'surfaceRaised' && issue.ratio === 1)); + assert.ok(issues.some(issue => issue.foreground === 'onPrimary' && issue.background === 'primary' && issue.ratio === 1)); + assert.ok(issues.every(issue => issue.ratio < 3)); + assert.ok(!themes.contrastIssues(themes.PRESETS.dark).some(issue => issue.foreground === 'text')); +}); + +test('theme schema accepts only a complete version-one theme and normalizes it', () => { + const theme = validTheme({ + name: ' Night owl ', + colors: {...themes.PRESETS.light, text: '#aBc123'}, + ignored: 'discarded', + }); + + assert.deepEqual(plain(themes.validateTheme(theme)), { + schemaVersion: 1, + id: 'night-owl_01', + name: 'Night owl', + base: 'light', + colors: {...plain(themes.PRESETS.light), text: '#ABC123'}, + }); +}); + +test('theme schema rejects unsupported versions, unsafe IDs, names, bases, and incomplete colors', () => { + assert.throws(() => themes.validateTheme(null), /Unsupported theme format/); + assert.throws(() => themes.validateTheme(validTheme({schemaVersion: 2})), /Unsupported theme format/); + assert.throws(() => themes.validateTheme(validTheme({id: '../theme'})), /Invalid theme ID/); + assert.throws(() => themes.validateTheme(validTheme({id: 'x'.repeat(81)})), /Invalid theme ID/); + assert.throws(() => themes.validateTheme(validTheme({name: ' '})), /between 1 and 80/); + assert.throws(() => themes.validateTheme(validTheme({name: 'x'.repeat(81)})), /between 1 and 80/); + assert.throws(() => themes.validateTheme(validTheme({base: 'system'})), /Choose a light, dim, or dark base/); + assert.throws(() => themes.validateTheme(validTheme({colors: {...themes.PRESETS.light, primary: 'blue'}})), /six-digit hex color for primary/); + assert.throws(() => themes.validateTheme(validTheme({colors: {...themes.PRESETS.light, primary: '#12345678'}})), /six-digit hex color for primary/); + assert.throws(() => themes.validateTheme(validTheme({colors: {}})), /six-digit hex color for background/); +}); + +test('theme factory uses a safe requested base and falls back to dim', () => { + const light = themes.createTheme('Light personal', 'light'); + assert.equal(light.id, 'generated-theme-id'); + assert.equal(light.base, 'light'); + assert.deepEqual(light.colors, themes.PRESETS.light); + + const fallback = themes.createTheme(); + assert.equal(fallback.base, 'dim'); + assert.deepEqual(fallback.colors, themes.PRESETS.dim); + assert.equal(themes.createTheme('Bad base', 'invalid').base, 'dim'); +}); + +test('library normalization removes malformed, duplicate, and inactive entries without losing valid themes', () => { + const first = validTheme({id: 'first', name: 'First'}); + const duplicate = validTheme({id: 'first', name: 'Second copy'}); + const second = validTheme({id: 'second', name: 'Second', base: 'dark', colors: {...themes.PRESETS.dark}}); + const malformed = validTheme({id: 'broken', colors: {...themes.PRESETS.light, warning: '#fff'}}); + + assert.deepEqual(plain(themes.normalizeLibrary({ + schemaVersion: 1, + activeId: 'first', + themes: [first, malformed, duplicate, second], + })), { + schemaVersion: 1, + activeId: 'first', + themes: [plain(themes.validateTheme(first)), plain(themes.validateTheme(second))], + }); + + assert.deepEqual(themes.normalizeLibrary({schemaVersion: 1, activeId: 'missing', themes: [first]}).activeId, null); + assert.deepEqual(plain(themes.normalizeLibrary({schemaVersion: 0, activeId: 'first', themes: [first]})), { + schemaVersion: 1, + activeId: null, + themes: [], + }); + assert.deepEqual(plain(themes.normalizeLibrary(undefined)), {schemaVersion: 1, activeId: null, themes: []}); +}); + +test('library normalization caps input at one hundred candidates before validation', () => { + const candidates = Array.from({length: 101}, (_, index) => validTheme({id: 'theme-' + index, name: 'Theme ' + index})); + const library = themes.normalizeLibrary({schemaVersion: 1, activeId: 'theme-100', themes: candidates}); + assert.equal(library.themes.length, 100); + assert.equal(library.activeId, null); + assert.equal(library.themes.at(-1).id, 'theme-99'); +}); + +test('deleted builtin choices persist only known unique ids and cannot stay active', () => { + const builtinIds = Object.keys(themes.BUILTIN_THEMES); + const deleted = themes.normalizeLibrary({ + schemaVersion: 1, + activeId: builtinIds[0], + themes: [], + deletedBuiltinIds: [builtinIds[0], 'unknown-theme', builtinIds[0], 42], + }); + assert.deepEqual(plain(deleted), { + schemaVersion: 1, + activeId: null, + themes: [], + deletedBuiltinIds: [builtinIds[0]], + }); + assert.deepEqual(plain(themes.availableBuiltinThemes(deleted).map(theme => theme.id)), builtinIds.slice(1)); + + const remainingActive = themes.normalizeLibrary({ + schemaVersion: 1, activeId: builtinIds[1], themes: [], deletedBuiltinIds: [builtinIds[0]], + }); + assert.equal(remainingActive.activeId, builtinIds[1]); + assert.equal(themes.availableBuiltinThemes(remainingActive).length, builtinIds.length - 1); + assert.deepEqual(plain(themes.normalizeLibrary({ schemaVersion: 1, activeId: null, themes: [], deletedBuiltinIds: ['unknown-theme'] })), { + schemaVersion: 1, activeId: null, themes: [], + }, 'unknown ids are discarded and an empty tombstone list preserves the legacy shape'); +}); + +test('export is a clean, canonical theme document without secrets or extraneous fields', () => { + const theme = validTheme({ + token: 'secret-token', + endpoint: 'https://api.example.test', + colors: {...themes.PRESETS.light, apiKey: 'secret-key'}, + }); + const exported = themes.exportTheme(theme); + const parsed = JSON.parse(exported); + + assert.equal(exported.endsWith('\n'), true); + assert.deepEqual(Object.keys(parsed).sort(), ['base', 'colors', 'id', 'name', 'schemaVersion']); + assert.equal('token' in parsed, false); + assert.equal('endpoint' in parsed, false); + assert.equal('apiKey' in parsed.colors, false); + assert.equal(parsed.colors.primary, '#006AFF'); +}); + +test('WitchSky AT URIs accept raw and PDSls forms and reject other collections', () => { + const uri = 'at://did:plc:l37td5yhxl2irrzrgvei4qay/app.witchsky.theme.colors/3mvpzxkrbms25'; + assert.deepEqual(plain(themes.parseWitchSkyUri(uri)), { + uri, + repo: 'did:plc:l37td5yhxl2irrzrgvei4qay', + rkey: '3mvpzxkrbms25', + }); + assert.deepEqual(plain(themes.parseWitchSkyUri('https://pds.ls/' + uri + '#record')), plain(themes.parseWitchSkyUri(uri))); + assert.throws(() => themes.parseWitchSkyUri('at://did:plc:l37td5yhxl2irrzrgvei4qay/app.bsky.feed.post/3mvpzxkrbms25'), /complete WitchSky/); + assert.throws(() => themes.parseWitchSkyUri('https://example.com/theme'), /complete WitchSky/); +}); + +test('WitchSky did:web URIs resolve root and path DID documents safely', () => { + const uri = 'at://did:web:vessel.darkworld.download/app.witchsky.theme.colors/3mv3ih7bfgs2m'; + assert.deepEqual(plain(themes.parseWitchSkyUri(uri)), { + uri, + repo: 'did:web:vessel.darkworld.download', + rkey: '3mv3ih7bfgs2m', + didDocumentUrl: 'https://vessel.darkworld.download/.well-known/did.json', + }); + assert.equal(themes.parseWitchSkyUri( + 'at://did:web:example.test%3A8443:team:alice/app.witchsky.theme.colors/abc123', + ).didDocumentUrl, 'https://example.test:8443/team/alice/did.json'); + assert.equal(themes.parseWitchSkyUri( + 'at://did:web:example.test:team%20one/app.witchsky.theme.colors/abc123', + ).didDocumentUrl, 'https://example.test/team%20one/did.json'); + for (const repo of [ + 'did:web:bad host.example', + 'did:web:example.test%2F..', + 'did:web:example.test:..', + 'did:web:example.test:%2Fadmin', + 'did:web:example.test:%ZZ', + ]) { + assert.throws(() => themes.parseWitchSkyUri(`at://${repo}/app.witchsky.theme.colors/abc123`), /complete WitchSky/); + } +}); + +test('WitchSky base and variants become separate complete Bsky+ themes', () => { + const record = { + $type: 'app.witchsky.theme.colors', + name: 'Coffee', + base: { name: 'Default', colors: { + canvas: '#1c1310', surface: '#2e2018', surfaceRaised: '#251a15', text: '#f4deb8', + textMuted: '#e8c28a', border: '#3d2b1f', accent: '#d4975c', accentSoft: '#4d3828', + onAccent: '#2c221a', positive: '#4caf7d', warning: '#e2b76f', critical: '#e06c6c', favorite: '#cc5577', + } }, + variants: [ + { name: 'Light', colors: { canvas: '#f7f2e9', surface: '#efe3d0', surfaceRaised: '#fdf8f0', text: '#1c1310' } }, + { name: 'Dark', colors: { canvas: '#050505', surface: '#171717', surfaceRaised: '#111111', text: '#f7f1e8' } }, + ], + }; + const imported = plain(themes.witchSkyThemes(record)); + + assert.deepEqual(imported.map(theme => theme.name), ['Coffee — Default', 'Coffee — Light', 'Coffee — Dark']); + assert.deepEqual(imported.map(theme => theme.base), ['dark', 'light', 'dark']); + assert.equal(imported[0].colors.background, '#1C1310'); + assert.equal(imported[0].colors.primary, '#D4975C'); + assert.equal(imported[0].colors.negative, '#E06C6C'); + assert.equal(imported[0].colors.like, '#CC5577'); + assert.equal(imported[1].colors.background, '#F7F2E9'); + assert.equal(imported[1].colors.positive, '#4CAF7D'); + imported.forEach(theme => assert.doesNotThrow(() => themes.validateTheme(theme))); +}); + +test('WitchSky conversion rejects malformed records and translucent colors', () => { + assert.throws(() => themes.witchSkyThemes(null), /not a WitchSky/); + assert.throws(() => themes.witchSkyThemes({ $type: 'app.witchsky.theme.colors', name: 'Broken', base: { colors: {} } }), /missing/); + assert.throws(() => themes.witchSkyThemes({ + $type: 'app.witchsky.theme.colors', name: 'Broken', + base: { colors: { canvas: '#00000080' } }, + }), /opaque/); +}); + +test('source palettes produce exact ALF light, dark, and dim inversions', () => { + const light = themes.palette('light'); + const dark = themes.palette('dark'); + const dim = themes.palette('dim'); + + assert.equal(light.contrast_0, '#FFFFFF'); + assert.equal(light.primary_25, '#F5F9FF'); + assert.equal(light.primary_500, '#006AFF'); + assert.equal(dark.contrast_0, '#000000'); + assert.equal(dark.contrast_1000, '#FFFFFF'); + assert.equal(dark.primary_25, '#001533'); + assert.equal(dark.primary_500, '#006AFF'); + assert.equal(dim.contrast_0, '#151D28'); + assert.equal(dim.primary_25, '#122136'); + assert.equal(dim.primary_500, '#0F73FF'); + assert.equal(dim.negative_975, '#FFF5F7'); +}); + +test('mapping is property-aware for neutral surfaces, borders, and text', () => { + const theme = validTheme({ + colors: { + ...themes.PRESETS.light, + background: '#101112', + hover: '#202122', + border: '#303132', + borderStrong: '#404142', + text: '#505152', + textMuted: '#606162', + textSubtle: '#707172', + textInverse: '#808182', + }, + }); + + assert.equal(themes.mapColor('#DCE2EA', 'background-color', theme, 'light'), '#202122'); + assert.equal(themes.mapColor('#DCE2EA', 'border-color', theme, 'light'), '#303132'); + assert.notEqual(themes.mapColor('#DCE2EA', 'color', theme, 'light'), '#202122'); + assert.equal(themes.mapColor('#000000', 'color', theme, 'light'), '#505152'); + assert.equal(themes.mapColor('#FFFFFF', 'color', theme, 'dark'), '#505152'); + assert.equal(themes.mapColor('#DCE2EA', 'outline-color', theme, 'light'), theme.colors.focus); +}); + +test('mapping handles primary, like, error, alpha overlays, and shadows separately', () => { + const theme = validTheme({ + colors: { + ...themes.PRESETS.light, + primary: '#112233', primaryHover: '#223344', primaryMuted: '#334455', link: '#445566', + like: '#556677', negative: '#667788', negativeMuted: '#778899', + overlay: '#8899AA', shadow: '#99AABB', + }, + }); + + assert.equal(themes.mapColor('#006AFF', 'background-color', theme, 'light'), '#112233'); + assert.equal(themes.mapColor('#006AFF', 'color', theme, 'light'), '#445566'); + assert.equal(themes.mapColor('#EC4899', 'fill', theme, 'light'), '#556677'); + assert.equal(themes.mapColor('#E91646', 'color', theme, 'light'), '#667788'); + assert.equal(themes.mapColor('rgba(0, 0, 0, 0.5)', 'background-color', theme, 'light'), 'rgba(136, 153, 170, 0.5)'); + assert.equal(themes.mapColor('#00000080', 'box-shadow', theme, 'light'), 'rgba(153, 170, 187, 0.5019607843137255)'); + assert.equal(themes.mapColor('transparent', 'background-color', theme, 'light'), null); + assert.equal(themes.mapColor('#123456', 'color', theme, 'light'), null); +}); + +test('contrast ratios use WCAG relative luminance', () => { + assert.equal(themes.contrastRatio('#000000', '#FFFFFF'), 21); + assert.equal(themes.contrastRatio('#123456', '#123456'), 1); + assert.equal(themes.contrastRatio('#123456', '#FEDCBA'), themes.contrastRatio('#FEDCBA', '#123456')); + assert.ok(themes.contrastRatio('#1D3A5F', '#FFFFFF') >= 4.5); +}); + +test('built-in Bluesky themes remain active choices and cannot be shadowed in saved libraries', () => { + const light = themes.BUILTIN_THEMES['bluesky-light']; + assert.deepEqual(Object.keys(themes.BUILTIN_THEMES).filter(id => id.startsWith('bluesky-')), + ['bluesky-light', 'bluesky-dim', 'bluesky-dark']); + assert.equal(light.name, 'Bluesky Light'); + assert.equal(themes.normalizeLibrary({ schemaVersion: 1, activeId: light.id, themes: [] }).activeId, light.id); + const impostor = { ...light, colors: { ...light.colors } }; + assert.deepEqual(plain(themes.normalizeLibrary({ schemaVersion: 1, activeId: light.id, themes: [impostor] })), { + schemaVersion: 1, activeId: light.id, themes: [], + }); +}); + +test('included Helium themes preserve imported names and palettes as frozen builtins', () => { + const ids = [ + 'bskyplus-coffee-espresso-light', + 'bskyplus-floras-moat-floras-moat', + 'bskyplus-witchsky-mu-pink-midnight', + 'bskyplus-snuffbait-dark-coven', + 'bskyplus-latte-mauve', + ]; + const imported = ids.map(id => themes.BUILTIN_THEMES[id]); + assert.deepEqual(imported.map(theme => theme.name), [ + 'Coffee / Espresso — Light', + "Flora's Moat — Flora's Moat", + 'Witchsky + mu Pink — Midnight', + 'snuffbait dark — Coven', + 'Latte — Mauve', + ]); + assert.deepEqual(imported.map(theme => theme.base), ['light', 'dark', 'dark', 'dark', 'light']); + assert.deepEqual(Object.keys(themes.PRESETS), ['light', 'dim', 'dark'], + 'the existing light, dim, and dark factories remain unchanged'); + + for (let index = 0; index < imported.length; index += 1) { + const theme = imported[index]; + assert.ok(theme, `included theme ${ids[index]} exists`); + assert.equal(theme.id, ids[index]); + assert.equal(themes.isBuiltinTheme(theme.id), true); + assert.deepEqual(plain(themes.validateTheme(theme)), plain(theme)); + assert.deepEqual(Object.keys(theme.colors).sort(), [...themes.KEYS].sort()); + assert.equal(Object.isFrozen(theme), true); + assert.equal(Object.isFrozen(theme.colors), true); + } + assert.equal(imported[0].colors.primary, '#A55A1F'); + assert.equal(imported[1].colors.background, '#333333'); + assert.equal(imported[2].colors.background, '#150B08'); + assert.equal(imported[3].colors.text, '#FFE0FF'); + assert.equal(imported[4].colors.primary, '#8839EF'); + + assert.equal(themes.normalizeLibrary(undefined).activeId, null, + 'adding included themes does not auto-activate one'); + const userTheme = validTheme({id: 'existing-user-theme', name: 'Existing user theme'}); + const saved = themes.normalizeLibrary({schemaVersion: 1, activeId: userTheme.id, themes: [userTheme]}); + assert.equal(saved.activeId, userTheme.id, 'an existing user theme remains active'); + assert.deepEqual(plain(saved.themes), [plain(themes.validateTheme(userTheme))]); + + const activeBuiltin = themes.normalizeLibrary({schemaVersion: 1, activeId: imported[0].id, themes: []}); + assert.equal(activeBuiltin.activeId, imported[0].id, 'included themes can be selected without storing a copy'); + const shadowedBuiltin = themes.normalizeLibrary({schemaVersion: 1, activeId: imported[0].id, themes: [imported[0]]}); + assert.deepEqual(plain(shadowedBuiltin), {schemaVersion: 1, activeId: imported[0].id, themes: []}); +}); diff --git a/test/test_theme_runtime.mjs b/test/test_theme_runtime.mjs new file mode 100644 index 0000000..1bab204 --- /dev/null +++ b/test/test_theme_runtime.mjs @@ -0,0 +1,317 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import puppeteer from 'puppeteer-core'; + +const executablePath = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const model = await readFile('extension/shared/theme-model.js', 'utf8'); +const runtime = await readFile('extension/shared/theme-runtime.js', 'utf8'); +const integratedSettings = await readFile('extension/shared/integrated-settings.js', 'utf8'); + +test('themes cover stylesheet, inline, SVG, pseudo-element and dynamic states, then restore native styles', async () => { + const browser = await puppeteer.launch({ executablePath, headless: true }); + const page = await browser.newPage(); + try { + await page.goto('https://example.com'); + await page.setContent(` +
handle
+ + Link +
Text
+
Custom text and background
+ + +
+ +
+
+
Composer
+ `); + await page.evaluate(() => { + window.listeners = []; + window.initialThemeRead = new Promise(resolve => { window.resolveInitialThemeRead = resolve; }); + window.chrome = { storage: { + local: { get: () => window.initialThemeRead }, + onChanged: { addListener: fn => window.listeners.push(fn) }, + } }; + }); + // UUIDs are supplied here so fixtures also work in restricted browser environments. + await page.addScriptTag({ content: model }); + await page.addScriptTag({ content: runtime }); + const original = await page.$eval('#inline', el => el.getAttribute('style')); + const photo = await page.$eval('#photo', el => el.src); + await page.evaluate(() => { + const m = window.BskyPlusThemes; + const theme = { schemaVersion: 1, id: 'test-theme', name: 'Test theme', base: 'dark', colors: { + ...m.PRESETS.dark, background: '#102030', surface: '#203040', surfaceRaised: '#304050', + text: '#F1F2F3', textMuted: '#AABBCC', textSubtle: '#8899AA', border: '#556677', + primary: '#AA3300', primaryHover: '#BB4400', link: '#55DDFF', onPrimary: '#EEEECC', + like: '#FF55AA', positive: '#33CC88', overlay: '#112233', shadow: '#223344', + } }; + window.library = { schemaVersion: 1, activeId: theme.id, themes: [theme] }; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: window.library } }, 'local')); + window.resolveInitialThemeRead({ bskyplusThemes: window.library }); + }); + const color = (selector, property, pseudo) => page.$eval(selector, (el, property, pseudo) => getComputedStyle(el, pseudo)[property], property, pseudo); + await page.waitForFunction(() => getComputedStyle(document.body).backgroundColor === 'rgb(16, 32, 48)'); + assert.equal(await color('body', 'backgroundColor'), 'rgb(16, 32, 48)'); + assert.equal(await color('.post', 'backgroundColor'), 'rgb(32, 48, 64)'); + assert.equal(await color('.post', 'borderTopColor'), 'rgb(85, 102, 119)'); + assert.equal(await color('.composer-edge', 'borderTopColor'), 'rgba(0, 0, 0, 0)'); + assert.equal(await color('.composer-edge', 'borderBottomColor'), 'rgba(0, 0, 0, 0)'); + await page.$eval('.composer-edge', element => { + element.style.borderTopColor = '#DCE2EA'; + element.style.borderBottomColor = '#DCE2EA'; + }); + await page.waitForFunction(() => + getComputedStyle(document.querySelector('.composer-edge')).borderTopColor === 'rgb(85, 102, 119)'); + assert.equal(await color('.composer-edge', 'borderBottomColor'), 'rgb(85, 102, 119)'); + await page.$eval('.composer-edge', element => { + element.style.borderTopColor = 'transparent'; + element.style.borderBottomColor = 'transparent'; + }); + await page.waitForFunction(() => + getComputedStyle(document.querySelector('.composer-edge')).borderTopColor === 'rgba(0, 0, 0, 0)'); + assert.equal(await color('.composer-edge', 'borderBottomColor'), 'rgba(0, 0, 0, 0)'); + assert.equal(await color('.muted', 'color'), 'rgb(170, 187, 204)'); + assert.equal(await color('.primary', 'backgroundColor'), 'rgb(170, 51, 0)'); + assert.equal(await color('.white', 'color'), 'rgb(238, 238, 204)'); + await page.hover('.primary'); + assert.equal(await color('.primary', 'backgroundColor'), 'rgb(187, 68, 0)'); + assert.equal(await color('input', 'color', '::placeholder'), 'rgb(136, 153, 170)'); + assert.equal(await color('.media', 'color'), 'rgb(85, 221, 255)'); + assert.equal(await color('#inline', 'backgroundColor'), 'rgb(48, 64, 80)'); + assert.equal(await color('#root', 'color'), 'rgb(18, 52, 86)'); + assert.equal(await color('#root', 'backgroundColor'), 'rgba(0, 0, 0, 0)'); + assert.equal(await color('#like', 'fill'), 'rgb(255, 85, 170)'); + assert.equal(await color('#repost', 'stroke'), 'rgb(51, 204, 136)'); + assert.equal(await color('#verified', 'fill'), 'rgb(238, 238, 204)'); + assert.equal(await color('#overlay', 'backgroundColor'), 'rgba(17, 34, 51, 0.6)'); + assert.match(await color('.shadow', 'boxShadow'), /rgba\(34, 51, 68, 0.4\)/); + assert.equal(await page.$eval('#inline', el => el.getAttribute('style')), original); + await page.$eval('#root', element => { + element.style.color = '#234567'; + element.style.backgroundColor = '#654321'; + }); + await page.waitForFunction(() => { + const style = getComputedStyle(document.querySelector('#root')); + return style.color === 'rgb(35, 69, 103)' && style.backgroundColor === 'rgb(101, 67, 33)'; + }); + await page.$eval('#root', element => { + element.style.removeProperty('color'); + element.style.removeProperty('background-color'); + }); + await page.waitForFunction(() => { + const style = getComputedStyle(document.querySelector('#root')); + return style.color === 'rgb(241, 242, 243)' && style.backgroundColor === 'rgb(16, 32, 48)'; + }); + assert.equal(await page.$eval('#photo', el => el.src), photo); + assert.match(await color('#art', 'backgroundImage'), /rgb\(0, 106, 255\)/); + assert.match(await color('#fade', 'backgroundImage'), /rgb\(16, 32, 48\)/); + + await page.evaluate(() => { + window.previewStartedAt = performance.now(); + const preview = structuredClone(window.library.themes[0]); + preview.id = 'preview-theme'; + preview.colors.background = '#654321'; + window.dispatchEvent(new CustomEvent('bskyplus:theme-preview', { detail: preview })); + }); + await page.waitForFunction(() => document.getAnimations().some(animation => + animation.effect.pseudoElement === '::view-transition-old(root)' || + animation.effect.pseudoElement === '::view-transition-new(root)')); + const previewState = await page.evaluate(() => ({ + background: getComputedStyle(document.body).backgroundColor, + elementAnimations: document.body.getAnimations().length, + durations: [...new Set(document.getAnimations() + .filter(animation => animation.effect.pseudoElement === '::view-transition-old(root)' || + animation.effect.pseudoElement === '::view-transition-new(root)') + .map(animation => animation.effect.getTiming().duration))], + })); + assert.equal(previewState.background, 'rgb(101, 67, 33)'); + assert.equal(previewState.elementAnimations, 0); + assert.deepEqual(previewState.durations, [1000]); + assert.equal(await page.$eval('html', element => element.getAttribute('data-bskyplus-theme-active')), 'preview-theme'); + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); + await page.waitForFunction(() => !document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + const cancellationElapsed = await page.evaluate(() => performance.now() - window.previewStartedAt); + assert.ok(cancellationElapsed < 900, `reduced motion cancels the active transition promptly (${cancellationElapsed}ms)`); + await page.evaluate(() => { + const preview = structuredClone(window.library.themes[0]); + preview.id = 'reduced-preview-theme'; + preview.colors.background = '#123456'; + window.dispatchEvent(new CustomEvent('bskyplus:theme-preview', { detail: preview })); + }); + await page.waitForFunction(() => document.documentElement.dataset.bskyplusThemeActive === 'reduced-preview-theme'); + assert.equal(await page.evaluate(() => document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))), false); + assert.equal(await color('body', 'backgroundColor'), 'rgb(18, 52, 86)'); + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'no-preference' }]); + await page.evaluate(() => window.dispatchEvent(new CustomEvent('bskyplus:theme-preview', { detail: null }))); + assert.equal(await color('body', 'backgroundColor'), 'rgb(16, 32, 48)'); + assert.equal(await page.$eval('html', element => element.getAttribute('data-bskyplus-theme-active')), 'test-theme'); + + await page.evaluate(() => { + document.querySelector('#inline').style.backgroundColor = '#F9FAFB'; + const extra = document.createElement('p'); + extra.id = 'dynamic'; extra.style.color = '#EC4899'; document.body.append(extra); + document.styleSheets[0].insertRule('.late { color: #006AFF; }'); + const late = document.createElement('span'); late.className = 'late'; document.body.append(late); + }); + await page.waitForFunction(() => getComputedStyle(document.querySelector('#dynamic')).color === 'rgb(255, 85, 170)'); + assert.equal(await color('#inline', 'backgroundColor'), 'rgb(32, 48, 64)'); + await page.waitForFunction(() => getComputedStyle(document.querySelector('.late')).color === 'rgb(85, 221, 255)'); + + await page.evaluate(() => { + history.replaceState(null, '', '/settings'); + const native = document.createElement('a'); + native.href = '/settings/about'; + const label = document.createElement('span'); + label.dir = 'auto'; label.style.color = '#000000'; label.textContent = 'About'; + native.append(label); document.body.append(native); + }); + await page.addScriptTag({ content: integratedSettings }); + await page.waitForSelector('[data-bskyplus-settings-link]'); + await page.evaluate(() => { + window.library.themes[0].colors.text = '#ABCDEF'; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: window.library } }, 'local')); + }); + await page.waitForFunction(() => getComputedStyle(document.querySelector('[data-bskyplus-settings-label]')).color === 'rgb(171, 205, 239)', { timeout: 2000 }); + assert.equal(await color('[data-bskyplus-settings-icon] path', 'fill'), 'rgb(171, 205, 239)'); + + await page.evaluate(() => { + document.documentElement.className = 'theme--dim'; + document.querySelector('#inline').style.backgroundColor = '#151D28'; + }); + await page.waitForFunction(() => getComputedStyle(document.querySelector('#inline')).backgroundColor === 'rgb(16, 32, 48)'); + await page.evaluate(() => { + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: { ...window.library, activeId: null } } }, 'local')); + }); + await page.waitForFunction(() => !document.documentElement.hasAttribute('data-bskyplus-theme-active')); + assert.equal(await page.$('#bskyplus-theme-style'), null); + assert.equal(await page.$('[data-bskyplus-theme-inline]'), null); + assert.equal(await color('.post', 'backgroundColor'), 'rgb(249, 250, 251)'); + assert.equal(await color('#like', 'fill'), 'rgb(236, 72, 153)'); + assert.equal(await color('#inline', 'backgroundColor'), 'rgb(21, 29, 40)'); + assert.equal(await color('#root', 'backgroundColor'), 'rgb(249, 250, 251)'); + assert.equal(await page.$eval('#root', element => element.getAttribute('style')), ''); + assert.equal(await color('[data-bskyplus-settings-label]', 'color'), 'rgb(0, 0, 0)'); + } finally { await browser.close(); } +}); + +test('runtime applies a persisted built-in theme ID without adding it to the saved library', async () => { + const browser = await puppeteer.launch({ executablePath, headless: true }); + const page = await browser.newPage(); + try { + await page.setContent(''); + await page.evaluate(() => { + const builtinId = 'bluesky-dark'; + window.chrome = { storage: { + local: { get: async () => ({ bskyplusThemes: { schemaVersion: 1, activeId: builtinId, themes: [] } }) }, + onChanged: { addListener: () => {} }, + } }; + }); + await page.addScriptTag({ content: model }); + await page.addScriptTag({ content: runtime }); + await page.waitForFunction(() => document.documentElement.dataset.bskyplusThemeActive === 'bluesky-dark'); + assert.equal(await page.$eval('html', element => element.dataset.bskyplusThemeActive), 'bluesky-dark'); + assert.equal(await page.evaluate(() => BskyPlusThemes.normalizeLibrary({ + schemaVersion: 1, activeId: 'bluesky-dark', themes: [], + }).themes.length), 0); + } finally { await browser.close(); } +}); + +test('popup theme selection animates palette changes after load and preserves runtime fallbacks', async () => { + const browser = await puppeteer.launch({ executablePath, headless: true }); + const page = await browser.newPage(); + try { + await page.goto('https://example.com'); + await page.setContent(''); + await page.evaluate(() => { + window.listeners = []; + window.themeRead = new Promise(resolve => { window.resolveThemeRead = resolve; }); + window.chrome = { storage: { + local: { get: () => window.themeRead }, + onChanged: { addListener: fn => window.listeners.push(fn) }, + } }; + }); + await page.addScriptTag({ content: model }); + await page.addScriptTag({ content: runtime }); + await page.evaluate(() => { + window.resolveThemeRead({ bskyplusThemes: { schemaVersion: 1, activeId: 'bluesky-dark', themes: [] } }); + return new Promise(resolve => setTimeout(resolve, 0)); + }); + await page.waitForFunction(() => document.documentElement.dataset.bskyplusThemeActive === 'bluesky-dark'); + const hasTransition = () => page.evaluate(() => document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + assert.equal(await hasTransition(), false, 'initial storage load applies without an animation'); + + await page.evaluate(() => { + const model = window.BskyPlusThemes; + const samePalette = { ...structuredClone(model.BUILTIN_THEMES['bluesky-dark']), id: 'same-palette', name: 'Same palette' }; + const library = { schemaVersion: 1, activeId: 'bluesky-dark', themes: [samePalette] }; + window.library = library; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: library } }, 'local')); + }); + assert.equal(await hasTransition(), false, 'unrelated library edits do not animate'); + await page.evaluate(() => { + const model = window.BskyPlusThemes; + const samePalette = { ...structuredClone(model.BUILTIN_THEMES['bluesky-dark']), id: 'same-palette', name: 'Same palette' }; + const library = { schemaVersion: 1, activeId: samePalette.id, themes: [samePalette] }; + window.library = library; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: library } }, 'local')); + }); + await page.waitForFunction(() => document.documentElement.dataset.bskyplusThemeActive === 'same-palette'); + assert.equal(await hasTransition(), false, 'selecting a theme with the same palette does not animate'); + + await page.evaluate(() => { + const model = window.BskyPlusThemes; + const popupTheme = { ...structuredClone(model.BUILTIN_THEMES['bluesky-dark']), id: 'popup-theme', name: 'Popup theme' }; + popupTheme.colors.background = '#123456'; + const library = { schemaVersion: 1, activeId: popupTheme.id, themes: [popupTheme] }; + window.library = library; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: library } }, 'local')); + }); + await page.waitForFunction(() => document.documentElement.dataset.bskyplusThemeActive === 'popup-theme'); + await page.waitForFunction(() => document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + const popupDurations = await page.evaluate(() => [...new Set(document.getAnimations() + .filter(animation => animation.effect.pseudoElement === '::view-transition-old(root)' || + animation.effect.pseudoElement === '::view-transition-new(root)') + .map(animation => animation.effect.getTiming().duration))]); + assert.deepEqual(popupDurations, [1000]); + await page.waitForFunction(() => !document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + + await page.evaluate(() => { + const library = { ...window.library, activeId: null }; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: library } }, 'local')); + }); + await page.waitForFunction(() => !document.documentElement.hasAttribute('data-bskyplus-theme-active')); + await page.waitForFunction(() => document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + await page.waitForFunction(() => !document.getAnimations().some(animation => + animation.effect.pseudoElement?.startsWith('::view-transition'))); + + await page.evaluate(() => { + Object.defineProperty(document, 'startViewTransition', { configurable: true, value: undefined }); + const popupTheme = { ...structuredClone(window.BskyPlusThemes.BUILTIN_THEMES['bluesky-dark']), id: 'fallback-theme' }; + const library = { schemaVersion: 1, activeId: popupTheme.id, themes: [popupTheme] }; + window.library = library; + window.listeners.forEach(fn => fn({ bskyplusThemes: { newValue: library } }, 'local')); + }); + await page.waitForFunction(() => document.documentElement.dataset.bskyplusThemeActive === 'fallback-theme'); + assert.equal(await hasTransition(), false, 'selection still applies when View Transitions are unavailable'); + } finally { await browser.close(); } +}); diff --git a/test/test_thread_unroll.mjs b/test/test_thread_unroll.mjs new file mode 100644 index 0000000..807201a --- /dev/null +++ b/test/test_thread_unroll.mjs @@ -0,0 +1,232 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import puppeteer from 'puppeteer-core'; + +// Mirrors the anchor/linear post structure in Bluesky's PostThread components. +const profile = '/profile/alice.test'; +const icon = ''; +const controls = n => `
+
+
+
+
+
`; +const avatar = n => `Alice`; +// Current thread RichText has data-word-wrap, but no postText test ID. +const text = (n, total) => `
Part ${n}: a longer paragraph of the thread, with a link inside the text. ${n}/${total}
`; +const media = n => `
Thread attachment
`; +function child(n, total = 3, author = 'alice.test') { + return `
+
${avatar(n)}
+ +
${text(n, total)}
${media(n)}${controls(n)}
`; +} +// Use div for PostHider's outer linked RN-web view, avoiding invalid nested parsing. +const childMarkup = (...args) => child(...args).replace('