diff --git a/.vale/styles/config/vocabularies/Unikraft/accept.txt b/.vale/styles/config/vocabularies/Unikraft/accept.txt index 88e61939..3d3fc7f7 100644 --- a/.vale/styles/config/vocabularies/Unikraft/accept.txt +++ b/.vale/styles/config/vocabularies/Unikraft/accept.txt @@ -31,6 +31,7 @@ CNAME COPY DCO EOF +ESM FROM HIPPA HMR diff --git a/package.json b/package.json index 0f9f9d21..06484987 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ "react": ">=19.0.0", "react-dom": ">=19.0.0", "regex-utilities": "^2.3.0", - "zudoku": "^0.86.0" + "zudoku": "^0.88.0" }, "devDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^9.14.0" diff --git a/pages/guides/overview.mdx b/pages/guides/overview.mdx index ebd42dd5..7ef0c08d 100644 --- a/pages/guides/overview.mdx +++ b/pages/guides/overview.mdx @@ -2,14 +2,31 @@ title: "Guides Overview" --- -This page lists all available guides for deploying applications and services on Unikraft Cloud, grouped by category. -These guides mimic the [examples](https://github.com/unikraft-cloud/examples) repository, and are constantly updated with new content. +This page lists all available guides for Unikraft Cloud. +The guides are in two groups: + +* **Tutorials** explain a platform topic that applies to any app. + Two examples are root filesystem formats and environment variables. +* **Example apps** show how to deploy one specific app or service. + They mimic the [examples](https://github.com/unikraft-cloud/examples) repository, and are constantly updated with new content. :::note Unikraft Cloud can run any workload—define it in a `Dockerfile` and it will run it. These guides are here to make that journey as fast as possible for you. ::: +## Tutorials + +- [Docker To Unikraft Cloud](/tutorials/docker-to-ukc) +- [KraftKit To Unikraft](/tutorials/kraftkit-to-unikraft) +- [Environment Variables](/tutorials/environment-variables) +- [Rootfs Formats](/tutorials/rootfs-formats) +- [Rootfs Compression](/tutorials/rootfs-compression) +- [Rootfses, Volumes and ROMs](/tutorials/rootfs-volumes-roms) +- [Scale To Zero Triggers](/tutorials/scale-to-zero-triggers) + +The sections below list the example apps, grouped by category. + {/* vale off */} ## HTTP Servers diff --git a/pages/introduction.mdx b/pages/introduction.mdx index 1efb3457..c534566c 100644 --- a/pages/introduction.mdx +++ b/pages/introduction.mdx @@ -18,7 +18,7 @@ New: GPU support in enterprise preview - read more [here](/platform/instances#gp ## Quick start - + 1. [Create a free account](https://console.unikraft.cloud/signup). diff --git a/pages/sdks/js.mdx b/pages/sdks/js.mdx index 3b8a5b6a..395086a8 100644 --- a/pages/sdks/js.mdx +++ b/pages/sdks/js.mdx @@ -1,561 +1,106 @@ --- title: JavaScript SDK +description: Install the @unikraft/cloud package and make your first request +navigation_label: Getting started navigation_icon: code --- -{/* vale off */} -{/* The prose below comes from the SDK README verbatim. */} -`@unikraft/cloud` is the official JavaScript/TypeScript SDK for the [Unikraft Cloud](https://unikraft.com) API. +`@unikraft/cloud` is the official JavaScript and TypeScript SDK for the [Unikraft Cloud](https://unikraft.com) API. +The package is fully typed, and it covers the per-metro [platform API](/api/platform/v1) and the global control plane API. ## Installation -```bash +```sh npm install @unikraft/cloud ``` -Requires Node.js 18 or later. +The SDK needs Node.js 22.12 or later, and it ships as ECMAScript modules (ESM) only. +From that version, Node.js can `require()` an ESM package, so CommonJS code can load it too. ## Quickstart +This example creates a client, starts an instance in the `fra` metro, waits until the instance runs, then deletes it. + ```ts title="quickstart.ts" import { UnikraftCloud } from "@unikraft/cloud"; const ukc = new UnikraftCloud({ token: process.env.UKC_TOKEN }); - -// By default the client is account-wide: every metro is asked in parallel and -// the results are merged, each tagged with the metro it came from. -for await (const inst of ukc.instances.list({ details: true })) { - console.log(inst.metro, inst.name, inst.state); -} - -// Creating a resource happens in one metro, so name it. const fra = ukc.metro("fra"); + const instance = await fra.instances.create({ image: "nginx:latest", - autostart: true, memory_mb: 256, + autostart: true, service_group: { services: [{ port: 443, handlers: ["tls", "http"], destination_port: 80 }], }, }); -// Operations chain off a reference, so a ref is written once. -await fra.instances.get({ name: instance.name! }).wait({ state: "running" }); -await ukc.instances.get({ name: instance.name! }).suspend(); -await fra.instances.delete([{ uuid: instance.uuid! }]); -``` - -## Authentication - -Pass a bearer `token` to the constructor, or set the `UKC_TOKEN` environment variable. -Create a token in the [Unikraft Cloud dashboard](https://console.unikraft.cloud). - -{/* vale on */} - -## Client options - -The `UnikraftCloud` constructor takes one configuration object, and every field is optional: - -| Option | Description | -| -------------------------- | -------------------------------------------------------------------------------------------------- | -| `token` | Bearer token, which falls back to the `UKC_TOKEN` environment variable | -| `metro` | Pin the client to one metro (for example, `fra`), or to a full `http(s)://` base address, which falls back to `UKC_METRO` | -| `metros` | The metros operations cover: `"all"` (the default), one metro, or a list | -| `baseUrl` | Explicit platform API base address, which overrides `metro` | -| `controlPlaneUrl` | Override the control plane API base address | -| `fetch` | Custom `fetch` implementation | -| `headers` | Extra headers sent with every request | -| `userAgent` | Override the `User-Agent` header | -| `proxyFromEnv` | Honour the proxy environment variables on Node, which defaults to `true` | - -`metros` takes precedence over `metro`, which stays the target for operations that need exactly one metro. -Set neither, and the scope covers every metro the account can reach, with `fra` as the target for single-metro operations. -The base address of a metro is `https://api..unikraft.cloud`. -A `baseUrl`, or a `metro` that already is an address, pins the client to that one endpoint, as described under [self-hosted and staging deployments](#self-hosted-and-staging-deployments). - -{/* vale off */} -## The two layers: porcelain and plumbing - -The SDK is explicitly two layers, and you choose per call which one you are in: - -| Layer | Where | What you get | -| ------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------- | -| **Porcelain** | `@unikraft/cloud` — `ukc.instances`, `ukc.volumes`, … | Short verbs, no envelope, auto-pagination, metro fan-out, chainable handles | -| **Plumbing** | `@unikraft/cloud/api/platform`, `.../api/controlplane` | The OpenAPI specification as written: `operationId` methods, raw envelope, one metro per call | - -The porcelain layer **holds** a plumbing client rather than extending one, so the two never blur together — and the raw client is always one property away: - -```ts -// Porcelain: unwrapped, metro-aware. -const inst = await ukc.instances.get({ name: "web" }); - -// Plumbing, from the same client and credentials. -const res = await ukc.api.platform.instances.getInstances({ count: 10, details: true }); -res.status; // "success" -res.data?.instances; - -// Plumbing, per resource (the escape hatch for anything not wrapped yet). -await ukc.instances.api.getInstanceLogs({ name: ["web"] }); - -// Plumbing, standalone — no porcelain involved. -import { InstancesApi, PlatformApi } from "@unikraft/cloud/api/platform"; -import { ControlPlaneApi } from "@unikraft/cloud/api/controlplane"; - -const api = new PlatformApi({ - baseUrl: "https://api.fra.unikraft.cloud", - token: process.env.UKC_TOKEN, -}); -await api.instances.getInstances({ count: 10 }); -``` - -Plumbing clients are metro-scoped by construction: they talk to whatever `baseUrl` names, and a single call can be redirected with `{ baseUrl }`. -Fanning out across metros is the porcelain layer's job. - -### Two APIs: platform and control plane - -Both Unikraft Cloud APIs defined by our [OpenAPI](https://github.com/unikraft-cloud/openapi) specification are wrapped: - -- **Platform** — metro-scoped resources (instances, volumes, services, certificates, autoscale, images). - Wrapped idiomatically at the top level; raw under `@unikraft/cloud/api/platform` and `ukc.api.platform`. -- **Control plane** — the global (non-metro) API for account, metros, images and self-hosted nodes. - Raw only, under `@unikraft/cloud/api/controlplane` and `ukc.api.controlplane`. - -```ts -for (const metro of (await ukc.api.controlplane.metros.listMetros()).data?.metros ?? []) { - console.log(metro.iata_code, metro.endpoint); -} -``` - -## Metros: one account, many regions - -The platform API is per-metro, but an account's instances are spread across them. -The porcelain layer treats **the metros in scope** as one namespace: reads fan out concurrently and are merged, and every result carries the `metro` it came from. - -```ts -const ukc = new UnikraftCloud({ token }); - -// Every metro the account can reach (discovered once, then cached). -for await (const inst of ukc.instances.list()) console.log(inst.metro, inst.name); - -// Explicit scopes — these also skip metro discovery entirely. -ukc.metro("fra").instances.list(); // one metro -ukc.metros(["fra", "dal"]).instances.list(); // several -ukc.instances.list({ metros: ["fra", "dal"] }); // just this call -ukc.instances.list({ metros: "all" }); // back to everything - -// What the account can reach. -for (const { metro, baseUrl } of await ukc.availableMetros()) console.log(metro, baseUrl); -``` - -Set the default scope at construction, too: - -```ts -new UnikraftCloud({ token }); // all metros (default) -new UnikraftCloud({ token, metro: "fra" }); // one metro -new UnikraftCloud({ token, metros: ["fra", "dal"] }); // several -``` - -`UKC_METRO` acts like `metro:` — setting it pins the client to that metro. - -| Code | Location | -| ----- | ------------------- | -| `fra` | Frankfurt, DE | -| `dal` | Dallas, TX, USA | -| `sin` | Singapore | -| `was` | Washington, DC, USA | -| `sfo` | San Francisco, USA | - -Metro discovery asks the control plane and trusts the endpoint it reports, so new [metros](/platform/metros) work without an SDK upgrade. -`KNOWN_METROS` lists the ones known when this version was published. - -### Rules the fan-out follows - -- **Reads** cover the whole scope. - Pages are interleaved in arrival order, so a slow metro never holds up a fast one. -- **A name identifies a resource within a metro.** - The same name can exist in several metros at once — usually because you deployed the same thing everywhere — so a name plus a wide scope may match more than one resource. - See [Names across metros](#names-across-metros). -- **`create` never fans out.** - It needs one metro: the client's, or the default metro (`metro:` / `UKC_METRO` / `fra`) when the scope is wider. -- **Bulk operations are bounded by the scope.** - Refs are located first and one call goes to each metro that matched, so `delete([{ name: "web" }])` under a wide scope deletes every `web` in scope. - Narrow the scope or qualify the ref to act on one. -- **The control plane is global**, so it is unaffected by scope. - -### Names across metros - -Names are scoped to a metro, so the same name can name a different resource in every metro. -Three ways to say what you mean: - -```ts -// 1. Qualify the ref. No search, no discovery — one request. -await ukc.instances.get({ name: "web", metro: "fra" }).suspend(); - -// 2. Narrow the scope, which qualifies every ref through it. -await ukc.metro("fra").instances.get({ name: "web" }).suspend(); - -// 3. Address every metro holding it, on purpose. -await ukc.instances.each({ name: "web" }).suspend(); // one result per metro -``` - -`{ uuid }` refs never need qualifying: a UUID identifies one resource wherever it lives. - -`get()` insists on exactly one match, because the next thing you write might be a mutation. -When a name matches in several metros it throws an `AmbiguousRefError` carrying the matches, so recovering costs no further requests: - -```ts -import { AmbiguousRefError } from "@unikraft/cloud"; - -try { - await ukc.instances.get({ name: "web" }).suspend(); -} catch (err) { - if (err instanceof AmbiguousRefError) { - err.metros; // ["fra", "dal", "sin"] - err.matches; // the instances themselves, each tagged with .metro - } -} -``` - -`each(ref)` is the deliberate plural. -It resolves the matches once, then runs each operation in the metro that holds it: - -```ts -const web = ukc.instances.each({ name: "web" }); - -await web.where(); // ["fra", "dal", "sin"] -await web.size(); // 3 -for (const inst of await web) console.log(inst); // the instances -await web.suspend(); // one result per metro -await web.edit().set({ memory_mb: 512 }).apply(); -``` - -Set operations return arrays and follow the same partial-failure rule as reads: successes are returned on the thrown `MetroFanoutError` as `err.results`. -`each()` exists on instances, volumes, services and certificates. - -### Partial failure - -A metro that is unreachable does not throw away the rest of the answer. -Healthy metros are drained first, then a `MetroFanoutError` naming the failures is thrown: - -```ts -import { MetroFanoutError } from "@unikraft/cloud"; - -try { - for await (const inst of ukc.instances.list()) use(inst); // fra, dal, was delivered -} catch (err) { - if (err instanceof MetroFanoutError) { - err.message; // "1 of 4 metros failed: sin (503)" - err.failures; // [{ metro: "sin", error: UnikraftCloudError }] - } -} -``` - -For a bulk operation, which cannot yield as it goes, the results that did succeed are attached to the thrown error as `err.results`. - -## Chainable handles - -Single-resource operations return a **handle**: a lazily-evaluated reference to one resource in one metro. -Awaiting a handle gives the resource; calling an operation on it returns another handle: - -```ts -await ukc.instances.get({ name: "web" }); // -> Instance -await ukc.instances.get({ name: "web" }).suspend(); // -> the suspended instance -await ukc.instances.get({ name: "web" }).update({ memory_mb: 512 }); - -await ukc.metro("fra").instances - .create({ image: "nginx:latest" }) - .wait({ state: "running", timeoutSeconds: 30 }) - .logs({ offset: -4096 }); - -await ukc.volumes.get({ name: "data" }).attach({ attach_to: { name: "web" }, at: "/data" }); -``` - -Nothing is sent until a handle is awaited or chained onto, and each step runs at most once however many times you await it. -What that costs depends on the scope: - -| Ref and scope | `get(ref).suspend()` | -| ------------------------------ | ----------------------------------------------------------------- | -| `{ name, metro }`, any scope | 1 request — the suspend. The ref says where. | -| `{ name }`, one metro in scope | 1 request — the scope says where. | -| `{ name }`, many metros | Locate first (one concurrent read per metro), then suspend where it lives — or throw `AmbiguousRefError` if several match. | - -Handles also answer where they landed, and mutating steps return what that endpoint reports (`suspend()` resolves to `{ uuid, name, state, previous_state }`, not a full instance): +await fra.instances.get({ uuid: instance.uuid! }).wait({ state: "running" }); +console.log(instance.name, instance.uuid); -```ts -const web = ukc.instances.get({ name: "web" }); -await web.where(); // "dal" -await web.resolve(); // { ref: { name: "web" }, metro: "dal", baseUrl: "..." } -``` - -Every idiomatic method is also available in non-chained form (`ukc.instances.logs({ name: "web" }, { offset: -4096 })`), which is exactly shorthand for `get(ref).logs(...)`. - -## Updating a resource - -The API models an update as a list of `{ prop, op, value }` triples, with `value` typed `unknown`. -That is the plumbing. -Idiomatically you write a **patch object** and the op is worked out for you — a value sets it, `null` removes it, and an omitted (or `undefined`) property is left alone, following JSON Merge Patch: - -```ts -await ukc.instances.get({ name: "web" }).update({ memory_mb: 512, vcpus: 2 }); -await ukc.instances.get({ name: "web" }).update({ env: { LOG_LEVEL: "debug" } }); -await ukc.instances.get({ name: "web" }).update({ autokill: null }); // remove it -await ukc.volumes.get({ name: "data" }).update({ size_mb: 2048 }); -await ukc.services.get({ name: "web" }).update({ soft_limit: 5, hard_limit: 20 }); -``` - -Every property is typed, so `memory_mb: "512"` and `tags: "prod"` no longer compile. -`image` takes the same string shorthand as `create`. - -When `set` is not what you mean — merging into a property, or removing individual members — stage the operations with `edit()` and send them as one request: - -```ts -await ukc.instances.get({ name: "web" }).edit() - .set({ memory_mb: 512 }) - .add({ env: { LOG_LEVEL: "debug" }, tags: ["prod"] }) - .del({ env: ["OLD_FLAG"], tags: ["staging"] }) - .apply(); - -// `null` in del() removes the property outright, not just some members. -await ukc.services.edit({ name: "web" }).del({ domains: null }).apply(); -``` - -`apply()` returns a handle like any other operation, so chaining continues: - -```ts -await ukc.metro("fra").instances - .edit({ name: "web" }) - .set({ memory_mb: 1024 }) - .apply() - .wait({ state: "running" }); -``` - -Both forms are one request, and both are available with a ref instead of a handle (`ukc.instances.update({ name: "web" }, { memory_mb: 512 })`, `ukc.instances.edit({ name: "web" })`). -The raw triples still work as an escape hatch: `update({ name: "web" }, [{ prop: "memory_mb", op: "set", value: 512 }])`. - -## Refs - -Every operation on an existing resource takes a **ref**: either `{ name }` or `{ uuid }`, never both. -The API validates each identifier it is given, so a name sent in the `uuid` filter fails with `Invalid uuid ''` — the ref makes you state which kind you hold, and only that field is sent. - -```ts -await ukc.instances.get({ name: "web" }); -await ukc.instances.get({ uuid: "550e8400-e29b-41d4-a716-446655440000" }); - -// A name belongs to a metro, so it can be qualified — see "Names across metros". -await ukc.instances.get({ name: "web", metro: "fra" }); - -// Bulk operations take one ref or an array of them. -await ukc.instances.stop([{ name: "web" }, { uuid: "550e8400-e29b-41d4-a716-446655440000" }]); -``` - -## Idiomatic methods - -| Resource | Methods | -| ------------------ | ----------------------------------------------------------------------------------------------- | -| `ukc.instances` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete`, `start`, `stop`, `suspend`, `wait`, `metrics`, `history`, `logs` | -| `ukc.volumes` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete`, `attach`, `detach` | -| `ukc.services` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete` | -| `ukc.certificates` | `create`, `get`, `each`, `list`, `update`, `delete` | -| `ukc.users` | `quotas`, `quotasByUuid`, `add` | -| `ukc.sandboxes` | `create`, `connect`, `list` | - -Handles add the per-resource operations: `refresh`, `start`, `stop`, `suspend`, `delete`, `update`, `edit`, `wait`, `logs`, `metrics`, `history` on an instance; `attach`, `detach`, `update`, `edit`, `delete` on a volume; `update`, `edit` and `delete` on a service group. - -Anything not listed — autoscale, the image registry, node information, the whole control plane — is reachable raw via `ukc.api.platform.*` and `ukc.api.controlplane.*`: - -```ts -await ukc.api.platform.autoscale.getAutoscaleConfigurations({ uuid: ["sg1"] }); -await ukc.api.platform.images.getImages({}); -``` - -See [`examples/`](https://github.com/unikraft-cloud/js-sdk/tree/HEAD/examples) for complete programs. - -## Sandboxes - -A **sandbox** is an instance running the [sandbox plugin](/features/plugins#sandbox-plugin): a microVM you hand commands to and move files in and out of, over the instance's own authenticated endpoint. -`Sandbox` wraps that plugin, so none of it is written by hand. - -```ts -import { Sandbox } from "@unikraft/cloud"; - -// Nothing is named, so the token comes from `UKC_TOKEN`, the image from -// `UKC_SANDBOX_IMAGE` or the default, and the metro from `UKC_METRO` or the -// default. `await using` deletes the sandbox at the end of this scope, -// however the scope ends. -await using sandbox = await Sandbox.create(); - -const { stdout } = await sandbox.exec("echo hello"); -console.log(stdout); // hello -``` - -`await using` needs TypeScript 5.2 or newer and a runtime with explicit resource management. -Without it, call `sandbox.delete()` yourself. - -### Configuring a sandbox - -`Sandbox.create` takes two arguments: the sandbox itself, then the client that reaches it. -The first accepts every field `POST /instances` accepts, plus `rom` and `pluginName` for the plugin underneath. - -```ts -const sandbox = await Sandbox.create( - { - image: "nginx:latest", - memory_mb: 1024, - env: { LOG_LEVEL: "debug" }, - rom: "your_org/your_plugin:latest", // your own plugin ROM - }, - { - token: process.env.UKC_TOKEN, - metro: "fra", - bootTimeoutSeconds: 60, - ready: { timeoutMs: 30_000 }, - }, -); - -// The instance underneath stays reachable, so its whole surface still works. -const { name, state, memory_mb } = await sandbox.instance; -console.log(`${name} in ${sandbox.metro}: ${state}, ${memory_mb} MiB`); -``` - -### Running commands - -`exec` runs a command to completion and hands back what it wrote. -Each command takes its own working directory and environment: - -```ts -const build = await sandbox.exec("ls -la", { cwd: "/tmp", env: { CI: "1" } }); -console.log(`exit ${build.exitcode}: ${build.stdout}${build.stderr}`); -``` - -`start` does not wait, so a long-running command stays under your control: write to its standard input, wait for it, then read its logs. -`signal("TERM")` ends one that does not stop on its own. - -```ts -const server = await sandbox.start("cat", { cwd: "/work" }); -await server.stdin("first line\n"); -await server.stdin("last line\n", { eof: true }); -await server.wait({ timeoutSeconds: 10 }); -const logs = await server.logs(); -console.log(logs.stdout); -await server.delete(); -``` - -### Moving files - -```ts -await sandbox.mkdir("/work/input"); -await sandbox.writeFile("/work/input/a.txt", "hello\n"); -await sandbox.upload("/work/input", "data.csv", "id,name\n1,fra\n"); -const bytes = await sandbox.readFile("/work/input/a.txt"); -console.log(new TextDecoder().decode(bytes)); -``` - -### Reconnecting, and going through a client - -A sandbox outlives the process that created it, so the UUID and the metro are all a later process needs to attach again: - -```ts -const again = await Sandbox.connect( - { uuid: sandbox.uuid, metro: sandbox.metro }, - { token: process.env.UKC_TOKEN }, -); +await fra.instances.delete([{ uuid: instance.uuid! }]); ``` -A client you already have opens the same door, and spends no second token: - -```ts -const ukc = new UnikraftCloud({ token: process.env.UKC_TOKEN }); -const sandbox = await ukc.metro("fra").sandboxes.create({ memory_mb: 512 }); -console.log((await sandbox.exec("uname -a")).stdout); +Three ideas from this example come back on every page of this section: -// Every sandbox in the metro, which is the instance list filtered by plugin. -for await (const each of ukc.metro("fra").sandboxes.list()) { - console.log(`- ${each.uuid}`); -} +- `ukc.metro("fra")` limits the client to one [metro](/sdks/js/metros). + Without a limit, a read covers every metro of the account. +- `fra.instances.get(...)` returns a [handle](/sdks/js/resources#handles). + A handle sends nothing until you await it, and every operation on it returns a new handle, so `.wait(...)` chains. +- `{ uuid }` is a [ref](/sdks/js/resources#refs). + A ref is `{ name }` or `{ uuid }`, and it names one resource. -await sandbox.delete(); -``` - -See [`examples/sandbox.ts`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/examples/sandbox.ts) for the whole program, and [sandboxes](/use-cases/sandboxes) for what the platform underneath gives them. - -## Errors +## Authentication -Any network failure or non-2xx response throws an `UnikraftCloudError`: +Every request carries a bearer token. +Create the token in the [Unikraft Cloud console](https://console.unikraft.cloud), then pass it to the constructor: ```ts -import { UnikraftCloudError } from "@unikraft/cloud"; - -try { - await ukc.metro("fra").instances.get({ name: "does-not-exist" }); -} catch (err) { - if (err instanceof UnikraftCloudError) { - console.error(err.kind, err.status, err.message, err.errors); - } -} +const ukc = new UnikraftCloud({ token: "..." }); ``` -`err.kind` is `"http"`, `"network"`, `"parse"`, or `"fanout"` (a multi-metro operation that partly failed, or an unusable scope — see `MetroFanoutError` above). - -## Self-hosted and staging deployments +Or set the `UKC_TOKEN` environment variable, and create the client with no arguments: -`metro` (and `UKC_METRO`) also accepts a full `http(s)://` base URL, which is used verbatim instead of being expanded into `https://api..unikraft.cloud`. -A trailing `/v1` is dropped, since every operation path already carries it: - -```bash -export UKC_METRO=https://api.staging.example.internal -``` - -A named endpoint is the only endpoint there is: no metro discovery is attempted and no hostnames are invented, whatever the scope says. -Point the control plane at a matching deployment with `controlPlaneUrl`: - -```ts -const ukc = new UnikraftCloud({ - token, - metro: "https://api.staging.example.internal", - controlPlaneUrl: "https://controlplane.staging.example.internal", -}); +```sh +export UKC_TOKEN=... ``` -## Runtime support - -`fetch` is used from the global scope. -On Node.js 18+ it is built in. -For older runtimes, or to customise transport, pass your own: - ```ts -import { UnikraftCloud } from "@unikraft/cloud"; -const ukc = new UnikraftCloud({ token, fetch: myFetch }); +const ukc = new UnikraftCloud(); ``` -## Debugging with a proxy (MITM) +## Two layers -On Node, the client honours the standard proxy environment variables so you can route traffic through a man-in-the-middle proxy such as [mitmproxy](https://mitmproxy.org), Charles, or Proxyman without any code change: +The SDK has two layers, and you choose the layer per call. -```bash -npm install undici # optional peer dependency, enables proxy support -export HTTPS_PROXY=http://127.0.0.1:8080 -export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem # trust the proxy CA -node your-script.js -``` +| Layer | Import | What you get | +| ------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| **Idiomatic** | `@unikraft/cloud` | Short verbs such as `ukc.instances.get`, results without the response envelope, automatic pagination, metro fan-out, chainable handles | +| **Raw** | `@unikraft/cloud/api/platform`, `@unikraft/cloud/api/controlplane` | One method per `operationId` of the OpenAPI specification, the raw response envelope, one metro per call | -`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` and `NO_PROXY` are all recognised, in upper- or lower-case. -Node's global `fetch` ignores these by itself, so the SDK applies them via undici's `EnvHttpProxyAgent`, imported lazily only when a proxy variable is set. -If `undici` is not installed, requests proceed unproxied and a one-time warning is logged. +Use the idiomatic layer by default. +The raw layer is one property away, as `ukc.api`, for every operation that the idiomatic layer doesn't cover yet. +See [the raw API](/sdks/js/resources#the-raw-api). -Opt out per client with `new UnikraftCloud({ proxyFromEnv: false })`. -Proxy support is Node-only; browsers and Deno ignore these variables. +## Pages in this section -{/* vale on */} +| Page | Content | +| -------------------------------- | -------------------------------------------------------------------------------- | +| [Client](/sdks/js/client) | Constructor options, endpoints, self-hosted deployments, runtime, proxy support | +| [Metros](/sdks/js/metros) | How one client covers many metros, and how names behave across them | +| [Resources](/sdks/js/resources) | Refs, handles, updates, the method list, and the raw API | +| [Errors](/sdks/js/errors) | The error classes, the error kinds, and how waits behave | +| [Sandboxes](/sdks/js/sandboxes) | Run commands and move files inside a microVM | ## Source -The SDK source lives at [github.com/unikraft-cloud/js-sdk](https://github.com/unikraft-cloud/js-sdk), and the published package at [npmjs.com/package/@unikraft/cloud](https://www.npmjs.com/package/@unikraft/cloud). +- Source code: [github.com/unikraft-cloud/js-sdk](https://github.com/unikraft-cloud/js-sdk) +- Package: [npmjs.com/package/@unikraft/cloud](https://www.npmjs.com/package/@unikraft/cloud) +- Complete programs: [`examples/`](https://github.com/unikraft-cloud/js-sdk/tree/HEAD/examples) ## Learn more * [Python SDK](/sdks/python): the client library for the same platform API in Python. * [Go SDK](/sdks/go): the client library for the same platform API in Go. -* [Metros](/platform/metros): the regions that the SDK fans out across. -* [Instances](/platform/instances): create and manage the instances that the SDK drives. -* [Services](/platform/services): the load-balanced networking that `ukc.services` configures. -* Unikraft Cloud's [REST API reference](/api/platform/v1), which the plumbing layer mirrors. +* [REST API reference](/api/platform/v1): the platform API that the raw layer mirrors. diff --git a/pages/sdks/js/client.mdx b/pages/sdks/js/client.mdx new file mode 100644 index 00000000..20e3a1a9 --- /dev/null +++ b/pages/sdks/js/client.mdx @@ -0,0 +1,102 @@ +--- +title: Client +description: Configure the UnikraftCloud client, its endpoints, and its transport +navigation_icon: settings +--- + +The `UnikraftCloud` class is the entry point of the SDK. +One client holds one token, one metro scope, and one transport, and every resource belongs to it. + +## Create a client + +The constructor takes one configuration object. +Every field is optional. + +```ts +import { UnikraftCloud } from "@unikraft/cloud"; + +const ukc = new UnikraftCloud({ + token: process.env.UKC_TOKEN, + metro: "fra", +}); +``` + +## Options + +| Option | Default | Description | +| ----------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- | +| `token` | `UKC_TOKEN` | The bearer token. | +| `metro` | `UKC_METRO` | One metro code, such as `fra`, or a full `http(s)://` address. Operations that need one metro use this one. | +| `metros` | `"all"` | The metros that operations cover: `"all"`, one metro code, or a list of codes. | +| `baseUrl` | | The platform API address. It overrides `metro`. | +| `controlPlaneUrl` | | The control plane API address. | +| `fetch` | The global `fetch` | A custom `fetch` implementation. | +| `headers` | | Extra headers that the client sends with every request. | +| `userAgent` | `@unikraft/cloud` | The `User-Agent` header. | +| `proxyFromEnv` | `true` | Read the proxy environment variables on Node.js. | + +`metros` sets the scope of reads and bulk operations. +`metro` sets the target of operations that need exactly one metro, such as `create`. +When you set both, `metros` wins for the scope, and `metro` stays the target. +When you set neither, the scope is every metro that the account can reach, and the target is `fra`. +[Metros](/sdks/js/metros) explains the scope in detail. + +## Endpoints + +A metro code expands to `https://api..unikraft.cloud`. +The client asks the control plane for the metros of the account, and it uses the endpoint that the control plane reports. +As a result, a new metro works without an SDK upgrade. + +## Self-hosted and staging deployments + +`metro` and `UKC_METRO` also accept a full `http(s)://` address. +The client uses the address as given. +It drops a trailing `/v1`, because every operation path already carries it. + +```sh +export UKC_METRO=https://api.staging.example.internal +``` + +A named address is the only endpoint that the client uses. +It does no metro discovery, and it invents no hostnames, whatever the scope says. +Point the control plane at the same deployment with `controlPlaneUrl`: + +```ts +const ukc = new UnikraftCloud({ + token, + metro: "https://api.staging.example.internal", + controlPlaneUrl: "https://controlplane.staging.example.internal", +}); +``` + +## Runtime + +The SDK needs Node.js 22.12 or later, and it ships as ECMAScript modules (ESM) only. +From that version, Node.js can `require()` an ESM package, so CommonJS code can load it too. + +The client uses the global `fetch`, which Node.js supplies. +To use another runtime, or to control the transport, pass your own: + +```ts +const ukc = new UnikraftCloud({ token, fetch: myFetch }); +``` + +## Proxy support + +On Node.js, the client reads the standard proxy environment variables. +This lets you route its traffic through a proxy such as [mitmproxy](https://mitmproxy.org), Charles, or Proxyman without a code change. + +```sh +npm install undici +export HTTPS_PROXY=http://127.0.0.1:8080 +export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem +node your-script.js +``` + +The client recognises `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY`, in either letter case. +The global `fetch` of Node.js ignores these variables, so the SDK applies them through the `EnvHttpProxyAgent` of `undici`. +`undici` is an optional peer dependency, and the SDK imports it only when you set a proxy variable. +If `undici` isn't installed, the SDK sends the requests without a proxy, and it logs one warning. + +To turn this off for one client, set `proxyFromEnv: false`. +Browsers and Deno ignore these variables. diff --git a/pages/sdks/js/errors.mdx b/pages/sdks/js/errors.mdx new file mode 100644 index 00000000..77ef33d2 --- /dev/null +++ b/pages/sdks/js/errors.mdx @@ -0,0 +1,80 @@ +--- +title: Errors +description: The error classes that the SDK throws, the error kinds, and how waits behave +navigation_icon: triangle-alert +--- + +The SDK throws for every network failure and for every response with a status outside 2xx. +It never returns an error envelope as a value. + +## `UnikraftCloudError` + +Every failure of a request or of a wait is an `UnikraftCloudError`, or a subclass of it. +Two other cases throw a native error instead: + +- A wrong argument throws a `TypeError` or a `RangeError`. For example, a reference with no `uuid` and no `name`, or a wait duration that isn't a usable number. +- A wait that your `AbortSignal` cancels rethrows the reason of that signal. + +Thus, check `instanceof UnikraftCloudError` before you read `kind`, `status`, or `errors`. + +```ts +import { UnikraftCloudError } from "@unikraft/cloud"; + +try { + await ukc.metro("fra").instances.get({ name: "does-not-exist" }); +} catch (err) { + if (err instanceof UnikraftCloudError) { + console.error(err.kind, err.status, err.message, err.errors); + } +} +``` + +| Property | Content | +| --------- | -------------------------------------------------------------------------- | +| `kind` | Which layer failed. See [error kinds](#error-kinds). | +| `status` | The HTTP status, when the server answered. | +| `message` | A sentence that names the failure. | +| `errors` | The error list from the response envelope, when the server sent one. | +| `body` | The parsed response body, when there is one. | +| `cause` | The failure underneath, for a wait that ran out of time. | + +## Error kinds + +| `kind` | Meaning | +| ----------- | ----------------------------------------------------------------------------------------------------------------------- | +| `"http"` | The server answered with a status outside 2xx. `err.status` and `err.errors` carry the detail. | +| `"network"` | The request got no answer: a refused connection, a reset, a DNS miss, or an unreachable proxy. | +| `"parse"` | The server answered, but the body isn't the JSON that the operation expects. | +| `"fanout"` | A multi-metro operation failed in part, or the scope is unusable. See [`MetroFanoutError`](#errors-of-the-metro-fan-out). | +| `"config"` | The SDK couldn't send the call as configured: a missing token, or two options that contradict each other. | +| `"timeout"` | A wait ran out of time. It carries no `status`, because no single request failed. The last failure is in `err.cause`. | + +## Errors of the metro fan-out + +Two subclasses come from operations that span metros. +[Metros](/sdks/js/metros) explains both in context. + +| Class | When | Extra properties | +| ------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- | +| `AmbiguousRefError` | A `{ name }` ref matched a resource in more than one metro. | `metros`, and `matches` with the resources | +| `MetroFanoutError` | One or more metros failed while the others answered. | `failures`, and `results` for a bulk operation | + +## Waits and retries + +The transport sends each request exactly once. +It has no retries, and nothing in the SDK retries a call that you made. + +A wait is a separate, explicit step, and it takes one of two shapes: + +- **The server waits.** + `instance.wait({ state: "running" })` sends the deadline to the platform, and the platform holds the connection open. + This is one request. +- **The SDK polls.** + Where the platform has nothing to hold open, the SDK asks again on a schedule. + The delay doubles from 100 ms to 2 s with jitter, and the deadline is 60 s. + The deadline and a `signal` both stop the probe in flight, not only the loop around it. + Only a failure that can still change gets another probe: a network fault, or a `404`, `502`, `503`, or `504`. + The SDK throws a `401` or a `403` at once, so a rejected token reports itself instead of a timeout. + +Both shapes reject on failure. +A poll that runs out of time throws `kind: "timeout"`. diff --git a/pages/sdks/js/metros.mdx b/pages/sdks/js/metros.mdx new file mode 100644 index 00000000..f22eeecf --- /dev/null +++ b/pages/sdks/js/metros.mdx @@ -0,0 +1,142 @@ +--- +title: Metros +description: How one client covers many metros, and how names behave across them +navigation_icon: earth +--- + +The platform API is per metro, but the instances of one account live in more than one metro. +The SDK treats the metros in scope as one namespace. +A read asks every metro at the same time and merges the answers, and every result carries the `metro` it came from. + +## The scope + +Set the scope for the client, for a group of calls, or for one call. + +```ts +// The client. The default is every metro the account can reach. +const ukc = new UnikraftCloud({ token }); +new UnikraftCloud({ token, metro: "fra" }); +new UnikraftCloud({ token, metros: ["fra", "dal"] }); + +// A group of calls. +ukc.metro("fra").instances.list(); +ukc.metros(["fra", "dal"]).instances.list(); + +// One call. +ukc.instances.list({ metros: ["fra", "dal"] }); +ukc.instances.list({ metros: "all" }); +``` + +An explicit scope skips metro discovery. +`UKC_METRO` works like `metro`, so it limits the client to that metro. + +To see which metros the account can reach: + +```ts +for (const { metro, baseUrl } of await ukc.availableMetros()) { + console.log(metro, baseUrl); +} +``` + +The client discovers the metros once, and it caches the result. +It asks the control plane and trusts the endpoint that the control plane reports, so a new [metro](/platform/metros) works without an SDK upgrade. +`KNOWN_METROS` lists the metros known at the release of this version: + +| Code | Location | +| ----- | ------------------- | +| `fra` | Frankfurt, DE | +| `dal` | Dallas, TX, USA | +| `sin` | Singapore | +| `was` | Washington, DC, USA | +| `sfo` | San Francisco, USA | + +## How operations behave in a scope + +- **A read covers the whole scope.** + Pages arrive in the order that the metros answer, so a slow metro doesn't delay a fast one. +- **`create` never fans out.** + It needs one metro: the metro of the client, or the default target (`metro`, `UKC_METRO`, or `fra`) when the scope is wider. +- **A bulk operation acts on the whole scope.** + The SDK locates the refs first, then sends one call to each metro that matched. + In a wide scope, `delete([{ name: "web" }])` deletes every `web` in scope. + Make the scope narrower, or qualify the ref, to act on one resource. +- **The control plane is global.** + The scope doesn't affect it. + +## Names across metros + +A name identifies a resource within one metro. +The same name can exist in more than one metro at once, often because you deployed the same thing everywhere. +As a result, a name with a wide scope can match more than one resource. +A `{ uuid }` ref never has this problem, because a UUID identifies one resource wherever it lives. + +You have three ways to say which resource you mean: + +```ts +// 1. Qualify the ref. One request, no search. +await ukc.instances.get({ name: "web", metro: "fra" }).suspend(); + +// 2. Narrow the scope, which qualifies every ref in it. +await ukc.metro("fra").instances.get({ name: "web" }).suspend(); + +// 3. Address every metro that holds the name. +await ukc.instances.each({ name: "web" }).suspend(); +``` + +### One match: `get()` + +`get()` requires exactly one match, because the next step is often a mutation. +If a name matches in more than one metro, `get()` throws an `AmbiguousRefError`. +The error carries the matches, so a recovery costs no further request: + +```ts +import { AmbiguousRefError } from "@unikraft/cloud"; + +try { + await ukc.instances.get({ name: "web" }).suspend(); +} catch (err) { + if (err instanceof AmbiguousRefError) { + console.log(err.metros); // ["fra", "dal", "sin"] + console.log(err.matches); // the instances, each with its metro + } +} +``` + +### Every match: `each()` + +`each(ref)` is the deliberate plural. +It resolves the matches once, then runs each operation in the metro that holds the match: + +```ts +const web = ukc.instances.each({ name: "web" }); + +await web.where(); // ["fra", "dal", "sin"] +await web.size(); // 3 +const instances = await web; +await web.suspend(); // one result per metro +await web.edit().set({ memory_mb: 512 }).apply(); +``` + +An operation on a set returns an array. +`each()` exists on instances, volumes, services, and certificates. + +## Partial failure + +One unreachable metro doesn't discard the rest of the answer. +The SDK drains the healthy metros first, then it throws a `MetroFanoutError` that names the failures: + +```ts +import { MetroFanoutError } from "@unikraft/cloud"; + +try { + for await (const inst of ukc.instances.list()) console.log(inst.name); +} catch (err) { + if (err instanceof MetroFanoutError) { + console.log(err.message); // "1 of 4 metros failed: sin (503)" + console.log(err.failures); // [{ metro: "sin", error: UnikraftCloudError }] + } +} +``` + +A bulk operation can't yield results as it goes, so the error carries the results that did succeed as `err.results`. +The same rule applies to a set from `each()`. diff --git a/pages/sdks/js/resources.mdx b/pages/sdks/js/resources.mdx new file mode 100644 index 00000000..bd7e0852 --- /dev/null +++ b/pages/sdks/js/resources.mdx @@ -0,0 +1,192 @@ +--- +title: Resources +description: Refs, handles, updates, the method list, and the raw API +navigation_icon: boxes +--- + +Every resource of the platform API has an idiomatic client on `ukc`: `ukc.instances`, `ukc.volumes`, `ukc.services`, `ukc.certificates`, and `ukc.users`. +This page covers what they have in common. + +## Refs + +Every operation on an existing resource takes a **ref**: `{ name }` or `{ uuid }`, never both. + +```ts +await ukc.instances.get({ name: "web" }); +await ukc.instances.get({ uuid: "550e8400-e29b-41d4-a716-446655440000" }); + +// A name belongs to a metro, so a ref can carry the metro. +await ukc.instances.get({ name: "web", metro: "fra" }); + +// A bulk operation takes one ref or an array of refs. +await ukc.instances.stop([{ name: "web" }, { name: "worker" }]); +``` + +The API validates each identifier, so a name in the `uuid` filter fails with `Invalid uuid ''`. +The ref makes you state which kind you hold, and the SDK sends only that field. + +## Handles + +A single-resource operation returns a **handle**: a lazy reference to one resource in one metro. +Await the handle to get the resource. +Call an operation on the handle to get another handle. + +```ts +await ukc.instances.get({ name: "web" }); // the instance +await ukc.instances.get({ name: "web" }).suspend(); // the suspended instance +await ukc.instances.get({ name: "web" }).update({ memory_mb: 512 }); + +await ukc.metro("fra").instances + .create({ image: "nginx:latest" }) + .wait({ state: "running", timeoutSeconds: 30 }) + .logs({ offset: -4096 }); + +await ukc.volumes.get({ name: "data" }).attach({ attach_to: { name: "web" }, at: "/data" }); +``` + +A handle sends nothing until you await it or chain onto it. +Each step runs at most once, even when you await it many times. + +### What a chain costs + +The number of requests depends on the ref and on the scope. +For `get(ref).suspend()`: + +| Ref and scope | Requests | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `{ name, metro }`, any scope | One: the suspend. The ref says where. | +| `{ name }`, one metro in scope | One: the suspend. The scope says where. | +| `{ name }`, many metros in scope | One concurrent read per metro to locate the resource, then the suspend in the metro that holds it. If more than one metro matches, the handle throws `AmbiguousRefError`. | + +### Where a handle landed + +A handle reports where it landed. +A mutating step returns what the endpoint reports, so `suspend()` resolves to `{ uuid, name, state, previous_state }`, not to a full instance. + +```ts +const web = ukc.instances.get({ name: "web" }); +await web.where(); // "dal" +await web.resolve(); // { ref: { name: "web" }, metro: "dal", baseUrl: "..." } +``` + +### The direct form + +Every method is also available directly, with a ref as the first argument. +`ukc.instances.logs({ name: "web" }, { offset: -4096 })` is shorthand for `ukc.instances.get({ name: "web" }).logs({ offset: -4096 })`. + +## Update a resource + +Write a **patch object**, and the SDK works out the operation for each property. +A value sets the property, `null` removes it, and an omitted or `undefined` property stays unchanged. +This follows JSON Merge Patch. + +```ts +await ukc.instances.get({ name: "web" }).update({ memory_mb: 512, vcpus: 2 }); +await ukc.instances.get({ name: "web" }).update({ env: { LOG_LEVEL: "debug" } }); +await ukc.instances.get({ name: "web" }).update({ autokill: null }); +await ukc.volumes.get({ name: "data" }).update({ size_mb: 2048 }); +await ukc.services.get({ name: "web" }).update({ soft_limit: 5, hard_limit: 20 }); +``` + +Every property has a type, so `memory_mb: "512"` doesn't compile. +`image` takes the same string shorthand as `create`. + +### Merge and remove members + +When a plain set isn't enough, stage the operations with `edit()` and send them as one request. +`set` replaces a property, `add` merges into it, and `del` removes members of it. + +```ts +await ukc.instances.get({ name: "web" }).edit() + .set({ memory_mb: 512 }) + .add({ env: { LOG_LEVEL: "debug" }, tags: ["prod"] }) + .del({ env: ["OLD_FLAG"], tags: ["staging"] }) + .apply(); +``` + +`null` in `del()` removes the whole property: `ukc.services.edit({ name: "web" }).del({ domains: null }).apply()`. + +`apply()` returns a handle, so the chain continues: + +```ts +await ukc.metro("fra").instances + .edit({ name: "web" }) + .set({ memory_mb: 1024 }) + .apply() + .wait({ state: "running" }); +``` + +Both forms are one request. +Both also take a ref instead of a handle: `ukc.instances.update({ name: "web" }, { memory_mb: 512 })` and `ukc.instances.edit({ name: "web" })`. + +The API models an update as a list of `{ prop, op, value }` triples, and the raw triples still work: `update({ name: "web" }, [{ prop: "memory_mb", op: "set", value: 512 }])`. + +## Methods + +| Resource | Methods | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `ukc.instances` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete`, `start`, `stop`, `suspend`, `wait`, `metrics`, `history`, `logs` | +| `ukc.volumes` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete`, `attach`, `detach` | +| `ukc.services` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete` | +| `ukc.certificates` | `create`, `get`, `each`, `list`, `update`, `delete` | +| `ukc.users` | `quotas`, `quotasByUuid` | +| `ukc.metro("fra").sandboxes` | `create`, `get`, `list`. See [Sandboxes](/sdks/js/sandboxes). | + +A handle adds the operations of its resource: + +| Handle | Operations | +| ------------- | -------------------------------------------------------------------------------------------------------------- | +| Instance | `refresh`, `start`, `stop`, `suspend`, `delete`, `update`, `edit`, `wait`, `logs`, `metrics`, `history` | +| Volume | `attach`, `detach`, `update`, `edit`, `delete` | +| Service group | `update`, `edit`, `delete` | + +## The raw API + +The idiomatic layer holds a raw client, and the raw client is one property away. +The raw layer mirrors the [OpenAPI specification](https://github.com/unikraft-cloud/openapi): one method per `operationId`, the response envelope untouched, and one metro per call. + +The SDK covers both Unikraft Cloud APIs: + +- **Platform**, per metro: instances, volumes, services, certificates, autoscale, and images. + Idiomatic at the top level, and raw under `ukc.api.platform` and `@unikraft/cloud/api/platform`. +- **Control plane**, global: the account, the metros, images, and self-hosted nodes. + Raw only, under `ukc.api.controlplane` and `@unikraft/cloud/api/controlplane`. + +```ts +// From the client, with the same credentials. +const res = await ukc.api.platform.instances.getInstances({ count: 10, details: true }); +console.log(res.status, res.data?.instances); + +// From a resource. +await ukc.instances.api.getInstanceLogs({ name: ["web"] }); + +// The control plane. +const metros = await ukc.api.controlplane.metros.listMetros(); +console.log(metros.data?.metros); +``` + +Everything that the idiomatic layer doesn't cover is reachable this way: autoscale, the image registry, node information, and the whole control plane. + +```ts +await ukc.api.platform.autoscale.getAutoscaleConfigurations({ uuid: ["sg1"] }); +await ukc.api.platform.images.getImages({}); +``` + +### Standalone raw clients + +A raw client also works on its own, without the idiomatic layer: + +```ts +import { PlatformApi } from "@unikraft/cloud/api/platform"; +import { ControlPlaneApi } from "@unikraft/cloud/api/controlplane"; + +const api = new PlatformApi({ + baseUrl: "https://api.fra.unikraft.cloud", + token: process.env.UKC_TOKEN, +}); +await api.instances.getInstances({ count: 10 }); +``` + +A raw client belongs to one metro. +It talks to its `baseUrl`, and one call can go elsewhere with `{ baseUrl }`. +The fan-out across metros is the job of the idiomatic layer. diff --git a/pages/sdks/js/sandboxes.mdx b/pages/sdks/js/sandboxes.mdx new file mode 100644 index 00000000..0d0d6870 --- /dev/null +++ b/pages/sdks/js/sandboxes.mdx @@ -0,0 +1,85 @@ +--- +title: Sandboxes +description: Run commands and move files inside a microVM with the Sandbox class +navigation_label: Overview +navigation_icon: box +--- + +A **sandbox** is an instance with the [sandbox plugin](/features/plugins#sandbox-plugin) attached: a microVM that runs the commands you give it, and that you move files in and out of. +The `Sandbox` class wraps the plugin, so you write none of that by hand. + +## Your first sandbox + +```ts +import { Sandbox } from "@unikraft/cloud"; + +await using sandbox = await Sandbox.create(); + +const { stdout } = await sandbox.exec("echo hello"); +console.log(stdout); // hello +``` + +`Sandbox.create()` reads the token from `UKC_TOKEN`. +With nothing else set, it boots `debian-slim:latest` with 2 GiB of memory in the default metro. +[Configuration](/sdks/js/sandboxes/configuration) lists what you can change. + +`await using` deletes the sandbox when the scope ends, even if the scope throws. +It needs TypeScript 5.2 or newer, or a runtime with explicit resource management. +Without it, call `sandbox.delete()` yourself. + +## Write a file and run it + +Write a script into the sandbox, run it, and check the exit code before you trust the output: + +```ts +import { Sandbox } from "@unikraft/cloud"; + +await using sandbox = await Sandbox.create(); + +await sandbox.writeFile("/work/hello.sh", "echo hello from $(uname -m)\n"); +const result = await sandbox.exec("sh /work/hello.sh"); + +if (result.exitcode !== 0) throw new Error(result.stderr); +console.log(result.stdout); // hello from x86_64 +``` + +## The instance underneath + +A sandbox is a real instance, so the whole [platform API](/sdks/js/resources) still applies to it. +`sandbox.instance` is a [handle](/sdks/js/resources#handles) on that instance: + +```ts +const { name, state, memory_mb } = await sandbox.instance; +console.log(`${name} in ${sandbox.metro}: ${state}, ${memory_mb} MiB`); + +await sandbox.instance.update({ memory_mb: 4096 }); +``` + +`sandbox.uuid` and `sandbox.metro` identify the instance. +`sandbox.api` is the raw plugin API, pinned to this sandbox. + +## Two ways in + +`Sandbox.create()` builds its own client from the options you give it. +A client that you already hold opens the same door through `ukc.metro("fra").sandboxes`, and spends no second token: + +```ts +import { UnikraftCloud } from "@unikraft/cloud"; + +const ukc = new UnikraftCloud({ token: process.env.UKC_TOKEN }); +const sandbox = await ukc.metro("fra").sandboxes.create({ memory_mb: 512 }); + +console.log((await sandbox.exec("uname -a")).stdout); +await sandbox.delete(); +``` + +`sandboxes` belongs to a metro client, not to `ukc`, because a sandbox lives in exactly one metro. + +## Next steps + +- [Configuration](/sdks/js/sandboxes/configuration): the image, the memory, the metro, snapshots, and your own plugin. +- [Commands](/sdks/js/sandboxes/commands): run a command to completion, or start one and control it. +- [Files](/sdks/js/sandboxes/files): read, write, and upload files. +- [Existing sandboxes](/sdks/js/sandboxes/connect): reconnect from another process, and list sandboxes. +- [`examples/sandbox.ts`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/examples/sandbox.ts): a complete program. +- [Sandboxes](/use-cases/sandboxes): what the platform underneath gives a sandbox. diff --git a/pages/sdks/js/sandboxes/commands.mdx b/pages/sdks/js/sandboxes/commands.mdx new file mode 100644 index 00000000..e9bb81e9 --- /dev/null +++ b/pages/sdks/js/sandboxes/commands.mdx @@ -0,0 +1,103 @@ +--- +title: Commands +description: Run commands inside a sandbox, wait for them, and read their output +navigation_icon: terminal +--- + +A sandbox runs commands in two ways. +`exec` runs a command to completion and returns its output. +`start` returns at once and gives you a `Command` to control. + +## Run a command to completion + +`exec` returns the exit code and both output streams: + +```ts +import { Sandbox } from "@unikraft/cloud"; + +await using sandbox = await Sandbox.create(); + +const result = await sandbox.exec("ls -la /etc"); +console.log(result.exitcode); // 0 +console.log(result.stdout); +``` + +## Set the working directory and the environment + +Each command takes its own `cwd` and `env`: + +```ts +const result = await sandbox.exec("npm test", { + cwd: "/work/app", + env: { CI: "1" }, +}); +``` + +## Limit how long you wait + +`timeoutSeconds` makes `exec` return when the time runs out. +This isn't an error: you get the output so far, `exitcode` is `null`, and the command keeps running. + +```ts +const result = await sandbox.exec("make", { timeoutSeconds: 30 }); + +if (result.exitcode === null) { + const build = sandbox.command(result.uuid); + await build.wait(); + console.log((await build.logs()).stdout); +} +``` + +## Start a long-running command + +`start` doesn't wait. +It returns a `Command`, and you decide what happens next: write to its standard input, wait for it, read its logs, or stop it. + +```ts +const server = await sandbox.start("cat", { cwd: "/work" }); + +await server.stdin("first line\n"); +await server.stdin("last line\n", { eof: true }); +await server.wait({ timeoutSeconds: 10 }); + +const logs = await server.logs(); +console.log(logs.stdout); + +await server.delete(); +``` + +## Stop a command + +`signal` sends a signal by name or by number: + +```ts +const server = await sandbox.start("sleep 3600"); +await server.signal("TERM"); +``` + +## Read the output later + +`exec` deletes the command record when the command finishes. +Pass `keep: true` to keep it, so another part of your program can read the logs by UUID: + +```ts +const { uuid } = await sandbox.exec("make", { keep: true }); + +const logs = await sandbox.command(uuid).logs(); +console.log(logs.stderr); +``` + +`logsRaw` reads one stream as bytes, and it takes a byte range. +A negative `offset` reads from the end of the stream: + +```ts +const tail = await sandbox.command(uuid).logsRaw("stdout", { offset: -4096 }); +console.log(new TextDecoder().decode(tail)); +``` + +`sandbox.commands()` returns every command that the sandbox knows about, in start order. + +## Reference + +Every method on `Command` is exactly one request, so you can see what a step costs. +See [`Command`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/src/resources/sandboxes/command.ts) and [`ExecOptions`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/src/resources/sandboxes/types.ts) in the source for the complete list. diff --git a/pages/sdks/js/sandboxes/configuration.mdx b/pages/sdks/js/sandboxes/configuration.mdx new file mode 100644 index 00000000..858e30c8 --- /dev/null +++ b/pages/sdks/js/sandboxes/configuration.mdx @@ -0,0 +1,111 @@ +--- +title: Configuration +description: The instance fields, the client options, snapshots, and your own plugin +navigation_icon: settings +--- + +`Sandbox.create(spec, options)` takes two arguments. +The first says what the sandbox is. +The second says how the call reaches it, and how long the call waits. + +```ts +import { Sandbox } from "@unikraft/cloud"; + +const sandbox = await Sandbox.create( + { + image: "nginx:latest", + memory_mb: 1024, + env: { LOG_LEVEL: "debug" }, + }, + { + token: process.env.UKC_TOKEN, + metro: "fra", + bootTimeoutSeconds: 60, + }, +); +``` + +## What the sandbox is + +The first argument accepts the fields that [`POST /instances`](/api/platform/v1) accepts, except `autostart`, `timeout_s`, `wait_timeout_ms`, and `replicas`. +It also accepts two fields for the plugin. +These are the fields with a sandbox default: + +| Field | Default | Description | +| ------------ | ------------------------ | ----------------------------------------------------------------------------------------- | +| `image` | `debian-slim:latest` | The instance image. `UKC_SANDBOX_IMAGE` replaces the default. | +| `memory_mb` | `2048` | The memory of the instance. | +| `autokill` | `300000` | The time in milliseconds that the platform keeps a stopped instance before it deletes it. | +| `rom` | `plugins/sandbox:latest` | The plugin ROM. Shorthand for one entry in `plugins`. | +| `pluginName` | `sandbox` | The plugin name, which is also its path segment. | + +An image for a sandbox must carry a shell and a main process that stays alive. +The SDK sets `autostart` and `timeout_s` for you, because a plugin can't answer in a stopped instance. +It excludes `wait_timeout_ms` and `replicas`, because a `Sandbox` addresses exactly one instance. +If you pass one of these four fields, `create` throws an [error](/sdks/js/errors) of kind `"config"`. + +## How the call behaves + +| Option | Default | Description | +| -------------------- | ----------- | ------------------------------------------------------------------------------------------ | +| `token` | `UKC_TOKEN` | The bearer token. | +| `metro` | `UKC_METRO` | The metro of the sandbox, as a code or as a full `http(s)://` address. | +| `client` | | An existing `UnikraftCloud` client to borrow, instead of a new one. | +| `bootTimeoutSeconds` | `30` | How long the call waits for the instance to reach `running`. Raise it for a large image. | +| `ready` | `{}` | How to wait for the plugin to answer, or `false` to return as soon as the instance exists. | +| `signal` | | An `AbortSignal` that cancels the call. | + +Every option of the [client constructor](/sdks/js/client#options) is also accepted, because `Sandbox.create()` builds a client from them. + +`create` waits for the plugin by default, and gives up after 60 seconds. +If the wait fails, `create` deletes the instance it made, so a broken sandbox doesn't linger. +To wait longer, or to keep a failed sandbox for inspection: + +```ts +await Sandbox.create({}, { ready: { timeoutMs: 120_000 } }); +await Sandbox.create({}, { ready: false }); +``` + +## Through a client + +`ukc.metro("fra").sandboxes.create(spec, options)` takes the same first argument. +Its options exclude `token`, `metro`, `client`, and the other client fields, because the call runs through the client that you reached it from. + +```ts +const sandbox = await ukc.metro("fra").sandboxes.create( + { image: "nginx:latest", memory_mb: 1024 }, + { bootTimeoutSeconds: 60 }, +); +``` + +## Create from a snapshot + +A `template`, a `branch_from` source, or a `checkpoint` replaces the image. +The snapshot carries the image, the memory, and the plugins of its source, so the sandbox plugin comes with it. + +```ts +await using sandbox = await Sandbox.create({ template: { name: "my-template" } }); +``` + +For a snapshot, the SDK applies no default image, memory, or plugin entry. +Leave `rom` out, and pass `pluginName` only when the source attached the plugin under another name. +See [on-demand templates](/features/on-demand-templates), [branching](/features/branching), and [checkpoints](/features/checkpoints). + +## Your own plugin + +A sandbox can run a plugin that you built with the [plugin SDK](/sdks/plugin), as long as it speaks the sandbox API. +Name its ROM in `rom`: + +```ts +const sandbox = await Sandbox.create({ + image: "my-org/sandbox-base:latest", + rom: "my-org/my-plugin:latest", +}); +``` + +To give the plugin a `config`, attach it in `plugins` yourself, and omit `rom`. + +## Reference + +Every field and option has a type. +See [`SandboxSpec` and `SandboxCallOptions`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/src/resources/sandboxes/types.ts) in the source for the complete list. diff --git a/pages/sdks/js/sandboxes/connect.mdx b/pages/sdks/js/sandboxes/connect.mdx new file mode 100644 index 00000000..8ce223e5 --- /dev/null +++ b/pages/sdks/js/sandboxes/connect.mdx @@ -0,0 +1,70 @@ +--- +title: Existing sandboxes +description: Reconnect to a sandbox from another process, list sandboxes, and wait for readiness +navigation_icon: plug +--- + +A sandbox outlives the process that created it. +The UUID and the metro are all that a later process needs to attach again. + +## Reconnect + +```ts +import { Sandbox } from "@unikraft/cloud"; + +const sandbox = await Sandbox.connect( + { uuid: "550e8400-e29b-41d4-a716-446655440000", metro: "fra" }, + { token: process.env.UKC_TOKEN }, +); + +console.log((await sandbox.exec("cat /work/state.json")).stdout); +``` + +An instance UUID is per metro. +If the ref carries no `metro`, `connect` searches the scope for the UUID. +Name the metro to skip the search. + +Through a client that you already hold, the same operation is `get`: + +```ts +const sandbox = await ukc.metro("fra").sandboxes.get({ uuid }); +``` + +## List sandboxes + +A sandbox is an instance, so the list of sandboxes is the instance list, filtered by plugin. +`tags` narrows it further: + +```ts +for await (const sandbox of ukc.metro("fra").sandboxes.list({ tags: ["ci"] })) { + console.log(sandbox.uuid); +} +``` + +## Wait after a restart + +`connect` and `get` don't wait for the plugin, because the sandbox was already there. +After a start, or after a resume from `standby`, the plugin reloads, so wait for it before the first command: + +```ts +await sandbox.instance.start(); +await sandbox.ready({ timeoutMs: 10_000 }); +``` + +If the wait runs out of time, the error names the cause. +The SDK reads the state of the instance and reports, for example, that the image didn't boot, or that the main process exited. + +## Delete + +`sandbox.delete()` deletes the instance underneath the sandbox. +An `await using` declaration does this for you at the end of the scope. + +```ts +for await (const sandbox of ukc.metro("fra").sandboxes.list({ tags: ["ci"] })) { + await sandbox.delete(); +} +``` + +## Reference + +See [`ConnectSandboxOptions` and `ListSandboxesOptions`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/src/resources/sandboxes/types.ts) in the source for the complete list of options. diff --git a/pages/sdks/js/sandboxes/files.mdx b/pages/sdks/js/sandboxes/files.mdx new file mode 100644 index 00000000..910db31b --- /dev/null +++ b/pages/sdks/js/sandboxes/files.mdx @@ -0,0 +1,58 @@ +--- +title: Files +description: Read, write, and upload files, and create directories in a sandbox +navigation_icon: folder +--- + +A sandbox exposes its filesystem through four methods: `readFile`, `writeFile`, `mkdir`, and `upload`. +Every path is absolute, inside the sandbox. + +## Write and read a file + +`writeFile` creates a missing file, and truncates an existing one. +`readFile` returns the file as a `Uint8Array`. + +```ts +import { Sandbox } from "@unikraft/cloud"; + +await using sandbox = await Sandbox.create(); + +await sandbox.writeFile("/work/input/a.txt", "hello\n"); + +const bytes = await sandbox.readFile("/work/input/a.txt"); +console.log(new TextDecoder().decode(bytes)); // hello +``` + +A string is UTF-8. +Pass a `Uint8Array` for binary content. + +## Append to a file + +```ts +await sandbox.writeFile("/work/build.log", "step 2 done\n", { append: true }); +``` + +## Upload a file into a directory + +`upload` takes the directory, the file name, and the content, and it creates the missing parent directories: + +```ts +import { readFile } from "node:fs/promises"; + +const csv = await readFile("./data.csv"); +await sandbox.upload("/work/input", "data.csv", csv); +``` + +## Create a directory + +`mkdir` creates the missing parent directories too. +Pass `parents: false` to create only the last one. + +```ts +await sandbox.mkdir("/work/out/reports"); +``` + +## Reference + +Every method also accepts `signal` and `headers`, as the [client](/sdks/js/client) does. +See [`Sandbox`](https://github.com/unikraft-cloud/js-sdk/blob/HEAD/src/resources/sandboxes/sandbox.ts) in the source for the complete list. diff --git a/pages/sdks/overview.mdx b/pages/sdks/overview.mdx index 779cdea9..bf6ea51e 100644 --- a/pages/sdks/overview.mdx +++ b/pages/sdks/overview.mdx @@ -15,7 +15,7 @@ The SDKs on this page put those same APIs in your own programs, so instances, vo `@unikraft/cloud` for Node.js and TypeScript. - Fully typed, with chainable handles, multi-metro fan-out, and a [`Sandbox`](/sdks/js#sandboxes) class for running commands and moving files inside an instance. + Fully typed, with chainable handles, multi-metro fan-out, and a [`Sandbox`](/sdks/js/sandboxes) class for running commands and moving files inside an instance. `unikraft-cloud` for Python 3.10 and newer. diff --git a/pages/use-cases/sandboxes.mdx b/pages/use-cases/sandboxes.mdx index a9c2181f..8d03e742 100644 --- a/pages/use-cases/sandboxes.mdx +++ b/pages/use-cases/sandboxes.mdx @@ -42,7 +42,7 @@ Watch the video to see a sandbox boot in under 10 ms, scale to zero while idle, :::tip[Create a sandbox from your own code] -The [JavaScript SDK](/sdks/js#sandboxes) wraps the [sandbox plugin](/features/plugins#sandbox-plugin) in a `Sandbox` class, so a microVM is a few lines: +The [JavaScript SDK](/sdks/js/sandboxes) wraps the [sandbox plugin](/features/plugins#sandbox-plugin) in a `Sandbox` class, so a microVM is a few lines: ```ts import { Sandbox } from "@unikraft/cloud"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 644ac3ec..2aa21133 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,15 +27,21 @@ importers: specifier: ^2.3.0 version: 2.3.0 zudoku: - specifier: ^0.86.0 - version: 0.86.0(@oxc-project/types@0.140.0)(@types/json-schema@7.0.15)(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(supports-color@7.2.0)(use-sync-external-store@1.6.0(react@19.2.8)) + specifier: ^0.88.0 + version: 0.88.0(@oxc-project/types@0.140.0)(@types/json-schema@7.0.15)(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react@19.2.8)(react-dom@19.2.8(react@19.2.8))(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(supports-color@7.2.0)(use-sync-external-store@1.6.0(react@19.2.8)) devDependencies: + '@types/react': + specifier: ^19 + version: 19.2.17 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.17) '@typescript-eslint/eslint-plugin': specifier: ^8.0.0 - version: 8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/parser': specifier: ^8.0.0 - version: 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3) eslint: specifier: ^9.14.0 version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) @@ -1722,13 +1728,6 @@ packages: resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} @@ -3806,8 +3805,8 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - zudoku@0.86.0: - resolution: {integrity: sha512-NsXwupD6+YG9eIGQKreTZXthgc93jJc4v60KyQlquTr1s/7cNmeOIJhDyiNWM8Sr+qDYIK1CDn5GNHO7MkD4ug==} + zudoku@0.88.0: + resolution: {integrity: sha512-BbMOV2PL1X+772S12fnbEQdrAYJRewIMUrAoZLsUDRrygf3JrLWFUXVOCDxtzL+ZpJN4GMOctJ+tLTJuKKGBeA==} engines: {node: '>=22.22.0'} hasBin: true peerDependencies: @@ -3865,11 +3864,11 @@ snapshots: '@babel/runtime@7.29.7': {} - '@base-ui/react@1.8.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@base-ui/react@1.8.0(@types/react@19.2.17)(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@babel/runtime': 7.29.7 - '@base-ui/utils': 0.4.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@base-ui/utils': 0.4.0(@types/react@19.2.17)(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@floating-ui/react-dom': 2.1.9(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@floating-ui/utils': 0.2.12 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -3877,7 +3876,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@base-ui/utils@0.4.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@base-ui/utils@0.4.0(@types/react@19.2.17)(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@babel/runtime': 7.29.7 '@floating-ui/utils': 0.2.12 @@ -3888,7 +3887,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@devframes/hub@0.8.2(devframe@0.8.2(srvx@1.0.3))': + '@devframes/hub@0.8.2(devframe@0.8.2)(srvx@1.0.3)': dependencies: '@standard-schema/spec': 1.1.0 destr: 2.0.5 @@ -3899,14 +3898,14 @@ snapshots: tinyexec: 1.3.1 zigpty: 0.2.1 - '@devframes/json-render@0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(srvx@1.0.3)))(devframe@0.8.2(srvx@1.0.3))': + '@devframes/json-render@0.8.2(@devframes/hub@0.8.2(devframe@0.8.2))(devframe@0.8.2)(srvx@1.0.3)': dependencies: '@json-render/core': 0.19.0(zod@4.4.3) devframe: 0.8.2(srvx@1.0.3) nostics: 1.2.0 zod: 4.4.3 optionalDependencies: - '@devframes/hub': 0.8.2(devframe@0.8.2(srvx@1.0.3)) + '@devframes/hub': 0.8.2(devframe@0.8.2)(srvx@1.0.3) '@emnapi/core@1.11.1': dependencies: @@ -4030,7 +4029,7 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)': dependencies: eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) eslint-visitor-keys: 3.4.3 @@ -4042,8 +4041,6 @@ snapshots: '@eslint/object-schema': 2.1.7 debug: 4.4.3(supports-color@7.2.0) minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color '@eslint/config-helpers@0.4.2': dependencies: @@ -4064,8 +4061,6 @@ snapshots: js-yaml: 4.3.2 minimatch: 3.1.5 strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color '@eslint/js@9.39.5': {} @@ -4087,7 +4082,7 @@ snapshots: '@floating-ui/core': 1.8.0 '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@floating-ui/react-dom@2.1.9(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@floating-ui/dom': 1.8.0 react: 19.2.8 @@ -4100,8 +4095,6 @@ snapshots: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.14.2 meros: 1.3.2(@types/node@24.13.3) - transitivePeerDependencies: - - '@types/node' '@graphql-tools/executor@1.5.7(graphql@16.14.2)': dependencies: @@ -4244,8 +4237,6 @@ snapshots: unist-util-stringify-position: 4.0.0 unist-util-visit: 5.1.0 vfile: 6.0.3 - transitivePeerDependencies: - - supports-color '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': dependencies: @@ -4260,8 +4251,6 @@ snapshots: rollup: 4.63.1 source-map: 0.7.6 vfile: 6.0.3 - transitivePeerDependencies: - - supports-color '@n1ru4l/push-pull-async-iterable-iterator@3.2.0': {} @@ -4457,16 +4446,16 @@ snapshots: '@radix-ui/primitive@1.1.7': {} - '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-accordion@1.2.20(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collapsible': 1.1.20(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-collection': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4474,44 +4463,44 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-alert-dialog@1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-arrow@1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-aspect-ratio@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-aspect-ratio@1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-checkbox@1.3.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-checkbox@1.3.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4520,14 +4509,14 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-collapsible@1.1.20(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4536,11 +4525,11 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-collection@1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4560,18 +4549,18 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-dialog@1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) @@ -4589,11 +4578,11 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-dismissable-layer@1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4602,14 +4591,14 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-dropdown-menu@2.1.24(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4623,10 +4612,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-focus-scope@1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4634,16 +4623,16 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-hover-card@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-hover-card@1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-popper': 1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-portal': 1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4658,31 +4647,31 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-label@2.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-menu@2.1.24(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-portal': 1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-roving-focus': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) aria-hidden: 1.2.6 @@ -4693,41 +4682,41 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-navigation-menu@1.2.22(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-popover@1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-portal': 1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) aria-hidden: 1.2.6 @@ -4738,13 +4727,13 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-popper@1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@floating-ui/react-dom': 2.1.9(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-arrow': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.8) @@ -4756,9 +4745,9 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-portal@1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4766,7 +4755,7 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-presence@1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4775,7 +4764,7 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-primitive@2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4784,25 +4773,25 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-progress@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-progress@1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-radio-group@1.4.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-radio-group@1.4.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-roving-focus': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4811,15 +4800,15 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-roving-focus@1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.17)(react@19.2.8) @@ -4830,15 +4819,15 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-scroll-area@1.2.18(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4847,28 +4836,28 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-select@2.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-select@2.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-portal': 1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) aria-hidden: 1.2.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4877,24 +4866,24 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-separator@1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-slider@1.4.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.17)(react@19.2.8) @@ -4912,12 +4901,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-switch@1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 @@ -4926,15 +4915,15 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-tabs@1.1.21(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-roving-focus': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4942,14 +4931,14 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-toggle-group@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-toggle-group@1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-roving-focus': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-toggle': 1.1.18(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4957,10 +4946,10 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-toggle@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-toggle@1.1.18(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -4968,21 +4957,21 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-tooltip@1.2.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@radix-ui/primitive': 1.1.7 '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-portal': 1.1.17(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-presence': 1.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.8) '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: @@ -5043,9 +5032,9 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@radix-ui/react-visually-hidden@1.2.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: @@ -5360,7 +5349,7 @@ snapshots: tailwindcss: 4.3.3 vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) - '@tanem/react-nprogress@6.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@tanem/react-nprogress@6.0.3(react@19.2.8)(react-dom@19.2.8(react@19.2.8))': dependencies: '@babel/runtime': 7.29.7 hoist-non-react-statics: 3.3.2 @@ -5423,23 +5412,21 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.69.0 - '@typescript-eslint/type-utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.69.0 eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) ignore: 7.0.8 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 @@ -5448,8 +5435,6 @@ snapshots: debug: 4.4.3(supports-color@7.2.0) eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 - transitivePeerDependencies: - - supports-color '@typescript-eslint/project-service@8.69.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: @@ -5457,8 +5442,6 @@ snapshots: '@typescript-eslint/types': 8.69.0 debug: 4.4.3(supports-color@7.2.0) typescript: 6.0.3 - transitivePeerDependencies: - - supports-color '@typescript-eslint/scope-manager@8.69.0': dependencies: @@ -5469,17 +5452,15 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3) debug: 4.4.3(supports-color@7.2.0) eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 - transitivePeerDependencies: - - supports-color '@typescript-eslint/types@8.69.0': {} @@ -5495,19 +5476,15 @@ snapshots: tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.69.0(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0) '@typescript-eslint/scope-manager': 8.69.0 '@typescript-eslint/types': 8.69.0 '@typescript-eslint/typescript-estree': 8.69.0(supports-color@7.2.0)(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 - transitivePeerDependencies: - - supports-color '@typescript-eslint/visitor-keys@8.69.0': dependencies: @@ -5516,14 +5493,14 @@ snapshots: '@ungap/structured-clone@1.4.0': {} - '@unhead/bundler@3.2.3(@oxc-project/types@0.140.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(unhead@3.2.3(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + '@unhead/bundler@3.2.3(@oxc-project/types@0.140.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(unhead@3.2.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitejs/devtools-kit': 0.4.12(srvx@1.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) magic-string: 1.2.3 oxc-parser: 0.140.0 oxc-walker: 1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.1.5) ufo: 1.6.4 - unhead: 3.2.3(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + unhead: 3.2.3(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) optionalDependencies: esbuild: 0.28.2 @@ -5534,22 +5511,20 @@ snapshots: - '@farmfe/core' - '@modelcontextprotocol/client' - '@modelcontextprotocol/server' - - '@oxc-project/types' - '@rspack/core' - bun-types-no-globals - cac - ocache - - rollup - - srvx - unloader + - webpack - '@unhead/react@3.2.3(@oxc-project/types@0.140.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react@19.2.8)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + '@unhead/react@3.2.3(@oxc-project/types@0.140.0)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react@19.2.8)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@unhead/bundler': 3.2.3(@oxc-project/types@0.140.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(unhead@3.2.3(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + '@unhead/bundler': 3.2.3(@oxc-project/types@0.140.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(unhead@3.2.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) magic-string: 1.2.3 oxc-walker: 1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.1.5) react: 19.2.8 - unhead: 3.2.3(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + unhead: 3.2.3(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) optionalDependencies: vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) @@ -5557,24 +5532,18 @@ snapshots: - '@farmfe/core' - '@modelcontextprotocol/client' - '@modelcontextprotocol/server' - - '@oxc-project/types' - '@rspack/core' - '@unhead/cli' - bun-types-no-globals - cac - - esbuild - - lightningcss - ocache - - oxc-parser - - rolldown - - rollup - - srvx - unloader + - webpack '@vitejs/devtools-kit@0.4.12(srvx@1.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@devframes/hub': 0.8.2(devframe@0.8.2(srvx@1.0.3)) - '@devframes/json-render': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(srvx@1.0.3)))(devframe@0.8.2(srvx@1.0.3)) + '@devframes/hub': 0.8.2(devframe@0.8.2)(srvx@1.0.3) + '@devframes/json-render': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2))(devframe@0.8.2)(srvx@1.0.3) devframe: 0.8.2(srvx@1.0.3) local-pkg: 1.2.1 mlly: 1.8.2 @@ -5586,7 +5555,6 @@ snapshots: - '@modelcontextprotocol/server' - cac - ocache - - srvx '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: @@ -5734,17 +5702,14 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + cmdk@1.1.1(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)): dependencies: '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' collapse-white-space@2.1.0: {} @@ -5819,7 +5784,6 @@ snapshots: ufo: 1.6.4 transitivePeerDependencies: - ocache - - srvx devlop@1.1.0: dependencies: @@ -5910,7 +5874,7 @@ snapshots: eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))(supports-color@7.2.0) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2(supports-color@7.2.0) '@eslint/config-helpers': 0.4.2 @@ -5946,8 +5910,6 @@ snapshots: optionator: 0.9.4 optionalDependencies: jiti: 2.7.0 - transitivePeerDependencies: - - supports-color espree@10.4.0: dependencies: @@ -6054,7 +6016,7 @@ snapshots: format@0.2.2: {} - framer-motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + framer-motion@12.43.0(react@19.2.8)(react-dom@19.2.8(react@19.2.8)): dependencies: motion-dom: 12.43.0 motion-utils: 12.39.0 @@ -6157,8 +6119,6 @@ snapshots: mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) property-information: 7.2.0 style-to-js: 1.1.21 - transitivePeerDependencies: - - supports-color hast-util-raw@9.1.0: dependencies: @@ -6194,8 +6154,6 @@ snapshots: style-to-js: 1.1.21 unist-util-position: 5.0.0 zwitch: 2.0.4 - transitivePeerDependencies: - - supports-color hast-util-to-html@9.0.5: dependencies: @@ -6228,8 +6186,6 @@ snapshots: style-to-js: 1.1.21 unist-util-position: 5.0.0 vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color hast-util-to-parse5@8.0.1: dependencies: @@ -6521,8 +6477,6 @@ snapshots: parse-entities: 4.0.2 stringify-entities: 4.0.4 unist-util-visit-parents: 6.0.2 - transitivePeerDependencies: - - supports-color mdast-util-find-and-replace@3.0.2: dependencies: @@ -6545,8 +6499,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color mdast-util-frontmatter@2.0.1(supports-color@7.2.0): dependencies: @@ -6556,8 +6508,6 @@ snapshots: mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 micromark-extension-frontmatter: 2.0.0 - transitivePeerDependencies: - - supports-color mdast-util-gfm-autolink-literal@2.0.1: dependencies: @@ -6574,16 +6524,12 @@ snapshots: mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color mdast-util-gfm-strikethrough@2.0.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-gfm-table@2.0.0(supports-color@7.2.0): dependencies: @@ -6592,8 +6538,6 @@ snapshots: markdown-table: 3.0.4 mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-gfm-task-list-item@2.0.0(supports-color@7.2.0): dependencies: @@ -6601,8 +6545,6 @@ snapshots: devlop: 1.1.0 mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-gfm@3.1.0(supports-color@7.2.0): dependencies: @@ -6613,8 +6555,6 @@ snapshots: mdast-util-gfm-table: 2.0.0(supports-color@7.2.0) mdast-util-gfm-task-list-item: 2.0.0(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-mdx-expression@2.0.1(supports-color@7.2.0): dependencies: @@ -6624,8 +6564,6 @@ snapshots: devlop: 1.1.0 mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-mdx-jsx@3.2.0(supports-color@7.2.0): dependencies: @@ -6641,8 +6579,6 @@ snapshots: stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color mdast-util-mdx@3.0.0(supports-color@7.2.0): dependencies: @@ -6651,8 +6587,6 @@ snapshots: mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-mdxjs-esm@2.0.1(supports-color@7.2.0): dependencies: @@ -6662,8 +6596,6 @@ snapshots: devlop: 1.1.0 mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color mdast-util-phrasing@4.1.0: dependencies: @@ -6994,8 +6926,6 @@ snapshots: micromark-util-subtokenize: 2.1.0 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color minimatch@10.2.6: dependencies: @@ -7020,13 +6950,15 @@ snapshots: motion-utils@12.39.0: {} - motion@12.42.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + motion@12.42.2(react@19.2.8)(react-dom@19.2.8(react@19.2.8)): dependencies: - framer-motion: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + framer-motion: 12.43.0(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) tslib: 2.8.1 optionalDependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@emotion/is-prop-valid' mrmime@2.0.1: {} @@ -7040,7 +6972,7 @@ snapshots: natural-compare@1.4.0: {} - next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next-themes@0.4.6(react@19.2.8)(react-dom@19.2.8(react@19.2.8)): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -7249,8 +7181,6 @@ snapshots: unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 - transitivePeerDependencies: - - supports-color react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.8): dependencies: @@ -7271,7 +7201,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-router@8.3.0(react@19.2.8)(react-dom@19.2.8(react@19.2.8)): dependencies: cookie-es: 3.1.1 react: 19.2.8 @@ -7334,8 +7264,6 @@ snapshots: parse-srcset: 1.0.2 unified: 11.0.5 unist-util-visit: 5.1.0 - transitivePeerDependencies: - - supports-color rehype-raw@7.0.0: dependencies: @@ -7348,8 +7276,6 @@ snapshots: '@types/estree': 1.0.9 '@types/hast': 3.0.5 hast-util-to-estree: 3.1.3(supports-color@7.2.0) - transitivePeerDependencies: - - supports-color rehype-slug@6.0.0: dependencies: @@ -7376,8 +7302,6 @@ snapshots: mdast-util-directive: 3.1.0(supports-color@7.2.0) micromark-extension-directive: 3.0.2 unified: 11.0.5 - transitivePeerDependencies: - - supports-color remark-frontmatter@5.0.0(supports-color@7.2.0): dependencies: @@ -7385,8 +7309,6 @@ snapshots: mdast-util-frontmatter: 2.0.1(supports-color@7.2.0) micromark-extension-frontmatter: 2.0.0 unified: 11.0.5 - transitivePeerDependencies: - - supports-color remark-gfm@4.0.1(supports-color@7.2.0): dependencies: @@ -7396,8 +7318,6 @@ snapshots: remark-parse: 11.0.0(supports-color@7.2.0) remark-stringify: 11.0.0 unified: 11.0.5 - transitivePeerDependencies: - - supports-color remark-mdx-frontmatter@5.2.0: dependencies: @@ -7412,8 +7332,6 @@ snapshots: dependencies: mdast-util-mdx: 3.0.0(supports-color@7.2.0) micromark-extension-mdxjs: 3.0.0 - transitivePeerDependencies: - - supports-color remark-parse@11.0.0(supports-color@7.2.0): dependencies: @@ -7421,8 +7339,6 @@ snapshots: mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) micromark-util-types: 2.0.2 unified: 11.0.5 - transitivePeerDependencies: - - supports-color remark-rehype@11.1.2: dependencies: @@ -7651,7 +7567,7 @@ snapshots: undici-types@7.18.2: {} - unhead@3.2.3(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + unhead@3.2.3(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): dependencies: hookable: 6.1.1 unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.1.5)(rollup@4.63.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) @@ -7661,9 +7577,6 @@ snapshots: - '@farmfe/core' - '@rspack/core' - bun-types-no-globals - - esbuild - - rolldown - - rollup - unloader - webpack @@ -7756,14 +7669,11 @@ snapshots: util-deprecate@1.0.2: {} - vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + vaul@1.1.2(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)): dependencies: - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' vfile-location@5.0.3: dependencies: @@ -7833,10 +7743,10 @@ snapshots: zod@4.4.3: {} - zudoku@0.86.0(@oxc-project/types@0.140.0)(@types/json-schema@7.0.15)(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(supports-color@7.2.0)(use-sync-external-store@1.6.0(react@19.2.8)): + zudoku@0.88.0(@oxc-project/types@0.140.0)(@types/json-schema@7.0.15)(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react@19.2.8)(react-dom@19.2.8(react@19.2.8))(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(supports-color@7.2.0)(use-sync-external-store@1.6.0(react@19.2.8)): dependencies: '@apidevtools/json-schema-ref-parser': 15.5.0(@types/json-schema@7.0.15) - '@base-ui/react': 1.8.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@base-ui/react': 1.8.0(@types/react@19.2.17)(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@envelop/core': 5.5.1 '@graphiql/toolkit': 0.12.1(@types/node@24.13.3)(graphql@16.14.2) '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) @@ -7846,30 +7756,30 @@ snapshots: '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) '@mdx-js/rollup': 3.1.1(rollup@4.63.1)(supports-color@7.2.0) '@pothos/core': 4.13.1(graphql@16.14.2) - '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-alert-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-aspect-ratio': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-hover-card': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-progress': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-radio-group': 1.4.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-select': 2.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-slider': 1.4.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-accordion': 1.2.20(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-alert-dialog': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-aspect-ratio': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-checkbox': 1.3.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-collapsible': 1.1.20(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-dialog': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-dropdown-menu': 2.1.24(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-hover-card': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-label': 2.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-navigation-menu': 1.2.22(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-popover': 1.1.23(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-progress': 1.1.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-radio-group': 1.4.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-scroll-area': 1.2.18(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-select': 2.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-separator': 1.1.15(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-slider': 1.4.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8) - '@radix-ui/react-switch': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-switch': 1.3.7(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-tabs': 1.1.21(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-toggle': 1.1.18(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-toggle-group': 1.1.19(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-tooltip': 1.2.16(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@scalar/openapi-parser': 0.28.10 '@scalar/snippetz': 0.9.23 '@shikijs/langs': 4.3.1 @@ -7878,17 +7788,17 @@ snapshots: '@shikijs/transformers': 4.3.1 '@tailwindcss/typography': 0.5.20(tailwindcss@4.3.3) '@tailwindcss/vite': 4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) - '@tanem/react-nprogress': 6.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanem/react-nprogress': 6.0.3(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) '@tanstack/react-query': 5.101.4(react@19.2.8) '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@unhead/react': 3.2.3(@oxc-project/types@0.140.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react@19.2.8)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + '@unhead/react': 3.2.3(@oxc-project/types@0.140.0)(lightningcss@1.33.0)(oxc-parser@0.140.0)(react@19.2.8)(rolldown@1.1.5)(rollup@4.63.1)(srvx@1.0.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) '@vitejs/plugin-react': 6.0.4(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) '@x0k/json-schema-merge': 1.0.4 bs58: 6.0.0 class-variance-authority: 0.7.1 clsx: 2.1.1 - cmdk: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + cmdk: 1.1.1(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) dotenv: 17.4.2 embla-carousel-react: 8.6.0(react@19.2.8) esbuild: 0.28.2 @@ -7915,9 +7825,9 @@ snapshots: mdast-util-mdx: 3.0.0(supports-color@7.2.0) mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) micromark-extension-mdxjs: 3.0.0 - motion: 12.42.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + motion: 12.42.2(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) nanoevents: 9.1.0 - next-themes: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next-themes: 0.4.6(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) oauth4webapi: 3.8.6 openapi-types: 12.1.3 pagefind: 1.5.2 @@ -7931,7 +7841,7 @@ snapshots: react-hook-form: 7.83.0(react@19.2.8) react-is: 19.2.8 react-markdown: 10.1.0(@types/react@19.2.17)(react@19.2.8)(supports-color@7.2.0) - react-router: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-router: 8.3.0(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) rehype-mdx-import-media: 1.4.0(supports-color@7.2.0) rehype-raw: 7.0.0 rehype-slug: 6.0.0 @@ -7950,7 +7860,7 @@ snapshots: tw-animate-css: 1.4.0 unified: 11.0.5 unist-util-visit: 5.1.0 - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + vaul: 1.1.2(@types/react@19.2.17)(@types/react-dom@19.2.3(@types/react@19.2.17))(react@19.2.8)(react-dom@19.2.8(react@19.2.8)) vfile: 6.0.3 vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) yaml: 2.9.0 @@ -7963,11 +7873,8 @@ snapshots: - '@farmfe/core' - '@modelcontextprotocol/client' - '@modelcontextprotocol/server' - - '@oxc-project/types' - '@rolldown/plugin-babel' - '@rspack/core' - - '@types/json-schema' - - '@types/node' - '@unhead/cli' - '@vitejs/devtools' - babel-plugin-react-compiler @@ -7976,24 +7883,16 @@ snapshots: - date-fns - graphql-ws - immer - - jiti - less - - lightningcss - ocache - - oxc-parser - - rolldown - - rollup - rxjs - sass - sass-embedded - - srvx - stylus - sugarss - - supports-color - terser - tsx - unloader - - use-sync-external-store - webpack zustand@5.0.14(@types/react@19.2.17)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): diff --git a/scripts/update_zudoku_guides.py b/scripts/update_zudoku_guides.py index d4fdad02..cdf32cf8 100755 --- a/scripts/update_zudoku_guides.py +++ b/scripts/update_zudoku_guides.py @@ -1,9 +1,15 @@ #!/usr/bin/env python3 """ -Rewrite the guides list in zudoku.config.tsx based on the MDX files in pages/guides/. +Rewrite the example apps list in zudoku.config.tsx based on the MDX files in pages/guides/. + +It replaces everything between two sentinel comments in zudoku.config.tsx: + + // AUTO-GENERATED:GUIDES-START + ... + // AUTO-GENERATED:GUIDES-END For each MDX file (except overview.mdx) the title is read from the YAML front-matter. -Entries are sorted alphabetically by title. The overview entry is always first. +Entries are sorted alphabetically by title. Usage: update_zudoku_guides.py GUIDES_DIR ZUDOKU_CONFIG """ @@ -16,12 +22,10 @@ FRONT_MATTER_TITLE = re.compile(r'^title:\s*["\']?(.+?)["\']?\s*$', re.MULTILINE) -# Matches the entire items array inside the "Guides" navigation category. -# Captures the indentation of the first item so we can reproduce it. -GUIDES_ITEMS_PATTERN = re.compile( - r'(label:\s*"Guides"[^[]*items:\s*\[)' # up to and including "items: [" - r'(.*?)' # the current list content (group 2) - r'(\s*\])', # closing "]" with optional whitespace +SENTINEL_PATTERN = re.compile( + r'([ \t]*)// AUTO-GENERATED:GUIDES-START\n' + r'.*?' + r'([ \t]*)// AUTO-GENERATED:GUIDES-END', re.DOTALL, ) @@ -46,41 +50,36 @@ def build_items_block(guides_dir: Path, indent: str) -> str: entries.sort(key=lambda t: t[0].casefold()) - lines: list[str] = [] - lines.append(f'{indent}//TODO: Please keep this list sorted by titles, not filenames !!') - lines.append(f'{indent}"/guides/overview", // Guides Overview') - for title, slug in entries: - lines.append(f'{indent}"/guides/{slug}", // {title}') - - return "\n".join(lines) + "\n" + lines = [f'{indent}"/guides/{slug}", // {title}' for title, slug in entries] + return "\n".join(lines) def update_config(guides_dir: Path, config_path: Path) -> None: content = config_path.read_text(encoding="utf-8") - match = GUIDES_ITEMS_PATTERN.search(content) + match = SENTINEL_PATTERN.search(content) if not match: - print("❌ Could not locate the Guides items list in zudoku.config.tsx", file=sys.stderr) + print( + "❌ Could not locate AUTO-GENERATED:GUIDES-START/END sentinels " + f"in {config_path}", + file=sys.stderr, + ) sys.exit(1) - # Detect indentation from the first non-empty line inside the current block - current_block = match.group(2) - indent_match = re.search(r'\n(\s+)"/', current_block) - indent = indent_match.group(1) if indent_match else " " - + indent = match.group(1) new_block = build_items_block(guides_dir, indent) new_content = ( - content[: match.start(2)] - + "\n" - + new_block - + " ]" # closing "]" with fixed indentation, replacing the captured \s*\] - + content[match.end(3):] + content[: match.start()] + + f'{indent}// AUTO-GENERATED:GUIDES-START\n' + + new_block + "\n" + + f'{indent}// AUTO-GENERATED:GUIDES-END' + + content[match.end():] ) config_path.write_text(new_content, encoding="utf-8") print(f" ✅ Updated guides list in {config_path.name} " - f"({len(new_block.splitlines()) - 2} guide entries)") + f"({len(new_block.splitlines())} guide entries)") def main() -> None: diff --git a/src/TopNavMenus.tsx b/src/TopNavMenus.tsx new file mode 100644 index 00000000..a14a5ee2 --- /dev/null +++ b/src/TopNavMenus.tsx @@ -0,0 +1,223 @@ +/// +import { useMemo } from "react"; +import type { NavigationItem } from "zudoku"; +import { useZudoku } from "zudoku/hooks"; +import type { LucideIcon } from "zudoku/icons"; +import { Link, useHref } from "zudoku/router"; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, +} from "zudoku/ui/NavigationMenu"; + +export type TopNavMenu = { + label: string; + icon?: LucideIcon; + /** + * Labels of top-level `navigation` items. The menu links to each one, and + * shows in the tab row where the first one was. + */ + tabs: string[]; +}; + +type Tab = { + label: string; + icon?: LucideIcon; + to: string; + href: string; + paths: string[]; +}; + +type ResolvedMenu = TopNavMenu & { items: Tab[]; position: number }; + +const isExternal = (to: string) => /^[a-z][a-z\d+.-]*:/i.test(to); + +const withSlash = (path: string) => (path.startsWith("/") ? path : `/${path}`); + +// Keep in step with getFirstMatchingPath() in Zudoku, because the tab row +// links each tab to this path, and the CSS finds the tab by that href. +const firstPath = (item: NavigationItem): string | undefined => { + switch (item.type) { + case "doc": + case "custom-page": + return withSlash(item.path); + case "link": + return item.to; + case "category": { + if (item.link) { + return item.link.type === "doc" ? withSlash(item.link.path) : item.link.to; + } + const pageIn = (items: NavigationItem[]): string | undefined => { + for (const child of items) { + const path = + child.type === "category" ? pageIn(child.items) : firstPath(child); + if (path) return path; + } + }; + return pageIn(item.items); + } + default: + return undefined; + } +}; + +const allPaths = (item: NavigationItem): string[] => { + switch (item.type) { + case "doc": + case "custom-page": + return [withSlash(item.path)]; + case "link": + return isExternal(item.to) ? [] : [item.to]; + case "category": + return [ + ...(item.link ? [firstPath({ ...item, items: [] })!] : []), + ...item.items.flatMap(allPaths), + ]; + default: + return []; + } +}; + +// A link item such as the Platform API also owns the pages under its path. +const isOn = (pathname: string, paths: string[]) => + paths.some((path) => pathname === path || pathname.startsWith(`${path}/`)); + +const tabSelector = (href: string) => + `div:has(> [data-top-nav-menu]) > nav:not([data-top-nav-menu]) li:has(> a[href=${JSON.stringify(href)}])`; + + +const buildCss = (menus: ResolvedMenu[]) => { + const rules = [ + `div:has(> [data-top-nav-menu]) { justify-content: flex-start; column-gap: 2rem; }`, + `div:has(> [data-top-nav-menu]) > nav:not([data-top-nav-menu]), + div:has(> [data-top-nav-menu]) > nav:not([data-top-nav-menu]) > ul { display: contents; }`, + `div:has(> [data-top-nav-menu]) > :not(nav) { order: 9999; margin-inline-start: auto; }`, + `.top-nav-menu-trigger[data-active]::after { + content: ""; position: absolute; inset-inline: 0; bottom: 0; height: 2px; + background: var(--color-primary); + }`, + ]; + + menus.forEach((menu, index) => { + const first = tabSelector(menu.items[0]!.href); + rules.push(`${first}, ${first} ~ li { order: ${2 * (index + 1)}; }`); + rules.push( + `${menu.items.map((tab) => tabSelector(tab.href)).join(",\n")} { display: none; }`, + ); + }); + + return rules.join("\n"); +}; + +/** + * Groups top-level navigation tabs into drop-down menus in the desktop tab row. + * Use the return value as the "top-navigation-side" slot. + * + * The tabs stay in `navigation`, because they give their pages a sidebar and + * they are the entries in the mobile menu. Only the desktop tab row hides them. + */ +export const topNavMenus = (menus: TopNavMenu[]) => { + const TopNavMenus = ({ location }: { location: { pathname: string } }) => { + const { options } = useZudoku(); + const base = useHref("/").replace(/\/$/, ""); + + const resolved = useMemo(() => { + const navigation = options.navigation ?? []; + + return menus + .flatMap((menu): ResolvedMenu[] => { + const items = menu.tabs.flatMap((label): Tab[] => { + const item = navigation.find((entry) => entry.label === label); + const to = item && firstPath(item); + if (!item || !to) { + if (import.meta.env.DEV) { + console.warn( + `[topNavMenus] "${menu.label}": no top-level navigation item has the label "${label}".`, + ); + } + return []; + } + const icon = "icon" in item ? item.icon : undefined; + return [ + { + label, + icon: typeof icon === "string" ? undefined : icon, + to, + href: isExternal(to) ? to : `${base}${to}`, + paths: allPaths(item), + }, + ]; + }); + if (items.length === 0) return []; + + const position = navigation.findIndex( + (entry) => entry.label === items[0]!.label, + ); + return [{ ...menu, items, position }]; + }) + .sort((a, b) => a.position - b.position); + }, [options.navigation, base]); + + const css = useMemo(() => buildCss(resolved), [resolved]); + + return ( + <> +