Skip to content

feat(ui): premium app features — installability, WCO, share, badging#2982

Open
tomayac wants to merge 25 commits into
npmx-dev:mainfrom
tomayac:pwa-and-premium-app-features
Open

feat(ui): premium app features — installability, WCO, share, badging#2982
tomayac wants to merge 25 commits into
npmx-dev:mainfrom
tomayac:pwa-and-premium-app-features

Conversation

@tomayac

@tomayac tomayac commented Jul 1, 2026

Copy link
Copy Markdown

Summary

  • Enable PWA installability (@vite-pwa/nuxt) with a manifest id and generated install-UI screenshots.
  • Add Web Share API support on package pages and a Window Controls Overlay + app shortcuts layout.
  • Add the App Badging API to surface new-likes counts on the app icon.
  • Replace the custom toggle component with the native <input type="checkbox" switch> polyfill, kept live-synced with the accent color/theme.
  • Keep theme-color in sync with --bg so the WCO title-bar strip matches the header.

Test plan

  • pnpm test:unit — 1652/1652 passing
  • pnpm run test:types — passing (fixed 2 pre-existing type errors in the switch polyfill plugin)
  • vp lint — 0 errors (3 pre-existing style warnings, unrelated to correctness)
  • pnpm run build — succeeds

tomayac added 10 commits June 18, 2026 19:34
Enable the service worker (previously disabled), configure Workbox with
network-only navigation fallback to preserve ISR freshness, and add
standalone display mode to the manifest so browsers offer installation.

Add a PwaPrompt component that surfaces a toast when a new SW version is
waiting, and an install button in settings that appears only when the
browser fires beforeinstallprompt.
Adds a Share button to the package header button group (alongside Compare
and Likes) that invokes navigator.share with the package name, description,
and current URL. The button only renders when the browser supports the Web
Share API and is also registered as a command palette action.
Enable display_override: window-controls-overlay so the app header fills
the installed-PWA title bar. The header background becomes the drag handle;
the nav is explicitly non-draggable. env(titlebar-area-y/x) push content
below the OS controls and clear the macOS traffic lights.

Add three manifest shortcuts (Search, Compare, Settings) so users can
jump straight to key destinations from the home-screen/taskbar icon.
Use input-switch-polyfill so Safari renders a native OS switch control
and other browsers get a consistent polyfilled fallback. The elaborate
hand-painted CSS is replaced by a single accent-color custom property
that integrates with the user's chosen accent. Polyfill JS is loaded
lazily only when 'switch' is absent from HTMLInputElement.prototype.
useAppBadge wraps navigator.setAppBadge/clearAppBadge with feature
detection. useLikesBadge activates when the npm connector is connected:
it loads the user's packages once, then polls /api/social/likes/:pkg
every 10 minutes and sets the badge to the count of new likes received
since the previous check. The baseline is persisted in localStorage per
npm username so the badge never fires on the very first visit.
Adds a Playwright-based script (scripts/generate-pwa-screenshots.ts)
that captures the homepage and a package page in light + dark mode at
desktop (1280×800) and mobile (390×844) viewports.

