diff --git a/.changeset/notification-initial.md b/.changeset/notification-initial.md new file mode 100644 index 000000000..d3d6c1eda --- /dev/null +++ b/.changeset/notification-initial.md @@ -0,0 +1,14 @@ +--- +"@solid-primitives/notification": minor +--- + +Add `@solid-primitives/notification` package (Stage 0) + +New primitives for the browser [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API). + +- **`isNotificationSupported()`** — SSR-safe runtime check for Notifications API availability. +- **`makeNotification(title, options?)`** — Non-reactive helper returning `[show, close]`. `show()` creates and returns a `Notification` instance (or `null` when permission is not `"granted"`); calling it again replaces the previous notification. No Solid lifecycle dependency. +- **`createNotification(title, options?)`** — Reactive primitive returning `{ show, close, notification, supported }`. Accepts reactive accessors for `title` and `options` — their current values are read at `show()` time. The `notification` accessor tracks the live instance, updating to `null` when it is dismissed by the OS or closed programmatically. Cleans up automatically on owner disposal. +- **`createNotificationPermission()`** — Reactive permission manager returning `{ permission, requestPermission }`. The `permission` accessor reflects `Notification.permission` and updates after each `requestPermission()` call. Degrades gracefully to `"denied"` on the server. + +Peer dependencies: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10`. diff --git a/.changeset/sweet-olives-talk.md b/.changeset/sweet-olives-talk.md new file mode 100644 index 000000000..a2c30d8f5 --- /dev/null +++ b/.changeset/sweet-olives-talk.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/permission": major +--- + +updated to Solid-2.0 diff --git a/packages/notification/LICENSE b/packages/notification/LICENSE new file mode 100644 index 000000000..38b41d975 --- /dev/null +++ b/packages/notification/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Solid Primitives Working Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/notification/README.md b/packages/notification/README.md new file mode 100644 index 000000000..d8ef38d4d --- /dev/null +++ b/packages/notification/README.md @@ -0,0 +1,186 @@ +

+ Solid Primitives notification +

+ +# @solid-primitives/notification + +[![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/notification?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/notification) +[![version](https://img.shields.io/npm/v/@solid-primitives/notification?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/notification) +[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) + +Primitives for the browser [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) with reactive permission management. + +- **`isNotificationSupported`** — SSR-safe check for Notifications API availability. +- **`makeNotification`** — Non-reactive helper returning `[show, close]`. No Solid lifecycle dependency. +- **`createNotification`** — Reactive primitive that tracks the live `Notification` instance and cleans up on owner disposal. +- **`createNotificationPermission`** — Reactive permission manager that exposes a live permission signal and a `requestPermission` function. + +## Installation + +```bash +npm install @solid-primitives/notification +# or +yarn add @solid-primitives/notification +# or +pnpm add @solid-primitives/notification +``` + +## How to use it + +### `isNotificationSupported` + +Returns `true` when the Notifications API is available. Always `false` on the server. + +```ts +import { isNotificationSupported } from "@solid-primitives/notification"; + +if (isNotificationSupported()) { + console.log("notifications available"); +} +``` + +--- + +### `makeNotification` + +Non-reactive helper with no Solid lifecycle dependency. Both returned functions are no-ops when the API is unavailable. + +`show()` returns `null` when `Notification.permission` is not `"granted"` — use `createNotificationPermission` to request permission first. + +Because `makeNotification` has no reactive owner, **cleanup is the caller's responsibility**. Inside a reactive scope, register `close` with `onCleanup`: + +```ts +import { onCleanup } from "solid-js"; +import { makeNotification } from "@solid-primitives/notification"; + +const [show, close] = makeNotification("New message", { body: "Hello!" }); + +// Register cleanup with the current reactive owner +onCleanup(close); + +button.addEventListener("click", () => show()); + +// Or close programmatically at any time +close(); +``` + +Outside a reactive scope (e.g. in plain event handlers), call `close()` directly when done. + +--- + +### `createNotification` + +Reactive primitive tied to the current reactive owner. + +- `title` and `options` can be plain values **or** reactive accessors — their current values are read each time `show()` is called. +- `notification` is a reactive `Accessor` that reflects the live instance, updating to `null` when the notification is dismissed (either programmatically or by the OS). +- The notification is automatically closed when the reactive owner is disposed. +- Pass an optional `handlers` object to respond to notification events. + +```ts +import { createNotification } from "@solid-primitives/notification"; + +const { show, close, notification, supported } = createNotification( + () => `You have ${unread()} messages`, + { icon: "/icon.png" }, + { + onClick: n => { window.focus(); }, + onClose: n => { console.log("dismissed"); }, + onError: n => { console.error("notification failed"); }, + }, +); + +// Show a notification (reads reactive title at call time) +show(); + +// React to visibility changes +createEffect(() => { + if (notification()) console.log("notification visible"); + else console.log("notification gone"); +}); + +// Close programmatically +close(); +``` + +--- + +### `createNotificationPermission` + +Reactive permission manager built on the browser [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API). + +The `permission` accessor reflects the **live** permission state and updates automatically whenever it changes — including after `requestPermission()` resolves or the user edits their browser settings directly. + +Permission values follow Permissions API vocabulary: `"granted"`, `"denied"`, `"prompt"` (not yet asked), or `"unknown"` while the initial async query is still resolving. Note that the Notifications API uses `"default"` for the same concept that the Permissions API calls `"prompt"`. + +On the server or when the API is unavailable, `permission` always returns `"unknown"` and `requestPermission` resolves immediately to `"denied"`. + +```ts +import { createNotificationPermission } from "@solid-primitives/notification"; + +const { permission, requestPermission } = createNotificationPermission(); + +// Gate UI on permission state + + + + +// Await the result (returns the raw NotificationPermission value) +const result = await requestPermission(); +// result: "granted" | "denied" | "default" +``` + +--- + +### Full example + +```tsx +import { + createNotification, + createNotificationPermission, + isNotificationSupported, +} from "@solid-primitives/notification"; + +const NotificationDemo: Component = () => { + const { permission, requestPermission } = createNotificationPermission(); + const { show, close, notification } = createNotification( + "Solid Primitives", + { body: "Hello from SolidJS!" }, + { onClick: () => window.focus() }, + ); + + return ( + Not supported

}> +

Permission: {permission()}

+

Active: {notification() ? "yes" : "no"}

