{"_id":"@affino/listbox-core","_rev":"2-a0a89ed329c2534a65d39c38ba10584b","name":"@affino/listbox-core","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@affino/listbox-core","version":"1.0.0","keywords":["listbox","selection","headless","accessibility"],"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","_id":"@affino/listbox-core@1.0.0","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"homepage":"https://affino.dev","bugs":{"url":"https://github.com/affinio/affinio/issues"},"dist":{"shasum":"e370c6f3892fc81b19acc61868184e73182ca26b","tarball":"https://registry.npmjs.org/@affino/listbox-core/-/listbox-core-1.0.0.tgz","fileCount":6,"integrity":"sha512-SOlwlhDOru8veGZ/MYsu6wL/SOU4Ol86/Z2GAzAbk8+Ca2F243RUdj13ZMmuLbBXTV3pVKcJ0nvkzeZC6QeJ2Q==","signatures":[{"sig":"MEYCIQCeYn69Qe1B/rvMwMVHj8hgja4ptzkIVub035cy28DbLAIhAKLRDCNmNMMgeyunwfkeCZiUfSGHqwXAZrLt4sFzOnYm","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":10634},"main":"dist/index.js","type":"module","_from":"file:affino-listbox-core-1.0.0.tgz","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc -p tsconfig.json","clean":"rm -rf dist tsconfig.tsbuildinfo"},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"_resolved":"/tmp/30018b7d1effaefd0f5b204e2b76d03b/affino-listbox-core-1.0.0.tgz","_integrity":"sha512-SOlwlhDOru8veGZ/MYsu6wL/SOU4Ol86/Z2GAzAbk8+Ca2F243RUdj13ZMmuLbBXTV3pVKcJ0nvkzeZC6QeJ2Q==","repository":{"url":"git+https://github.com/affinio/affinio.git#main","type":"git"},"_npmVersion":"10.8.2","description":"Headless listbox state machine built on @affino/selection-core","directories":{},"sideEffects":false,"_nodeVersion":"20.19.6","dependencies":{"@affino/selection-core":"^1.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.15","@vitest/coverage-v8":"^4.0.15"},"_npmOperationalInternal":{"tmp":"tmp/listbox-core_1.0.0_1769943309082_0.444778418385646","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@affino/listbox-core","version":"1.1.0","author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"type":"module","description":"Headless listbox state machine built on @affino/selection-core","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"main":"dist/index.js","types":"dist/index.d.ts","sideEffects":false,"dependencies":{"@affino/selection-core":"^1.1.0"},"devDependencies":{"@vitest/coverage-v8":"^4.0.15","vitest":"^4.0.15"},"repository":{"type":"git","url":"git+https://github.com/affinio/affinio.git#main"},"homepage":"https://affino.dev","license":"MIT","keywords":["listbox","selection","headless","accessibility"],"bugs":{"url":"https://github.com/affinio/affinio/issues"},"module":"dist/index.js","scripts":{"clean":"rm -rf dist tsconfig.tsbuildinfo","build":"pnpm run clean && tsc -p tsconfig.json","test":"vitest run"},"_id":"@affino/listbox-core@1.1.0","_integrity":"sha512-qJbLMSpmTmcCq9hfPulLn0LYvl6k4onX6ibQiM4TRtlo682JBt0UYJ3w0M5rYtd9Ze2FuD8x1WITuvUNCeoFUw==","_resolved":"/tmp/949c14a659baf0884610381541d7d71c/affino-listbox-core-1.1.0.tgz","_from":"file:affino-listbox-core-1.1.0.tgz","_nodeVersion":"20.20.0","_npmVersion":"10.8.2","dist":{"integrity":"sha512-qJbLMSpmTmcCq9hfPulLn0LYvl6k4onX6ibQiM4TRtlo682JBt0UYJ3w0M5rYtd9Ze2FuD8x1WITuvUNCeoFUw==","shasum":"577c792caa672a1e1b9ead5fd595f2bfccb49bfe","tarball":"https://registry.npmjs.org/@affino/listbox-core/-/listbox-core-1.1.0.tgz","fileCount":6,"unpackedSize":14305,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGulHRhRWTbzagtdzDrZiGliEWcoiHCD3UPY9DzC1mULAiEAu3fNGSXwS7FBAnriDwMZMzQRTlaC8ZG3jZK7UBBy/PE="}]},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"directories":{},"maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/listbox-core_1.1.0_1771153550693_0.5107137676299129"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-01T10:55:09.028Z","modified":"2026-02-15T11:05:50.960Z","1.0.0":"2026-02-01T10:55:09.219Z","1.1.0":"2026-02-15T11:05:50.839Z"},"bugs":{"url":"https://github.com/affinio/affinio/issues"},"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","homepage":"https://affino.dev","keywords":["listbox","selection","headless","accessibility"],"repository":{"type":"git","url":"git+https://github.com/affinio/affinio.git#main"},"description":"Headless listbox state machine built on @affino/selection-core","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"readme":"# @affino/listbox-core\n\nHeadless listbox state machine built on top of the linear selection primitives provided by `@affino/selection-core`.\n\n## Features\n\n- Maintains focus (active option) alongside linear selection ranges\n- Skips disabled options when navigating with the keyboard\n- Supports single toggle, range extension, select-all, and clear intents\n- Framework agnostic: feed its snapshots into Vue, React, or vanilla renderers\n\n## Usage\n\n```ts\nimport {\n  createListboxState,\n  moveListboxFocus,\n  activateListboxIndex,\n} from \"@affino/listbox-core\"\n\nconst context = {\n  optionCount: options.length,\n  isDisabled: (index: number) => options[index]?.disabled ?? false,\n}\n\nlet state = createListboxState()\nstate = activateListboxIndex({ state, context, index: 0 })\nstate = moveListboxFocus({ state, context, delta: 1, extend: true })\n```\n\n## Adapter contract\n\n`listbox-core` is intentionally DOM-agnostic. Adapters are responsible for mapping UI events to pure operations.\n\nRequired context invariants:\n\n- `context.optionCount` must match the rendered option collection length.\n- `context.isDisabled(index)` must be stable for the same render tick.\n- `context.optionCount` and `isDisabled` must describe the same option ordering used by the DOM.\n\nState ownership rules:\n\n- Keep a single source of truth for `ListboxState` in the adapter.\n- Treat returned `ListboxState` as immutable snapshots; always replace, never mutate.\n- Recompute `context` from current rendered options before each operation.\n\nOperation mapping (recommended):\n\n- `ArrowDown` -> `moveListboxFocus({ delta: 1 })`\n- `ArrowUp` -> `moveListboxFocus({ delta: -1 })`\n- `Home` -> `activateListboxIndex({ index: 0 })`\n- `End` -> `activateListboxIndex({ index: optionCount - 1 })`\n- `Shift + Arrow*` -> `moveListboxFocus({ extend: true, ... })`\n- `Space` on active option -> `toggleActiveListboxOption({ state })`\n- pointer click on option `i` -> `activateListboxIndex({ index: i, toggle: isMultiSelect })`\n- clear action -> `clearListboxSelection({ preserveActiveIndex: true, state })`\n- select all action -> `selectAllListboxOptions({ context })`\n\nBehavioral guarantees adapters can rely on:\n\n- Disabled options are skipped during focus navigation.\n- Selecting a disabled option index only updates `activeIndex` (selection is unchanged).\n- Invalid counts (`NaN`, `Infinity`, `<= 0`) are treated as empty context.\n- `isDisabled` exceptions are swallowed and treated as \"enabled\".\n\nCommon wrapper mistakes to avoid:\n\n- Building `context` from global document queries instead of surface-scoped options.\n- Mutating `state.selection` ranges in-place.\n- Applying both core operation and additional ad-hoc selection mutation in the same event handler.\n\nSee `packages/selection-vue` for a concrete adapter integration pattern.\n","readmeFilename":"README.md"}