@codecademy/gamut
72.0.272.0.3-alpha.8100dc.0
agent-tools/skills/gamut-select-dropdown/SKILL.md+
agent-tools/skills/gamut-select-dropdown/SKILL.mdNew file+183
Index: package/agent-tools/skills/gamut-select-dropdown/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-select-dropdown/SKILL.md
+++ package/agent-tools/skills/gamut-select-dropdown/SKILL.md
@@ -0,0 +1,183 @@
+---
+name: gamut-select-dropdown
+description: Use when implementing or auditing SelectDropdown — single/multi modes, controlled vs uncontrolled value, creatable options, FormGroup wiring, and react-select action meta. Pair with gamut-forms for FormGroup/validation patterns.
+---
+
+# Gamut SelectDropdown
+
+Styled dropdown built on react-select. Supports single and multi-select, searchable menus, creatable options, icons, groups, and abbreviations.
+
+Source: `@codecademy/gamut` — [SelectDropdown.tsx](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx)
+
+See also: [`gamut-forms`](../gamut-forms/SKILL.md) — FormGroup wiring, error regions, and validation UX.
+
+Storybook: [Atoms / FormInputs / SelectDropdown](https://gamut.codecademy.com/?path=/docs-atoms-forminputs-selectdropdown--docs)
+
+---
+
+## When to use SelectDropdown vs Select
+
+Use `Select` for standard single-select forms with minimal bundle cost. Use `SelectDropdown` when designs specify the styled dropdown menu, search, multi-select tags, creatable options, icons, groups, or abbreviations. SelectDropdown has a larger JavaScript dependency (react-select).
+
+---
+
+## Options
+
+`options` accepts plain strings or option objects. `value` is always a string and references an option's `value`.
+
+| Field | Required | Notes |
+| -------------- | -------- | -------------------------------------------------------------------- |
+| `label` | yes | Display text |
+| `value` | yes | Unique string; what `value` / `string[]` reference |
+| `disabled` | no | Option cannot be selected |
+| `subtitle` | no | Secondary text below the label |
+| `rightLabel` | no | Text on the right side of the option |
+| `icon` | no | A `@codecademy/gamut-icons` component |
+| `abbreviation` | no | Short text shown in the input while the full label shows in the menu |
+
+Grouped options: `{ label, options: [...], divider? }` (extends react-select `GroupBase`; `divider` draws a rule above the group).
+
+---
+
+## Controlled vs uncontrolled
+
+SelectDropdown does **not** accept `defaultValue`.
+
+| Mode | Uncontrolled | Controlled |
+| ---------------- | -------------------------------------------------- | --------------------------------------------------------------------------------- |
+| Single | Not supported | `value` (string) + update in `onChange` |
+| Multi | Omit `value` or pass non-array (`undefined`, `''`) | `value: string[]` + update in `onChange` |
+| Creatable single | Not supported | Same as single; `onCreateOption` appends to `options` |
+| Creatable multi | Omit `value`; `onCreateOption` for options | `value: string[]`; update in `onChange` on every change including `create-option` |
+
+Single-select selection is derived from the `value` prop only — internal state is not kept. Multi-select without `value: string[]` keeps selection in internal `multiValues`.
+
+**Controlled creatable multi pitfall:** Updating `options` alone without syncing `value` in `onChange` clears selection when options re-render.
+
+---
+
+## onChange contract
+
+`onChange` receives option object(s), not `event.target.value`:
+
+```tsx
+// Single
+onChange={(option) => setValue(option.value)}
+
+// Multi
+onChange={(selected) => setValue(selected.map((o) => o.value))}
+```
+
+Second argument is react-select `ActionMeta`. For creatable creates: `meta.action === 'create-option'`. Do **not** pass `onCreateOption` to react-select directly — Gamut invokes it from `changeHandler` while still forwarding `create-option` to consumer `onChange`.
+
+---
+
+## Creatable
+
+- `isCreatable` forces `isSearchable: true` (TypeScript enforces this).
+- `onCreateOption(inputValue)` — convenience hook to append to `options`.
+- `onChange(selected, meta)` — use `meta.action === 'create-option'` to sync controlled `value` and `options` together.
+- `isValidNewOption` — return `false` to hide the Add row.
+- `validationMessage` — replaces menu "No options" text; mirror in `FormGroup` `error` for field-level feedback.
+
+**Validation after blur:** react-select clears input on blur. Handle `onInputChange`: validate on `input-change`, re-validate from last typed value on `input-blur` so FormGroup error persists.
+
+---
+
+## FormGroup wiring
+
+- `FormGroup` `htmlFor` must match control `id` / `name`.
+- Pass `name` on SelectDropdown (required for forms).
+- Pass `aria-label` (required for forms); it must match the FormGroupLabel `htmlFor` / `name`.
+- Pass `error` boolean when FormGroup has an error.
+- Generic FormGroup live-region behavior: see [`gamut-forms`](../gamut-forms/SKILL.md).
+
+```tsx
+<FormGroup htmlFor="country" isSoloField label="Country" error={errors.country}>
+ <SelectDropdown
+ name="country"
+ aria-label="country"
+ options={options}
+ value={value}
+ error={Boolean(errors.country)}
+ onChange={(option) => setValue(option.value)}
+ />
+</FormGroup>
+```
+
+---
+
+## Styling & layout props
+
+| Prop | Type | Default | Notes |
+| ------------------- | ------------------------ | -------- | --------------------------------------------------------- |
+| `size` | `'small' \| 'medium'` | `medium` | Control height/density |
+| `shownOptionsLimit` | `1`–`6` | `6` | Visible options before the menu scrolls |
+| `inputWidth` | `string \| number` | — | Width of the input independent of the menu |
+| `dropdownWidth` | `string \| number` | — | Width of the menu independent of the input |
+| `menuAlignment` | `'left' \| 'right'` | `left` | Menu edge alignment |
+| `zIndex` | `number` | auto | Menu z-index |
+| `inputProps` | `{ hidden?, combobox? }` | — | `data-*` / `aria-*` only, forwarded to the input elements |
+
+---
+
+## Examples
+
+### Single (controlled)
+
+```tsx
+const [value, setValue] = useState('us');
+
+<SelectDropdown
+ name="country"
+ options={options}
+ value={value}
+ onChange={(option) => setValue(option.value)}
+/>;
+```
+
+### Multi (uncontrolled)
+
+```tsx
+<SelectDropdown
+ multiple
+ name="tags"
+ options={options}
+ onChange={(selected) => console.log(selected)}
+/>
+```
+
+### Creatable multi (uncontrolled)
+
+```tsx
+const [options, setOptions] = useState(['Apple', 'Banana']);
+
+<SelectDropdown
+ isCreatable
+ multiple
+ name="fruits"
+ options={options}
+ onCreateOption={(v) => setOptions((prev) => [...prev, v])}
+/>;
+```
+
+### Creatable multi (controlled)
+
+```tsx
+const [options, setOptions] = useState(['Apple', 'Banana']);
+const [value, setValue] = useState<string[]>([]);
+
+<SelectDropdown
+ isCreatable
+ multiple
+ name="fruits"
+ options={options}
+ value={value}
+ onChange={(selected, meta) => {
+ setValue(selected.map((o) => o.value));
+ if (meta.action === 'create-option' && meta.option) {
+ setOptions((prev) => [...prev, meta.option.value]);
+ }
+ }}
+/>;
+```