diff --git a/.changeset/vue-bindings.md b/.changeset/vue-bindings.md new file mode 100644 index 0000000..3260128 --- /dev/null +++ b/.changeset/vue-bindings.md @@ -0,0 +1,57 @@ +--- +'@dunky.dev/vue-state-machine': minor +--- + +Add `@dunky.dev/vue-state-machine` — the Vue bindings target, for `vue` `^3.4.0`. + +A first-class Vue bridge with the same four exports as the React and Solid +targets. `useMachine` builds the machine and connector once in `setup()`, +exposes the connector's memoized snapshot as a `ComputedRef`, and keeps props +fresh through `setProps`. After mount it starts the machine, then each +`ComponentEffect`, as React does; an effect re-runs only when one of its named +prop deps changes, after the DOM has been patched, and each run gets its own +snapshot of the props, so its cleanup undoes exactly what it set up. On unmount +the machine stops before the component's children unmount, then the effect +cleanups run. Under `` a deactivated component pauses — machine +stopped, effects torn down — and resumes with its state intact, like React's +``; one mounted into a view that is already deactivated holds still +until the view activates. Server rendering starts nothing and runs no effect. +`useSelector` returns a readonly ref holding the selected value as-is; it +re-selects on machine changes and when a reactive value the selector reads +changes, and takes the machine as a value, ref, or getter. `normalize` reuses +the shared DOM translation from `@dunky.dev/state-machine-dom` with Vue's +listener names (`onPointerenter`, `onKeydown` — Vue derives the DOM event by +hyphenating a listener's camel tail). `mergeProps` chains handlers with the +`defaultPrevented` veto, which Vue's own `mergeProps` doesn't — a consumer's +handler array included, and a capture listener like `onClickCapture` too — and +merges `class`/`style` of any shape as `[consumer, library]`. + +```vue + + + +``` + +Rules a consumer needs. Declare boolean props with `default: undefined`: an +absent `Boolean` prop is cast to `false`, which would make `open` permanently +controlled and drop a `modal: true` default. Replace prop values instead of +mutating them in place: props reach the machine through `setProps`, which +compares by identity. In an async `setup()`, call `useMachine` before the first +`await` — Vue binds only the lifecycle hooks registered before it. And send from +event handlers, `watch` callbacks, or hooks rather than `watchEffect`, a +`computed`, or a render function: the bridge runs its own callbacks untracked, +but the machine's actions and guards run inside whatever sent. diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 740db21..7ecf292 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,5 +1,5 @@ { - "*.{ts,tsx}": ["oxlint --fix --no-error-on-unmatched-pattern", "oxfmt"], + "*.{ts,tsx,vue}": ["oxlint --fix --no-error-on-unmatched-pattern", "oxfmt"], "*.astro": ["oxlint --fix --no-error-on-unmatched-pattern"], "*.{md,mdx}": ["oxfmt"] } diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index f073b9b..a3176b8 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -82,6 +82,7 @@ record: | +-- react -> aria-hidden +-- solid -> aria-hidden + +-- vue -> aria-hidden +-- native -> aria-hidden (RN's web-aligned alias, fanned out per platform) +-- opentui -> visible={false} (no accessibility tree; the visual analog) ``` diff --git a/AGENTS.md b/AGENTS.md index 030105b..ac3fe92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,9 +5,9 @@ repo. This file is the canonical entry point: read it first, every time. This is Dunky's state-machine monorepo: UI behavior authored once as plain TypeScript state machines (`packages/core`), rendered anywhere -through thin per-substrate targets (`react`, `solid`, `native`, `opentui`), with -a benchmark suite, per-substrate sandboxes, and the docs website -alongside. +through thin per-substrate targets (`react`, `solid`, `vue`, `native`, +`opentui`), with a benchmark suite, per-substrate sandboxes, and the +docs website alongside. ## Preflight @@ -24,12 +24,12 @@ editing files in that scope — it overrides anything here for that scope ## Scopes -| Scope | Path | What it is | -| --------- | ------------- | ----------------------------------------------------------------------------------------- | -| Packages | `packages/**` | The core machine, substrate targets (react, solid, native, opentui), and shared internals | -| Benchmark | `benchmark/` | Perf suite comparing against competitor libraries | -| Sandbox | `sandbox/` | Per-substrate demo apps for manual verification | -| Website | `website/` | The docs site | +| Scope | Path | What it is | +| --------- | ------------- | ---------------------------------------------------------------------------------------------- | +| Packages | `packages/**` | The core machine, substrate targets (react, solid, vue, native, opentui), and shared internals | +| Benchmark | `benchmark/` | Perf suite comparing against competitor libraries | +| Sandbox | `sandbox/` | Per-substrate demo apps for manual verification | +| Website | `website/` | The docs site | Some changes are cross-scope: a change in `core/` may need follow-up in the targets, sandboxes, and docs — and vice versa. Check what else your diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 54cec89..786ba9a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -29,7 +29,7 @@ The host | bridged per target v +------------------------------------------------------------------------+ -| (react, solid, native, opentui, …) | +| (react, solid, vue, native, opentui, …) | | Runtime-specific bridge | | • lifecycle (build + start/stop) • normalize bindings -> props | | • selector subscription | @@ -58,7 +58,7 @@ actions. Nothing in `core/` knows that React or the DOM exists. substrate-agnostic event and attr vocabulary (`onPress`, `role`, …); `shared/utils` owns cross-target helpers (mergeProps, composeHandlers). -**`/`** is the substrate side — `react`, `solid`, `native`, `opentui`, and any +**`/`** is the substrate side — `react`, `solid`, `vue`, `native`, `opentui`, and any future renderer. Each target is the runtime bridge for one environment: the lifecycle bridge, the event normalization, and the selector subscription all live here. @@ -93,12 +93,12 @@ Zag, whose machines read props directly.) ## Project structure -| File / location | What it owns | -| --------------------------- | ----------------------------------------------------------------- | -| `packages/core/` | State-machine engine (plain-mutation kernel) | -| `packages/shared/bindings/` | Substrate-agnostic event + attr vocabulary (onPress, role, …) | -| `packages/shared/utils/` | mergeProps, composeHandlers | -| `packages//` | Hook + normalize per substrate (react, solid, native, opentui, …) | +| File / location | What it owns | +| --------------------------- | ---------------------------------------------------------------------- | +| `packages/core/` | State-machine engine (plain-mutation kernel) | +| `packages/shared/bindings/` | Substrate-agnostic event + attr vocabulary (onPress, role, …) | +| `packages/shared/utils/` | mergeProps, composeHandlers | +| `packages//` | Hook + normalize per substrate (react, solid, vue, native, opentui, …) | ## The map @@ -121,9 +121,9 @@ shared/bindings substrate-agnostic event + attr vocabulary shared/utils cross-target, cross-component helpers +-- (mergeProps, composeHandlers) - one substrate (react, solid, native, opentui, …) + one substrate (react, solid, vue, native, opentui, …) | runtime, hooks, and props translator -+-- use-machine lifecycle bridge (build + start/stop + useSyncExternalStore) ++-- use-machine lifecycle bridge (build + start/stop + snapshot subscription) +-- use-selector fine-grained leaf subscription (O(readers)) +-- normalize bindings -> target props ``` @@ -136,7 +136,7 @@ Three package groups, three jobs: event + attr vocabulary; `shared/utils` owns agnostic helpers (prop merging, handler composition). - **`/`** — _the substrate side_. One folder per renderer - (`react`, `solid`, `native`, `opentui`). Owns its runtime bridge and its props translator. + (`react`, `solid`, `vue`, `native`, `opentui`). Owns its runtime bridge and its props translator. ## The machine parts @@ -177,7 +177,7 @@ whether it needs props/platform or not: | Term | What it is | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **host** | The agnostic core — `packages/core/*`. Declares what behavior is. | -| **target** | A substrate-specific bridge package and its render environment — `packages//*` (`react`, `solid`, `native`, `opentui`, …). | +| **target** | A substrate-specific bridge package and its render environment — `packages//*` (`react`, `solid`, `vue`, `native`, `opentui`, …). | | **machine** | A state-graph config consumed by `machine()`; returns a startable service. | | **connect** | A function returning the logical surface a view spreads onto elements. | | **bindings** | The substrate-agnostic event + attr vocabulary — lives in `shared/bindings`, consumed by every target's normalize. Each target's rename map and drop set are vocabulary-typed (`HandlerTargets`/`AttrTargets`, `HandlerKey`/`AttrKey`), so a typo'd or unknown key is a compile error. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eeb6a21..0cb6dac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,7 @@ the short version: ```bash pnpm -C sandbox/react dev +pnpm -C sandbox/vue dev pnpm -C sandbox/opentui dev # terminal — needs Bun pnpm -C sandbox/native dev # Expo — needs a simulator or device ``` diff --git a/packages/dom/src/index.ts b/packages/dom/src/index.ts index 9bac11e..b0b1eae 100644 --- a/packages/dom/src/index.ts +++ b/packages/dom/src/index.ts @@ -1,9 +1,12 @@ import type { AttrTargets, HandlerTargets } from '@dunky.dev/state-machine-bindings' /** - * Handler names shared verbatim by every DOM target. The two divergent keys + * Handler names shared by every DOM target. The two divergent keys * (`onValueChange`, `onDoublePress`) are deliberately absent — each target - * adds its own. + * adds its own. Each value is `on` + the camelCased DOM event: React and Solid + * use them verbatim, and Vue derives its listener names from them by + * lowercasing everything after the leading capital (`onPointerEnter` → + * `onPointerenter`), so a new entry must follow that shape. */ export const DOM_HANDLER_MAP: HandlerTargets = { onPress: 'onClick', @@ -26,7 +29,7 @@ export const DOM_HANDLER_MAP: HandlerTargets = { /** * The `aria-` projection of the logical attr vocabulary — pure DOM truth, * identical in every DOM target. `focusable` is deliberately absent: its - * target prop differs in casing (React `tabIndex`, Solid `tabindex`). + * target prop differs in casing (React `tabIndex`, Solid and Vue `tabindex`). */ export const DOM_ATTR_MAP: AttrTargets = { describedBy: 'aria-describedby', @@ -84,8 +87,9 @@ export const DOM_ATTR_MAP: AttrTargets = { } /** - * The DOM event fields the payload adapters read. React's synthetic events - * and Solid's native events expose the same names, so one shape serves both. + * The DOM event fields the payload adapters read. React's synthetic events and + * the native events Solid and Vue hand over expose the same names, so one + * shape serves every target. */ export type AnyEvent = { target?: { value?: unknown; checked?: unknown; type?: string } diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md new file mode 100644 index 0000000..466039c --- /dev/null +++ b/packages/vue/CHANGELOG.md @@ -0,0 +1 @@ +# @dunky.dev/vue-state-machine diff --git a/packages/vue/LICENSE b/packages/vue/LICENSE new file mode 100644 index 0000000..08a9692 --- /dev/null +++ b/packages/vue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ivan Banov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 0000000..b7ee76b --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,459 @@ +# `@dunky.dev/vue-state-machine` + +The **Vue bindings** for [`@dunky.dev/state-machine`](../core/README.md). + +The behavior lives in the core machine — plain TypeScript, no renderer. This +package is the thin Vue edge that runs it. It does four things: + +1. **`useMachine`** — build the machine once, run its lifecycle, expose its + snapshot as a computed ref, run the component's platform effects. +2. **`useSelector`** — wake a leaf component only when one slice changes. +3. **`normalize`** — translate the machine's agnostic bindings (`onPress`, + `checked`) into real DOM props (`onClick`, `aria-checked`). +4. **`mergeProps`** — merge the consumer's props with the component's. + +``` + core (agnostic) + | + | config + connect() behavior + snapshot -> view api + | + v + this package (Vue) + | + | useMachine build + start the machine, subscribe + | | + | v + | api computed ref of the snapshot + | | + | v + | normalize() DOM / ARIA / events + | + v + ', + ) + expect(escapeListener[0]).not.toHaveBeenCalled() + expect(onStart).not.toHaveBeenCalled() + }) + + it('subscribes useSelector to nothing, so a machine outliving the render keeps no listener', async () => { + const shared = machine(createToggleConfig({})) + const subscribe = vi.spyOn(shared, 'subscribe') + const Reader = defineComponent({ + setup() { + const open = useSelector(shared, () => shared.matches('open')) + return () => h('span', String(open.value)) + }, + }) + expect(await renderToString(createSSRApp(Reader))).toBe('false') + expect(subscribe).not.toHaveBeenCalled() + }) + + it('renders a useSelector value that reflects a send made during setup', async () => { + const Reader = defineComponent({ + setup() { + // Per request, and observed by nothing reactive: only the selector itself reads it. + const m = machine(createToggleConfig({})) + const open = useSelector(m, () => m.matches('open')) + void open.value // a read during setup must not pin the value for the render + m.send({ type: 'toggle' }) + return () => h('span', String(open.value)) + }, + }) + expect(await renderToString(createSSRApp(Reader))).toBe('true') + }) +}) diff --git a/packages/vue/tests/unhandled-effect-error.test.ts b/packages/vue/tests/unhandled-effect-error.test.ts new file mode 100644 index 0000000..45dc22c --- /dev/null +++ b/packages/vue/tests/unhandled-effect-error.test.ts @@ -0,0 +1,36 @@ +// @vitest-environment jsdom +// An effect error no errorHandler catches. Its own file: once an error escapes a flush, Vue's +// post-flush queue stays stuck for the rest of the module graph, which would break any test +// that ran after it. +import { createApp, defineComponent } from 'vue' +import { expect, it, vi } from 'vitest' +import { type ComponentEffect, useMachine } from '@dunky.dev/vue-state-machine' +import { + connectToggle, + createToggleConfig, + type ToggleMachine, + type ToggleProps, +} from './fixtures/toggle' + +it('starts every effect before Vue rethrows the failure', () => { + const started: string[] = [] + const effects: ComponentEffect[] = [ + [ + () => { + throw new Error('effect failed') + }, + [], + ], + [() => void started.push('next'), []], + ] + const Comp = defineComponent({ + setup() { + const props: ToggleProps = {} + useMachine(createToggleConfig, connectToggle, effects, props) + return () => null + }, + }) + vi.spyOn(console, 'warn').mockImplementation(() => {}) // Vue's "unhandled error" notice + expect(() => createApp(Comp).mount(document.createElement('div'))).toThrow('effect failed') + expect(started).toEqual(['next']) +}) diff --git a/packages/vue/tests/use-machine.test.ts b/packages/vue/tests/use-machine.test.ts new file mode 100644 index 0000000..cf860b0 --- /dev/null +++ b/packages/vue/tests/use-machine.test.ts @@ -0,0 +1,577 @@ +// @vitest-environment jsdom +/** + * `useMachine` — the Vue bridge composable. Pins the contract the README + * documents: build ONCE in setup, start after mount and stop before unmount, + * keep props fresh via a shallow setProps, run the connector's reactions with + * the latest callbacks, run each ComponentEffect post-mount as its own + * dep-keyed effect (React's effect order), and pause everything while a + * holds the component deactivated (React's ). + */ +import { + createApp, + defineComponent, + effectScope, + h, + KeepAlive, + nextTick, + onMounted, + onUnmounted, + ref, + Suspense, + type PropType, + watchEffect, +} from 'vue' +import { flushPromises, mount } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { act as write, type Connect, type TransitionConfig } from '@dunky.dev/state-machine' +import { type ComponentEffect, useMachine } from '@dunky.dev/vue-state-machine' +import { + connectToggle, + createToggleConfig, + type ToggleApi, + type ToggleMachine, + type ToggleProps, +} from './fixtures/toggle' + +type Effect = ComponentEffect + +// `default: undefined`: Vue casts an absent Boolean prop to `false`, which the +// core would read as a real value. +const toggleProps = { + label: String, + defaultOpen: { type: Boolean, default: undefined }, + onOpenChange: Function as PropType<(open: boolean) => void>, +} + +afterEach(() => { + vi.clearAllMocks() + document.body.innerHTML = '' +}) + +// `log` records the machine lifecycle next to whatever the effects record, so +// the tests can assert the relative order. +function harness(effects: Effect[] = [], log: string[] = []) { + const sink: { api?: ToggleApi; machine?: ToggleMachine; renders: number } = { renders: 0 } + const Comp = defineComponent({ + props: toggleProps, + setup(props) { + const { api, machine } = useMachine(createToggleConfig, connectToggle, effects, props) + machine.onStart(() => log.push('start')) + machine.onStop(() => log.push('stop')) + sink.machine = machine + return () => { + sink.api = api.value + sink.renders++ + return h('div', { id: 'toggle-root' }, api.value.label ?? '∅') + } + }, + }) + return { sink, Comp, log } +} + +// Records the effect's runs and cleanups into the same log as the lifecycle. +const loggingEffect = (log: string[], deps: (keyof ToggleProps)[] = []): Effect => [ + () => { + log.push('effect') + return () => log.push('cleanup') + }, + deps, +] + +describe('useMachine — lifecycle', () => { + it('returns { api, machine }: api is a ref of the connect() output, machine the service', () => { + const { sink, Comp } = harness() + mount(Comp, { props: { label: 'hi' } }) + expect(sink.api).toMatchObject({ open: false, label: 'hi', count: 0 }) + expect(typeof sink.machine!.send).toBe('function') + }) + + it('re-renders when the snapshot changes', async () => { + const { sink, Comp } = harness() + mount(Comp) + const before = sink.renders + sink.api!.toggle() + await nextTick() + expect(sink.renders).toBeGreaterThan(before) + expect(sink.api).toMatchObject({ open: true, count: 1 }) + }) +}) + +describe('useMachine — snapshot timing', () => { + it('reads the snapshot once a transition completes, never in the middle of one', async () => { + type EditState = 'idle' | 'editing' + type EditEvent = { type: 'edit' } + // The draft exists only once `editing`'s entry action has run — after the state + // change has already notified. + const config: TransitionConfig = { + initial: 'idle', + context: {}, + states: { + idle: { on: { edit: { target: 'editing' } } }, + editing: { entry: [write(() => ({ draft: 'hi' }))] }, + }, + } + const connectDraft: Connect = ({ + state, + context, + }) => (state === 'editing' ? context.draft!.length : -1) + let send!: (event: EditEvent) => void + const Comp = defineComponent({ + setup() { + const { api, machine } = useMachine(() => config, connectDraft, [], {}) + send = machine.send + return () => h('div', String(api.value)) + }, + }) + const wrapper = mount(Comp) + expect(() => send({ type: 'edit' })).not.toThrow() + await nextTick() + expect(wrapper.text()).toBe('2') + }) +}) + +describe('useMachine — build once', () => { + it('builds the machine ONCE: state survives prop changes (no rebuild)', async () => { + const { sink, Comp } = harness() + const wrapper = mount(Comp, { props: { label: 'a' } }) + sink.api!.toggle() // → open, count 1 + await nextTick() + + await wrapper.setProps({ label: 'b' }) // a prop change must NOT rebuild/reset state + expect(sink.api).toMatchObject({ open: true, count: 1 }) + }) +}) + +describe('useMachine — props freshness via setProps', () => { + it('flows later prop changes into the snapshot', async () => { + const { sink, Comp } = harness() + const wrapper = mount(Comp, { props: { label: 'first' } }) + await wrapper.setProps({ label: 'second' }) + expect(sink.api!.label).toBe('second') + }) + + it('value-dedups: an equal-valued prop update keeps the snapshot identity', async () => { + const { sink, Comp } = harness() + const wrapper = mount(Comp, { props: { label: 'x' } }) + const before = sink.api + await wrapper.setProps({ label: 'x' }) + expect(sink.api).toBe(before) + }) + + it('reactions call the latest prop callback', async () => { + const first = vi.fn() + const second = vi.fn() + const { sink, Comp } = harness() + const wrapper = mount(Comp, { props: { onOpenChange: first } }) + await wrapper.setProps({ onOpenChange: second }) + sink.api!.toggle() + expect(second).toHaveBeenCalledWith(true) + expect(first).not.toHaveBeenCalled() + }) + + it('never walks into prop values — setProps is shallow, and so is the props watch', async () => { + const reads = vi.fn() + const nested = { + get deep() { + reads() + return 1 + }, + } + const label = ref('a') + const Comp = defineComponent({ + setup() { + // A getter is a valid props source too (MaybeRefOrGetter). + const props = () => ({ label: label.value, nested }) as ToggleProps + const { api } = useMachine(createToggleConfig, connectToggle, [], props) + return () => h('div', api.value.label) + }, + }) + const wrapper = mount(Comp) + label.value = 'b' + await nextTick() + expect(wrapper.text()).toBe('b') + expect(reads).not.toHaveBeenCalled() + }) + + it('hands props over exactly as Vue resolved them — an absent Boolean arrives as false', () => { + let seen: Record | undefined + const Comp = defineComponent({ + props: { ...toggleProps, plain: Boolean }, + setup(props) { + const effects: Effect[] = [[(_machine, p) => void (seen = { ...p }), []]] + useMachine(createToggleConfig, connectToggle, effects, props) + return () => null + }, + }) + mount(Comp) + // `plain` shows the cast; `defaultOpen` shows the `default: undefined` fix. + expect(seen).toMatchObject({ plain: false, defaultOpen: undefined }) + }) +}) + +describe('useMachine — sends from tracked code', () => { + it('runs no prop callback under the tracking of the effect that sent', async () => { + const unrelated = ref(0) + const go = ref(false) + const onOpenChange = vi.fn(() => void unrelated.value) // a callback reading reactive state + const { sink, Comp } = harness() + mount(Comp, { props: { onOpenChange } }) + const scope = effectScope() + scope.run(() => watchEffect(() => void (go.value && sink.machine!.send({ type: 'toggle' })))) + go.value = true + await nextTick() + unrelated.value++ // must not re-run the watchEffect, which would toggle again + await nextTick() + expect(onOpenChange.mock.calls).toEqual([[true]]) + expect(sink.api!.open).toBe(true) + scope.stop() + }) + + it('leaves nothing behind in the effect scope a send runs inside', () => { + const { sink, Comp } = harness() + mount(Comp, { props: { onOpenChange: () => {} } }) + // Vue 3.4/3.5 keep a scope's effects in an array; 3.6 links them, and has no such field. + const scope = effectScope() + const held = () => (scope as unknown as { effects?: unknown[] }).effects?.length ?? 0 + scope.run(() => { + for (let i = 0; i < 10; i++) sink.machine!.send({ type: 'toggle' }) // ten reactions + }) + expect(held()).toBe(0) + scope.stop() + }) +}) + +describe('useMachine — reactions follow the machine lifecycle', () => { + it('fires the connect reaction while mounted, never after unmount', () => { + const onOpenChange = vi.fn() + const { sink, Comp } = harness() + const wrapper = mount(Comp, { props: { onOpenChange } }) + expect(onOpenChange).not.toHaveBeenCalled() // not on subscribe + sink.api!.toggle() + expect(onOpenChange).toHaveBeenLastCalledWith(true) + + // Unmount stops the machine, which unhooks the reactions: a send may still + // transition, but the callback must not fire. + wrapper.unmount() + sink.api!.toggle() + expect(onOpenChange).toHaveBeenCalledOnce() + }) + + it('lets a machine kept past unmount restart without calling the component back', () => { + const onOpenChange = vi.fn() + const { sink, Comp } = harness() + mount(Comp, { props: { onOpenChange } }).unmount() + sink.machine!.start() + sink.machine!.send({ type: 'toggle' }) + expect(onOpenChange).not.toHaveBeenCalled() + }) +}) + +describe('useMachine — component effects', () => { + it('runs each effect after mount, once the machine has started', () => { + const log: string[] = [] + let rendered: Element | null = null + const effects: Effect[] = [ + [ + () => { + rendered = document.getElementById('toggle-root') + log.push('effect') + }, + [], + ], + ] + const { Comp } = harness(effects, log) + mount(Comp, { attachTo: document.body }) + expect(log).toEqual(['start', 'effect']) + expect(rendered).not.toBeNull() + }) + + it('re-runs an effect ONLY when one of its named prop deps changes', async () => { + const fn = vi.fn() + const { Comp } = harness([[fn, ['label']]]) + const wrapper = mount(Comp, { props: { label: 'a' } }) + expect(fn).toHaveBeenCalledOnce() + + await wrapper.setProps({ label: 'a', onOpenChange: () => {} }) // dep equal, non-dep changed + expect(fn).toHaveBeenCalledOnce() + + await wrapper.setProps({ label: 'b' }) + expect(fn).toHaveBeenCalledTimes(2) + }) + + it('does NOT re-run when the effect body reads a prop outside its deps', async () => { + // The authored deps list is the whole re-run contract, as with React's dep + // array: a prop the body merely reads must not become a hidden dependency. + const fn = vi.fn((_machine: ToggleMachine, props: ToggleProps) => void props.label) + const { Comp } = harness([[fn, []]]) + const wrapper = mount(Comp, { props: { label: 'a' } }) + await wrapper.setProps({ label: 'b' }) + expect(fn).toHaveBeenCalledOnce() + }) + + it('runs the previous cleanup BEFORE re-running on a dep change', async () => { + const log: string[] = [] + const { Comp } = harness([loggingEffect(log, ['label'])], log) + const wrapper = mount(Comp, { props: { label: 'a' } }) + await wrapper.setProps({ label: 'b' }) + expect(log).toEqual(['start', 'effect', 'cleanup', 'effect']) + }) + + it("hands each run a snapshot of its props, so a cleanup undoes that run's setup", async () => { + const registered = new Set() + const effects: Effect[] = [ + [ + (_machine, props) => { + registered.add(props.label) + return () => registered.delete(props.label) + }, + ['label'], + ], + ] + const { Comp } = harness(effects) + const wrapper = mount(Comp, { props: { label: 'a' } }) + await wrapper.setProps({ label: 'b' }) + expect([...registered]).toEqual(['b']) + }) + + it('sets an effect up again even when its previous cleanup throws', async () => { + const log: string[] = [] + const effects: Effect[] = [ + [ + () => { + log.push('effect') + return () => { + throw new Error('cleanup failed') + } + }, + ['label'], + ], + ] + const { Comp } = harness(effects, log) + const errorHandler = vi.fn() + const wrapper = mount(Comp, { props: { label: 'a' }, global: { config: { errorHandler } } }) + await wrapper.setProps({ label: 'b' }) + expect(errorHandler).toHaveBeenCalledOnce() + expect(log).toEqual(['start', 'effect', 'effect']) + }) + + it('reports both a throwing cleanup and the setup that then throws', async () => { + const effects: Effect[] = [ + [ + (_machine, props) => { + if (props.label === 'b') throw new Error('setup failed') + return () => { + throw new Error('cleanup failed') + } + }, + ['label'], + ], + ] + const { Comp } = harness(effects) + const errorHandler = vi.fn() + const wrapper = mount(Comp, { props: { label: 'a' }, global: { config: { errorHandler } } }) + await wrapper.setProps({ label: 'b' }) + const reported = errorHandler.mock.calls.map(([error]) => + error instanceof AggregateError ? error.errors.map(e => e.message) : [error.message], + ) + expect(reported).toEqual([['cleanup failed', 'setup failed']]) + }) + + it('re-runs after the DOM reflects the dep change', async () => { + const seen: (string | null | undefined)[] = [] + const effects: Effect[] = [ + [() => void seen.push(document.getElementById('toggle-root')?.textContent), ['label']], + ] + const { Comp } = harness(effects) + const wrapper = mount(Comp, { props: { label: 'a' }, attachTo: document.body }) + await wrapper.setProps({ label: 'b' }) + expect(seen).toEqual(['a', 'b']) + }) + + it('reports a throwing effect and still runs and cleans up the others', () => { + const log: string[] = [] + const throwing: Effect = [ + () => { + throw new Error('effect failed') + }, + [], + ] + const { Comp } = harness([loggingEffect(log), throwing, loggingEffect(log)], log) + // A plain app: test-utils rethrows mount errors even past the app's errorHandler. + const app = createApp(Comp) + app.config.errorHandler = vi.fn() + app.mount(document.createElement('div')) + app.unmount() + expect(app.config.errorHandler).toHaveBeenCalledOnce() + expect(log).toEqual(['start', 'effect', 'effect', 'stop', 'cleanup', 'cleanup']) + }) + + it("cleans up once the component's DOM is gone, as React's passive effects do", () => { + let root: Element | null = null + let attachedAtCleanup: boolean | undefined + const effects: Effect[] = [ + [ + () => { + root = document.getElementById('toggle-root') + return () => void (attachedAtCleanup = root?.parentNode != null) + }, + [], + ], + ] + const { Comp } = harness(effects) + mount(Comp, { attachTo: document.body }).unmount() + expect(root).not.toBeNull() + expect(attachedAtCleanup).toBe(false) + }) + + it('cleans up on unmount, after the machine stops', () => { + const log: string[] = [] + const { Comp } = harness([loggingEffect(log)], log) + mount(Comp).unmount() + expect(log).toEqual(['start', 'effect', 'stop', 'cleanup']) + }) +}) + +describe('useMachine — compound components', () => { + it("reflects a child's send from its own onMounted, which runs before the machine starts", async () => { + const log: string[] = [] + const Child = defineComponent({ + props: { machine: { type: Object as PropType, required: true } }, + setup(props) { + onMounted(() => { + log.push('child:send') + props.machine.send({ type: 'toggle' }) + }) + return () => null + }, + }) + const Parent = defineComponent({ + setup() { + const props: ToggleProps = {} + const { api, machine } = useMachine(createToggleConfig, connectToggle, [], props) + machine.onStart(() => log.push('start')) + return () => h('div', [api.value.open ? 'open' : 'closed', h(Child, { machine })]) + }, + }) + const wrapper = mount(Parent) + await nextTick() + expect(log).toEqual(['child:send', 'start']) + expect(wrapper.text()).toBe('open') + }) +}) + +describe('useMachine — compound teardown', () => { + it("stops before its parts unmount, so a part's teardown send fires no reactions", () => { + const onOpenChange = vi.fn() + const Part = defineComponent({ + props: { machine: { type: Object as PropType, required: true } }, + setup(props) { + onUnmounted(() => props.machine.send({ type: 'toggle' })) // e.g. a part unregistering + return () => null + }, + }) + const Root = defineComponent({ + setup() { + const props: ToggleProps = { onOpenChange } + const { machine } = useMachine(createToggleConfig, connectToggle, [], props) + return () => h(Part, { machine }) + }, + }) + mount(Root).unmount() + expect(onOpenChange).not.toHaveBeenCalled() + }) +}) + +describe('useMachine — pauses like React ', () => { + it('stops the machine and its effects while deactivated, resumes them with state intact', async () => { + const log: string[] = [] + const { sink, Comp } = harness([loggingEffect(log)], log) + const shown = ref(true) + const Other = defineComponent({ render: () => h('span') }) + mount(defineComponent({ render: () => h(KeepAlive, null, [shown.value ? h(Comp) : h(Other)]) })) + expect(log).toEqual(['start', 'effect']) // onActivated also fires on first mount: no double start + sink.api!.toggle() // → open, count 1 + + shown.value = false + await nextTick() + expect(log).toEqual(['start', 'effect', 'stop', 'cleanup']) + + shown.value = true + await nextTick() + expect(log).toEqual(['start', 'effect', 'stop', 'cleanup', 'start', 'effect']) + expect(sink.api).toMatchObject({ open: true, count: 1 }) + }) +}) + +describe('useMachine — mounting into a deactivated view', () => { + it('holds still until the view activates, then starts', async () => { + const log: string[] = [] + const { Comp } = harness([loggingEffect(log)], log) + const shown = ref(true) + const loaded = ref(false) + const Page = defineComponent({ render: () => (loaded.value ? h(Comp) : null) }) + mount( + defineComponent({ render: () => h(KeepAlive, null, () => (shown.value ? h(Page) : null)) }), + ) + shown.value = false + await nextTick() + loaded.value = true // mounts Comp into the cached, deactivated view + await nextTick() + expect(log).toEqual([]) + + shown.value = true + await nextTick() + expect(log).toEqual(['start', 'effect']) + }) +}) + +describe('useMachine — an async component inside a view', () => { + it('starts an async component once it has mounted, not when it first activates', async () => { + vi.spyOn(console, 'info').mockImplementation(() => {}) // Vue's "experimental" notice + const log: string[] = [] + const effects: Effect[] = [ + [() => void log.push(`effect(mounted=${!!document.getElementById('async-root')})`), []], + ] + const Async = defineComponent({ + async setup() { + const props: ToggleProps = {} + const { machine } = useMachine(createToggleConfig, connectToggle, effects, props) + machine.onStart(() => log.push('start')) + await Promise.resolve() + return () => h('div', { id: 'async-root' }) + }, + }) + // The cached view's activation reaches its descendants before an async one has mounted. + const Page = defineComponent({ render: () => h(Suspense, null, () => h(Async)) }) + mount(defineComponent({ render: () => h(KeepAlive, null, () => h(Page)) }), { + attachTo: document.body, + }) + await flushPromises() + expect(log).toEqual(['start', 'effect(mounted=true)']) + }) +}) + +describe('useMachine — async setup', () => { + it('starts under when called before the first await', async () => { + vi.spyOn(console, 'info').mockImplementation(() => {}) // Vue's "experimental" notice + const log: string[] = [] + const Async = defineComponent({ + async setup() { + const props: ToggleProps = {} + const { api, machine } = useMachine( + createToggleConfig, + connectToggle, + [loggingEffect(log)], + props, + ) + machine.onStart(() => log.push('start')) + await Promise.resolve() + return () => h('div', String(api.value.open)) + }, + }) + const wrapper = mount(defineComponent({ render: () => h(Suspense, null, () => h(Async)) })) + await flushPromises() + expect(wrapper.text()).toBe('false') + expect(log).toEqual(['start', 'effect']) + }) +}) diff --git a/packages/vue/tests/use-selector.test.ts b/packages/vue/tests/use-selector.test.ts new file mode 100644 index 0000000..af71e84 --- /dev/null +++ b/packages/vue/tests/use-selector.test.ts @@ -0,0 +1,225 @@ +// @vitest-environment jsdom +// `useSelector` — fine-grained leaf subscription: the ref updates only when the +// selected value changes, hands the value back untouched, and disposes with its +// effect scope (a component's, or a bare effectScope()). +import { defineComponent, effectScope, h, nextTick, ref, shallowRef, type PropType } from 'vue' +import { mount } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + act as write, + machine, + type EqualityFn, + type TransitionConfig, +} from '@dunky.dev/state-machine' +import { useSelector } from '@dunky.dev/vue-state-machine' + +type S = 'idle' +interface Ctx { + a: number + b: number + item: { id: string } +} +type Ev = { type: 'incA' } | { type: 'incB' } + +const config: TransitionConfig = { + initial: 'idle', + context: { a: 0, b: 0, item: { id: 'x' } }, + states: { + idle: { + on: { + // context writes go through setContext (via `act`) so the bus notifies — + // a raw in-place `context.a++` mutates the value but never wakes subscribers. + incA: write($ => ({ a: $.context.a + 1 })), + incB: write($ => ({ b: $.context.b + 1 })), + }, + }, + }, +} + +type TestMachine = ReturnType> + +function makeMachine(): TestMachine { + const m = machine(config) + m.start() + return m +} + +// A leaf component rendering one selection; `renders` logs each rendered value. +function defineLeaf(m: TestMachine, selector: () => T, isEqual?: EqualityFn) { + const renders = vi.fn<(value: T) => void>() + const Leaf = defineComponent({ + setup() { + const selected = useSelector(m, selector, isEqual) + return () => { + renders(selected.value) + return h('span', String(selected.value)) + } + }, + }) + return { Leaf, renders } +} + +afterEach(() => vi.clearAllMocks()) + +describe('useSelector — value-deduped updates', () => { + it('O(readers): a slice change re-renders only the leaf that selected it', async () => { + const m = makeMachine() + const a = defineLeaf(m, () => m.context.a) + const b = defineLeaf(m, () => m.context.b) + mount(defineComponent({ render: () => h('div', [h(a.Leaf), h(b.Leaf)]) })) + + m.send({ type: 'incA' }) + await nextTick() + m.send({ type: 'incB' }) + await nextTick() + expect(a.renders.mock.calls).toEqual([[0], [1]]) + expect(b.renders.mock.calls).toEqual([[0], [1]]) + }) + + it('defaults to Object.is equality (a re-derived equal value does not re-render)', async () => { + const m = makeMachine() + const { Leaf, renders } = defineLeaf(m, () => m.context.a > 0) + mount(Leaf) + m.send({ type: 'incA' }) // false → true + await nextTick() + m.send({ type: 'incA' }) // true → true + await nextTick() + expect(renders.mock.calls).toEqual([[false], [true]]) + }) + + it('uses the provided isEqual to dedup an object selection', async () => { + const m = makeMachine() + const { Leaf, renders } = defineLeaf( + m, + () => ({ a: m.context.a }), + (x, y) => x.a === y.a, + ) + mount(Leaf) + m.send({ type: 'incB' }) // a fresh but equal {a} → no re-render + await nextTick() + m.send({ type: 'incA' }) + await nextTick() + expect(renders.mock.calls).toEqual([[{ a: 0 }], [{ a: 1 }]]) + }) +}) + +describe('useSelector — reactive inputs', () => { + it('re-selects when a reactive value it reads changes while the machine does not', async () => { + const m = makeMachine() + const target = ref(0) + const { Leaf, renders } = defineLeaf(m, () => m.context.a === target.value) + mount(Leaf) + target.value = 1 // the machine is untouched: 0 === 1 + await nextTick() + m.send({ type: 'incA' }) // and back through the machine: 1 === 1 + await nextTick() + expect(renders.mock.calls).toEqual([[true], [false], [true]]) + }) + + it('tracks a reactive read the selector skipped on an earlier run', async () => { + const m = makeMachine() + const target = ref(1) + // `a > 0 &&` short-circuits while a is 0, so the first run never reads `target`. + const { Leaf, renders } = defineLeaf(m, () => m.context.a > 0 && m.context.a === target.value) + mount(Leaf) + m.send({ type: 'incA' }) // a machine-triggered run now reads `target` + await nextTick() + target.value = 2 + await nextTick() + expect(renders.mock.calls).toEqual([[false], [true], [false]]) + }) + + it('follows a machine swapped through a ref', async () => { + const [first, second] = [makeMachine(), makeMachine()] + const current = shallowRef(first) + const selector = vi.fn(() => current.value.context.a) + const scope = effectScope() + const a = scope.run(() => useSelector(current, selector))! + current.value = second + await nextTick() + + expect(a.value).toBe(0) + const evaluations = selector.mock.calls.length + first.send({ type: 'incA' }) // the old machine no longer marks the selection stale + void a.value + expect(selector.mock.calls.length).toBe(evaluations) + second.send({ type: 'incA' }) + expect(a.value).toBe(1) + scope.stop() + }) + + it('reads props only after Vue has patched them all', async () => { + const m = makeMachine() + const Leaf = defineComponent({ + props: { + items: { type: Array as PropType, required: true }, + index: { type: Number, required: true }, + }, + setup(props) { + const label = useSelector(m, () => props.items[props.index]!.toUpperCase()) + return () => h('span', label.value) + }, + }) + const items = ref(['a', 'b', 'c']) + const index = ref(2) + const errorHandler = vi.fn() + const wrapper = mount( + defineComponent({ render: () => h(Leaf, { items: items.value, index: index.value }) }), + { global: { config: { errorHandler } } }, + ) + items.value = ['x'] // alone, index 2 is out of range... + index.value = 0 // ...but both land in the same patch + await nextTick() + expect(errorHandler).not.toHaveBeenCalled() + expect(wrapper.text()).toBe('X') + }) + + it('rethrows a selector failure where it is read, then recovers on the next change', () => { + const m = makeMachine() + const broken = ref(false) + const scope = effectScope() + const a = scope.run(() => + useSelector(m, () => { + if (broken.value) throw new Error('selector failed') + return m.context.a + }), + )! + expect(a.value).toBe(0) + broken.value = true + expect(() => a.value).toThrow('selector failed') + broken.value = false + m.send({ type: 'incA' }) + expect(a.value).toBe(1) + scope.stop() + }) +}) + +describe('useSelector — the returned ref', () => { + it('hands the selected value back as-is (no proxy), behind a readonly ref', () => { + const m = makeMachine() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const scope = effectScope() + const item = scope.run(() => useSelector(m, () => m.context.item))! + expect(item.value).toBe(m.context.item) + + ;(item as { value: unknown }).value = null + expect(item.value).toBe(m.context.item) + expect(warn).toHaveBeenCalled() // Vue's readonly-write warning + scope.stop() + }) + + it('disposes with its effect scope: a stopped scope stops following the machine', () => { + const m = makeMachine() + const selector = vi.fn(() => m.context.a) + const scope = effectScope() + const a = scope.run(() => useSelector(m, selector))! + m.send({ type: 'incA' }) + expect(a.value).toBe(1) + + scope.stop() + const evaluations = selector.mock.calls.length + m.send({ type: 'incA' }) + void a.value // a live subscription would have marked the selection stale + expect(selector.mock.calls.length).toBe(evaluations) + }) +}) diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 0000000..3fd6989 --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "tests"], + "exclude": ["**/node_modules", "**/dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d1998c..5edfb55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 7.0.2 vitest: specifier: ^4.1.7 - version: 4.1.7(@types/node@22.19.19)(jsdom@29.1.1)(vite@8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.7(@types/node@22.19.19)(jsdom@29.1.1)(vite@8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) benchmark: dependencies: @@ -227,10 +227,35 @@ importers: version: 2.0.0-rc.1 vite-plugin-solid: specifier: ^3.0.0-next.27 - version: 3.0.0-next.27(@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1))(solid-js@2.0.0-rc.1)(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 3.0.0-next.27(@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1))(solid-js@2.0.0-rc.1)(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) vitest: specifier: ^4.1.7 - version: 4.1.7(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.7(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + + packages/vue: + dependencies: + '@dunky.dev/state-machine': + specifier: workspace:* + version: link:../core + '@dunky.dev/state-machine-dom': + specifier: workspace:* + version: link:../dom + '@dunky.dev/state-machine-utils': + specifier: workspace:* + version: link:../shared/utils + devDependencies: + '@dunky.dev/state-machine-bindings': + specifier: workspace:* + version: link:../shared/bindings + '@vue/test-utils': + specifier: ^2.5.1 + version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.43(typescript@7.0.2)) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 + vue: + specifier: ^3.5.43 + version: 3.5.43(typescript@7.0.2) sandbox/native: dependencies: @@ -386,30 +411,61 @@ importers: specifier: ^3.0.0-next.27 version: 3.0.0-next.27(@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1))(solid-js@2.0.0-rc.1)(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + sandbox/vue: + dependencies: + '@dunky.dev/state-machine': + specifier: workspace:^ + version: link:../../packages/core + '@dunky.dev/state-machine-bindings': + specifier: workspace:^ + version: link:../../packages/shared/bindings + '@dunky.dev/vue-state-machine': + specifier: workspace:^ + version: link:../../packages/vue + '@sandbox/cmdk-core': + specifier: workspace:^ + version: link:../shared + vue: + specifier: ^3.5.43 + version: 3.5.43(typescript@6.0.3) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.9 + version: 6.0.9(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.43(typescript@6.0.3)) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.3.2 + version: 8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vue-tsc: + specifier: ^3.3.12 + version: 3.3.12(typescript@6.0.3) + website: dependencies: '@astrojs/starlight': specifier: ^0.40.0 - version: 0.40.0(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@6.0.3) + version: 0.40.0(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@6.0.3) '@dunky.dev/state-machine': specifier: workspace:^ version: link:../packages/core '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(react@19.2.6) + version: 2.0.1(react@19.2.6)(vue@3.5.43(typescript@6.0.3)) '@vercel/speed-insights': specifier: ^2.0.0 - version: 2.0.0(react@19.2.6) + version: 2.0.0(react@19.2.6)(vue@3.5.43(typescript@6.0.3)) devDependencies: '@astrojs/mdx': specifier: ^6.0.3 - version: 6.0.3(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 6.0.3(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) '@tailwindcss/postcss': specifier: ^4.3.1 version: 4.3.1 astro: specifier: ^6.4.6 - version: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + version: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) tailwindcss: specifier: ^4.3.1 version: 4.3.1 @@ -617,6 +673,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@8.0.0-rc.6': resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==} engines: {node: ^22.18.0 || >=24.11.0} @@ -1004,6 +1065,10 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@babel/types@8.0.0-rc.6': resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==} engines: {node: ^22.18.0 || >=24.11.0} @@ -1898,6 +1963,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@one-ini/wasm@0.2.1': + resolution: {integrity: sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==} + '@opentui/core-darwin-arm64@0.4.1': resolution: {integrity: sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww==} cpu: [arm64] @@ -2091,6 +2159,9 @@ packages: '@oxc-project/types@0.135.0': resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} + '@oxc-resolver/binding-android-arm-eabi@11.20.0': resolution: {integrity: sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==} cpu: [arm] @@ -2618,6 +2689,12 @@ packages: '@types/react': optional: true + '@rolldown/binding-android-arm-eabi@1.2.12': + resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@rolldown/binding-android-arm64@1.0.2': resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2630,6 +2707,12 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.2.12': + resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.2': resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2642,6 +2725,12 @@ packages: cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.2.12': + resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.2': resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2654,6 +2743,12 @@ packages: cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.2.12': + resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.2': resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2666,6 +2761,12 @@ packages: cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.2.12': + resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2678,6 +2779,12 @@ packages: cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.2': resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2692,6 +2799,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.2.12': + resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.2': resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2706,6 +2820,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.2.12': + resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.2': resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2720,6 +2841,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.2': resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2734,6 +2862,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.2.12': + resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.2': resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2748,6 +2883,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.2.12': + resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.2': resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2762,6 +2904,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-x64-musl@1.2.12': + resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.2': resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2774,6 +2923,12 @@ packages: cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.2.12': + resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.2': resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2796,6 +2951,12 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.2.12': + resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.2': resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2808,6 +2969,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.2.12': + resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -3453,6 +3620,13 @@ packages: babel-plugin-react-compiler: optional: true + '@vitejs/plugin-vue@6.0.9': + resolution: {integrity: sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + '@vitest/expect@4.1.7': resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} @@ -3482,6 +3656,55 @@ packages: '@vitest/utils@4.1.7': resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/compiler-core@3.5.43': + resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==} + + '@vue/compiler-dom@3.5.43': + resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==} + + '@vue/compiler-sfc@3.5.43': + resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==} + + '@vue/compiler-ssr@3.5.43': + resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==} + + '@vue/language-core@3.3.12': + resolution: {integrity: sha512-k7uIKR0n4CBidk233y7cac9osnDQ5WRidTl6nsrNvpZ3nf8cx9iJxRVsRzVsoPRHbowT/iKgGMTS9RGov3jeIA==} + + '@vue/reactivity@3.5.43': + resolution: {integrity: sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==} + + '@vue/runtime-core@3.5.43': + resolution: {integrity: sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==} + + '@vue/runtime-dom@3.5.43': + resolution: {integrity: sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==} + + '@vue/server-renderer@3.5.43': + resolution: {integrity: sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==} + + '@vue/shared@3.5.43': + resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==} + + '@vue/test-utils@2.5.1': + resolution: {integrity: sha512-V/4a99odJ7hapr0bRt35XxljGk8hefq31gSOnlKTcEq2fUeoj2qb+FcpJYfQu4qfYhW9Ug+O2+CZ3QmZ7gXDzA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} @@ -3525,6 +3748,10 @@ packages: '@zag-js/vanilla@1.41.2': resolution: {integrity: sha512-5eGvJ6VqW3byU2L/u2hwrwYV0kBNtisuGrxF2HXS3o/4CVqbxRVw43HWXRi7st3eB6sPFNSFqpxx5lPfzQUCmg==} + abbrev@5.0.0: + resolution: {integrity: sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -3551,6 +3778,9 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -3962,6 +4192,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3988,6 +4222,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + connect@3.7.0: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} @@ -4184,6 +4421,11 @@ packages: oxc-resolver: optional: true + editorconfig@3.0.2: + resolution: {integrity: sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==} + engines: {node: '>=20'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -4224,6 +4466,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + entities@8.0.0: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} @@ -4896,6 +5142,14 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-beautify@2.0.3: + resolution: {integrity: sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4965,30 +5219,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -4996,6 +5280,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -5003,6 +5294,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -5010,6 +5308,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -5017,22 +5322,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5541,6 +5869,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -5549,6 +5880,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -5597,6 +5933,11 @@ packages: resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} engines: {node: '>=18'} + nopt@10.0.1: + resolution: {integrity: sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -5779,6 +6120,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -5819,6 +6163,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -5853,6 +6201,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -5892,6 +6244,9 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-compare@3.0.1: resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} @@ -6184,6 +6539,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.2.12: + resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.62.0: resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -6923,6 +7283,49 @@ packages: yaml: optional: true + vite@8.3.2: + resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.3: resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: @@ -6975,6 +7378,26 @@ packages: vlq@1.0.1: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + vue-component-type-helpers@3.3.12: + resolution: {integrity: sha512-HwCqmgTfLFskQc4P5TRnbduCMyPat2Ezu0elLWOGn0dD/6BKHP15B5bxce6aDi+E4kblPtru8amIBQzu0Di3qQ==} + + vue-tsc@3.3.12: + resolution: {integrity: sha512-kbpaz18O9jPVkXVHD1BEjpUXM6WhXSu+62L1sGGr6Wbzd8GbvkBQWjoj3KZ/C0aq2MZllce7UdtWzwCIYsBMGA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.43: + resolution: {integrity: sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -7237,13 +7660,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@6.0.3(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': + '@astrojs/mdx@6.0.3(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.0 '@astrojs/markdown-remark': 7.2.0 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + astro: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) es-module-lexer: 2.1.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -7267,17 +7690,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.40.0(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.40.0(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(typescript@6.0.3)': dependencies: '@astrojs/markdown-remark': 7.2.0 - '@astrojs/mdx': 6.0.3(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + '@astrojs/mdx': 6.0.3(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) - astro-expressive-code: 0.43.1(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + astro: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + astro-expressive-code: 0.43.1(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -7329,10 +7752,10 @@ snapshots: '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -7344,8 +7767,8 @@ snapshots: '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -7361,7 +7784,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.29.7': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/helper-compilation-targets@7.29.7': dependencies: @@ -7407,18 +7830,18 @@ snapshots: '@babel/helper-member-expression-to-functions@7.29.7': dependencies: '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/helper-module-imports@7.29.7': dependencies: '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -7433,7 +7856,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.29.7': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/helper-plugin-utils@7.29.7': {} @@ -7458,7 +7881,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.29.7': dependencies: '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -7476,14 +7899,14 @@ snapshots: dependencies: '@babel/template': 7.29.7 '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/highlight@7.25.9': dependencies: @@ -7496,6 +7919,10 @@ snapshots: dependencies: '@babel/types': 7.29.7 + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + '@babel/parser@8.0.0-rc.6': dependencies: '@babel/types': 8.0.0-rc.6 @@ -7826,7 +8253,7 @@ snapshots: '@babel/helper-module-imports': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -7916,17 +8343,17 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -7936,6 +8363,11 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.0.0-rc.6': dependencies: '@babel/helper-string-parser': 8.0.0-rc.6 @@ -8150,7 +8582,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.18.6 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 html-entities: 2.3.3 parse5: 7.3.0 validate-html-nesting: 1.2.4 @@ -8696,8 +9128,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.15 - postcss-nested: 6.2.0(postcss@8.5.15) + postcss: 8.5.28 + postcss-nested: 6.2.0(postcss@8.5.28) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -8997,6 +9429,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@one-ini/wasm@0.2.1': {} + '@opentui/core-darwin-arm64@0.4.1': optional: true @@ -9124,6 +9558,8 @@ snapshots: '@oxc-project/types@0.135.0': {} + '@oxc-project/types@0.152.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.20.0': optional: true @@ -9403,7 +9839,7 @@ snapshots: '@react-native/codegen@0.81.5(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 glob: 7.2.3 hermes-parser: 0.29.1 invariant: 2.2.4 @@ -9549,78 +9985,117 @@ snapshots: optionalDependencies: '@types/react': 19.2.15 + '@rolldown/binding-android-arm-eabi@1.2.12': + optional: true + '@rolldown/binding-android-arm64@1.0.2': optional: true '@rolldown/binding-android-arm64@1.1.1': optional: true + '@rolldown/binding-android-arm64@1.2.12': + optional: true + '@rolldown/binding-darwin-arm64@1.0.2': optional: true '@rolldown/binding-darwin-arm64@1.1.1': optional: true + '@rolldown/binding-darwin-arm64@1.2.12': + optional: true + '@rolldown/binding-darwin-x64@1.0.2': optional: true '@rolldown/binding-darwin-x64@1.1.1': optional: true + '@rolldown/binding-darwin-x64@1.2.12': + optional: true + '@rolldown/binding-freebsd-x64@1.0.2': optional: true '@rolldown/binding-freebsd-x64@1.1.1': optional: true + '@rolldown/binding-freebsd-x64@1.2.12': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.1.1': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.2': optional: true '@rolldown/binding-linux-arm64-gnu@1.1.1': optional: true + '@rolldown/binding-linux-arm64-gnu@1.2.12': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.2': optional: true '@rolldown/binding-linux-arm64-musl@1.1.1': optional: true + '@rolldown/binding-linux-arm64-musl@1.2.12': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.2': optional: true '@rolldown/binding-linux-ppc64-gnu@1.1.1': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.2': optional: true '@rolldown/binding-linux-s390x-gnu@1.1.1': optional: true + '@rolldown/binding-linux-s390x-gnu@1.2.12': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.2': optional: true '@rolldown/binding-linux-x64-gnu@1.1.1': optional: true + '@rolldown/binding-linux-x64-gnu@1.2.12': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.2': optional: true '@rolldown/binding-linux-x64-musl@1.1.1': optional: true + '@rolldown/binding-linux-x64-musl@1.2.12': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.2': optional: true '@rolldown/binding-openharmony-arm64@1.1.1': optional: true + '@rolldown/binding-openharmony-arm64@1.2.12': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.2': dependencies: '@emnapi/core': 1.10.0 @@ -9641,12 +10116,18 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.1.1': optional: true + '@rolldown/binding-win32-arm64-msvc@1.2.12': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.2': optional: true '@rolldown/binding-win32-x64-msvc@1.1.1': optional: true + '@rolldown/binding-win32-x64-msvc@1.2.12': + optional: true + '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@5.4.0(rollup@4.62.0)': @@ -9805,6 +10286,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@solidjs/vite-plugin@3.0.0-next.28(@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1))(solid-js@2.0.0-rc.1)(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/core': 7.29.7 + '@dom-expressions/compiler': 0.50.0-next.40 + '@solidjs/web': 2.0.0-rc.1(solid-js@2.0.0-rc.1) + '@types/babel__core': 7.20.5 + babel-preset-solid: 2.0.0-rc.0(@babel/core@7.29.7)(solid-js@2.0.0-rc.1) + merge-anything: 5.1.7 + solid-js: 2.0.0-rc.1 + vite: 8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + transitivePeerDependencies: + - supports-color + '@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1)': dependencies: seroval: 1.5.4 @@ -9925,16 +10421,16 @@ snapshots: '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/bun@1.3.14': dependencies: @@ -10111,13 +10607,15 @@ snapshots: '@urql/core': 5.2.0 wonka: 6.3.6 - '@vercel/analytics@2.0.1(react@19.2.6)': + '@vercel/analytics@2.0.1(react@19.2.6)(vue@3.5.43(typescript@6.0.3))': optionalDependencies: react: 19.2.6 + vue: 3.5.43(typescript@6.0.3) - '@vercel/speed-insights@2.0.0(react@19.2.6)': + '@vercel/speed-insights@2.0.0(react@19.2.6)(vue@3.5.43(typescript@6.0.3))': optionalDependencies: react: 19.2.6 + vue: 3.5.43(typescript@6.0.3) '@vitejs/plugin-react@6.0.2(babel-plugin-react-compiler@1.0.0)(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: @@ -10126,6 +10624,12 @@ snapshots: optionalDependencies: babel-plugin-react-compiler: 1.0.0 + '@vitejs/plugin-vue@6.0.9(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.43(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vue: 3.5.43(typescript@6.0.3) + '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 @@ -10135,21 +10639,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.7(vite@8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.7(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@4.1.7': dependencies: @@ -10175,6 +10679,91 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.2.0 + + '@vue/compiler-core@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.43 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.43': + dependencies: + '@vue/compiler-core': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/compiler-sfc@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.43 + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-ssr': 3.5.43 + '@vue/shared': 3.5.43 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.43': + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/language-core@3.3.12': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.4 + + '@vue/reactivity@3.5.43': + dependencies: + '@vue/shared': 3.5.43 + + '@vue/runtime-core@3.5.43': + dependencies: + '@vue/reactivity': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/runtime-dom@3.5.43': + dependencies: + '@vue/reactivity': 3.5.43 + '@vue/runtime-core': 3.5.43 + '@vue/shared': 3.5.43 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.43': + dependencies: + '@vue/compiler-ssr': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/shared@3.5.43': {} + + '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.43(typescript@7.0.2))': + dependencies: + '@vue/compiler-dom': 3.5.43 + js-beautify: 2.0.3 + vue: 3.5.43(typescript@7.0.2) + vue-component-type-helpers: 3.3.12 + optionalDependencies: + '@vue/server-renderer': 3.5.43 + '@xmldom/xmldom@0.8.13': {} '@xmldom/xmldom@0.9.10': {} @@ -10224,6 +10813,8 @@ snapshots: '@zag-js/types': 1.41.2 '@zag-js/utils': 1.41.2 + abbrev@5.0.0: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -10246,6 +10837,8 @@ snapshots: agent-base@7.1.4: {} + alien-signals@3.2.1: {} + anser@1.4.10: {} ansi-colors@4.1.3: {} @@ -10315,12 +10908,12 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.43.1(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): + astro-expressive-code@0.43.1(astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: - astro: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + astro: 6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) rehype-expressive-code: 0.43.1 - astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + astro@6.4.8(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(rollup@4.62.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: '@astrojs/compiler': 4.0.0 '@astrojs/internal-helpers': 0.10.0 @@ -10372,8 +10965,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5 vfile: 6.0.3 - vite: 7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) - vitefu: 1.1.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + vite: 7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vitefu: 1.1.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -10443,7 +11036,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 @@ -10473,7 +11066,7 @@ snapshots: babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 babel-plugin-react-native-web@0.21.2: {} @@ -10769,6 +11362,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -10795,6 +11390,11 @@ snapshots: concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + connect@3.7.0: dependencies: debug: 2.6.9 @@ -10955,6 +11555,13 @@ snapshots: optionalDependencies: oxc-resolver: 11.20.0 + editorconfig@3.0.2: + dependencies: + '@one-ini/wasm': 0.2.1 + commander: 14.0.3 + minimatch: 10.2.5 + semver: 7.8.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.364: {} @@ -10983,6 +11590,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + entities@8.0.0: {} env-editor@0.4.2: {} @@ -11765,7 +12374,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -11848,6 +12457,16 @@ snapshots: jiti@2.7.0: {} + js-beautify@2.0.3: + dependencies: + config-chain: 1.1.13 + editorconfig: 3.0.2 + glob: 13.0.6 + js-cookie: 3.0.8 + nopt: 10.0.1 + + js-cookie@3.0.8: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -11932,36 +12551,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -11978,6 +12630,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + lines-and-columns@1.2.4: {} lint-staged@17.0.7: @@ -12602,8 +13270,8 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 flow-enums-runtime: 0.0.6 metro: 0.83.7 metro-babel-transformer: 0.83.7 @@ -12622,8 +13290,8 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 flow-enums-runtime: 0.0.6 metro: 0.84.4 metro-babel-transformer: 0.84.4 @@ -13104,6 +13772,8 @@ snapshots: ms@2.1.3: {} + muggle-string@0.4.1: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -13112,6 +13782,8 @@ snapshots: nanoid@3.3.12: {} + nanoid@3.3.19: {} + negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -13140,6 +13812,10 @@ snapshots: node-releases@2.0.46: {} + nopt@10.0.1: + dependencies: + abbrev: 5.0.0 + normalize-path@3.0.0: {} npm-package-arg@11.0.3: @@ -13404,6 +14080,8 @@ snapshots: parseurl@1.3.3: {} + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -13429,6 +14107,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.7: {} + pify@4.0.1: {} pirates@4.0.7: {} @@ -13441,9 +14121,9 @@ snapshots: pngjs@3.4.0: {} - postcss-nested@6.2.0(postcss@8.5.15): + postcss-nested@6.2.0(postcss@8.5.28): dependencies: - postcss: 8.5.15 + postcss: 8.5.28 postcss-selector-parser: 6.1.4 postcss-selector-parser@6.1.4: @@ -13463,6 +14143,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@2.8.8: {} pretty-bytes@5.6.0: {} @@ -13496,6 +14182,8 @@ snapshots: property-information@7.2.0: {} + proto-list@1.2.4: {} + proxy-compare@3.0.1: {} publint@0.3.21: @@ -13955,6 +14643,27 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.1 '@rolldown/binding-win32-x64-msvc': 1.1.1 + rolldown@1.2.12: + dependencies: + '@oxc-project/types': 0.152.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.12 + '@rolldown/binding-android-arm64': 1.2.12 + '@rolldown/binding-darwin-arm64': 1.2.12 + '@rolldown/binding-darwin-x64': 1.2.12 + '@rolldown/binding-freebsd-x64': 1.2.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 + '@rolldown/binding-linux-arm64-gnu': 1.2.12 + '@rolldown/binding-linux-arm64-musl': 1.2.12 + '@rolldown/binding-linux-ppc64-gnu': 1.2.12 + '@rolldown/binding-linux-s390x-gnu': 1.2.12 + '@rolldown/binding-linux-x64-gnu': 1.2.12 + '@rolldown/binding-linux-x64-musl': 1.2.12 + '@rolldown/binding-openharmony-arm64': 1.2.12 + '@rolldown/binding-win32-arm64-msvc': 1.2.12 + '@rolldown/binding-win32-x64-msvc': 1.2.12 + rollup@4.62.0: dependencies: '@types/estree': 1.0.9 @@ -14418,8 +15127,7 @@ snapshots: type-fest@0.7.1: {} - typescript@6.0.3: - optional: true + typescript@6.0.3: {} typescript@7.0.2: optionalDependencies: @@ -14606,30 +15314,56 @@ snapshots: - supports-color - vite - vite@7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + vite-plugin-solid@3.0.0-next.27(@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1))(solid-js@2.0.0-rc.1)(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): + dependencies: + '@solidjs/vite-plugin': 3.0.0-next.28(@solidjs/web@2.0.0-rc.1(solid-js@2.0.0-rc.1))(solid-js@2.0.0-rc.1)(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + transitivePeerDependencies: + - '@solidjs/web' + - '@testing-library/jest-dom' + - solid-js + - supports-color + - vite + + vite@7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.28 rollup: 4.62.0 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 fsevents: 2.3.3 jiti: 2.7.0 - lightningcss: 1.32.0 + lightningcss: 1.33.0 terser: 5.48.0 tsx: 4.22.4 yaml: 2.9.0 - vite@8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.28 rolldown: 1.0.2 tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.2 + esbuild: 0.28.0 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.48.0 + tsx: 4.22.4 + yaml: 2.9.0 + + vite@8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.12 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.19.19 esbuild: 0.28.0 @@ -14639,12 +15373,12 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.2 + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.12 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 @@ -14655,18 +15389,22 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vitefu@1.1.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): + vitefu@1.1.3(vite@7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): optionalDependencies: - vite: 7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 7.3.5(@types/node@24.13.2)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) vitefu@1.1.3(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): optionalDependencies: vite: 8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) - vitest@4.1.7(@types/node@22.19.19)(jsdom@29.1.1)(vite@8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): + optionalDependencies: + vite: 8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + + vitest@4.1.7(@types/node@22.19.19)(jsdom@29.1.1)(vite@8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.7(vite@8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -14683,7 +15421,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.3.2(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.19 @@ -14691,10 +15429,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.7(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.7(@types/node@24.13.2)(jsdom@29.1.1)(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.7(vite@8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -14711,7 +15449,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.3.2(@types/node@24.13.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 @@ -14721,6 +15459,36 @@ snapshots: vlq@1.0.1: {} + vscode-uri@3.2.0: {} + + vue-component-type-helpers@3.3.12: {} + + vue-tsc@3.3.12(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.12 + typescript: 6.0.3 + + vue@3.5.43(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-sfc': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/server-renderer': 3.5.43 + '@vue/shared': 3.5.43 + optionalDependencies: + typescript: 6.0.3 + + vue@3.5.43(typescript@7.0.2): + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-sfc': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/server-renderer': 3.5.43 + '@vue/shared': 3.5.43 + optionalDependencies: + typescript: 7.0.2 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 diff --git a/sandbox/README.md b/sandbox/README.md index 4f45dc5..f0873c7 100644 --- a/sandbox/README.md +++ b/sandbox/README.md @@ -1,7 +1,7 @@ -# cmdk sandbox — one machine, four substrates +# cmdk sandbox — one machine, five substrates A ⌘K **command palette** driven by a single substrate-agnostic state machine, -rendered four ways. The interesting parts — fuzzy filtering, arrow-key +rendered five ways. The interesting parts — fuzzy filtering, arrow-key navigation with wraparound, active-row tracking, selection — all live in `shared/`, the same bytes on every target. Each app only supplies the markup and runs its substrate's `normalize()` over the bindings the shared `connect()` @@ -10,9 +10,10 @@ produces. ``` sandbox/ +-- shared/ @sandbox/cmdk-core — the machine + connect() + commands (NO framework) -| + src/styles.css — the one stylesheet the React and Solid apps share +| + src/styles.css — the one stylesheet the React, Solid, and Vue apps share +-- react/ Vite + React DOM → normalize → onClick / aria-* / role +-- solid/ Vite + Solid → normalize → onClick / aria-* / tabindex ++-- vue/ Vite + Vue SFCs → normalize → onClick / onKeydown / aria-* +-- opentui/ Bun + @opentui/react → normalize → onMouseDown / focusable / cells +-- native/ Expo + React Native → normalize → onPress / accessibilityState ``` @@ -34,6 +35,9 @@ pnpm -C sandbox/react dev # DOM (Solid) — opens at http://localhost:5173 pnpm -C sandbox/solid dev +# DOM (Vue) — opens at http://localhost:5173 +pnpm -C sandbox/vue dev + # Terminal — needs Bun. Press ⌘K / Ctrl+K to open the palette. pnpm -C sandbox/opentui dev @@ -41,5 +45,5 @@ pnpm -C sandbox/opentui dev pnpm -C sandbox/native start # then press i / a, or scan the QR ``` -All four consume the workspace packages straight from their TypeScript `src/` +All five consume the workspace packages straight from their TypeScript `src/` (Vite alias / Bun workspace / Metro watch-folders) — no build step. diff --git a/sandbox/shared/src/styles.css b/sandbox/shared/src/styles.css index 0586f67..98a73de 100644 --- a/sandbox/shared/src/styles.css +++ b/sandbox/shared/src/styles.css @@ -1,6 +1,6 @@ -/* The one stylesheet the React and Solid apps share — same classes, same - look; each app only supplies markup. The non-DOM targets (opentui, native) - style natively and don't use this. */ +/* The one stylesheet the React, Solid, and Vue apps share — same classes, + same look; each app only supplies markup. The non-DOM targets (opentui, + native) style natively and don't use this. */ /* Page */ diff --git a/sandbox/vue/index.html b/sandbox/vue/index.html new file mode 100644 index 0000000..e637c32 --- /dev/null +++ b/sandbox/vue/index.html @@ -0,0 +1,12 @@ + + + + + + cmdk · Vue + + +
+ + + diff --git a/sandbox/vue/package.json b/sandbox/vue/package.json new file mode 100644 index 0000000..426a6e2 --- /dev/null +++ b/sandbox/vue/package.json @@ -0,0 +1,24 @@ +{ + "name": "@sandbox/cmdk-vue", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@dunky.dev/state-machine": "workspace:^", + "@dunky.dev/state-machine-bindings": "workspace:^", + "@dunky.dev/vue-state-machine": "workspace:^", + "@sandbox/cmdk-core": "workspace:^", + "vue": "^3.5.43" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.9", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vue-tsc": "^3.3.12" + } +} diff --git a/sandbox/vue/src/app.vue b/sandbox/vue/src/app.vue new file mode 100644 index 0000000..0c79803 --- /dev/null +++ b/sandbox/vue/src/app.vue @@ -0,0 +1,29 @@ + + + diff --git a/sandbox/vue/src/command-palette.vue b/sandbox/vue/src/command-palette.vue new file mode 100644 index 0000000..f90dc7e --- /dev/null +++ b/sandbox/vue/src/command-palette.vue @@ -0,0 +1,82 @@ + + + diff --git a/sandbox/vue/src/main.ts b/sandbox/vue/src/main.ts new file mode 100644 index 0000000..c5dfdf4 --- /dev/null +++ b/sandbox/vue/src/main.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import App from './app.vue' + +// The stylesheet the web sandboxes share. +import '../../shared/src/styles.css' + +const root = document.getElementById('root') +if (!root) throw new Error('missing #root') + +createApp(App).mount(root) diff --git a/sandbox/vue/tsconfig.json b/sandbox/vue/tsconfig.json new file mode 100644 index 0000000..dd9f72f --- /dev/null +++ b/sandbox/vue/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"] +} diff --git a/sandbox/vue/vite.config.ts b/sandbox/vue/vite.config.ts new file mode 100644 index 0000000..f9bc751 --- /dev/null +++ b/sandbox/vue/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'node:path' + +// The @dunky.dev/* packages and the shared cmdk core all point `main` at their TS +// `src/index.ts` (no build step). Alias each to its source so Vite transpiles them +// directly — the whole point of the sandbox is to run the workspace source live. +const repo = resolve(import.meta.dirname, '../..') + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@dunky.dev/state-machine': resolve(repo, 'packages/core/src'), + '@dunky.dev/vue-state-machine': resolve(repo, 'packages/vue/src'), + '@dunky.dev/state-machine-utils': resolve(repo, 'packages/shared/utils/src'), + '@dunky.dev/state-machine-bindings': resolve(repo, 'packages/shared/bindings/src'), + '@dunky.dev/state-machine-dom': resolve(repo, 'packages/dom/src'), + '@sandbox/cmdk-core': resolve(repo, 'sandbox/shared/src'), + }, + }, +}) diff --git a/tsconfig.build.json b/tsconfig.build.json index ba5a59a..9c5e1a4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -6,6 +6,7 @@ { "path": "./packages/dom" }, { "path": "./packages/react" }, { "path": "./packages/solid" }, + { "path": "./packages/vue" }, { "path": "./packages/native" }, { "path": "./packages/opentui" }, { "path": "./packages/shared/utils" }, diff --git a/tsconfig.json b/tsconfig.json index 8436124..ff10e6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "@dunky.dev/state-machine": ["./packages/core/src"], "@dunky.dev/react-state-machine": ["./packages/react/src"], "@dunky.dev/solid-state-machine": ["./packages/solid/src"], + "@dunky.dev/vue-state-machine": ["./packages/vue/src"], "@dunky.dev/native-state-machine": ["./packages/native/src"], "@dunky.dev/opentui-state-machine": ["./packages/opentui/src"], "@dunky.dev/state-machine-utils": ["./packages/shared/utils/src"], diff --git a/tsdown.config.ts b/tsdown.config.ts index 937d196..71c494b 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ 'packages/dom', 'packages/react', 'packages/solid', + 'packages/vue', 'packages/native', 'packages/opentui', 'packages/shared/utils', diff --git a/website/astro.config.ts b/website/astro.config.ts index 20f7ef6..5360aaa 100644 --- a/website/astro.config.ts +++ b/website/astro.config.ts @@ -129,6 +129,7 @@ export default defineConfig({ items: [ { label: 'React', link: 'libs/react' }, { label: 'Solid', link: 'libs/solid' }, + { label: 'Vue', link: 'libs/vue' }, { label: 'React Native', link: 'libs/react-native' }, { label: 'OpenTUI', link: 'libs/opentui' }, ], diff --git a/website/src/content/docs/api/effects.mdx b/website/src/content/docs/api/effects.mdx index 6c26bb4..ef8a318 100644 --- a/website/src/content/docs/api/effects.mdx +++ b/website/src/content/docs/api/effects.mdx @@ -88,7 +88,7 @@ function Disclosure(props: DisclosureProps) { } ``` -The machine only sees `send({ type: 'close' })`; it has no idea an Escape key exists. On React Native the machine is unchanged; the effect swaps `keydown` for `BackHandler`. On [Solid](/libs/solid) the same tuple runs as its own `createEffect` — no rules-of-hooks constraint, but the list stays a module constant by convention. +The machine only sees `send({ type: 'close' })`; it has no idea an Escape key exists. On React Native the machine is unchanged; the effect swaps `keydown` for `BackHandler`. On [Solid](/libs/solid) the same tuple runs as its own `createEffect`, and on [Vue](/libs/vue) as a post-mount watcher over its deps — no rules-of-hooks constraint on either, but the list stays a module constant by convention. ## Named effects diff --git a/website/src/content/docs/libs/vue.mdx b/website/src/content/docs/libs/vue.mdx new file mode 100644 index 0000000..4a829e4 --- /dev/null +++ b/website/src/content/docs/libs/vue.mdx @@ -0,0 +1,210 @@ +--- +title: Vue +description: Vue bindings for @dunky.dev/state-machine. +--- + +import Install from '../../../components/install.astro' + + + +Targets Vue 3.4 and later (`vue` `^3.4.0`). The bridge uses only the reactivity and lifecycle APIs both Vue renderers share, so it also runs inside Vue 3.6's Vapor components (verified on the release candidate). + +The Vue package is a thin edge layer. Behavior lives in the core machine and the component's `connect` function; this package only adapts them to Vue: lifecycle, reactivity, prop translation, and platform effects. **The machine itself is unchanged** — the same `createDialogConfig` and `connectDialog` run here as on any other target. + +The connector's snapshot is exposed as a **computed ref**: a template unwraps it (`api.isOpen`), script reads `api.value.isOpen`, and it changes only when the machine or the props do. + +## `useMachine` + +The one bridge composable. Every component calls it with the four agnostic pieces and gets back `{ api, machine }` — the view API to render from, and the running service (for `send` and `useSelector`): + +```vue + + + +``` + +`useMachine` builds the machine and connector **once** (`setup()` runs once per instance, so the first props seed context; later changes flow through `setProps`, not a rebuild), starts after mount, stops on unmount, and exposes the connect output as a computed ref. Inside `` it pauses while deactivated — the machine stops and its effects are torn down, then both resume with state intact; mounted into a view that is already deactivated, it holds still until the view activates — and on the server nothing starts: `renderToString` renders the initial snapshot. In an async `setup()`, call it before the first `await`: Vue only binds lifecycle hooks registered before it. And send from event handlers, `watch` callbacks, or hooks rather than `watchEffect`, a `computed`, or a render function: the bridge runs its own callbacks untracked, but the machine's actions and guards run inside whatever sent. + +### Boolean props + +Vue casts an **absent** `Boolean` prop to `false`, but the machine reads `undefined` as "not set". Left as-is, `open` would always look controlled and a `modal: true` default would be lost, so declare every boolean prop with `default: undefined` — as in the `withDefaults` call above, or `{ type: Boolean, default: undefined }` in a runtime declaration. + +### Replace prop values, don't mutate them + +Props reach the machine through `setProps`, which compares each prop by identity, as React does. A value mutated in place — `items.value.push(x)` on an array the parent passes down — keeps its identity, so the machine never sees the change; replace it instead: `items.value = [...items.value, x]`. + +### The three imports + +Those three values are where the dialog's behavior actually lives, and none of it is Vue. The machine and its `connect` live in a `./dialog` module, written once and run unchanged on any platform; the DOM listeners come from `./dialog-effects` (see [`ComponentEffect`](#componenteffect-platform-effects) below): + +```ts +// dialog.ts: plain functions, no Vue +import { setup, type Connect, type Machine } from '@dunky.dev/state-machine' + +export type DialogProps = { + open?: boolean + onOpenChange?: (open: boolean) => void + closeOnEscape?: boolean +} + +type State = 'closed' | 'open' +type Context = { closeOnEscape: boolean } +type Event = { type: 'open' } | { type: 'close' } +type Api = { + isOpen: boolean + triggerProps: Record + contentProps: Record +} +export type DialogMachine = Machine + +export const createDialogConfig = (props: DialogProps) => + setup.as().createMachine({ + initial: props.open ? 'open' : 'closed', // props seed the machine ONCE + context: { closeOnEscape: props.closeOnEscape ?? true }, + states: { + closed: { on: { open: { target: 'open' } } }, + open: { on: { close: { target: 'closed' } } }, + }, + }) + +export const connectDialog: Connect = ({ + state, + send, +}) => ({ + isOpen: state === 'open', + triggerProps: { + onPress: () => send({ type: 'open' }), + expanded: state === 'open', + }, + contentProps: { role: 'dialog', modal: true }, +}) +``` + +So `useMachine` is the only Vue-specific piece: `createDialogConfig` is the machine definition, `connectDialog` is the snapshot-to-view-API mapping, and `dialogEffects` are the DOM listeners. Every target imports the **same** `./dialog` module — only the bridge differs. See [Setup](/api/setup) for configs and [Connector](/api/connector) for how `connect` works in depth. + +## `normalize`: bindings → DOM props + +`connect` returns substrate-agnostic bindings (`onPress`, `role`, `describedBy`). `normalize` translates them to the DOM/ARIA props `v-bind` and `h()` take: + +```ts +normalize(api.value.triggerProps) +// { onClick, 'aria-expanded', role, tabindex, ... } +``` + +The machine binding maps handlers (`onPress` → `onClick`), ARIA props (`describedBy` → `aria-describedby`), ARIA state (`checked` → `aria-checked`), and focus (`focusable` → `tabindex`). Multi-word events use Vue's listener casing — `onPointerenter`, `onKeydown` — because Vue derives the DOM event by hyphenating the camel tail (`onPointerEnter` would listen to `pointer-enter`). [Check out the full mapping here](https://github.com/dunky-dev/state-machine/blob/main/packages/vue/src/normalize.ts). + +`undefined` values are dropped. Unknown keys pass through unchanged. + +## `mergeProps`: consumer + component props + +When a consumer's props land on the same element the component controls: + +```vue + + + +``` + +- **Event handlers are chained, consumer-first.** If the consumer calls `e.preventDefault()`, the component's handler is skipped — a clean veto. An array of consumer handlers stays an array with the component's handler appended, and a capture listener (`onClickCapture`) vetoes the plain `onClick` too — a passive one can't, since the browser ignores its `preventDefault`. +- **`class` and `style` are merged** into a `[consumer, library]` array, whatever shape each side uses; Vue normalizes it, and the library's style wins a conflicting key. +- **Everything else: component wins** (`id`, `role`, `aria-*`). + +Turn `inheritAttrs` off when you spread `useAttrs()` like this, or Vue also falls the attrs through to the root element and every handler runs twice. Don't confuse this with Vue's own `mergeProps` from `vue`, which concatenates handlers with no veto. + +## `useSelector`: fine-grained leaf subscription + +For a leaf component that should only re-render when one slice of the machine changes (useful when a single machine drives many rows and each row should only wake for its own value). It returns a **readonly ref** — a template unwraps it: + +```vue + + + +``` + +The selector re-runs on every machine change and whenever a reactive value it reads changes — `props.value` above — and the ref updates only when the selected value changes: `Object.is` by default. Pass the machine as a getter (or a ref) so a swapped machine is followed. For object selections, pass a custom equality function so a re-derived equal object doesn't bump the ref: + +```ts +const position = useSelector( + () => props.machine, + () => ({ x: props.machine.context.x, y: props.machine.context.y }), + (a, b) => a.x === b.x && a.y === b.y, +) +``` + +The selected value comes back as-is, never wrapped in a reactive proxy, and the subscription is disposed with the component — or with the surrounding `effectScope()`. On the server nothing subscribes, and each read runs the selector afresh. + +## `ComponentEffect`: platform effects + +Some behavior can't live in the machine because it touches the DOM or reads props the machine never sees. Declare each as a `[setup/teardown, depPropNames]` tuple; `useMachine` runs each entry after mount and re-runs it (cleanup → setup) when one of its named prop deps changes: + +```ts +// dialog-effects.ts +import type { ComponentEffect } from '@dunky.dev/vue-state-machine' +import type { DialogMachine, DialogProps } from './dialog' + +type Effect = ComponentEffect + +const onEscapeKey: Effect = [ + (machine, props) => { + if (props.closeOnEscape === false) return // undefined keeps the default: on + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') machine.send({ type: 'close' }) + } + document.addEventListener('keydown', handler) + return () => document.removeEventListener('keydown', handler) + }, + ['closeOnEscape'], // re-run only when this prop changes +] + +export const dialogEffects = [onEscapeKey] +``` + +The tuple shape is **identical to every other target** — the same effects run everywhere. The deps are prop NAMES (typed `(keyof Props)[]`, so a typo is a compile error); the Vue bridge watches exactly those props, compares each by value, and re-runs after the DOM update, so a prop the effect merely reads never becomes a hidden dependency. Each run gets its own snapshot of the props, as a React effect closes over its render's, so a cleanup undoes exactly what its run set up. As on React, effects start once the machine has; they're cleaned up after it stops, in `onUnmounted` — children before parents, unlike React. If one throws, the others still run and the error goes to Vue's error handling. An effect re-runs after the render its dep change caused, so a machine change it makes — a controlled option echoed into the machine — renders in the pass after: a `post` watcher queued by the same prop change still reads the previous DOM, while `onUpdated` runs after every render, the echoed state's included. The machine only receives `send({ type: 'close' })`; it has no idea a keyboard event exists. + +See [Effects](/api/effects) for the full mental model of where each type of effect lives.