@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]);
+    }
+  }}
+/>;
+```