diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..654c6d4 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets). + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..ec98e35 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/initial-release.md b/.changeset/initial-release.md new file mode 100644 index 0000000..a439c29 --- /dev/null +++ b/.changeset/initial-release.md @@ -0,0 +1,5 @@ +--- +"@dennation/menu": minor +--- + +Initial release. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9afdbc0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9eb56d3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: release + +permissions: + contents: write + pull-requests: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + registry-url: https://registry.npmjs.org + - run: pnpm install --frozen-lockfile + # Opens/updates a "Version Packages" PR from pending changesets, and + # publishes to npm when such a PR is merged (version no longer on npm). + - uses: changesets/action@v1 + with: + version: pnpm version + publish: pnpm release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index f3decb5..62e6bcb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # @dennation/menu -Router-agnostic navigation menu: a data model (`defineMenu`), a React renderer (``), and a TanStack Router adapter (`menuInputFromRouteTree`). +Headless, typesafe, router-agnostic navigation menu — you own every element and style; it owns the tree, the disclosure state, and the active branch. A data model (`defineMenu`), a React renderer (``), and a `useMenu` hook. + +`` renders none of its own markup: it drives recursion and state, you supply the `renderItem` component and the shell. It gives you a11y primitives (a stable `id` for `aria-controls`/`aria-expanded`), but keyboard/focus handling is yours. ```bash pnpm add @dennation/menu @@ -10,52 +12,378 @@ pnpm add @dennation/menu ```tsx import { defineMenu } from "@dennation/menu"; -import { menuInputFromRouteTree } from "@dennation/menu/adapters/tanstack-router"; -import { routeTree } from "./route-tree.gen"; const menu = defineMenu({ - ...menuInputFromRouteTree(routeTree, { omit: ["/about"] }), - // add a custom child into a generated section — `parent` is type-checked against the routes - "/changelog": { title: "Changelog", parent: "/components" }, - "/button": { title: "Button", icon: }, // overrides the generated entry - "https://github.com/dennation/menu": { title: "GitHub" }, + "/": { title: "Dashboard" }, + "/projects": { title: "Projects" }, + "/projects/active": { title: "Active", parent: "/projects" }, + "/projects/archived": { title: "Archived", parent: "/projects" }, + "/settings": { title: "Settings" }, + "/settings/profile": { title: "Profile", parent: "/settings" }, + "/settings/billing": { title: "Billing", parent: "/settings" }, + "https://docs.example.com": { title: "Docs" }, // external URL as its own href }); ``` -Render it with your own `Container` and `Item` — `` owns the open/closed state and recursion, the `Item` owns the link, icon, and active state: +Render it with your own `Item` and wrap the items in your own shell via the +render-prop `children` — `` owns the open/closed state, the active item, +and recursion; the `Item` renders the link. `Item` props are +discriminated on `collapsible`, so `open`/`toggle` exist only on a section; +`isActive` is on every item: ```tsx -import { Menu } from "@dennation/menu/react"; +import { Menu, type MenuItemProps, type MenuProps } from "@dennation/menu/react"; + +function Item(props: MenuItemProps) { + const { item, level, isActive } = props; + const indent = level > 0 ? "pl-4" : ""; + + const link = item.href ? ( + + {item.title} + + ) : ( + + {item.title} + + ); -; + // A collapsible section: the link plus a toggle, then its children. + if (props.collapsible) { + return ( +
  • +
    + {link} + +
    + {props.children} +
  • + ); + } + + // A leaf link or an always-open group header — no toggle. + return ( +
  • + {link} + {props.children} +
  • + ); +} ``` +Bind your `Item` and shell once in an `AppMenu`, then pass only data props at +each call site: + +```tsx +function AppMenu(props: Omit) { + return ( + + {(items) => } + + ); +} +``` + +The shell is fully yours — put a search box, a header, or a footer around +`{items}` however you need. `` renders only the entries. + +Finally, make it aware of the current page with `useMenu`: it highlights the +active item, expands its branch, and gives you the breadcrumb `trail`. You tell +it which item is active (resolve the id however your router works); it holds the +open/closed state and hands `` a store via `menuProps`: + +```tsx +import { findMenuItemBy } from "@dennation/menu"; +import { useMenu } from "@dennation/menu/react"; + +function Sidebar() { + const { pathname } = useLocation(); + const { menuProps, trail, setActive } = useMenu(menu); + + // Resolve the active item however you like (exact, prefix, router match) and + // pass its id — it need not equal the path. + useEffect(() => { + const active = findMenuItemBy(menu, (i) => i.href === pathname); + setActive(active?.id); + }, [pathname]); + + return ( + <> + + + + ); +} +``` + +That's a complete router-aware sidebar. `useMenu` is optional — `` +on its own still renders and toggles sections, it just won't track the active +route. Because the state lives in a store, toggling one section re-renders only +that node, never the whole `Sidebar`. + ## API +### Core (`@dennation/menu`) + - **`defineMenu(input)`** — resolves a keyed `MenuInput` into a nested `Menu`. The input is an object keyed by identity (the entry's `href` by default, or an arbitrary id when `href: false`); hierarchy comes from `parent`, not nesting. Siblings sort by `order`, then insertion order. Items with an unknown `parent` are hoisted to the top level with a dev warning; a `parent` cycle warns too. Each output `MenuItem` carries its input key as a stable `id` — use it for React keys, `aria-controls`, or matching a node back to your data. -- **``** (`/react`) — the renderer. Custom JSX goes in the `before`/`after` slots: `(item, { open, level }) => ReactNode`. -- **`menuInputFromRouteTree(routeTree, options?)`** (`/adapters/tanstack-router`) — walks a TanStack route tree into a `MenuInput` keyed by `fullPath`. Pathless and layout routes are transparent; `omit` drops a route with its subtree. How a route describes itself is read from `staticData.menu` (override the source with `getRouteMenu`): +- **`getMenuTrail(menu, id)` / `findMenuItem(menu, id)`** — the ancestor chain / the item for an exact `id`. **`…By(menu, predicate)`** variants match by any condition (first pre-order match) — resolve an active item by prefix/router-match/role off React and feed its `id` to `useMenu`. + +### `` (`@dennation/menu/react`) + +The renderer. Props: + +| Prop | Type | | +| --- | --- | --- | +| `menu` | `Menu` | The tree from `defineMenu`. | +| `renderItem` | `ComponentType>` | Component each entry renders through. | +| `children` | `(items: ReactNode) => ReactNode` | Wraps the items in your shell. | +| `renderBeforeItem?` / `renderAfterItem?` | `ComponentType>` | Component rendered around each item (divider, group heading). | +| `store?` | `MenuStateStore` | State store from `useMenu`. Omit → `` owns state. | +| `defaultOpen?` | `MenuOpenState` | Initial disclosure when `` owns it. | +| `onOpenChange?` | `(next: MenuOpenState) => void` | Notified on every toggle (persistence). | + +### `useMenu(menu, options?)` (`@dennation/menu/react`) + +Owns the active item and disclosure state. + +**Options** - ```tsx - createFileRoute("/button")({ - component: ButtonPage, - staticData: { menu: { title: "Button", order: 2 } }, - }); - ``` +| Option | Type | | +| --- | --- | --- | +| `defaultActiveId?` | `string` | Initial active item; its branch is expanded. | +| `defaultOpen?` | `MenuOpenState` | Initial disclosure state. | + +**Returns** + +| Field | Type | | +| --- | --- | --- | +| `menuProps` | `{ menu, store }` | Spread into ``. | +| `activeId` | `string \| undefined` | Current active id (readonly). | +| `activeItem` | `MenuItem \| undefined` | Current active item (readonly). | +| `setActive` | `(id: string \| undefined) => void` | Set active by id; expands its branch. | +| `trail` | `MenuItem[]` | Root → active item (breadcrumbs). | +| `isOpen` | `(id: string) => boolean` | Whether a section is open. | +| `open` / `close` / `toggle` | `(id: string) => void` | Drive one section. | +| `subscribeOpenState` | `(listener) => () => void` | Subscribe to changes (no re-render). | + +### Types + +**Menu item** — an entry: `MenuItemInput` on the way into `defineMenu`, `MenuItem` on the way out. + +| Field | Type | Where | | +| --- | --- | --- | --- | +| `title` | `string` | both | Display text. | +| `href` | `string \| false` (in) / `string` (out) | both | Link target; `false` on input → non-navigable container. Defaults to the entry key. | +| `id` | `string` | out | Stable identity — the input key. | +| `parent` | `string` | in | Parent entry's key → nesting. | +| `order` | `number` | in | Sort hint among siblings (lower first). | +| `items` | `MenuItem[]` | out | Resolved children. | +| `defaultOpen` | `boolean` | both | Start a section expanded (default `true`). | +| `collapsible` | `boolean` | both | `false` → always-open group header. | +| `meta` | `M` | both | Opaque per-item data (see below). | + +**`MenuItemProps`** — what your `renderItem` receives, discriminated on `collapsible`: + +| Field | Type | | +| --- | --- | --- | +| `item` | `MenuItem` | The entry to render. | +| `level` | `number` | Nesting depth (`0` at the top). | +| `isActive` | `boolean` | Whether this is the active item (from `useMenu`'s `setActive`). | +| `containsActive` | `boolean` | Whether the active item is anywhere in this item's subtree — highlight a section whose child is active. | +| `collapsible` | `boolean` | Discriminant. | +| `open` / `toggle` | `boolean` / `() => void` | Present only when `collapsible: true`. | +| `children` | `ReactNode` | The rendered nested level (required on a section, optional on a leaf). | + +**`MenuItemPropsOf`** — the same props with `M` inferred from a `menu`, so a standalone `Item` doesn't repeat the meta type. See [Menu with icons](#menu-with-icons). + +**`MenuSlotProps`** — what a `renderBeforeItem`/`renderAfterItem` slot receives: the `item` plus its state, without `toggle`/`children` (a slot renders *next to* the item, it doesn't control or contain it): + +| Field | Type | | +| --- | --- | --- | +| `item` | `MenuItem` | The item this slot is rendered next to. | +| `level` | `number` | Nesting depth (`0` at the top). | +| `open` | `boolean` | Whether the item's section is expanded. | +| `isActive` | `boolean` | Whether the item is the active item. | +| `containsActive` | `boolean` | Whether the active item is in the item's subtree. | ### Per-item `meta` -Items carry consumer metadata (badges, flags, counters) via a generic `M` that threads through the whole chain. It defaults to `never`; give it a shape with `defineMenu(…)` and `meta` is that type everywhere, input and output. Make it optional with `defineMenu(…)`. `meta` passes through verbatim — neither `defineMenu` nor the renderer reads it. +Attach your own data to items (badges, flags, counters) via `meta`. `defineMenu` +**infers** its type `M` from the input's `meta` fields — the library carries it +verbatim and never reads it, and it's that type everywhere (input, output, `Item`): -## Development +```tsx +const menu = defineMenu({ "/inbox": { title: "Inbox", meta: { count: 3 } } }); +menu[0].meta.count; // number +``` -```bash -pnpm install -pnpm run build # vite build — 3 entry points -pnpm run typecheck -pnpm run test +## Adapters + +An adapter turns a framework's route/config shape into a `MenuInput` you spread into `defineMenu`. It's only a *source* — `defineMenu` stays the single place that assembles the tree, so you can mix adapters with manual entries in one spread. + +### TanStack Router + +```tsx +import { defineMenu } from "@dennation/menu"; +import { menuInputFromRouteTree } from "@dennation/menu/adapters/tanstack-router"; +import { routeTree } from "./routeTree.gen"; + +const menu = defineMenu(menuInputFromRouteTree(routeTree, { omit: ["/login"] })); +``` + +Because it returns plain `MenuInput`, you can mix it with manual entries in the +same `defineMenu` spread — see [Custom entries in a generated menu](#custom-entries-in-a-generated-menu). + +**`menuInputFromRouteTree(routeTree, options?)`** walks the route tree into a `MenuInput` keyed by `fullPath`. Pathless and layout routes are transparent; `omit` drops a route with its subtree (typed against the tree). How a route describes itself is read from `staticData.menu` — override the source with `getRouteMenu`: + +```tsx +createFileRoute("/button")({ + component: ButtonPage, + staticData: { menu: { title: "Button", order: 2, meta: { badge: "new" } } }, +}); +``` + +`title` falls back to a title-cased last path segment; `order`/`meta` come from `staticData.menu`. + +Register `menu` on `staticData` once, so it's typed at every route (including +`meta` under your own type): + +```tsx +import type { RouteMenuEntry } from "@dennation/menu/adapters/tanstack-router"; + +declare module "@tanstack/router-core" { + interface StaticDataRouteOption { + menu?: RouteMenuEntry; + } +} +``` + +That register is the **only** place you name `Meta`: it flows through +`menuInputFromRouteTree` into `defineMenu`, so `defineMenu(menuInputFromRouteTree(routeTree))` +is typed `Menu` with no explicit type argument. + +## Guides + +Recipes for the cases that don't fit the happy path. + +### External links + +An entry's `href` can be any URL — it's just the key. Branch in your `Item` on +the shape you care about: + +```tsx +{item.href?.startsWith("http") ? ( + {item.title} +) : ( + {item.title} +)} +``` + +### Group headers (non-navigable) + +Give a container `href: false` and it becomes an id-only entry with no link; +children point their `parent` at that id. Add `collapsible: false` for a header +that stays open: + +```tsx +defineMenu({ + team: { title: "Team", href: false, collapsible: false }, + "/members": { title: "Members", parent: "team" }, + "/roles": { title: "Roles", parent: "team" }, +}); +``` + +### Persisting open sections + +`subscribeOpenState` fires on every change without re-rendering; seed the next +mount from `defaultOpen`: + +```tsx +const saved = JSON.parse(localStorage.getItem("menu-open") ?? "{}"); +const { menuProps, subscribeOpenState } = useMenu(menu, { defaultOpen: saved }); + +useEffect( + () => + subscribeOpenState((open) => + localStorage.setItem("menu-open", JSON.stringify(open)), + ), + [subscribeOpenState], +); +``` + +### A search box (or anything) in the shell + +The render-prop `children` gives you full control of the wrapper — put a search +box, header, or footer around `{items}`. Its state is yours; keep the shell a +stable component so that state survives re-renders: + +```tsx + + {(items) => ( + + )} + ``` +If the search filters the menu, filter the `menu` data outside `` and pass +the result — disclosure state is keyed by `id`, so filtering never loses open +sections. + +### Custom entries in a generated menu + +`menuInputFromRouteTree` returns plain `MenuInput`, so spread it and add, +override, or re-parent entries in the same object — `parent` is type-checked +against the route paths: + +```tsx +const menu = defineMenu({ + ...menuInputFromRouteTree(routeTree), + "/changelog": { title: "Changelog", parent: "/settings" }, // entry with no route + "/settings": { title: "Settings", order: 0 }, // override generated + "https://docs.example.com": { title: "Docs" }, // external link +}); +``` + +### Menu with icons + +The model has no `icon` field — it stays free of React. Put the icon in `meta` +(which the library carries but never reads) and render it in your `Item`: + +```tsx +import type { MenuItemPropsOf } from "@dennation/menu/react"; + +const menu = defineMenu({ + "/": { title: "Dashboard", meta: { icon: } }, + "/settings": { title: "Settings", meta: { icon: } }, +}); + +// `MenuItemPropsOf` infers the meta type from `menu` — no repeat. +function Item(props: MenuItemPropsOf) { + return ( + + {props.item.meta?.icon} + {props.item.title} + + ); +} +``` + +With the TanStack adapter, icons ride along in `staticData.menu.meta`. + ## License MIT diff --git a/package.json b/package.json index 50010b6..ade4af8 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,35 @@ { "name": "@dennation/menu", - "version": "0.1.0", - "description": "Router-agnostic navigation menu model, React renderer, and TanStack Router adapter", + "version": "0.0.0", + "description": "Headless, typesafe, router-agnostic navigation menu — a data model, a React renderer, and a useMenu hook.", + "keywords": [ + "menu", + "navigation", + "nav", + "sidebar", + "breadcrumbs", + "tree", + "headless", + "typesafe", + "router-agnostic", + "react", + "react-hook", + "tanstack-router", + "typescript" + ], + "homepage": "https://github.com/dennation/menu#readme", + "bugs": "https://github.com/dennation/menu/issues", + "license": "MIT", + "author": "Denis Goncharenko", + "repository": { + "type": "git", + "url": "git+https://github.com/dennation/menu.git" + }, "type": "module", "sideEffects": false, + "publishConfig": { + "access": "public" + }, "exports": { ".": { "types": "./dist/index.d.ts", @@ -25,10 +51,15 @@ "build": "vite build", "dev": "vite build --watch", "typecheck": "tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "prepublishOnly": "pnpm run build", + "changeset": "changeset", + "version": "changeset version", + "release": "changeset publish" }, "devDependencies": { "@biomejs/biome": "2.3.14", + "@changesets/cli": "^2.31.1", "@tanstack/react-router": "^1.170.4", "@tanstack/router-core": "^1.171.2", "@testing-library/react": "^16.1.0", @@ -59,10 +90,5 @@ "optional": true } }, - "packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8", - "repository": { - "type": "git", - "url": "git+https://github.com/dennation/menu.git" - }, - "license": "MIT" + "packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da51888..621e88d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@biomejs/biome': specifier: 2.3.14 version: 2.3.14 + '@changesets/cli': + specifier: ^2.31.1 + version: 2.31.1(@types/node@22.20.1) '@tanstack/react-router': specifier: ^1.170.4 version: 1.170.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -135,6 +138,61 @@ packages: cpu: [x64] os: [win32] + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -319,9 +377,24 @@ packages: cpu: [x64] os: [win32] + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@microsoft/api-extractor-model@7.33.10': resolution: {integrity: sha512-uPUK17xGxeQ3av6TN7awp+dTTSTvx8fZC0XFL1gyt7hFapYuxND3XLXH8vkmI7UjfW92oogzFAFqHSifmJROaw==} @@ -335,6 +408,18 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@rollup/pluginutils@5.4.0': resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} engines: {node: '>=14.0.0'} @@ -573,6 +658,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} @@ -671,6 +759,10 @@ packages: alien-signals@0.4.14: resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -682,9 +774,16 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -696,6 +795,10 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + brace-expansion@2.1.2: resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} @@ -703,10 +806,17 @@ packages: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -722,6 +832,10 @@ packages: cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + cssstyle@4.6.0: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} @@ -752,13 +866,25 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -779,6 +905,11 @@ packages: engines: {node: '>=18'} hasBin: true + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -792,12 +923,22 @@ packages: exsolve@1.1.0: resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-uri@3.1.4: resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -807,10 +948,26 @@ packages: picomatch: optional: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + fs-extra@11.3.6: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -819,6 +976,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -846,10 +1011,22 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} + hasBin: true + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -858,19 +1035,50 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + isbot@5.2.1: resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} engines: {node: '>=18'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + hasBin: true + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -883,6 +1091,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} @@ -893,6 +1104,13 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -903,6 +1121,14 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + minimatch@10.2.3: resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} engines: {node: 18 || 20 || >=22} @@ -914,6 +1140,10 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -932,25 +1162,71 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -961,6 +1237,11 @@ packages: resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -972,6 +1253,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -984,15 +1268,27 @@ packages: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1001,6 +1297,9 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1026,9 +1325,25 @@ packages: resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} engines: {node: '>=10'} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1037,6 +1352,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -1050,6 +1368,14 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} @@ -1061,6 +1387,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1083,6 +1413,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -1102,6 +1436,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -1225,6 +1563,11 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1315,6 +1658,149 @@ snapshots: '@biomejs/cli-win32-x64@2.3.14': optional: true + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.4 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.4 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.1(@types/node@22.20.1)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@22.20.1) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.4 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.4 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.3.0 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.0 + prettier: 2.8.8 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -1413,8 +1899,31 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@inquirer/external-editor@1.0.3(@types/node@22.20.1)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.1 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + '@microsoft/api-extractor-model@7.33.10(@types/node@22.20.1)': dependencies: '@microsoft/tsdoc': 0.16.0 @@ -1450,6 +1959,18 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': dependencies: '@types/estree': 1.0.9 @@ -1634,6 +2155,8 @@ snapshots: '@types/estree@1.0.9': {} + '@types/node@12.20.55': {} + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 @@ -1756,6 +2279,8 @@ snapshots: alien-signals@0.4.14: {} + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-styles@5.2.0: {} @@ -1764,16 +2289,24 @@ snapshots: dependencies: sprintf-js: 1.0.3 + argparse@2.0.1: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 + array-union@2.1.0: {} + assertion-error@2.0.1: {} balanced-match@1.0.2: {} balanced-match@4.0.4: {} + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + brace-expansion@2.1.2: dependencies: balanced-match: 1.0.2 @@ -1782,8 +2315,14 @@ snapshots: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + chai@6.2.2: {} + chardet@2.2.0: {} + compare-versions@6.1.1: {} confbox@0.1.8: {} @@ -1794,6 +2333,12 @@ snapshots: cookie-es@3.1.1: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 @@ -1816,10 +2361,21 @@ snapshots: dequal@2.0.3: {} + detect-indent@6.1.0: {} + diff@8.0.4: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + dom-accessibility-api@0.5.16: {} + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + entities@6.0.1: {} entities@7.0.1: {} @@ -1857,6 +2413,8 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esprima@4.0.1: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -1867,25 +2425,73 @@ snapshots: exsolve@1.1.0: {} + extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-uri@3.1.4: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + fs-extra@11.3.6: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + graceful-fs@4.2.11: {} has-flag@4.0.0: {} @@ -1914,24 +2520,57 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@4.2.0: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + import-lazy@4.0.0: {} is-core-module@2.16.2: dependencies: hasown: 2.0.4 + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + isbot@5.2.1: {} + isexe@2.0.0: {} + jju@1.4.0: {} js-tokens@4.0.0: {} + js-yaml@3.15.0: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0: dependencies: cssstyle: 4.6.0 @@ -1961,6 +2600,10 @@ snapshots: json-schema-traverse@1.0.0: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.2.1: dependencies: universalify: 2.0.1 @@ -1975,6 +2618,12 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + lru-cache@10.4.3: {} lz-string@1.5.0: {} @@ -1983,6 +2632,13 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + minimatch@10.2.3: dependencies: brace-expansion: 5.0.7 @@ -1998,6 +2654,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + mri@1.2.0: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -2008,20 +2666,52 @@ snapshots: obug@2.1.4: {} + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + parse5@7.3.0: dependencies: entities: 6.0.1 path-browserify@1.0.1: {} + path-exists@4.0.0: {} + + path-key@3.1.1: {} + path-parse@1.0.7: {} + path-type@4.0.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.5: {} + pify@4.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -2040,6 +2730,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@2.8.8: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -2050,6 +2742,8 @@ snapshots: quansync@0.2.11: {} + queue-microtask@1.2.3: {} + react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 @@ -2059,8 +2753,17 @@ snapshots: react@19.2.7: {} + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.15.0 + pify: 4.0.1 + strip-bom: 3.0.0 + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -2068,6 +2771,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 @@ -2101,6 +2806,10 @@ snapshots: rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -2117,12 +2826,27 @@ snapshots: seroval@1.5.6: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + + slash@3.0.0: {} + source-map-js@1.2.1: {} source-map@0.6.1: {} + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + sprintf-js@1.0.3: {} stackback@0.0.2: {} @@ -2131,6 +2855,12 @@ snapshots: string-argv@0.3.2: {} + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + supports-color@8.1.1: dependencies: has-flag: 4.0.0 @@ -2139,6 +2869,8 @@ snapshots: symbol-tree@3.2.4: {} + term-size@2.2.1: {} + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -2156,6 +2888,10 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -2170,6 +2906,8 @@ snapshots: undici-types@6.21.0: {} + universalify@0.1.2: {} + universalify@2.0.1: {} use-sync-external-store@1.6.0(react@19.2.7): @@ -2254,6 +2992,10 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 diff --git a/src/__tests__/defineMenu.test.ts b/src/__tests__/defineMenu.test.ts index 22594b4..fbf1323 100644 --- a/src/__tests__/defineMenu.test.ts +++ b/src/__tests__/defineMenu.test.ts @@ -39,6 +39,16 @@ describe("defineMenu", () => { expect(menu[0].items?.map((i) => i.href)).toEqual(["/button"]); }); + it("sorts by `order` at nested levels too, not just the top", () => { + // The single stable sort must order children, not only root siblings. + const menu = defineMenu({ + "/s": { title: "S" }, + "/s/b": { title: "B", parent: "/s", order: 2 }, + "/s/a": { title: "A", parent: "/s", order: 1 }, + }); + expect(menu[0].items?.map((i) => i.title)).toEqual(["A", "B"]); + }); + it("supports a non-navigable container via `href: false`, addressed by its key", () => { const menu = defineMenu({ grp: { title: "Group", href: false }, @@ -87,10 +97,13 @@ describe("defineMenu", () => { }; const menu = defineMenu({ ...generated, - "/button": { title: "Button (override)", icon: "icon" }, + "/button": { title: "Button (override)", defaultOpen: false }, }); expect(menu.map((i) => i.href)).toEqual(["/button", "/about"]); - expect(menu[0]).toMatchObject({ title: "Button (override)", icon: "icon" }); + expect(menu[0]).toMatchObject({ + title: "Button (override)", + defaultOpen: false, + }); }); it("lets a later entry attach into an earlier section (custom child)", () => { @@ -102,33 +115,36 @@ describe("defineMenu", () => { expect(menu[0].items?.map((i) => i.href)).toEqual(["/button", "/x"]); }); - it("carries typed `meta` through verbatim (required when the type is plain)", () => { + it("carries typed `meta` through verbatim, inferring the type from the input", () => { interface Meta { badge: string; } - const menu = defineMenu({ - "/components": { title: "Components", meta: { badge: "section" } }, + const menu = defineMenu({ + "/components": { + title: "Components", + meta: { badge: "section" } as Meta, + }, "/search": { title: "Search", parent: "/components", - meta: { badge: "new" }, + meta: { badge: "new" } as Meta, }, }); const [section] = menu; - // `meta` is required, so no optional chaining on `.meta` on the output. + // `meta` inferred as `Meta`, required — no optional chaining on `.meta`. expect(section.meta.badge).toBe("section"); expect(section.items?.[0].meta.badge).toBe("new"); }); - it("makes `meta` optional (and omittable) when the type admits undefined", () => { + it("infers an optional `meta` type; omitted values just aren't present", () => { interface Meta { badge?: string; } - const menu = defineMenu({ - "/a": { title: "A" }, // meta omitted — allowed - "/b": { title: "B", meta: { badge: "new" } }, + const menu = defineMenu({ + "/a": { title: "A", meta: {} as Meta }, // meta type given, value empty + "/b": { title: "B", meta: { badge: "new" } as Meta }, }); - expect(menu[0]).not.toHaveProperty("meta"); - expect(menu[1].meta).toEqual({ badge: "new" }); + expect(menu[0].meta?.badge).toBeUndefined(); + expect(menu[1].meta?.badge).toBe("new"); }); }); diff --git a/src/__tests__/getMenuTrail.test.ts b/src/__tests__/getMenuTrail.test.ts new file mode 100644 index 0000000..6fc07e3 --- /dev/null +++ b/src/__tests__/getMenuTrail.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { defineMenu } from "../defineMenu"; +import { findMenuItem, findMenuItemBy } from "../findMenuItem"; +import { getMenuTrail, getMenuTrailBy } from "../getMenuTrail"; + +const menu = defineMenu({ + "/": { title: "Home" }, + "/components": { title: "Components" }, + "/components/button": { title: "Button", parent: "/components" }, +}); + +describe("getMenuTrail", () => { + it("returns the chain from the top level down to the id", () => { + expect(getMenuTrail(menu, "/components/button").map((i) => i.id)).toEqual([ + "/components", + "/components/button", + ]); + }); + + it("returns a single item for a top-level id", () => { + expect(getMenuTrail(menu, "/").map((i) => i.id)).toEqual(["/"]); + }); + + it("returns empty for an unknown id", () => { + expect(getMenuTrail(menu, "/missing")).toEqual([]); + }); +}); + +describe("findMenuItem", () => { + it("finds a nested item", () => { + expect(findMenuItem(menu, "/components/button")?.title).toBe("Button"); + }); + + it("returns undefined for an unknown id", () => { + expect(findMenuItem(menu, "/missing")).toBeUndefined(); + }); +}); + +describe("by predicate", () => { + it("getMenuTrailBy returns the branch of the first matching item", () => { + const trail = getMenuTrailBy(menu, (i) => i.title === "Button"); + expect(trail.map((i) => i.id)).toEqual([ + "/components", + "/components/button", + ]); + }); + + it("findMenuItemBy returns the first match (pre-order)", () => { + expect(findMenuItemBy(menu, (i) => i.title === "Button")?.id).toBe( + "/components/button", + ); + expect(findMenuItemBy(menu, () => false)).toBeUndefined(); + }); +}); diff --git a/src/adapters/tanstack-router/__tests__/menuInputFromRouteTree.test.ts b/src/adapters/tanstack-router/__tests__/menuInputFromRouteTree.test.ts index bdbcce1..c9e101d 100644 --- a/src/adapters/tanstack-router/__tests__/menuInputFromRouteTree.test.ts +++ b/src/adapters/tanstack-router/__tests__/menuInputFromRouteTree.test.ts @@ -5,7 +5,19 @@ import { } from "@tanstack/react-router"; import { describe, expect, it } from "vitest"; import { defineMenu } from "../../../defineMenu"; -import { menuInputFromRouteTree } from "../index"; +import { menuInputFromRouteTree, type RouteMenuEntry } from "../index"; + +interface Meta { + badge?: string; +} + +// The consumer registers `menu` on staticData under their own meta type — this +// is what types `staticData: { menu: { … } }` at each route. +declare module "@tanstack/router-core" { + interface StaticDataRouteOption { + menu?: RouteMenuEntry; + } +} /** Build an initialized route tree (router init populates `fullPath`). */ function buildTree() { @@ -16,7 +28,8 @@ function buildTree() { const button = createRoute({ getParentRoute: () => root, path: "button", - staticData: { menu: { title: "Button", order: 1 } }, + // `meta` is typed as `Meta` here thanks to the augmentation above. + staticData: { menu: { title: "Button", order: 1, meta: { badge: "new" } } }, }); // Section: a route with a path AND children. const components = createRoute({ @@ -62,6 +75,16 @@ describe("menuInputFromRouteTree", () => { expect(input["/about"]?.title).toBe("About"); }); + it("infers meta end-to-end from the register — no explicit type argument", () => { + // `M` flows from the registered StaticDataRouteOption through + // menuInputFromRouteTree into defineMenu — the menu is typed `Menu`. + const menu = defineMenu(menuInputFromRouteTree(buildTree())); + const button = menu.find((i) => i.href === "/button"); + // Typed as `Meta` (string | undefined), not `never`/`unknown`. + const badge: string | undefined = button?.meta?.badge; + expect(badge).toBe("new"); + }); + it("`omit`s a route together with its subtree", () => { const input = menuInputFromRouteTree(buildTree(), { omit: ["/components"], diff --git a/src/adapters/tanstack-router/index.ts b/src/adapters/tanstack-router/index.ts index 0312822..d27fa17 100644 --- a/src/adapters/tanstack-router/index.ts +++ b/src/adapters/tanstack-router/index.ts @@ -1,19 +1,28 @@ import type { AnyRoute } from "@tanstack/react-router"; -import type { RoutePaths } from "@tanstack/router-core"; -import type { ReactNode } from "react"; +import type { RoutePaths, StaticDataRouteOption } from "@tanstack/router-core"; import type { MenuItemInput } from "../../types"; /** - * How a route describes itself in a menu, read by the adapter's default - * `getRouteMenu` from `route.options.staticData.menu`. Composition decisions - * (excluding, overriding, re-parenting) belong to the authoring layer — `omit` - * here, keyed override and `parent` in `defineMenu`. + * The meta type registered on `staticData.menu` (see {@link RouteMenuEntry}), or + * `unknown` if not registered. Used as the default `M`, so `menuInputFromRouteTree` + * picks up your meta without an explicit type argument. + */ +export type RegisteredMeta = StaticDataRouteOption extends { + menu?: RouteMenuEntry; +} + ? M + : unknown; + +/** + * How a route describes itself in a menu. Register it on `staticData` in your + * app to type `staticData.menu` (including its `meta`) at the route: * * ```tsx - * createFileRoute('/button')({ - * component: ButtonPage, - * staticData: { menu: { title: 'Button', order: 1 } }, - * }) + * declare module "@tanstack/router-core" { + * interface StaticDataRouteOption { + * menu?: RouteMenuEntry; + * } + * } * ``` */ export interface RouteMenuEntry { @@ -21,19 +30,10 @@ export interface RouteMenuEntry { title?: string; /** Sort hint among siblings (lower first). */ order?: number; - icon?: ReactNode; /** Opaque per-item metadata, carried onto the menu node verbatim. */ meta?: M; } -declare module "@tanstack/router-core" { - interface StaticDataRouteOption { - // `unknown` meta: a module augmentation can't be generic, so the concrete - // meta type is applied at the `menuInputFromRouteTree` call site. - menu?: RouteMenuEntry; - } -} - export interface MenuInputFromRouteTreeOptions< TRouteTree extends AnyRoute, M = never, @@ -54,22 +54,14 @@ export type RouteMenuInput = Partial< >; /** - * Build a keyed menu input from a TanStack Router route tree. Each entry is - * keyed by `fullPath`, with `parent` pointing at the nearest navigable ancestor; - * spread the result into `defineMenu` to resolve it into a tree: - * - * ```tsx - * const menu = defineMenu({ - * ...menuInputFromRouteTree(routeTree, { omit: ['/about'] }), - * '/changelog': { title: 'Changelog', parent: '/components' }, - * '/button': { title: 'Button', icon: }, // overrides the generated one - * }) - * ``` - * - * The root and pathless/layout routes are transparent — their children attach to - * the nearest navigable ancestor. Routes in `omit` are dropped with their subtree. + * Build a keyed menu input from a TanStack Router route tree, keyed by `fullPath` + * with `parent` at the nearest navigable ancestor; spread into `defineMenu`. + * Pathless/layout routes are transparent; `omit` drops a route with its subtree. */ -export function menuInputFromRouteTree( +export function menuInputFromRouteTree< + TRouteTree extends AnyRoute, + M = RegisteredMeta, +>( routeTree: TRouteTree, options: MenuInputFromRouteTreeOptions = {}, ): RouteMenuInput { @@ -107,7 +99,6 @@ function toEntry( title: route?.title ?? titleFromPath(fullPath), ...(parentKey != null && { parent: parentKey }), ...(route?.order != null && { order: route.order }), - ...(route?.icon != null && { icon: route.icon }), ...(route?.meta !== undefined && { meta: route.meta }), }; } @@ -130,7 +121,12 @@ function childrenOf(route: AnyRoute): AnyRoute[] { function defaultGetRouteMenu( route: AnyRoute, ): RouteMenuEntry | undefined { - return route.options?.staticData?.menu; + // The adapter doesn't augment `StaticDataRouteOption` (that's the consumer's, + // so they can type `menu`/`meta`); read it through a local cast. + const staticData = route.options?.staticData as + | { menu?: RouteMenuEntry } + | undefined; + return staticData?.menu; } /** Title-case the last non-empty segment (`/user-settings` → `User Settings`). */ diff --git a/src/defineMenu.ts b/src/defineMenu.ts index 5a455f1..5154dc8 100644 --- a/src/defineMenu.ts +++ b/src/defineMenu.ts @@ -13,6 +13,22 @@ const UNORDERED = Number.MAX_SAFE_INTEGER; */ type MenuKeys = Extract; +/** + * The input `defineMenu` accepts, expressed against itself so `parent` is checked + * against the object's own keys ({@link MenuKeys}). `meta` is left `unknown` here + * and its real type is inferred separately by {@link MetaOf}. + */ +type DefineMenuInput = Record< + string, + MenuItemInput, unknown> | undefined +>; + +/** + * The per-item `meta` type inferred from a menu input — the `meta` field of its + * values (`NonNullable` drops the `Partial`/`undefined` from adapter results). + */ +type MetaOf = NonNullable extends { meta?: infer M } ? M : never; + /** * Meta-opaque views for the internal pipeline: the runtime never inspects `meta` * (it just rides through), so the resolver works at `unknown` meta and the @@ -27,17 +43,14 @@ type LooseNode = MenuItem; * equals), the input-only `parent`/`order` are stripped, and `href` falls back * to the entry key. An unknown `parent` hoists the entry to the top level. * - * `M` types the opaque per-item `meta`. It comes first so it can be given - * explicitly (`defineMenu(…)`) while `T` is still inferred from the - * argument; it passes through verbatim and is never read here. + * The per-item `meta` type is **inferred** from the input's `meta` fields (see + * {@link MetaOf}), so `defineMenu(menuInputFromRouteTree(tree))` yields the menu + * typed with your registered meta — no explicit type argument. `meta` passes + * through verbatim and is never read here. */ -export function defineMenu< - M = never, - const T extends Record< - string, - MenuItemInput, M> | undefined - > = Record>, ->(input: T): Menu { +export function defineMenu>( + input: T, +): Menu> { // One stable sort over the flat list. `Array#sort` is stable, so every // parent's bucket comes out ordered without a second per-level pass. const entries = Object.entries( @@ -46,25 +59,33 @@ export function defineMenu< .filter((entry): entry is [string, LooseInput] => entry[1] != null) .sort(([, a], [, b]) => (a.order ?? UNORDERED) - (b.order ?? UNORDERED)); - const nodeByKey = new Map( - entries.map(([key, item]) => [key, toNode(key, item)]), - ); + // Build every node up front; `nodeByKey` is only for resolving `parent`. + const placed = entries.map(([key, item]) => ({ + key, + item, + node: toNode(key, item), + })); + const nodeByKey = new Map(placed.map(({ key, node }) => [key, node])); const roots: LooseNode[] = []; - for (const [key, item] of entries) { - const node = nodeByKey.get(key) as LooseNode; + for (const { item, node } of placed) { const parent = item.parent == null ? undefined : nodeByKey.get(item.parent); if (parent) { parent.items ??= []; parent.items.push(node); } else { - if (item.parent != null) warnUnknownParent(item); + if (isDev && item.parent != null) warnUnknownParent(item); roots.push(node); } } - if (isDev) warnUnreachable(roots, entries.length); - return roots as unknown as Menu; + // A `parent` cycle links its nodes to each other but off the tree. + if (isDev && countNodes(roots) !== entries.length) + console.warn( + "[menu] cyclic `parent` detected; the cycle's items were dropped", + ); + + return roots as unknown as Menu>; } /** Build the output node: strip the input-only fields and resolve `href`. */ @@ -73,25 +94,20 @@ function toNode( { href, parent, order, ...fields }: LooseInput, ): LooseNode { // `meta`, when present, rides through in `...fields` untouched. - const target = href === false ? undefined : (href ?? key); - return { id: key, ...fields, ...(target != null && { href: target }) }; + const resolvedHref = href === false ? undefined : (href ?? key); + return { + id: key, + ...fields, + ...(resolvedHref != null && { href: resolvedHref }), + }; } function warnUnknownParent({ title, parent }: LooseInput): void { - if (!isDev) return; console.warn( `[menu] item "${title}" has unknown parent "${parent}"; hoisting to top level`, ); } -/** A cyclic `parent` chain links nodes to each other but off the tree. */ -function warnUnreachable(roots: LooseNode[], total: number): void { - if (countNodes(roots) === total) return; - console.warn( - "[menu] cyclic `parent` detected; the items in the cycle were dropped", - ); -} - function countNodes(nodes: LooseNode[]): number { return nodes.reduce( (count, node) => count + 1 + countNodes(node.items ?? []), diff --git a/src/findMenuItem.ts b/src/findMenuItem.ts new file mode 100644 index 0000000..5d25aa5 --- /dev/null +++ b/src/findMenuItem.ts @@ -0,0 +1,28 @@ +import { + getMenuTrail, + getMenuTrailBy, + type MenuItemPredicate, +} from "./getMenuTrail"; +import type { Menu, MenuItem } from "./types"; + +/** + * The first item (pre-order) matching `predicate`, or `undefined` — the tail of + * its trail, so the two share one traversal. Use it to resolve an item by any + * condition (router match, role, flag) and feed its `id` to `useMenu.setActive`. + * "First match" like `Array.find`: for prefix/most-specific resolution, make the + * predicate select exactly one item. + */ +export function findMenuItemBy( + menu: Menu, + predicate: MenuItemPredicate, +): MenuItem | undefined { + return getMenuTrailBy(menu, predicate).at(-1); +} + +/** {@link findMenuItemBy} for the common case of an exact `id` match. */ +export function findMenuItem( + menu: Menu, + id: string, +): MenuItem | undefined { + return getMenuTrail(menu, id).at(-1); +} diff --git a/src/getMenuTrail.ts b/src/getMenuTrail.ts new file mode 100644 index 0000000..1c776bd --- /dev/null +++ b/src/getMenuTrail.ts @@ -0,0 +1,34 @@ +import type { Menu, MenuItem } from "./types"; + +/** A test applied to an item during a tree walk. */ +export type MenuItemPredicate = (item: MenuItem) => boolean; + +/** + * The chain of items from the top level down to the first one matching + * `predicate`, inclusive: for a match on `/components/button` → + * `[Components, Button]`. Empty if nothing matches. + * + * This is both the breadcrumb trail and the set of ancestors to expand for an + * active item — `useMenu` uses it to open the branch of the active item. + */ +export function getMenuTrailBy( + menu: Menu, + predicate: MenuItemPredicate, +): MenuItem[] { + for (const item of menu) { + if (predicate(item)) return [item]; + if (item.items) { + const below = getMenuTrailBy(item.items, predicate); + if (below.length) return [item, ...below]; + } + } + return []; +} + +/** {@link getMenuTrailBy} for the common case of an exact `id` match. */ +export function getMenuTrail( + menu: Menu, + id: string, +): MenuItem[] { + return getMenuTrailBy(menu, (item) => item.id === id); +} diff --git a/src/index.ts b/src/index.ts index fd0642c..d1eccd3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,8 @@ export { defineMenu } from "./defineMenu"; -export type { - Menu, - MenuInput, - MenuItem, - MenuItemInput, - MenuItemState, - MenuSlot, -} from "./types"; +export { findMenuItem, findMenuItemBy } from "./findMenuItem"; +export { + getMenuTrail, + getMenuTrailBy, + type MenuItemPredicate, +} from "./getMenuTrail"; +export type { Menu, MenuInput, MenuItem, MenuItemInput } from "./types"; diff --git a/src/react/Menu.tsx b/src/react/Menu.tsx index c1b8294..eeb427c 100644 --- a/src/react/Menu.tsx +++ b/src/react/Menu.tsx @@ -1,13 +1,39 @@ import type { ComponentType, ReactNode } from "react"; -import { Fragment, useState } from "react"; -import type { MenuItem, MenuItemState, Menu as MenuModel } from "../types"; +import { + createContext, + memo, + useCallback, + useContext, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from "react"; +import type { MenuItem, Menu as MenuModel } from "../types"; +import { createMenuStateStore, type MenuStateStore } from "./menuStateStore"; /** Expanded state for a section that doesn't set `defaultOpen`. */ const DEFAULT_OPEN = true; -/** The single outer shell wrapping the whole menu (the consumer's `