diff --git a/.specs/checkbox.md b/.specs/checkbox.md index 3f991f52..3ff9fb8d 100644 --- a/.specs/checkbox.md +++ b/.specs/checkbox.md @@ -2,36 +2,55 @@ name: checkbox category: inputs structure: monolithic -status: implemented -spec_version: 2 -checksum: d0a27b8ca2563f33c09954c4657f76af2b4edab6e87bb5367d0ab69d366a2acb +status: approved +spec_version: 3 +checksum: 2fd0e74289221cb07d676ea9cecad5c547ffedc51452a69e0e9e552b05ab2cb3 created: 2026-05-22 -last_updated: 2026-05-22 +last_updated: 2026-06-24 --- + # Checkbox — Component Spec ## Purpose -Collects or edits user input. Migrated from the existing implementation at `packages/webkit/src/components/webkit/inputs/checkbox/`. +Square checkbox control only — the box, the native input, and the check glyph, with no label or description chrome. Use it when you compose your own label, or reach for `FieldCheckbox` / `FieldCheckboxBlock` when you want built-in text. Supports a single boolean (`binary`) or collecting a `value` into an array model. + +## Usage + +```vue + + + +``` ## Props | Prop | Type | Default | Required | JSDoc | |---|---|---|---|---| -| `modelValue` | `string` | `'undefined'` | no | model Value. | -| `value` | `string` | `'undefined'` | no | value. | -| `binary` | `boolean` | `false` | no | binary. | +| `modelValue` | `unknown` | `undefined` | no | Bound value: a boolean in `binary` mode, otherwise the array/value the control reflects. | +| `value` | `unknown` | `undefined` | no | This checkbox's value, added to or removed from the model array when not `binary`. | +| `binary` | `boolean` | `false` | no | Toggles a single boolean instead of collecting `value` into an array. | | `disabled` | `boolean` | `false` | no | Disables interaction and applies disabled tokens. | -| `readonly` | `boolean` | `false` | no | readonly. | -| `inputId` | `string` | `'undefined'` | no | input Id. | +| `readonly` | `boolean` | `false` | no | Prevents changes while the control stays focusable. | +| `inputId` | `string` | `undefined` | no | Forwarded to the native input `id`; pair with an external label's `for`. | | `name` | `string` | `undefined` | no | HTML name for form submission. | -| `tabindex` | `number` | `'undefined'` | no | tabindex. | +| `tabindex` | `number \| undefined` | `undefined` | no | Tab order for the native input. | ## Events | Event | Payload | Notes | |---|---|---| -| `update:modelValue` | `unknown` | — | +| `update:modelValue` | `unknown` | Emitted on toggle; payload is the next boolean (`binary`) or the updated value array. | ## Slots @@ -39,25 +58,32 @@ Collects or edits user input. Migrated from the existing implementation at `pack ## States -- Visual states: `default`, `hover`, `focus-visible`, `active`, `disabled` +- Visual states: `default`, `hover`, `focus-visible`, `active`, `disabled`, `readonly`, `checked` +- `data-checked` mirrors the checked state - `data-disabled` mirrors the `disabled` prop +- `data-readonly` mirrors the `readonly` prop ## Motion & Animations -| Trigger | Animation / Transition | Token | Reduced-motion fallback | +| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback | |---|---|---|---| -| state change | `transition-colors duration-150 ease-out` | inline | `motion-reduce:transition-none` | +| hover / active state change | `transition-opacity duration-fast-02 ease-productive-entrance` on the hover/active ghost overlays | semantic (matches catalog) | `motion-reduce:transition-none` | ## Tokens | Region | Token (DESIGN.md) | |---|---| -| typography | .text-body-sm | | surface | `var(--bg-surface)` | -| text | `var(--text-default)` | -| spacing | `var(--spacing-3)` | -| shape | `var(--shape-elements)` | +| border | `var(--border-default)` | +| checked surface | `var(--primary)` | +| checked glyph | `var(--primary-contrast)` | +| shape | `var(--shape-button)` | +| hover overlay | `var(--bg-hover)` | +| active overlay | `var(--bg-active)` | +| disabled surface | `var(--bg-disabled)` | +| disabled glyph | `var(--text-disabled)` | | ring | `var(--ring-color)` | +| ring offset | `var(--bg-canvas)` | ## Theme gaps @@ -67,17 +93,17 @@ Collects or edits user input. Migrated from the existing implementation at `pack ## 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)]` -- Keyboard map: `Tab` focuses; `Enter`/`Space` activates; `Escape` closes overlays where applicable. -- ARIA: root uses appropriate roles (`button`, `dialog`, `status`, etc.) per sub-component. -- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons), including disabled state. -- `motion-reduce:transition-none motion-reduce:transform-none` on animated states. -- Touch target ≥40×40 px where the control is interactive. +- Visible focus: descendant native input drives the ring — `has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[var(--ring-color)] has-[:focus-visible]:ring-offset-1 has-[:focus-visible]:ring-offset-[var(--bg-canvas)]` +- Keyboard map: `Tab` focuses the native input; `Space` toggles checked state (native checkbox behavior). +- ARIA: renders a real `` with `aria-checked`; control-only, so associate copy via `inputId` + an external label `for`, or use `FieldCheckbox`. +- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons), including the disabled state. +- `motion-reduce:transition-none` on the animated ghost overlays. +- Touch target: the box is 1.125rem (18px) — justified deviation for a control-only primitive; the larger ≥40×40 px hit area is provided by the field wrappers (`FieldCheckbox` / `FieldCheckboxBlock`) or the consumer's label. ## Stories (Storybook) - Default -- Disabled +- Disabled — demonstrates the `disabled` prop (checked and unchecked). ## Constraints — DO NOT @@ -94,7 +120,8 @@ Collects or edits user input. Migrated from the existing implementation at `pack - Do not inherit artifacts as-is from another design system, Figma file, library, or pre-existing `CONTRACT.md` / `README.md`. Rewrite to our conventions. See `.claude/rules/migration.md`. - Do not add Figma references to Storybook stories. No `parameters.design`, no `parameters.figma`, no Figma URLs in `docs.description.*`, no `@storybook/addon-designs` import. The Figma link is owned by `.figma.ts` (Code Connect). See `.claude/docs/COMPONENT_REQUIREMENTS.md`. - Do not use `parameters.actions.argTypesRegex` (deprecated in Storybook 8 and silently misroutes Vue 3 emits) or `parameters.actions.handles` (DOM-only). Declare every event explicitly in `argTypes` with a camelCase `on` key and `{ action: '' }`. Do not use the legacy CSF2 `Name.args = {...}` form — always object-style CSF3. -- Do not add bespoke Storybook stories beyond Default + per `kind` + per `size` + Disabled, unless the spec's "Stories (Storybook)" section explicitly justifies the addition. +- Do not add bespoke Storybook stories beyond Default + Types + Sizes + state stories (`Loading`, `Disabled`) for the props the component actually declares, unless the spec's "Stories (Storybook)" section explicitly justifies the addition. Do not split Types/Sizes into one-story-per-variant — the composite stories are the canonical pattern. +- Do not duplicate the `## Usage` block from the spec inside the Storybook story body. The block is injected once into `parameters.docs.description.component` by the storybook-write skill; copy it nowhere else. - Do not edit `.claude/docs/DESIGN.md`, `.claude/docs/COMPONENT_REQUIREMENTS.md`, or `.claude/docs/PRIMEVUE_ABSTRACTION.md`. - Do not edit the root `package.json` or `.github/workflows/*`. - Do not change `structure` after `status: approved`. To change structure, bump `spec_version` and re-author the spec. diff --git a/apps/storybook/src/stories/components/inputs/Checkbox.stories.js b/apps/storybook/src/stories/components/inputs/Checkbox.stories.js index a29e3f65..013409dc 100644 --- a/apps/storybook/src/stories/components/inputs/Checkbox.stories.js +++ b/apps/storybook/src/stories/components/inputs/Checkbox.stories.js @@ -1,14 +1,13 @@ -import { ref } from 'vue' - import Checkbox from '@aziontech/webkit/checkbox' +import { ref, watch } from 'vue' /** @type {import('@storybook/vue3').Meta} */ const meta = { - title: 'Components/Inputs/Checkbox', + title: 'Components/Inputs/Checkbox', component: Checkbox, tags: ['autodocs'], parameters: { - layout: 'padded', + layout: 'centered', backgrounds: { default: 'dark' }, a11y: { config: { @@ -20,25 +19,44 @@ const meta = { }, docs: { description: { - component: - 'Control only — the square checkbox input with no label or description. Pair with an external label or use FieldCheckbox / FieldCheckboxBlock for built-in text.' + component: [ + 'Square checkbox control only — the box, the native input, and the check glyph, with no label or description chrome. Use it when you compose your own label, or reach for `FieldCheckbox` / `FieldCheckboxBlock` when you want built-in text. Supports a single boolean (`binary`) or collecting a `value` into an array model.', + '', + '## Usage', + '', + '```vue', + '', + '', + '', + '```' + ].join('\n') } } }, argTypes: { modelValue: { control: 'boolean', - description: 'model Value.', + description: 'Bound value: a boolean in binary mode, otherwise the array/value the control reflects.', table: { type: { summary: 'unknown' }, category: 'props' } }, value: { control: false, - description: 'value.', + description: "This checkbox's value, added to or removed from the model array when not binary.", table: { type: { summary: 'unknown' }, category: 'props' } }, binary: { control: 'boolean', - description: 'binary.', + description: 'Toggles a single boolean instead of collecting value into an array.', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' } }, disabled: { @@ -48,12 +66,12 @@ const meta = { }, readonly: { control: 'boolean', - description: 'readonly.', + description: 'Prevents changes while the control stays focusable.', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' } }, inputId: { control: 'text', - description: 'input Id.', + description: 'Forwarded to the native input id; pair with an external label for attribute.', table: { type: { summary: 'string' }, category: 'props' } }, name: { @@ -63,76 +81,68 @@ const meta = { }, tabindex: { control: 'number', - description: 'tabindex.', - table: { type: { summary: 'number' }, category: 'props' } + description: 'Tab order for the native input.', + table: { type: { summary: 'number | undefined' }, category: 'props' } }, 'onUpdate:modelValue': { action: 'update:modelValue', - description: 'Emitted when the value changes.', + description: 'Emitted on toggle; payload is the next boolean (binary) or the updated value array.', table: { type: { summary: 'unknown' }, category: 'events' } } + }, + args: { + binary: true, + disabled: false, + readonly: false } } export default meta -export const Default = { - render: () => ({ - components: { Checkbox }, - setup() { - const value = ref(false) - return { value } - }, - template: ` - - ` - }), - parameters: { - docs: { - description: { - story: 'Binary control only — associate copy via an external label or FieldCheckbox.' +const Template = (args) => ({ + components: { Checkbox }, + setup() { + const value = ref(args.modelValue ?? false) + + watch( + () => args.modelValue, + (next) => { + value.value = next ?? false } + ) + + const onUpdate = (next) => { + value.value = next + args['onUpdate:modelValue']?.(next) } + + return { args, value, onUpdate } + }, + template: '' +}) + +/** @type {import('@storybook/vue3').StoryObj} */ +export const Default = { + render: Template, + parameters: { + docs: { description: { story: 'Binary control only — associate copy via an external label or FieldCheckbox.' } } } } export const Disabled = { - render: () => ({ + render: (args) => ({ components: { Checkbox }, setup() { - const checked = ref(true) - const unchecked = ref(false) - return { checked, unchecked } + return { args } }, template: `
- - + +
` }), parameters: { - docs: { - description: { - story: 'Disabled checked and unchecked controls without label chrome.' - } - } + docs: { description: { story: 'Disabled control in both checked and unchecked states.' } } } } diff --git a/packages/webkit/src/components/inputs/checkbox/checkbox.vue b/packages/webkit/src/components/inputs/checkbox/checkbox.vue index 57b5a067..69a1e919 100644 --- a/packages/webkit/src/components/inputs/checkbox/checkbox.vue +++ b/packages/webkit/src/components/inputs/checkbox/checkbox.vue @@ -1,30 +1,27 @@