+ + + + + +
+ ); +}; +``` + +## Types + +```ts +/** Event handler callbacks for `createNotification`. */ +type NotificationEventHandlers = { + /** Called when the user clicks the notification. */ + onClick?: (notification: Notification) => void; + /** Called when the notification is dismissed, whether by the user, the OS, or `close()`. */ + onClose?: (notification: Notification) => void; + /** Called when the notification fails to display. */ + onError?: (notification: Notification) => void; +}; +``` + +## Browser Support + +The [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility) is supported in all modern browsers. It is not available in iOS Safari (as of 2025) or on the server. All primitives degrade gracefully — `show()` returns `null`, `close()` is a no-op, and `permission()` returns `"unknown"`. + +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/notification/dev/index.tsx b/packages/notification/dev/index.tsx new file mode 100644 index 000000000..1aeecbefa --- /dev/null +++ b/packages/notification/dev/index.tsx @@ -0,0 +1,62 @@ +import { type Component, createSignal, Show } from "solid-js"; +import { + isNotificationSupported, + createNotification, + createNotificationPermission, +} from "../src/index.js"; + +const App: Component = () => { + const supported = isNotificationSupported(); + const [body, setBody] = createSignal("Hello from Solid Primitives!"); + const { permission, requestPermission } = createNotificationPermission(); + const { show, close, notification } = createNotification( + () => "Solid Primitives Notification", + () => ({ body: body() }), + ); + + return ( +
+
+

Notification Primitive

+ + Notifications API is not supported in this browser.

} + > +

+ Permission: {permission()} +

+

+ Active notification: {notification() ? "visible" : "none"} +

+ + + +
+ + + + + +
+
+
+
+ ); +}; + +export default App; diff --git a/packages/notification/package.json b/packages/notification/package.json new file mode 100644 index 000000000..a8f18e92e --- /dev/null +++ b/packages/notification/package.json @@ -0,0 +1,70 @@ +{ + "name": "@solid-primitives/notification", + "version": "0.0.100", + "description": "Primitives for the browser Notifications API with reactive permission management", + "author": "David Di Biase ", + "contributors": [], + "license": "MIT", + "homepage": "https://primitives.solidjs.community/package/notification", + "repository": { + "type": "git", + "url": "git+https://github.com/solidjs-community/solid-primitives.git" + }, + "bugs": { + "url": "https://github.com/solidjs-community/solid-primitives/issues" + }, + "primitive": { + "name": "notification", + "stage": 0, + "list": [ + "isNotificationSupported", + "makeNotification", + "createNotification", + "createNotificationPermission" + ], + "category": "Browser APIs" + }, + "keywords": [ + "solid", + "notification", + "browser", + "permission", + "primitives" + ], + "private": false, + "sideEffects": false, + "files": [ + "dist" + ], + "type": "module", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "browser": {}, + "exports": { + "import": { + "@solid-primitives/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "typesVersions": {}, + "scripts": { + "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts", + "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts", + "vitest": "vitest -c ../../configs/vitest.config.ts", + "test": "pnpm run vitest", + "test:ssr": "pnpm run vitest --mode ssr" + }, + "peerDependencies": { + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" + }, + "dependencies": { + "@solid-primitives/permission": "workspace:^", + "@solid-primitives/utils": "workspace:^" + }, + "devDependencies": { + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" + } +} diff --git a/packages/notification/src/index.ts b/packages/notification/src/index.ts new file mode 100644 index 000000000..d6fa2df68 --- /dev/null +++ b/packages/notification/src/index.ts @@ -0,0 +1,226 @@ +import { createSignal, onCleanup, type Accessor } from "solid-js"; +import { isServer } from "@solidjs/web"; +import { INTERNAL_OPTIONS, isDev, noop, access, type MaybeAccessor } from "@solid-primitives/utils"; +import { createPermission } from "@solid-primitives/permission"; + +/** + * Returns `true` when the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) + * is available in the current environment. + */ +export const isNotificationSupported = (): boolean => !isServer && "Notification" in window; + +/** + * Non-reactive notification helper. No Solid lifecycle dependency. + * Both returned functions are no-ops / return `null` when the API is unavailable. + * + * Permission must be `"granted"` before calling `show()` — use + * `createNotificationPermission` to request it reactively. + * + * @param title Notification title. + * @param options Standard `NotificationOptions` (body, icon, tag, etc.). + * @returns `[show, close]` — `show()` creates and returns the `Notification` + * (or `null` when permission is not `"granted"`); `close()` dismisses it. + * + * @example + * ```ts + * const [show, close] = makeNotification("New message", { body: "Hello!" }); + * button.addEventListener("click", () => show()); + * ``` + */ +export function makeNotification( + title: string, + options?: NotificationOptions, +): [show: () => Notification | null, close: VoidFunction] { + if (!isNotificationSupported()) return [() => null, noop]; + + let current: Notification | undefined; + let closeHandler: VoidFunction | undefined; + + const close: VoidFunction = () => { + if (current && closeHandler) { + current.removeEventListener("close", closeHandler); + closeHandler = undefined; + } + current?.close(); + current = undefined; + }; + + const show = (): Notification | null => { + if (Notification.permission !== "granted") { + // eslint-disable-next-line no-console + if (isDev) console.warn( + `[@solid-primitives/notification] show() called with Notification.permission "${Notification.permission}" — must be "granted".`, + ); + return null; + } + close(); + const n = new Notification(title, options); + current = n; + closeHandler = () => { + if (current === n) { + current = undefined; + closeHandler = undefined; + } + }; + n.addEventListener("close", closeHandler); + return n; + }; + + return [show, close]; +} + +/** Event handler callbacks for `createNotification`. */ +export type NotificationEventHandlers = { + /** Called when the user clicks the notification. */ + onClick?: (notification: Notification) => void; + /** Called when the notification is dismissed, whether by the user, the OS, or `close()`. */ + onClose?: (notification: Notification) => void; + /** Called when the notification fails to display. */ + onError?: (notification: Notification) => void; +}; + +/** + * Reactive notification primitive tied to the current reactive owner. + * + * Accepts reactive `title` and `options` — their current values are read each + * time `show()` is called. The `notification` accessor tracks the live + * `Notification` instance, updating to `null` when it is dismissed or closed. + * The notification is closed automatically on owner disposal. + * + * Permission must be `"granted"` before calling `show()` — use + * `createNotificationPermission` to request it reactively. + * + * @param title Notification title, or a reactive accessor returning one. + * @param options Standard `NotificationOptions`, or a reactive accessor. + * @param handlers Optional event callbacks (`onClick`, `onClose`, `onError`). + * @returns `{ show, close, notification, supported }` + * + * @example + * ```ts + * const { show, close, notification } = createNotification( + * () => `You have ${unread()} messages`, + * { icon: "/icon.png" }, + * { onClick: () => window.focus() }, + * ); + * ``` + */ +export function createNotification( + title: MaybeAccessor, + options?: MaybeAccessor, + handlers?: NotificationEventHandlers, +): { + show: () => Notification | null; + close: VoidFunction; + notification: Accessor; + supported: boolean; +} { + const supported = isNotificationSupported(); + + if (!supported) { + return { show: () => null, close: noop, notification: () => null, supported }; + } + + const [notification, setNotification] = createSignal(null, INTERNAL_OPTIONS); + let current: Notification | null = null; + let currentCleanup: VoidFunction | undefined; + + const close: VoidFunction = () => { + const n = current; + currentCleanup?.(); + currentCleanup = undefined; + n?.close(); + current = null; + setNotification(null); + if (n) handlers?.onClose?.(n); + }; + + const show = (): Notification | null => { + if (Notification.permission !== "granted") { + // eslint-disable-next-line no-console + if (isDev) console.warn( + `[@solid-primitives/notification] show() called with Notification.permission "${Notification.permission}" — must be "granted".`, + ); + return null; + } + close(); + const n = new Notification(access(title), access(options)); + current = n; + + const onCloseEvent = () => { + if (current === n) { + currentCleanup?.(); + currentCleanup = undefined; + current = null; + setNotification(null); + handlers?.onClose?.(n); + } + }; + + n.addEventListener("close", onCloseEvent); + const cleanups: VoidFunction[] = [() => n.removeEventListener("close", onCloseEvent)]; + + if (handlers?.onClick) { + const h = () => handlers.onClick!(n); + n.addEventListener("click", h); + cleanups.push(() => n.removeEventListener("click", h)); + } + + if (handlers?.onError) { + const h = () => handlers.onError!(n); + n.addEventListener("error", h); + cleanups.push(() => n.removeEventListener("error", h)); + } + + currentCleanup = () => cleanups.forEach(fn => fn()); + setNotification(n); + return n; + }; + + onCleanup(close); + + return { show, close, notification, supported }; +} + +/** + * Reactive notification permission manager built on `createPermission`. + * + * The `permission` accessor reflects the live state from the browser + * [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) + * and updates automatically whenever permission changes — including after + * `requestPermission()` resolves or the user edits browser settings. + * + * Permission values follow the Permissions API vocabulary: `"granted"`, + * `"denied"`, `"prompt"` (not yet asked), or `"unknown"` while the query + * is still resolving. Note that the Notifications API uses `"default"` for + * the same concept that the Permissions API calls `"prompt"`. + * + * @returns `{ permission, requestPermission }` + * + * @example + * ```ts + * const { permission, requestPermission } = createNotificationPermission(); + * + * + * + * + * ``` + */ +export function createNotificationPermission(): { + permission: Accessor; + requestPermission: () => Promise; +} { + if (!isNotificationSupported()) { + return { + permission: () => "unknown" as const, + requestPermission: () => Promise.resolve("denied" as NotificationPermission), + }; + } + + const permission = createPermission("notifications"); + + // createPermission tracks state via the change event — no manual update needed + const requestPermission = async (): Promise => + Notification.requestPermission(); + + return { permission, requestPermission }; +} diff --git a/packages/notification/test/index.test.ts b/packages/notification/test/index.test.ts new file mode 100644 index 000000000..e53a88d9b --- /dev/null +++ b/packages/notification/test/index.test.ts @@ -0,0 +1,502 @@ +import { describe, test, expect, vi, beforeAll, afterAll, beforeEach } from "vitest"; +import { createRoot, createSignal, flush, onCleanup } from "solid-js"; +import { + isNotificationSupported, + makeNotification, + createNotification, + createNotificationPermission, +} from "../src/index.js"; + +// ── Mock Notification API ───────────────────────────────────────────────────── + +class MockNotification { + static permission: NotificationPermission = "granted"; + static requestPermission = vi.fn().mockResolvedValue("granted" as NotificationPermission); + static instances: MockNotification[] = []; + + title: string; + private listeners: Map void)[]> = new Map(); + + constructor(title: string, _options?: NotificationOptions) { + this.title = title; + MockNotification.instances.push(this); + } + + close = vi.fn().mockImplementation(() => { + this.listeners.get("close")?.forEach(fn => fn()); + }); + + addEventListener = vi.fn().mockImplementation((event: string, fn: () => void) => { + const list = this.listeners.get(event) ?? []; + list.push(fn); + this.listeners.set(event, list); + }); + + removeEventListener = vi.fn().mockImplementation((event: string, fn: () => void) => { + const list = this.listeners.get(event) ?? []; + this.listeners.set( + event, + list.filter(f => f !== fn), + ); + }); + + simulateClose() { + this.listeners.get("close")?.forEach(fn => fn()); + } + + simulateClick() { + this.listeners.get("click")?.forEach(fn => fn()); + } + + simulateError() { + this.listeners.get("error")?.forEach(fn => fn()); + } +} + +// ── Mock Permissions API ────────────────────────────────────────────────────── + +const mockPermStatus = { + state: "granted" as PermissionState, + _listeners: [] as (() => void)[], + addEventListener(_: string, fn: () => void) { + this._listeners.push(fn); + }, + removeEventListener(_: string, fn: () => void) { + const i = this._listeners.indexOf(fn); + if (i >= 0) this._listeners.splice(i, 1); + }, + dispatchChange(state: PermissionState) { + this.state = state; + this._listeners.forEach(fn => fn()); + }, +}; + +// ── Global setup ────────────────────────────────────────────────────────────── + +beforeAll(() => { + Object.defineProperty(window, "Notification", { + value: MockNotification, + configurable: true, + writable: true, + }); + + (navigator as any).permissions ??= {} as any; + navigator.permissions.query = vi.fn().mockImplementation(({ name }: PermissionDescriptor) => { + if (name === "notifications") return Promise.resolve(mockPermStatus); + return Promise.reject(new Error(`Unhandled permission: ${name}`)); + }); +}); + +afterAll(() => { + Object.defineProperty(window, "Notification", { value: undefined, configurable: true }); +}); + +beforeEach(() => { + MockNotification.instances = []; + MockNotification.permission = "granted"; + MockNotification.requestPermission.mockClear().mockResolvedValue("granted"); + mockPermStatus.state = "granted"; + mockPermStatus._listeners = []; +}); + +// ── isNotificationSupported ─────────────────────────────────────────────────── + +describe("isNotificationSupported", () => { + test("returns true when Notification is available", () => { + expect(isNotificationSupported()).toBe(true); + }); +}); + +// ── makeNotification ────────────────────────────────────────────────────────── + +describe("makeNotification", () => { + test("show creates a Notification with the given title", () => { + const [show] = makeNotification("Hello"); + show(); + expect(MockNotification.instances).toHaveLength(1); + expect(MockNotification.instances[0]!.title).toBe("Hello"); + }); + + test("show returns the Notification instance", () => { + const [show] = makeNotification("Hello"); + expect(show()).toBeInstanceOf(MockNotification); + }); + + test("show returns null when permission is not granted", () => { + MockNotification.permission = "denied"; + const [show] = makeNotification("Hello"); + expect(show()).toBeNull(); + expect(MockNotification.instances).toHaveLength(0); + }); + + test("close dismisses the current notification", () => { + const [show, close] = makeNotification("Hello"); + show(); + const instance = MockNotification.instances[0]!; + close(); + expect(instance.close).toHaveBeenCalled(); + }); + + test("close removes the event listener before closing", () => { + const [show, close] = makeNotification("Hello"); + show(); + const instance = MockNotification.instances[0]!; + close(); + expect(instance.removeEventListener).toHaveBeenCalledWith("close", expect.any(Function)); + }); + + test("show replaces an existing notification", () => { + const [show] = makeNotification("Hello"); + show(); + const first = MockNotification.instances[0]!; + show(); + expect(first.close).toHaveBeenCalled(); + expect(MockNotification.instances).toHaveLength(2); + }); + + test("external close clears internal reference so close() becomes a no-op", () => { + const [show, close] = makeNotification("Hello"); + show(); + const instance = MockNotification.instances[0]!; + instance.simulateClose(); + instance.close.mockClear(); + close(); + expect(instance.close).not.toHaveBeenCalled(); + }); + + test("close can be registered with onCleanup by the caller for reactive cleanup", () => { + const { dispose } = createRoot(dispose => { + const [show, close] = makeNotification("Hello"); + onCleanup(close); + show(); + return { dispose }; + }); + + const instance = MockNotification.instances[0]!; + instance.close.mockClear(); + + dispose(); + expect(instance.close).toHaveBeenCalled(); + }); +}); + +// ── createNotification ──────────────────────────────────────────────────────── + +describe("createNotification", () => { + test("initial state: notification is null, supported is true", () => { + createRoot(dispose => { + const { notification, supported } = createNotification("Hello"); + expect(notification()).toBeNull(); + expect(supported).toBe(true); + dispose(); + }); + }); + + test("show creates a Notification and updates the signal", () => { + const { show, notification, dispose } = createRoot(dispose => { + const { show, notification } = createNotification("Hello"); + return { show, notification, dispose }; + }); + + show(); + flush(); + expect(notification()).toBeInstanceOf(MockNotification); + expect((notification() as MockNotification).title).toBe("Hello"); + + dispose(); + }); + + test("show returns null and warns when permission is not granted", () => { + MockNotification.permission = "denied"; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const { show, notification, dispose } = createRoot(dispose => { + const { show, notification } = createNotification("Hello"); + return { show, notification, dispose }; + }); + + expect(show()).toBeNull(); + flush(); + expect(notification()).toBeNull(); + expect(warnSpy).toHaveBeenCalled(); + + warnSpy.mockRestore(); + dispose(); + }); + + test("close dismisses the notification and sets signal to null", () => { + const { show, close, notification, dispose } = createRoot(dispose => { + const { show, close, notification } = createNotification("Hello"); + return { show, close, notification, dispose }; + }); + + show(); + flush(); + + close(); + flush(); + expect(notification()).toBeNull(); + expect(MockNotification.instances[0]!.close).toHaveBeenCalled(); + + dispose(); + }); + + test("close removes the event listener before closing", () => { + const { show, close, dispose } = createRoot(dispose => { + const { show, close } = createNotification("Hello"); + return { show, close, dispose }; + }); + + show(); + flush(); + close(); + expect(MockNotification.instances[0]!.removeEventListener).toHaveBeenCalledWith( + "close", + expect.any(Function), + ); + + dispose(); + }); + + test("external close (OS dismiss) sets signal to null", () => { + const { show, notification, dispose } = createRoot(dispose => { + const { show, notification } = createNotification("Hello"); + return { show, notification, dispose }; + }); + + show(); + flush(); + + MockNotification.instances[0]!.simulateClose(); + flush(); + expect(notification()).toBeNull(); + + dispose(); + }); + + test("show replaces an existing notification", () => { + const { show, notification, dispose } = createRoot(dispose => { + const { show, notification } = createNotification("Hello"); + return { show, notification, dispose }; + }); + + show(); + flush(); + const first = notification(); + + show(); + flush(); + expect(notification()).not.toBe(first); + expect((first as MockNotification).close).toHaveBeenCalled(); + + dispose(); + }); + + test("dispose closes the notification", () => { + const { show, dispose } = createRoot(dispose => { + const { show } = createNotification("Hello"); + return { show, dispose }; + }); + + show(); + flush(); + const instance = MockNotification.instances[0]!; + instance.close.mockClear(); + + dispose(); + expect(instance.close).toHaveBeenCalled(); + }); + + test("reactive title: reads current accessor value at show() time", () => { + const [title, setTitle] = createSignal("First"); + + const { show, notification, dispose } = createRoot(dispose => { + const { show, notification } = createNotification(title); + return { show, notification, dispose }; + }); + + show(); + flush(); + expect((notification() as MockNotification).title).toBe("First"); + + setTitle("Second"); + flush(); + // not re-shown automatically — title only read on next show() call + expect((notification() as MockNotification).title).toBe("First"); + + show(); + flush(); + expect((notification() as MockNotification).title).toBe("Second"); + + dispose(); + }); + + // ── Event callbacks ───────────────────────────────────────────────────────── + + test("onClick fires when click event is dispatched", () => { + const onClick = vi.fn(); + + const { show, dispose } = createRoot(dispose => { + const { show } = createNotification("Hello", undefined, { onClick }); + return { show, dispose }; + }); + + show(); + flush(); + MockNotification.instances[0]!.simulateClick(); + + expect(onClick).toHaveBeenCalledOnce(); + expect(onClick).toHaveBeenCalledWith(MockNotification.instances[0]); + + dispose(); + }); + + test("onClose fires when OS dismisses the notification", () => { + const onClose = vi.fn(); + + const { show, dispose } = createRoot(dispose => { + const { show } = createNotification("Hello", undefined, { onClose }); + return { show, dispose }; + }); + + show(); + flush(); + MockNotification.instances[0]!.simulateClose(); + + expect(onClose).toHaveBeenCalledOnce(); + expect(onClose).toHaveBeenCalledWith(MockNotification.instances[0]); + + dispose(); + }); + + test("onClose fires when close() is called programmatically", () => { + const onClose = vi.fn(); + + const { show, close, dispose } = createRoot(dispose => { + const { show, close } = createNotification("Hello", undefined, { onClose }); + return { show, close, dispose }; + }); + + show(); + flush(); + close(); + + expect(onClose).toHaveBeenCalledOnce(); + + dispose(); + }); + + test("onError fires when error event is dispatched", () => { + const onError = vi.fn(); + + const { show, dispose } = createRoot(dispose => { + const { show } = createNotification("Hello", undefined, { onError }); + return { show, dispose }; + }); + + show(); + flush(); + MockNotification.instances[0]!.simulateError(); + + expect(onError).toHaveBeenCalledOnce(); + expect(onError).toHaveBeenCalledWith(MockNotification.instances[0]); + + dispose(); + }); + + test("event listeners are removed when close() is called", () => { + const onClick = vi.fn(); + const onClose = vi.fn(); + + const { show, close, dispose } = createRoot(dispose => { + const { show, close } = createNotification("Hello", undefined, { onClick, onClose }); + return { show, close, dispose }; + }); + + show(); + flush(); + close(); + onClose.mockClear(); + + // After close(), simulating OS events should not trigger callbacks + MockNotification.instances[0]!.simulateClick(); + MockNotification.instances[0]!.simulateClose(); + + expect(onClick).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + + dispose(); + }); +}); + +// ── createNotificationPermission ────────────────────────────────────────────── + +describe("createNotificationPermission", () => { + test("permission starts as unknown before query resolves", () => { + createRoot(dispose => { + const { permission } = createNotificationPermission(); + expect(permission()).toBe("unknown"); + dispose(); + }); + }); + + test("permission resolves to current state after query", async () => { + mockPermStatus.state = "granted"; + + const { permission, dispose } = createRoot(dispose => { + const { permission } = createNotificationPermission(); + return { permission, dispose }; + }); + + expect(permission()).toBe("unknown"); + await Promise.resolve(); + flush(); + expect(permission()).toBe("granted"); + + dispose(); + }); + + test("permission updates reactively when state changes externally", async () => { + mockPermStatus.state = "granted"; + + const { permission, dispose } = createRoot(dispose => { + const { permission } = createNotificationPermission(); + return { permission, dispose }; + }); + + await Promise.resolve(); + flush(); + expect(permission()).toBe("granted"); + + mockPermStatus.dispatchChange("denied"); + flush(); + expect(permission()).toBe("denied"); + + dispose(); + }); + + test("requestPermission calls Notification.requestPermission", async () => { + const { requestPermission, dispose } = createRoot(dispose => { + const { requestPermission } = createNotificationPermission(); + return { requestPermission, dispose }; + }); + + await requestPermission(); + expect(MockNotification.requestPermission).toHaveBeenCalledOnce(); + + dispose(); + }); + + test("requestPermission returns the resolved permission value", async () => { + MockNotification.requestPermission.mockResolvedValue("granted"); + + const { requestPermission, dispose } = createRoot(dispose => { + const { requestPermission } = createNotificationPermission(); + return { requestPermission, dispose }; + }); + + expect(await requestPermission()).toBe("granted"); + + dispose(); + }); +}); diff --git a/packages/notification/test/server.test.ts b/packages/notification/test/server.test.ts new file mode 100644 index 000000000..e19f755c0 --- /dev/null +++ b/packages/notification/test/server.test.ts @@ -0,0 +1,42 @@ +import { describe, test, expect } from "vitest"; +import { + isNotificationSupported, + makeNotification, + createNotification, + createNotificationPermission, +} from "../src/index.js"; + +describe("isNotificationSupported (SSR)", () => { + test("returns false on the server", () => { + expect(isNotificationSupported()).toBe(false); + }); +}); + +describe("makeNotification (SSR)", () => { + test("returns no-op functions without throwing", () => { + const [show, close] = makeNotification("Hello", { body: "World" }); + expect(typeof show).toBe("function"); + expect(typeof close).toBe("function"); + expect(show()).toBeNull(); + expect(() => close()).not.toThrow(); + }); +}); + +describe("createNotification (SSR)", () => { + test("returns static defaults without throwing", () => { + const { show, close, notification, supported } = createNotification("Hello"); + expect(supported).toBe(false); + expect(notification()).toBeNull(); + expect(show()).toBeNull(); + expect(() => close()).not.toThrow(); + }); +}); + +describe("createNotificationPermission (SSR)", () => { + test("returns unknown permission and resolves denied without throwing", async () => { + const { permission, requestPermission } = createNotificationPermission(); + expect(permission()).toBe("unknown"); + const result = await requestPermission(); + expect(result).toBe("denied"); + }); +}); diff --git a/packages/notification/tsconfig.json b/packages/notification/tsconfig.json new file mode 100644 index 000000000..cc6a7fe80 --- /dev/null +++ b/packages/notification/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "dist", + "rootDir": "src" + }, + "references": [ + { + "path": "../permission" + }, + { + "path": "../utils" + } + ], + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/permission/README.md b/packages/permission/README.md index fdc864749..2931b08c7 100644 --- a/packages/permission/README.md +++ b/packages/permission/README.md @@ -8,25 +8,89 @@ [![size](https://img.shields.io/npm/v/@solid-primitives/permission?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/permission) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) -Creates a primitive to query user permissions. +Reactive wrapper around the browser [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API). Queries a named permission and returns a live signal that updates automatically whenever the permission state changes. ## Installation -``` +```bash npm install @solid-primitives/permission # or yarn add @solid-primitives/permission +# or +pnpm add @solid-primitives/permission ``` ## How to use it +### `createPermission` + +Queries a browser permission by name (or descriptor object) and returns a reactive accessor reflecting its current state. + +```ts +import { createPermission } from "@solid-primitives/permission"; + +const permission = createPermission("microphone"); +// permission(): "unknown" | "granted" | "denied" | "prompt" +``` + +The signal starts as `"unknown"` — the Permissions API query is async and the initial value is not available synchronously. After the first microtask, the signal resolves to the current state and begins tracking changes. + +The signal updates automatically when the permission changes — for example when the user grants or revokes access in browser settings, or after an API call prompts the user. + +**Accepted values** follow the [PermissionName](https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query#name) vocabulary. Pass either a plain string or a full `PermissionDescriptor` object: + ```ts -const state: "unknown" | PermissionState = createPermission(descriptor: PermissionDescription | PermissionName); +// Plain name +const mic = createPermission("microphone"); + +// Descriptor object (required for some permissions) +const cam = createPermission({ name: "camera" }); + +// Used by @solid-primitives/notification +const notifs = createPermission("notifications"); ``` -## Demo +**Return values** map to [PermissionState](https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus/state): + +| Value | Meaning | +|-------|---------| +| `"unknown"` | Initial state — query has not resolved yet | +| `"granted"` | Permission has been granted | +| `"denied"` | Permission has been denied | +| `"prompt"` | Not yet asked; prompting the user is possible | + +### SSR -TODO +On the server, `createPermission` returns a static `() => "unknown"` accessor. No query is made and no listeners are registered. + +### Reactive usage example + +```tsx +import { createPermission } from "@solid-primitives/permission"; + +const CameraGate: Component = () => { + const permission = createPermission("camera"); + + return ( + + +

