diff --git a/.specs/pick-list.md b/.specs/pick-list.md new file mode 100644 index 00000000..4adc5291 --- /dev/null +++ b/.specs/pick-list.md @@ -0,0 +1,149 @@ +--- +name: pick-list +category: data +structure: monolithic +status: approved +spec_version: 1 +checksum: 99e777f34e83355b73625e5cecba7c8ac182a1b3e972993e812cf3052a906117 +created: 2026-06-25 +last_updated: 2026-06-25 +--- + +# Pick List — Component Spec + +## Purpose + +Dual-list transfer control: a labelled **source** listbox and a labelled **target** listbox with controls to move items between them and (optionally) reorder items within a list. Use it when the consumer needs to build an ordered subset from a pool of options where both the chosen set and the remaining pool stay visible. Derived from the PrimeVue PickList behavior but rewritten to our conventions (no Figma); the visual surface is a best-effort first draft to validate. + +## Usage + +```vue + + + +``` + +## Props + +| Prop | Type | Default | Required | JSDoc | +|---|---|---|---|---| +| `modelValue` | `[unknown[], unknown[]]` | `() => [[], []]` | no | Bound pair of lists as `[sourceItems, targetItems]` (v-model). | +| `dataKey` | `string` | `''` | no | Item field that uniquely identifies a record; enables selection tracking and stable keys. | +| `sourceHeader` | `string` | `''` | no | Heading text for the source list; also its accessible name. | +| `targetHeader` | `string` | `''` | no | Heading text for the target list; also its accessible name. | +| `disabled` | `boolean` | `false` | no | Disables all selection and move controls and applies disabled tokens. | +| `reorderable` | `boolean` | `false` | no | Shows up/down reorder controls that move selected items within their own list. | + +## Events + +| Event | Payload | Notes | +|---|---|---| +| `update:modelValue` | `[unknown[], unknown[]]` | v-model update with the new `[sourceItems, targetItems]` pair. | +| `move` | `{ direction: 'to-target' \| 'to-source'; items: unknown[] }` | Fired after items move between lists, with the moved items. | +| `reorder` | `{ list: 'source' \| 'target'; items: unknown[] }` | Fired after a reorder, with the items in the affected list. | + +## Slots + +| Slot | Scope | Notes | +|---|---|---| +| `item` | `{ item: unknown; index: number; list: 'source' \| 'target' }` | Renders a single row; receives the item, its index, and which list it belongs to. | +| `sourceHeader` | `—` | Overrides the source heading content. | +| `targetHeader` | `—` | Overrides the target heading content. | + +## States + +- Visual states: `default`, `hover` (option), `focus-visible` (option / control), `selected` (option), `disabled` +- `data-disabled` mirrors the `disabled` prop +- `data-reorderable` mirrors the `reorderable` prop +- Each option carries `data-selected` mirroring its selection and `aria-selected` + +## Motion & Animations + +_none_ + +## Tokens + +| Region | Token (DESIGN.md) | +|---|---| +| typography (header) | `.text-label-lg` | +| typography (option) | `.text-body-md` | +| surface (list) | `var(--bg-surface)` | +| surface (option hover) | `var(--bg-hover)` | +| surface (option selected) | `var(--bg-selected)` | +| text | `var(--text-default)` | +| text (header) | `var(--text-muted)` | +| text (disabled) | `var(--text-disabled)` | +| surface (disabled) | `var(--bg-disabled)` | +| border | `var(--border-default)` | +| ring | `var(--ring-color)` | +| spacing (list padding) | `var(--spacing-xs)` | +| spacing (option padding) | `var(--spacing-sm)` | +| gap (lists + controls) | `var(--spacing-sm)` | +| shape (list) | `var(--shape-card)` | +| shape (option) | `var(--shape-elements)` | + +## Theme gaps + +| Figma variable | Temporary primitive | Follow-up | +|---|---|---| +| _none_ | — | — | + +## Accessibility (WCAG 2.1 AA) + +- Visible focus: `focus-visible:ring-2 focus-visible:ring-[var(--ring-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-canvas)]` on each option and on every move/reorder control. +- Keyboard map: `Tab` moves between the two listboxes and the control column; within a listbox `ArrowUp`/`ArrowDown` move the active option, `Space`/`Enter` toggle its selection; move and reorder controls are activated with `Enter`/`Space`. +- ARIA: each list is `role="listbox"` with `aria-label` from its header text; options are `role="option"` with `aria-selected`; the source/target listboxes set `aria-multiselectable="true"`; every move/reorder control is an icon-only button with an explicit `aria-label`; `aria-disabled` mirrors `disabled`. +- Contrast ≥4.5:1 (text) / ≥3:1 (borders + icons), including the disabled state. +- No motion in this component, so no `motion-reduce:*` is required. +- Touch target ≥40×40 px for every move/reorder control (medium icon-button height). + +## Stories (Storybook) + +- Default +- Disabled +- WithReorder — justified: the `reorderable` prop adds an extra control column (up/down) that is hidden by default; the story documents the reorder controls and within-list ordering behavior in isolation. + +## Constraints — DO NOT + + + +- Do not add props beyond the Props table above. If you need a prop that is not listed, emit `BLOCKED: missing prop ` and stop — do not invent. +- Do not add events beyond the Events table above. Same rule for slots and sub-components. +- Do not invent imports. Every `@aziontech/webkit/*` path must exist in `packages/webkit/package.json#exports`. Every relative import must resolve to a real file. Every npm package must be installed. +- Do not use HEX/RGB/HSL colors, Tailwind palette names (e.g. `bg-blue-500`), raw typography classes (e.g. `text-sm`), `any`, `@ts-ignore`, or `class` inside `defineProps`. +- Do not install or import positioning/animation libraries (`@floating-ui/*`, `popper.js`, `tippy.js`, `gsap`, `framer-motion`, `motion`, `@vueuse/motion`, `@formkit/auto-animate`, drag-drop runtimes, scroll virtualization libs). Use CSS + Vue primitives (``, ``). See `.claude/rules/dependencies.md`. +- Do not improvise animations. Every `animate-*` / `transition-*` class must come from `packages/theme/src/tokens/semantic/animations.js`; every motion-bearing class pairs with `motion-reduce:*` on the same class string; no component-local `@keyframes`. +- Do not create class presets in JavaScript (`const kindClasses = {...}`, `const sharedClasses = [...]`, `const sizeClasses = {...}`, `const rootClasses = computed(...)`). Variants live on `data-*` attributes consumed by Tailwind `data-[attr=value]:`. All utilities live inline on the root element's `class` attribute. No `