-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add InputSelect composition #647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
+1,163
−0
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| --- | ||
| name: input-select | ||
| category: inputs | ||
| structure: composition | ||
| status: implemented | ||
| spec_version: 1 | ||
| figma: | ||
| url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=3714-10801 | ||
| node_id: 3714:10801 | ||
| checksum: 14d4a1918e1885897c4ffb714fa74e12a5e5d1fef68e34d2830b377aa88c6992 | ||
| created: 2026年06月15日 | ||
| last_updated: 2026年06月15日 | ||
| --- | ||
|
|
||
| # Input Select — Component Spec | ||
|
|
||
| ## Purpose | ||
|
|
||
| Select control for choosing one (`multiple={false}`) or many (`multiple={true}`) options from a list. The trigger field mirrors `InputText`'s visual API (size, hover/focus/filled/disabled/invalid/required states) so a Select sits next to a TextInput without visual drift. The dropdown is rendered via the native Popover API + CSS anchor positioning — no `@floating-ui` runtime. Each option carries a radio indicator in single mode or a checkbox indicator in multi mode, optionally a leading slot/icon and a trailing tag. Search and "Create new" footer are exposed as slots on `<InputSelectContent>`. | ||
|
|
||
| Aligned with Figma frames `3714:10801` (trigger states) and `3899:29361` (options panel). | ||
|
|
||
| ## Usage | ||
|
|
||
| ```vue | ||
| <script setup> | ||
| import InputSelect from '@aziontech/webkit/inputs/input-select' | ||
| import InputSelectTrigger from '@aziontech/webkit/inputs/input-select-trigger' | ||
| import InputSelectContent from '@aziontech/webkit/inputs/input-select-content' | ||
| import InputSelectGroup from '@aziontech/webkit/inputs/input-select-group' | ||
| import InputSelectOption from '@aziontech/webkit/inputs/input-select-option' | ||
|
|
||
| const value = defineModel({ default: '' }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <InputSelect v-model="value" size="medium" placeholder="Select an option"> | ||
| <InputSelectTrigger /> | ||
| <InputSelectContent> | ||
| <InputSelectGroup label="Group A"> | ||
| <InputSelectOption value="opt-1">Option 1</InputSelectOption> | ||
| <InputSelectOption value="opt-2">Option 2</InputSelectOption> | ||
| </InputSelectGroup> | ||
| <InputSelectGroup label="Group B"> | ||
| <InputSelectOption value="opt-3" icon="pi pi-heart">Option 3</InputSelectOption> | ||
| </InputSelectGroup> | ||
| </InputSelectContent> | ||
| </InputSelect> | ||
| </template> | ||
| ``` | ||
|
|
||
| ## Sub-components | ||
|
|
||
| - `input-select-trigger/input-select-trigger.vue` — Bordered trigger field with optional left icon slot, selected-value label, and chevron-down. Mirrors `InputText` size/state visuals. | ||
| - `input-select-content/input-select-content.vue` — Popover panel rendered via the native Popover API + CSS anchor positioning; exposes `#search` and `#footer` slots and scrolls its option list when content exceeds `max-h-[320px]`. | ||
| - `input-select-group/input-select-group.vue` — Section wrapper for a labelled group of options; renders an overline-style heading when `label` is set. | ||
| - `input-select-option/input-select-option.vue` — Selectable option row with a mode-aware indicator (radio in single mode, checkbox in multi mode), an optional `icon` (PrimeIcons), an optional leading `#left` slot, the option label, and an optional trailing `#tag` slot. | ||
|
|
||
| ## Props | ||
|
|
||
| | Prop | Type | Default | Required | JSDoc | | ||
| |---|---|---|---|---| | ||
| | `modelValue` | `string \| number \| unknown[]` | `undefined` | no | Two-way bound selection. Scalar in single mode; array in multi mode. | | ||
| | `multiple` | `boolean` | `false` | no | When true, switches the component to multi-select; options render checkbox indicators and `modelValue` is an array. | | ||
| | `open` | `boolean` | `undefined` | no | Controlled open state for the dropdown. Use with `v-model:open`. | | ||
| | `defaultOpen` | `boolean` | `false` | no | Initial open state when uncontrolled. | | ||
| | `placeholder` | `string` | `undefined` | no | Trigger placeholder shown when no option is selected. | | ||
| | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | no | Size token; affects trigger height (small=28px, medium=32px, large=40px) matching `InputText`. | | ||
| | `disabled` | `boolean` | `false` | no | Disables the trigger and prevents opening; applies disabled tokens. | | ||
| | `readonly` | `boolean` | `false` | no | Marks the field read-only; the current selection is visible but the dropdown cannot open. | | ||
| | `required` | `boolean` | `false` | no | Marks the field as required; sets `aria-required` on the trigger. Visual indicator (asterisk) is owned by the wrapping form-field, not by this component. | | ||
| | `invalid` | `boolean` | `false` | no | Applies the invalid border + ring tokens and sets `aria-invalid` on the trigger. | | ||
| | `displayValue` | `(value: unknown) => string` | `undefined` | no | Custom formatter used by the trigger to render the selected label(s); receives the current `modelValue`. | | ||
|
|
||
| ## Events | ||
|
|
||
| | Event | Payload | Notes | | ||
| |---|---|---| | ||
| | `update:modelValue` | `unknown` | Fires when an option is selected (single) or toggled (multi). | | ||
| | `update:open` | `boolean` | Fires when the dropdown opens or closes. | | ||
|
|
||
| ## Slots | ||
|
|
||
| | Slot | Scope | Notes | | ||
| |---|---|---| | ||
| | `default` | — | Composition slot; receives `<InputSelectTrigger>` and `<InputSelectContent>` (and the option tree inside it). | | ||
|
|
||
| ## States | ||
|
|
||
| - Visual states: `default`, `hover`, `focus-visible`, `filled`, `disabled`, `invalid`, `opened` | ||
| - `data-state` on the root and trigger: `open` | `closed` | ||
| - `data-size` on the trigger mirrors the `size` prop | ||
| - `data-disabled`, `data-invalid`, `data-required`, `data-readonly`, `data-filled` mirror the matching props (or the selection state for `data-filled`) | ||
| - `data-mode` on the root: `single` | `multiple` | ||
| - `data-selected` on `<InputSelectOption>` when the option's value is part of the current selection | ||
|
|
||
| ## Motion & Animations | ||
|
|
||
| | Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback | | ||
| |---|---|---|---| | ||
| | dropdown open / close (on `InputSelectContent` only) | popup scale-in / scale-out semantic utilities from `animations.js` | semantic (150ms in / 110ms out · cubic-bezier) | reduced-motion fallback via the matching `motion-reduce` utility | | ||
| | trigger state change (border/ring/bg) | `transition-colors duration-150 ease-out` | inline (matches catalog) | `motion-reduce:transition-none` | | ||
|
|
||
| ## Tokens | ||
|
|
||
| | Region | Token (DESIGN.md) | | ||
| |---|---| | ||
| | typography (trigger, option label) | `.text-label-sm` | | ||
| | typography (group heading) | `.text-overline-xs` | | ||
| | surface (trigger) | `var(--bg-surface)` | | ||
| | surface (trigger disabled) | `var(--bg-disabled)` | | ||
| | surface (panel) | `var(--bg-surface-raised)` | | ||
| | surface (option hover) | `var(--bg-hover)` | | ||
| | surface (option selected) | `var(--bg-selected)` | | ||
| | text | `var(--text-default)` | | ||
| | text (placeholder, group heading) | `var(--text-muted)` | | ||
| | text (disabled) | `var(--text-disabled)` | | ||
| | border (default) | `var(--border-default)` | | ||
| | border (hover) | `var(--border-strong)` | | ||
| | border (invalid) | `var(--danger-border)` | | ||
| | ring (focus) | `var(--ring-color)` | | ||
| | ring (invalid focus) | `var(--danger)` | | ||
| | spacing (trigger horizontal padding) | `var(--spacing-sm)` | | ||
| | spacing (panel padding) | `var(--spacing-xxs)` | | ||
| | spacing (option padding) | `var(--spacing-xs)` | | ||
| | spacing (option gap) | `var(--spacing-xs)` | | ||
| | shape | `var(--shape-elements)` | | ||
| | shadow (panel) | `var(--shadow-xs)` | | ||
|
|
||
| ## 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 trigger and on every option row. | ||
| - Keyboard map (trigger): `Tab` focuses; `Space`/`Enter`/`ArrowDown` opens the panel; `Escape` closes it. | ||
| - Keyboard map (open panel): `ArrowDown`/`ArrowUp` move active option; `Home`/`End` jump to first/last; `Enter`/`Space` select (single) or toggle (multi); `Escape` closes; `Tab` closes and moves focus to the next form control. | ||
| - ARIA: trigger uses `role="combobox"` with `aria-haspopup="listbox"`, `aria-expanded` bound to open state, `aria-controls` pointing to the content element. Content uses `role="listbox"` with `aria-multiselectable` mirroring `multiple`. Each option uses `role="option"` with `aria-selected` mirroring its selection. Decorative icons (`pi pi-chevron-down`, `pi-heart`, etc.) are `aria-hidden="true"`. | ||
| - `aria-invalid` and `aria-required` on the trigger mirror the matching props. | ||
| - Contrast ≥4.5:1 (text) / ≥3:1 (icons + borders), including disabled state. | ||
| - `motion-reduce:transition-none motion-reduce:animate-none` on every transition/animation-bearing class. | ||
| - Touch target ×ばつ40 px on `size="large"`; smaller sizes are documented as dense-layout-only (consistent with `InputText`). | ||
|
|
||
| ## Stories (Storybook) | ||
|
|
||
| - Default | ||
| - Sizes — composite story rendering all three sizes side by side (canonical composite per `storybook-write` skill). | ||
| - Single — justification: documents the radio-style indicator and scalar `modelValue` shape (the default mode); pairs with `Multiple` to make the two modes browseable in one click. | ||
| - Multiple — justification: documents the checkbox-style indicator and array `modelValue` shape that the `multiple` prop unlocks. | ||
| - WithGroups — justification: documents the `<InputSelectGroup label="...">` heading anatomy from Figma `3899:29361` (Group region), which is not reachable from Default. | ||
| - WithSearchAndFooter — justification: documents the `#search` and `#footer` slots on `<InputSelectContent>` (Top/Bottom regions in Figma `3899:29361`), which are slot-only and invisible from Default. | ||
| - WithOptionExtras — justification: documents the `icon`, `#left`, and `#tag` slots on `<InputSelectOption>` in one composite frame. | ||
| - Filled — justification: pre-populated `modelValue` shows the filled visual state, which is implicit (no prop). | ||
| - Invalid — justification: documents the `invalid` visual treatment, which is a top-level prop with distinct token bindings. | ||
| - Disabled | ||
|
|
||
| ## Constraints — DO NOT | ||
|
|
||
| <!-- This block is injected VERBATIM into every sub-agent prompt. | ||
| spec-validator rejects the spec if this block is missing or shorter than the template. --> | ||
|
|
||
| - Do not add props beyond the Props table above. If you need a prop that is not listed, emit `BLOCKED: missing prop <name>` 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 (`<Teleport>`, `<Transition>`). 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 `<style>` block, no component-local `.css`/`.scss`. See `.claude/rules/styling.md`. | ||
| - 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 `<name>.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<Event>` key and `{ action: '<emitted-name>' }`. 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 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. | ||
| - Do not create files outside the paths declared by your task (the orchestrator tells you exactly which files to write). | ||
| - Do not run `git` commands, `pnpm install`, or any command that changes the lockfile. | ||
| - If anything in the spec is ambiguous or contradicts the rules, emit `BLOCKED: <one-sentence reason>` and write nothing. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.