Checking camera permission…

+
+ + + + +

Camera access denied. Enable it in browser settings.

+
+ + + +
+ ); +}; +``` ## Changelog diff --git a/packages/permission/package.json b/packages/permission/package.json index f2cbe3381..96c119a30 100644 --- a/packages/permission/package.json +++ b/packages/permission/package.json @@ -41,7 +41,8 @@ "test:ssr": "pnpm run vitest --mode ssr" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" }, "keywords": [ "permission", @@ -51,6 +52,7 @@ ], "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" } } diff --git a/packages/permission/src/index.ts b/packages/permission/src/index.ts index 09f50c6a4..989c3086f 100644 --- a/packages/permission/src/index.ts +++ b/packages/permission/src/index.ts @@ -1,5 +1,5 @@ -import { type Accessor, createEffect, createSignal, on, onCleanup } from "solid-js"; -import { isServer } from "solid-js/web"; +import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js"; +import { isServer } from "@solidjs/web"; /** * Querying the permission API @@ -13,13 +13,17 @@ export const createPermission = ( if (isServer) { return () => "unknown"; } - const [permission, setPermission] = createSignal("unknown"); - const [status, setStatus] = createSignal(); + const [permission, setPermission] = createSignal("unknown", { + ownedWrite: true, + }); + const [status, setStatus] = createSignal(undefined, { + ownedWrite: true, + }); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (navigator) { navigator.permissions .query(typeof name === "string" ? { name } : name) - .then(setStatus) + .then(s => setStatus(() => s)) .catch(error => { if (error.name !== "TypeError" || (name !== "microphone" && name !== "camera")) { return; @@ -42,16 +46,23 @@ export const createPermission = ( }) : getUserMedia(constraints); }); + let removeChangeListener: VoidFunction | undefined; + createEffect( - on(status, status => { - if (status) { - setPermission(status.state); - const listener = () => setPermission(status.state); - status.addEventListener("change", listener); - onCleanup(() => status.removeEventListener("change", listener)); + () => status(), + currentStatus => { + removeChangeListener?.(); + removeChangeListener = undefined; + if (currentStatus) { + setPermission(currentStatus.state); + const listener = () => setPermission(currentStatus.state); + currentStatus.addEventListener("change", listener); + removeChangeListener = () => currentStatus.removeEventListener("change", listener); } - }), + }, ); + + onCleanup(() => removeChangeListener?.()); } return permission; }; diff --git a/packages/permission/test/index.test.ts b/packages/permission/test/index.test.ts index ae3a20652..46193510f 100644 --- a/packages/permission/test/index.test.ts +++ b/packages/permission/test/index.test.ts @@ -1,51 +1,40 @@ import { __permissions__ } from "./setup.js"; -import { createEffect, createRoot } from "solid-js"; +import { createRoot, flush } from "solid-js"; import { it, describe, expect } from "vitest"; import { createPermission } from "../src/index.js"; describe("createPermission", () => { it("reads permission", async () => { - let captured: unknown; - - const dispose = createRoot(dispose => { + const { permission, dispose } = createRoot(dispose => { const permission = createPermission("microphone" as PermissionName); - - createEffect(() => { - captured = permission(); - }); - - return dispose; + return { permission, dispose }; }); - expect(captured).toEqual("unknown"); + expect(permission()).toBe("unknown"); await Promise.resolve(); - expect(captured).toEqual("granted"); + flush(); + expect(permission()).toBe("granted"); dispose(); }); it("reads permission updates", async () => { - let captured: unknown; - - const dispose = createRoot(dispose => { + const { permission, dispose } = createRoot(dispose => { const permission = createPermission("camera" as PermissionName); - - createEffect(() => { - captured = permission(); - }); - - return dispose; + return { permission, dispose }; }); - expect(captured).toEqual("unknown"); + expect(permission()).toBe("unknown"); await Promise.resolve(); - expect(captured).toEqual("denied"); + flush(); + expect(permission()).toBe("denied"); __permissions__.camera.__dispatchEvent("granted"); - expect(captured).toEqual("granted"); + flush(); + expect(permission()).toBe("granted"); dispose(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70787571b..41fb58600 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -644,6 +644,22 @@ importers: specifier: ^1.9.7 version: 1.9.7 + packages/notification: + dependencies: + '@solid-primitives/permission': + specifier: workspace:^ + version: link:../permission + '@solid-primitives/utils': + specifier: workspace:^ + version: link:../utils + devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) + solid-js: + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 + packages/orientation: dependencies: '@solid-primitives/utils': @@ -685,9 +701,12 @@ importers: packages/permission: devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/platform: devDependencies: @@ -1119,10 +1138,10 @@ importers: version: link:../packages/utils '@tanstack/solid-router': specifier: ^1.168.16 - version: 1.169.1(solid-js@2.0.0-beta.10) + version: 1.169.1(solid-js@2.0.0-experimental.16) '@tanstack/solid-start': specifier: ^1.167.28 - version: 1.167.59(solid-js@2.0.0-beta.10)(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + version: 1.167.59(solid-js@2.0.0-experimental.16)(vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1149,10 +1168,10 @@ importers: version: 1.77.8 solid-dismiss: specifier: ^1.7.121 - version: 1.8.2(solid-js@2.0.0-beta.10) + version: 1.8.2(solid-js@2.0.0-experimental.16) solid-icons: specifier: ^1.1.0 - version: 1.1.0(solid-js@2.0.0-beta.10) + version: 1.1.0(solid-js@2.0.0-experimental.16) undici: specifier: ^5.28.2 version: 5.28.4 @@ -1189,7 +1208,7 @@ importers: version: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) vite-plugin-solid: specifier: ^2.11.12 - version: 2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + version: 2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) packages: @@ -2929,6 +2948,9 @@ packages: peerDependencies: solid-js: ^1.5.3 + '@solidjs/signals@0.11.3': + resolution: {integrity: sha512-udMfutYPOlcxKUmc5+n1QtarsxOiAlC6LJY2TqFyaMwdXgo+reiYUcYGDlOiAPXfCLE0lavZHQ/6GT5pJbXKBA==} + '@solidjs/signals@2.0.0-beta.10': resolution: {integrity: sha512-McdmbLNiSlz616zcykS8Rb1t9QTOTKdNAoaWd4/OjXEbcAUrPqRX1CWgR+caiWUk4qn0a+LesTTV4jZhFFPaSg==} @@ -6677,6 +6699,9 @@ packages: solid-js@2.0.0-beta.10: resolution: {integrity: sha512-EAfV6b1SC4c3wEBAoX4dMy063uTb4nfL5uXnN8yse4InH7RTw1LoB0I9HAy+pj3/GHqQE2tYZurlZtqU4pGyog==} + solid-js@2.0.0-experimental.16: + resolution: {integrity: sha512-zZ1dU7cR0EnvLnrYiRLQbCFiDw5blLdlqmofgLzKUYE1TCMWDcisBlSwz0Ez8l4yXB4adbdhtaYCuynH4xSq9A==} + solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -9466,47 +9491,47 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} - '@solid-devtools/debugger@0.28.1(solid-js@2.0.0-beta.10)': + '@solid-devtools/debugger@0.28.1(solid-js@2.0.0-experimental.16)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-beta.10) - '@solid-primitives/bounds': 0.1.5(solid-js@2.0.0-beta.10) - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/keyboard': 1.3.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 - - '@solid-devtools/logger@0.9.11(solid-js@2.0.0-beta.10)': + '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-experimental.16) + '@solid-primitives/bounds': 0.1.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/keyboard': 1.3.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 + + '@solid-devtools/logger@0.9.11(solid-js@2.0.0-experimental.16)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-devtools/debugger': 0.28.1(solid-js@2.0.0-beta.10) - '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-devtools/debugger': 0.28.1(solid-js@2.0.0-experimental.16) + '@solid-devtools/shared': 0.20.0(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-devtools/shared@0.20.0(solid-js@2.0.0-beta.10)': + '@solid-devtools/shared@0.20.0(solid-js@2.0.0-experimental.16)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/media': 2.3.5(solid-js@2.0.0-beta.10) - '@solid-primitives/refs': 1.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/styles': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 - - '@solid-primitives/bounds@0.1.5(solid-js@2.0.0-beta.10)': - dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/resize-observer': 2.1.5(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/media': 2.3.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/refs': 1.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/scheduled': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/styles': 0.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 + + '@solid-primitives/bounds@0.1.5(solid-js@2.0.0-experimental.16)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/resize-observer': 2.1.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 '@solid-primitives/composites@1.1.1(solid-js@1.9.7)': dependencies: @@ -9518,68 +9543,68 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solid-primitives/event-listener@2.4.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/event-listener@2.4.5(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/keyboard@1.3.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/keyboard@1.3.5(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/media@2.3.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/media@2.3.5(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 '@solid-primitives/refs@1.0.8(solid-js@1.9.7)': dependencies: '@solid-primitives/utils': 6.2.3(solid-js@1.9.7) solid-js: 1.9.7 - '@solid-primitives/refs@1.0.8(solid-js@2.0.0-beta.10)': + '@solid-primitives/refs@1.0.8(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/utils': 6.2.3(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.2.3(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/refs@1.1.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/refs@1.1.3(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/resize-observer@2.1.5(solid-js@2.0.0-beta.10)': + '@solid-primitives/resize-observer@2.1.5(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-beta.10) - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/event-listener': 2.4.5(solid-js@2.0.0-experimental.16) + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/static-store': 0.1.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/rootless@1.5.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/rootless@1.5.3(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/scheduled@1.5.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/scheduled@1.5.3(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 - '@solid-primitives/static-store@0.1.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/static-store@0.1.3(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 - '@solid-primitives/styles@0.1.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/styles@0.1.3(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-beta.10) - '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-beta.10) - solid-js: 2.0.0-beta.10 + '@solid-primitives/rootless': 1.5.3(solid-js@2.0.0-experimental.16) + '@solid-primitives/utils': 6.4.0(solid-js@2.0.0-experimental.16) + solid-js: 2.0.0-experimental.16 '@solid-primitives/throttle@1.2.0(solid-js@1.9.7)': dependencies: @@ -9593,22 +9618,24 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solid-primitives/utils@6.2.3(solid-js@2.0.0-beta.10)': + '@solid-primitives/utils@6.2.3(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 - '@solid-primitives/utils@6.4.0(solid-js@2.0.0-beta.10)': + '@solid-primitives/utils@6.4.0(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 - '@solidjs/meta@0.29.4(solid-js@2.0.0-beta.10)': + '@solidjs/meta@0.29.4(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 '@solidjs/router@0.8.4(solid-js@1.9.7)': dependencies: solid-js: 1.9.7 + '@solidjs/signals@0.11.3': {} + '@solidjs/signals@2.0.0-beta.10': {} '@solidjs/start@1.1.4(solid-js@2.0.0-beta.10)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': @@ -9731,7 +9758,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@tanstack/router-plugin@1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -9748,7 +9775,7 @@ snapshots: zod: 3.25.63 optionalDependencies: vite: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) - vite-plugin-solid: 2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + vite-plugin-solid: 2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) transitivePeerDependencies: - supports-color @@ -9793,45 +9820,45 @@ snapshots: - supports-color - vite - '@tanstack/solid-router@1.169.1(solid-js@2.0.0-beta.10)': + '@tanstack/solid-router@1.169.1(solid-js@2.0.0-experimental.16)': dependencies: - '@solid-devtools/logger': 0.9.11(solid-js@2.0.0-beta.10) - '@solid-primitives/refs': 1.0.8(solid-js@2.0.0-beta.10) - '@solidjs/meta': 0.29.4(solid-js@2.0.0-beta.10) + '@solid-devtools/logger': 0.9.11(solid-js@2.0.0-experimental.16) + '@solid-primitives/refs': 1.0.8(solid-js@2.0.0-experimental.16) + '@solidjs/meta': 0.29.4(solid-js@2.0.0-experimental.16) '@tanstack/history': 1.161.6 '@tanstack/router-core': 1.169.1 isbot: 5.1.39 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 - '@tanstack/solid-start-client@1.166.46(solid-js@2.0.0-beta.10)': + '@tanstack/solid-start-client@1.166.46(solid-js@2.0.0-experimental.16)': dependencies: '@tanstack/router-core': 1.169.1 - '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.10) + '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-experimental.16) '@tanstack/start-client-core': 1.168.1 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 - '@tanstack/solid-start-server@1.166.50(solid-js@2.0.0-beta.10)': + '@tanstack/solid-start-server@1.166.50(solid-js@2.0.0-experimental.16)': dependencies: - '@solidjs/meta': 0.29.4(solid-js@2.0.0-beta.10) + '@solidjs/meta': 0.29.4(solid-js@2.0.0-experimental.16) '@tanstack/history': 1.161.6 '@tanstack/router-core': 1.169.1 - '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.10) + '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-experimental.16) '@tanstack/start-client-core': 1.168.1 '@tanstack/start-server-core': 1.167.29 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 transitivePeerDependencies: - crossws - '@tanstack/solid-start@1.167.59(solid-js@2.0.0-beta.10)(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@tanstack/solid-start@1.167.59(solid-js@2.0.0-experimental.16)(vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: - '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-beta.10) - '@tanstack/solid-start-client': 1.166.46(solid-js@2.0.0-beta.10) - '@tanstack/solid-start-server': 1.166.50(solid-js@2.0.0-beta.10) + '@tanstack/solid-router': 1.169.1(solid-js@2.0.0-experimental.16) + '@tanstack/solid-start-client': 1.166.46(solid-js@2.0.0-experimental.16) + '@tanstack/solid-start-server': 1.166.50(solid-js@2.0.0-experimental.16) '@tanstack/start-client-core': 1.168.1 - '@tanstack/start-plugin-core': 1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + '@tanstack/start-plugin-core': 1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) '@tanstack/start-server-core': 1.167.29 pathe: 2.0.3 - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 optionalDependencies: vite: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) transitivePeerDependencies: @@ -9850,7 +9877,7 @@ snapshots: '@tanstack/start-fn-stubs@1.161.6': {} - '@tanstack/start-plugin-core@1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': + '@tanstack/start-plugin-core@1.169.17(vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.29.0 @@ -9858,7 +9885,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.169.1 '@tanstack/router-generator': 1.166.39 - '@tanstack/router-plugin': 1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) + '@tanstack/router-plugin': 1.167.32(vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)))(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) '@tanstack/router-utils': 1.161.7 '@tanstack/start-client-core': 1.168.1 '@tanstack/start-server-core': 1.167.29 @@ -13978,13 +14005,13 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - solid-dismiss@1.8.2(solid-js@2.0.0-beta.10): + solid-dismiss@1.8.2(solid-js@2.0.0-experimental.16): dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 - solid-icons@1.1.0(solid-js@2.0.0-beta.10): + solid-icons@1.1.0(solid-js@2.0.0-experimental.16): dependencies: - solid-js: 2.0.0-beta.10 + solid-js: 2.0.0-experimental.16 solid-js@1.9.7: dependencies: @@ -13999,6 +14026,13 @@ snapshots: seroval: 1.5.2 seroval-plugins: 1.5.2(seroval@1.5.2) + solid-js@2.0.0-experimental.16: + dependencies: + '@solidjs/signals': 0.11.3 + csstype: 3.1.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + solid-refresh@0.6.3(solid-js@2.0.0-beta.10): dependencies: '@babel/generator': 7.27.5 @@ -14008,6 +14042,15 @@ snapshots: transitivePeerDependencies: - supports-color + solid-refresh@0.6.3(solid-js@2.0.0-experimental.16): + dependencies: + '@babel/generator': 7.27.5 + '@babel/helper-module-imports': 7.28.6 + '@babel/types': 7.29.0 + solid-js: 2.0.0-experimental.16 + transitivePeerDependencies: + - supports-color + solid-transition-group@0.2.3(solid-js@1.9.7): dependencies: '@solid-primitives/refs': 1.0.8(solid-js@1.9.7) @@ -14693,14 +14736,14 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-solid@2.11.12(solid-js@2.0.0-beta.10)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)): + vite-plugin-solid@2.11.12(solid-js@2.0.0-experimental.16)(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)): dependencies: '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 babel-preset-solid: 1.9.6(@babel/core@7.29.0) merge-anything: 5.1.7 - solid-js: 2.0.0-beta.10 - solid-refresh: 0.6.3(solid-js@2.0.0-beta.10) + solid-js: 2.0.0-experimental.16 + solid-refresh: 0.6.3(solid-js@2.0.0-experimental.16) vite: 8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0) vitefu: 1.0.6(vite@8.0.10(@types/node@24.0.1)(esbuild@0.25.5)(jiti@2.6.1)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) transitivePeerDependencies: