From c65314bdee1311aa27c0e11af07fdcd9bb6c270a Mon Sep 17 00:00:00 2001 From: Ivan Banov Date: Sat, 18 Jul 2026 10:29:11 +0200 Subject: [PATCH 1/2] feat: add the Switch primitive Adds @dunky.dev/switch (core machine) and @dunky.dev/react-switch (React binding). Anatomy: Switch root owning checked state, with Control (switch role) wrapping a visual Thumb, plus a Label whose press also toggles. A binary on/off control per the WAI-ARIA APG switch pattern: controlled/uncontrolled checked state, press/Space toggles, disabled gates user intent in the machine, and every part carries data-state. Co-Authored-By: Claude Fable 5 --- .changeset/switch.md | 23 +++ packages/core/switch/README.md | 27 +++ packages/core/switch/SPEC.md | 111 +++++++++++ packages/core/switch/package.json | 40 ++++ packages/core/switch/src/connect.ts | 100 ++++++++++ packages/core/switch/src/index.ts | 11 + packages/core/switch/src/machine.ts | 64 ++++++ packages/core/switch/src/types.ts | 60 ++++++ packages/core/switch/tests/machine.test.ts | 188 ++++++++++++++++++ packages/react/switch/README.md | 32 +++ packages/react/switch/SPEC.md | 77 +++++++ packages/react/switch/package.json | 51 +++++ packages/react/switch/src/context.ts | 19 ++ packages/react/switch/src/effects.ts | 31 +++ packages/react/switch/src/index.ts | 8 + packages/react/switch/src/switch.tsx | 96 +++++++++ packages/react/switch/src/use-switch.ts | 17 ++ .../react/switch/stories/switch.stories.tsx | 127 ++++++++++++ packages/react/switch/tests/switch.test.tsx | 146 ++++++++++++++ pnpm-lock.yaml | 37 ++++ tsconfig.json | 2 + tsdown.config.ts | 2 + 22 files changed, 1269 insertions(+) create mode 100644 .changeset/switch.md create mode 100644 packages/core/switch/README.md create mode 100644 packages/core/switch/SPEC.md create mode 100644 packages/core/switch/package.json create mode 100644 packages/core/switch/src/connect.ts create mode 100644 packages/core/switch/src/index.ts create mode 100644 packages/core/switch/src/machine.ts create mode 100644 packages/core/switch/src/types.ts create mode 100644 packages/core/switch/tests/machine.test.ts create mode 100644 packages/react/switch/README.md create mode 100644 packages/react/switch/SPEC.md create mode 100644 packages/react/switch/package.json create mode 100644 packages/react/switch/src/context.ts create mode 100644 packages/react/switch/src/effects.ts create mode 100644 packages/react/switch/src/index.ts create mode 100644 packages/react/switch/src/switch.tsx create mode 100644 packages/react/switch/src/use-switch.ts create mode 100644 packages/react/switch/stories/switch.stories.tsx create mode 100644 packages/react/switch/tests/switch.test.tsx diff --git a/.changeset/switch.md b/.changeset/switch.md new file mode 100644 index 0000000..2f452dd --- /dev/null +++ b/.changeset/switch.md @@ -0,0 +1,23 @@ +--- +'@dunky.dev/switch': minor +'@dunky.dev/react-switch': minor +--- + +Add the Switch primitive — a binary on/off control following the WAI-ARIA APG +pattern, shipped as an agnostic core (`@dunky.dev/switch`) plus a React binding +(`@dunky.dev/react-switch`). + +```tsx +import { Switch } from '@dunky.dev/react-switch' + +function App() { + return ( + + + + + Airplane mode + + ) +} +``` diff --git a/packages/core/switch/README.md b/packages/core/switch/README.md new file mode 100644 index 0000000..6a5d3a3 --- /dev/null +++ b/packages/core/switch/README.md @@ -0,0 +1,27 @@ +# @dunky.dev/switch + +The framework-agnostic switch interaction, modeled as a state machine on +`@dunky.dev/state-machine`. Pure logic — no substrate, no framework. Consumers +pair it with a substrate driver rather than driving the machine directly. + +The behavior contract — scenarios, guarantees, driver obligations — lives in +[SPEC.md](./SPEC.md). + +## Install + +```sh +npm install @dunky.dev/switch +``` + +## Usage + +```ts +import { machine, connector } from '@dunky.dev/state-machine' +import { switchMachine, switchConnect } from '@dunky.dev/switch' + +// `id` is substrate-minted (SSR-safe); the connect derives the per-part ids. +const service = machine(switchMachine({ ...options, id: 'my-switch' })) +connector(service, switchConnect, options) // wires the consumer callbacks +service.start() +service.send({ type: 'toggle' }) +``` diff --git a/packages/core/switch/SPEC.md b/packages/core/switch/SPEC.md new file mode 100644 index 0000000..7ead151 --- /dev/null +++ b/packages/core/switch/SPEC.md @@ -0,0 +1,111 @@ +# SPEC / Switch + +## Reference + +- **W3C pattern**: [APG Switch](https://www.w3.org/WAI/ARIA/apg/patterns/switch/), + over the normative + [WAI-ARIA 1.2 `switch`](https://www.w3.org/TR/wai-aria-1.2/#switch) role + definition. +- **State machine**: built on `@dunky.dev/state-machine`. +- **Prior art**: API shape modeled on the Radix, Base UI, and Ark switches. + +## Overview + +A switch is a binary control that turns a setting on or off, taking effect +immediately — airplane mode, notifications, dark theme. Unlike a checkbox it +never collects a choice for later submission and never has a mixed state: it +is always exactly on or off. + +## Anatomy + +``` + — root; owns checked state, renders nothing of its own + |_ — the interactive element: carries the switch role and checked state + | |_ — the knob; purely visual, styled off data-state + |_