The script auto-starts nuxt preview, takes all 8 screenshots, then
stops the server. Pass --url <url> to skip the local server and
screenshot any running instance (useful in CI: --url https://npmx.dev).

The PWA manifest now includes a screenshots[] array (nuxt.config.ts)
with form_factor 'wide'/'narrow' entries, enabling Chrome's richer
install dialog on desktop (Chrome 108+) and Android (Chrome 94+).

Usage:
  pnpm build && pnpm generate:screenshots
  # commit public/screenshots/*.png
Add id: '/' to the web app manifest for stable PWA identity across
manifest URL changes (spec-recommended practice).

Generate all 8 PWA screenshots (desktop/mobile × dark/light × home/package)
and commit them so Vercel CI picks them up. Chrome 108+ on desktop will
show these in the richer install dialog carousel.
Window Controls Overlay:
- Fix header and scroll container being 15 px short of right edge by
  resetting scrollbar-gutter to auto in WCO mode (html { scrollbar-gutter:
  stable } reserved a gutter even when overflow: hidden removed the bar)
- Make header position:fixed and span full viewport width (inset-inline: 0)
  in WCO mode so its border-bottom reaches the window edge
- Add #app-scroll fixed scroll container starting at the header's bottom
  border so the scrollbar track never appears in the title bar
- Solid opaque header background (--bg) and no backdrop-filter in WCO mode
- Full-width nav with two-value padding-inline to handle both macOS traffic
  lights (left) and Windows min/max/close (right)
- All interactive descendants declare no-drag; empty header space is drag
- Keep theme-color meta in sync with --bg (oklch) by writing directly to the
  DOM node and guarding against @unhead re-asserting the PWA module's static
  value after onMounted

Share button:
- Register 'v' keyboard shortcut; programmatic click anchors the share sheet
  at the button position instead of the mouse cursor
- Include og:image as a shareable file when the browser supports file sharing
- Expose click() on ButtonBase via defineExpose for programmatic triggering

input-switch-polyfill:
- Bump to 1.12.0
- Add MutationObserver that re-syncs --switch-accent on every color-mode or
  accent-color change so switches react live without a page reload
Adds an ambient module declaration for the untyped input-switch-polyfill
package and fixes a noUncheckedIndexedAccess violation when reading the
first matched switch element.
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Jul 4, 2026 5:17pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Jul 4, 2026 5:17pm
npmx-lunaria Ignored Ignored Jul 4, 2026 5:17pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds PWA install/update support and manifest updates, package sharing, badge handling, window-controls-overlay shell behaviour, and switch polyfill support, with matching locale, schema, dependency, and screenshot tooling changes.

Changes

PWA, sharing, badges, and shell support

Layer / File(s) Summary
PWA config, manifest, and screenshots
nuxt.config.ts, scripts/generate-pwa-screenshots.ts, package.json, app/pages/settings.vue, app/components/PwaPrompt.client.vue, i18n/locales/en.json, i18n/schema.json, test/unit/a11y-component-coverage.spec.ts
Enables service-worker generation with workbox config, expands the manifest with install metadata, shortcuts, and screenshots, adds a screenshot-generation script, an update/install prompt component, a settings install section, corresponding locale/schema strings, and a11y test allowlist entries.
Package sharing UI and command wiring
app/components/Button/Base.vue, app/components/Package/ShareButton.client.vue, app/components/Package/Header.vue, i18n/locales/en.json, i18n/schema.json
Adds a Web Share API-gated share button with OG image attachment, exposes a click method on the base button, wires a package-share command into the header, renders the share button in package metrics, and adds share locale/schema strings.
App badge and likes polling
app/composables/useAppBadge.ts, app/composables/useLikesBadge.ts, app/plugins/likes-badge.client.ts
Adds an app badge composable and a likes-polling composable with localStorage persistence and interval-based refresh, initialised via a client plugin.
Window controls overlay shell and header
app/app.vue, app/components/AppHeader.vue
Synchronises the theme-colour meta tag with the --bg CSS variable, restructures the layout with a dedicated scroll container, and applies fixed-position header/scroll styling for window-controls-overlay display mode.
Switch polyfill and toggle styling
app/plugins/input-switch-polyfill.client.ts, nuxt.config.ts, package.json, app/components/Settings/Toggle.client.vue, pnpm-workspace.yaml
Adds a client plugin that conditionally loads a switch input polyfill and synchronises accent colour, registers the polyfill stylesheet/dependency, pins Vue, and simplifies the settings toggle input styling.

Sequence Diagram(s)

sequenceDiagram
  participant ServiceWorker
  participant PwaPrompt
  participant SettingsPage
  participant User

  ServiceWorker->>PwaPrompt: needRefresh
  PwaPrompt->>User: show update toast
  User->>PwaPrompt: updateServiceWorker()
  User->>SettingsPage: open install section
  SettingsPage->>User: show install button
  User->>SettingsPage: $pwa.install()
Loading
sequenceDiagram
  participant User
  participant PackageHeader
  participant ShareButton
  participant Navigator

  User->>PackageHeader: choose share command or button
  PackageHeader->>ShareButton: trigger sharePackage()
  ShareButton->>ShareButton: read og:image and build ShareData
  ShareButton->>Navigator: navigator.share(data)
  Navigator-->>User: native share sheet
Loading
sequenceDiagram
  participant LikesBadgePlugin
  participant useLikesBadge
  participant API
  participant LocalStorage
  participant useAppBadge

  LikesBadgePlugin->>useLikesBadge: initialise
  useLikesBadge->>API: list likes for cached packages
  API-->>useLikesBadge: counts
  useLikesBadge->>LocalStorage: loadStored / saveStored
  useLikesBadge->>useAppBadge: setBadge() or clearBadge()
Loading
sequenceDiagram
  participant BrowserChrome
  participant AppShell
  participant AppHeader
  participant AppScroll

  BrowserChrome->>AppShell: display-mode: window-controls-overlay
  AppShell->>AppHeader: fixed top header styles
  AppShell->>AppScroll: fixed internal scroll region
  AppShell->>BrowserChrome: theme-color meta updated from --bg
Loading

Suggested reviewers: danielroe, ghostdevv

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately highlights the main additions: installability, Window Controls Overlay, sharing, and app badging.
Description check ✅ Passed The description matches the changeset and summarises the PWA, share, WCO, badging, toggle polyfill, and theme-colour updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Hello! Thank you for opening your first PR to npmx, @tomayac! 🚀

Here’s what will happen next:

  1. Our GitHub bots will run to check your changes.
    If they spot any issues you will see some error messages on this PR.
    Don’t hesitate to ask any questions if you’re not sure what these mean!

  2. In a few minutes, you’ll be able to see a preview of your changes on Vercel

  3. One or more of our maintainers will take a look and may ask you to make changes.
    We try to be responsive, but don’t worry if this takes a few days.

@github-actions

This comment was marked as resolved.

@socket-security

This comment was marked as resolved.

@socket-security

This comment was marked as resolved.

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
app/composables/useLikesBadge.ts (1)

54-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Non-null assertion on array index bypasses the strict index-safety guideline.

userPackages.value[i]! at Line 58 uses a non-null assertion instead of checking the value before use. As per coding guidelines, "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index." Prefer destructuring the package name alongside the result to avoid re-indexing.

♻️ Proposed fix
     const current: Record<string, number> = {}
-    for (let i = 0; i < userPackages.value.length; i++) {
-      const r = results[i]
-      if (r?.status === 'fulfilled') {
-        current[userPackages.value[i]!] = r.value.totalLikes
-      }
-    }
+    userPackages.value.forEach((pkg, i) => {
+      const r = results[i]
+      if (r?.status === 'fulfilled') {
+        current[pkg] = r.value.totalLikes
+      }
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/composables/useLikesBadge.ts` around lines 54 - 60, The loop in
useLikesBadge is re-indexing userPackages.value and using a non-null assertion
on the package name, which bypasses strict index safety. Update the logic in the
current/results processing to avoid userPackages.value[i]! by destructuring or
otherwise carrying the package name together with the corresponding promise
result before awaiting, then use that safe value when populating current.

Source: Coding guidelines

app/app.vue (1)

248-280: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Header-height magic number duplicated across files.

calc(env(titlebar-area-y, 0px) + 3.5rem + 1px) (Line 275) must stay in sync with AppHeader.vue's nav height (min-h-14 = 3.5rem) plus its border-bottom. Any future change to the header's height in AppHeader.vue will silently desync this offset, causing the scrollbar/content to start at the wrong position in WCO mode.

Consider exposing the header height as a shared CSS custom property (e.g. set on :root from AppHeader.vue, consumed here) to keep the two files in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/app.vue` around lines 248 - 280, The WCO scroll offset in App.vue is
duplicating AppHeader.vue’s header height, so update the fixed `#app-scroll`
positioning to derive its top offset from a shared CSS custom property instead
of hardcoding 3.5rem + 1px. Set that custom property from AppHeader.vue based on
the nav’s actual height and border, then consume it in the `@media` (display-mode:
window-controls-overlay) block so both files stay in sync if the header changes.
app/components/AppHeader.vue (1)

260-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Interactive-element selector may miss some focusable/clickable elements.

The :is(a, button, input, select, [role='button'], [role='combobox']) list doesn't cover textarea, [role='menuitem']/[role='tab'], or arbitrary elements with a tabindex and click handler that some components (e.g. LogoContextMenu) might render. Any such element left with inherited drag would swallow clicks under WCO.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/AppHeader.vue` around lines 260 - 263, The interactive-element
selector in AppHeader’s header drag override is too narrow and can leave
focusable/clickable elements inheriting drag behavior. Update the selector in
the `header :deep(:is(...))` rule to also cover missing interactive cases such
as `textarea`, `[role='menuitem']`, `[role='tab']`, and generic
tabbable/clickable elements (for example those with `tabindex`), so components
like `LogoContextMenu` don’t have clicks swallowed under WCO.
test/unit/a11y-component-coverage.spec.ts (1)

64-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Please test PwaPrompt.client.vue instead of skipping it.

This component does not need a real service worker to render; a vitest mount can stub useNuxtApp().$pwa.needRefresh and exercise the new alert and action buttons. Keeping it on the skip list leaves the update prompt without any accessibility coverage. As per coding guidelines, **/*.{test,spec}.{ts,tsx}: "Write unit tests for core functionality using vitest".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/a11y-component-coverage.spec.ts` at line 64, The a11y coverage list
is incorrectly skipping PwaPrompt.client.vue, so it never gets tested. Remove
the skip entry from the coverage spec and add a vitest mount-based test for
PwaPrompt.client.vue that stubs useNuxtApp().$pwa.needRefresh to render the
prompt and verify the alert plus action buttons. Locate the change in the
a11y-component-coverage.spec.ts entry for PwaPrompt.client.vue and update the
related component test coverage accordingly.

Source: Coding guidelines

app/components/Package/Header.vue (1)

81-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share through one code path.

This duplicates the payload building in app/components/Package/ShareButton.client.vue but omits the optional og:image file, so the command-palette action already behaves differently from the visible share button. Please extract a shared helper/composable and reuse it from both entry points.

Also applies to: 114-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/Package/Header.vue` around lines 81 - 87, The package sharing
logic is duplicated between Header.vue’s sharePackage and
ShareButton.client.vue, and the Header path currently omits the optional
og:image file, causing behavior drift. Extract the payload-building and
navigator.share invocation into a shared helper/composable, then reuse it from
both share entry points so both paths include the same fields and fallbacks,
including the og:image file when available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/Package/ShareButton.client.vue`:
- Around line 45-50: The share flow in ShareButton.client.vue is setting
shareData.files after getOgImageFile() returns, but the real payload is not
being validated before navigator.share is called. Update the ShareButton logic
around the shareData object so the final ShareData shape is checked with files
included before mutating shareData, using getOgImageFile() and the
navigator.share call as the key points to adjust. Keep the URL/text fallback
available by only attaching the file once the full payload is confirmed valid.

In `@app/composables/useLikesBadge.ts`:
- Around line 25-43: The `useLikesBadge` logic has a race between the `npmUser`
refresh watcher and the immediate `checkLikes()` watcher, which can persist a
stale package baseline for a new user. Update `useLikesBadge` so `checkLikes()`
only runs after the package list refresh for the current `npmUser` has
completed, either by sharing the refresh path in the `npmUser` watcher or by
introducing a readiness guard that blocks the immediate watcher until
`userPackages` is current. Make the fix around the `watch(npmUser, ...)` block
and the `checkLikes()` watcher so the first poll always uses the new user’s
package list.

In `@scripts/generate-pwa-screenshots.ts`:
- Around line 42-44: The `generate-pwa-screenshots.ts` argument parsing for
`--url` does not validate that a value follows the flag before reading
`args[urlFlagIdx + 1]`. Update the `explicitUrl` handling to first confirm
`--url` is present and that the next array entry exists and is a valid non-empty
string; if it is missing, fail fast with a clear error instead of falling back
to the preview-server path. Keep the fix localized to the
`args`/`urlFlagIdx`/`explicitUrl` logic so the script’s existing flow remains
unchanged.
- Around line 96-109: The startup flow in startPreviewServer currently leaves
the spawned nuxt preview running if waitForServer(url) fails, and
server.on('error') throws outside main().catch(). Change the preview startup to
reject/propagate errors through the Promise returned by startPreviewServer, and
make sure the child process is terminated/cleaned up before rejecting or
rethrowing. Use the existing startPreviewServer, waitForServer, and server event
handlers as the main places to fix this.

---

Nitpick comments:
In `@app/app.vue`:
- Around line 248-280: The WCO scroll offset in App.vue is duplicating
AppHeader.vue’s header height, so update the fixed `#app-scroll` positioning to
derive its top offset from a shared CSS custom property instead of hardcoding
3.5rem + 1px. Set that custom property from AppHeader.vue based on the nav’s
actual height and border, then consume it in the `@media` (display-mode:
window-controls-overlay) block so both files stay in sync if the header changes.

In `@app/components/AppHeader.vue`:
- Around line 260-263: The interactive-element selector in AppHeader’s header
drag override is too narrow and can leave focusable/clickable elements
inheriting drag behavior. Update the selector in the `header :deep(:is(...))`
rule to also cover missing interactive cases such as `textarea`,
`[role='menuitem']`, `[role='tab']`, and generic tabbable/clickable elements
(for example those with `tabindex`), so components like `LogoContextMenu` don’t
have clicks swallowed under WCO.

In `@app/components/Package/Header.vue`:
- Around line 81-87: The package sharing logic is duplicated between
Header.vue’s sharePackage and ShareButton.client.vue, and the Header path
currently omits the optional og:image file, causing behavior drift. Extract the
payload-building and navigator.share invocation into a shared helper/composable,
then reuse it from both share entry points so both paths include the same fields
and fallbacks, including the og:image file when available.

In `@app/composables/useLikesBadge.ts`:
- Around line 54-60: The loop in useLikesBadge is re-indexing userPackages.value
and using a non-null assertion on the package name, which bypasses strict index
safety. Update the logic in the current/results processing to avoid
userPackages.value[i]! by destructuring or otherwise carrying the package name
together with the corresponding promise result before awaiting, then use that
safe value when populating current.

In `@test/unit/a11y-component-coverage.spec.ts`:
- Line 64: The a11y coverage list is incorrectly skipping PwaPrompt.client.vue,
so it never gets tested. Remove the skip entry from the coverage spec and add a
vitest mount-based test for PwaPrompt.client.vue that stubs
useNuxtApp().$pwa.needRefresh to render the prompt and verify the alert plus
action buttons. Locate the change in the a11y-component-coverage.spec.ts entry
for PwaPrompt.client.vue and update the related component test coverage
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc5c172b-099f-4e84-8a6c-497d259c7474

📥 Commits

Reviewing files that changed from the base of the PR and between 5efdb53 and 7e682b9.

⛔ Files ignored due to path filters (9)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/screenshots/desktop-dark-home.png is excluded by !**/*.png
  • public/screenshots/desktop-dark-package.png is excluded by !**/*.png
  • public/screenshots/desktop-light-home.png is excluded by !**/*.png
  • public/screenshots/desktop-light-package.png is excluded by !**/*.png
  • public/screenshots/mobile-dark-home.png is excluded by !**/*.png
  • public/screenshots/mobile-dark-package.png is excluded by !**/*.png
  • public/screenshots/mobile-light-home.png is excluded by !**/*.png
  • public/screenshots/mobile-light-package.png is excluded by !**/*.png
📒 Files selected for processing (18)
  • app/app.vue
  • app/components/AppHeader.vue
  • app/components/Button/Base.vue
  • app/components/Package/Header.vue
  • app/components/Package/ShareButton.client.vue
  • app/components/PwaPrompt.client.vue
  • app/components/Settings/Toggle.client.vue
  • app/composables/useAppBadge.ts
  • app/composables/useLikesBadge.ts
  • app/pages/settings.vue
  • app/plugins/input-switch-polyfill.client.ts
  • app/plugins/likes-badge.client.ts
  • app/types/input-switch-polyfill.d.ts
  • i18n/locales/en.json
  • nuxt.config.ts
  • package.json
  • scripts/generate-pwa-screenshots.ts
  • test/unit/a11y-component-coverage.spec.ts

Comment thread app/components/Package/ShareButton.client.vue Outdated
Comment thread app/composables/useLikesBadge.ts Outdated
Comment thread scripts/generate-pwa-screenshots.ts
Comment thread scripts/generate-pwa-screenshots.ts
@github-actions

This comment was marked as resolved.

tomayac added 2 commits July 1, 2026 16:57
navigator.canShare() was only checked against a dummy file to test
generic files-sharing support, not against the actual title/text/url/files
combination. Some implementations support sharing files or url/text but
not both together, so attaching the file unconditionally could make
navigator.share() reject silently. Now the file is only attached once
canShare() confirms the full payload is shareable.
Previously a missing value after --url silently fell through to the
preview-server path instead of erroring, which is confusing when the
flag was clearly intended to be used.

@gameroman gameroman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs a discussion first

@gameroman gameroman added the needs discussion An idea that needs more discussion to understand the scope and impact. label Jul 1, 2026
@tomayac

tomayac commented Jul 1, 2026

Copy link
Copy Markdown
Author

This probably needs a discussion first

(I was chatting about this with @danielroe.)

@gameroman gameroman requested a review from danielroe July 1, 2026 15:05
@gameroman gameroman added the front Frontend, Design label Jul 1, 2026
@tomayac tomayac changed the title feat(pwa): premium app features — installability, WCO, share, badging feat(ui): premium app features — installability, WCO, share, badging Jul 1, 2026
- Drop the standalone input-switch-polyfill.d.ts ambient module file
  (knip flagged it as an unused file) in favor of a targeted
  @ts-expect-error at the one dynamic-import call site.
- Regenerate i18n/schema.json to include the new pwa.*, settings.app,
  package.links.share and package.share_aria_label keys.
- Pin vue to 3.5.39 in pnpm-workspace.yaml overrides; the regenerated
  lockfile had resolved two separate vue versions (3.5.34 and 3.5.39).
@tomayac

tomayac commented Jul 1, 2026

Copy link
Copy Markdown
Author

Installed PWA with Window Controls Overlay enabled

Screenshot 2026-07-01 at 18 38 42

Installed PWA with Window Controls Overlay disabled

Screenshot 2026-07-01 at 18 40 12

Share button for packages

Screenshot 2026-07-01 at 18 38 58

App icon shortcuts

Screenshot 2026-07-01 at 18 40 01

Install prompt with screenshots

Screenshot 2026-07-01 at 18 41 30

Accent colors for switches

(Uses native switch on Safari with support for dragging the toggle, polyfill on other browsers)

Screenshot 2026-07-01 at 18 44 51

Package sub-headers use sticky top-14 to clear the fixed <header> when
the viewport is the scroll container. In WCO mode #app-scroll already
starts below the header, so that offset left a redundant 3.5rem gap
between the title bar and the sticky sub-header instead of gluing it
to the header.
@vite-pwa/nuxt's client plugin only registers a beforeinstallprompt
listener when pwa.client.installPrompt is truthy; it defaults to
false. Without it, showInstallPrompt never flips to true and the
Settings "Install app" button never appears, even though the browser
fires the event normally.

@ghostdevv ghostdevv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple distinct, but related, changes in this PR - could you split them up into multiple PRs?

Comment thread app/components/Button/Base.vue Outdated

defineExpose({
focus: () => el.value?.focus(),
click: () => el.value?.click(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc vue would auto expose this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against Vue's own source (@vue/runtime-core, getComponentPublicInstance): once a component calls defineExpose(), the proxy a parent's template ref sees only contains what's explicitly listed, plus Vue's $-prefixed magic properties ($el, $props, $emit, etc., from publicPropertiesMap) — there's no fallback to arbitrary DOM methods like .click().

// @vue/runtime-core
function getComponentPublicInstance(instance) {
  if (instance.exposed) {
    return instance.exposeProxy || (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
      get(target, key) {
        if (key in target) return target[key]
        else if (key in publicPropertiesMap) return publicPropertiesMap[key](instance)
      },
      ...

So without this line, buttonRef.value?.click() in ShareButton.client.vue (used for the v keyboard shortcut, so the share sheet anchors at the button rather than the cursor) would just be undefined. It's consistent with the existing focus/getBoundingClientRect entries above it, which already manually forward DOM methods the same way rather than relying on $el — keeping click explicit here matches that pattern. Happy to switch to buttonRef.value?.$el?.click() at the call site instead if you'd prefer avoiding the extra expose entry, though that trades one explicit line for a slightly fuzzier $el cast.

Comment thread pnpm-workspace.yaml
sharp: 0.34.5
vite: npm:@voidzero-dev/vite-plus-core@0.1.20
vitest: npm:@voidzero-dev/vite-plus-test@0.1.20
vue: 3.5.39

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving a pnpm-lock.yaml merge conflict caused pnpm to resolve two different vue versions (3.5.34 via @nuxt/test-utils, 3.5.39 elsewhere), despite dedupePeers already being set. Two Vue instances in one app is a real hazard since reactivity tracking is per-instance. Pinning forces a single resolved copy.

Comment thread scripts/generate-pwa-screenshots.ts Outdated
Comment thread scripts/generate-pwa-screenshots.ts Outdated
@@ -0,0 +1,26 @@
export default defineNuxtPlugin(async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have switches styled with role="switch" - what's the benefit of polyfiling this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason to use the switch attribute with the polyfill is that on WebKit you get the real switch control (and the polyfill isn't even loaded), and the polyfill supports all the features the real control supports, like the accent color and also dragging the thumb to change the state of the toggle, which CSS-based solutions don't support.

Drops the main() wrapper in favor of top-level await now that the
project runs as ESM, per review feedback.
Not a convention used elsewhere in the repo.
…ate PRs

Per review feedback, keeps this PR focused on PWA installability, WCO,
and app badging. The Share button and native switch polyfill now live
in npmx-dev#2997 and npmx-dev#2998 respectively.
@tomayac

tomayac commented Jul 4, 2026

Copy link
Copy Markdown
Author

Per the review feedback about scope, I've split this PR:

This PR is now focused on PWA installability, WCO, and app badging.

app/plugins/likes-badge.client.ts registered useLikesBadge() as a
global Nuxt plugin, which runs for every mountSuspended() call in
component tests — including standalone tests that mock useConnector
for just the component under test. That caused useLikesBadge()'s own
useConnector() call to resolve the real, createSharedComposable-backed
implementation instead of the mock, which HeaderConnectorModal.spec.ts
and use-command-palette-commands.spec.ts's assertions depend on.

Moving the call into app.vue keeps it always active in the real app
(app.vue is always mounted there) while no longer running during tests
that mount an unrelated component in isolation.
Adds settings.connector.showLikesBadge (default on), surfaced as a
toggle in the connector modal's connected-state preferences — so it's
only shown once connected, alongside the existing auto-open-url
toggle. useLikesBadge() now skips fetching, badging, and polling
entirely while the setting is off, clearing any existing badge
immediately when toggled off.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

front Frontend, Design needs discussion An idea that needs more discussion to understand the scope and impact.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants