@codecademy/gamut

72.0.272.0.3-alpha.67eeac.0
agent-tools/skills/gamut-datalist/SKILL.md
+agent-tools/skills/gamut-datalist/SKILL.mdNew file
+274
Index: package/agent-tools/skills/gamut-datalist/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-datalist/SKILL.md
+++ package/agent-tools/skills/gamut-datalist/SKILL.md
@@ -0,0 +1,274 @@
+---
+name: gamut-datalist
+description: Use this skill when building a DataList for item-focused layouts with row expansion, row selection, or rich per-item content — including expandedContent, onRowSelect, onRowExpand, variant (default/card), useLocalQuery, and empty/loading states. Do not use for bulk data comparison tables (see gamut-datatable), or for small static lists (see gamut-list).
+---
+
+# Gamut DataList
+
+Item-focused list for managing, engaging with, and expanding individual rows. Use when users interact with items — opening details, selecting for bulk actions, or viewing expanded layouts — rather than scanning and comparing data across rows.
+
+Source: `@codecademy/gamut` — [DataList.tsx](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/DataList/DataList.tsx)
+
+See also: [`gamut-datatable`](../gamut-datatable/SKILL.md) — query-focused table for bulk data comparison. [`gamut-list`](../gamut-list/SKILL.md) — lower-level list primitives for fully custom layouts. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — ARIA and keyboard interaction.
+
+Storybook: [Organisms / Lists & Tables / DataList](https://gamut.codecademy.com/?path=/docs-organisms-lists-tables-datalist--docs)
+
+## Components
+
+```tsx
+import { DataList } from '@codecademy/gamut';
+import { useLocalQuery } from '@codecademy/gamut';
+```
+
+| Symbol          | Role                                                                                     |
+| --------------- | ---------------------------------------------------------------------------------------- |
+| `DataList`      | Root component; supports expansion, selection, and card/default variants                 |
+| `useLocalQuery` | Client-side hook for sort/filter state; spread its return value directly into `DataList` |
+
+## When to use DataList
+
+- Users **open, expand, or engage** with individual items (content libraries, assignment lists, course catalogs).
+- Rows need **expandable detail panels** with rich layouts.
+- Users need to **select rows** for bulk actions.
+- Items contain **icons, graphics, or complex layouts** rather than plain metrics.
+- Optional filtering or sorting across shared attributes is needed at the list level.
+
+**Do not use DataList when:**
+
+- The goal is to **compare data across rows** (scores, metrics, statuses in columns) → use [`DataTable`](../gamut-datatable/SKILL.md).
+- The list is small, static, and needs fully custom row layouts → use [`List`](../gamut-list/SKILL.md) directly.
+- You need a table with horizontal scrolling → use `DataTable` (DataList is not scrollable).
+
+## Design principles
+
+- **Engage with individual items**: design each row to support the action users take on it (open, launch, select, expand).
+- **Customize items with rich content**: icons, progress indicators, graphics, and other atoms are appropriate within rows.
+- **Keep item controls visible**: action controls should be on the right side of the row.
+- **Place lists inside main containers**: avoid overflow by placing DataList in appropriately sized parent layouts.
+- **Use DataTable for comparison-first designs**: if the design is primarily about scanning data between items, reach for DataTable instead.
+
+## Variants
+
+```tsx
+<DataList variant="default" />
+```
+
+| `variant` | Use for                                                                            |
+| --------- | ---------------------------------------------------------------------------------- |
+| `default` | Standard bordered rows; best for most item-management layouts                      |
+| `card`    | Rows with vertical gap; best for content that doesn't need to be visually adjacent |
+
+## Props
+
+| Prop                    | Type                                                   | Default             | Notes                                                                    |
+| ----------------------- | ------------------------------------------------------ | ------------------- | ------------------------------------------------------------------------ |
+| `id`                    | `string`                                               | required            | Unique ID for the list                                                   |
+| `idKey`                 | `keyof Row`                                            | required            | Row identifier — must be a `string \| number` field                      |
+| `rows`                  | `Row[]`                                                | required            | Data array                                                               |
+| `columns`               | `ColumnConfig<Row>[]`                                  | required            | Column definitions                                                       |
+| `query`                 | `Query<Row>`                                           | —                   | Current sort/filter state                                                |
+| `onQueryChange`         | `OnQueryChange<Row>`                                   | —                   | Called when sort or filter changes                                       |
+| `selected`              | `Row[IdKey][]`                                         | —                   | Array of selected row IDs                                                |
+| `onRowSelect`           | `RowStateChange<'select' \| 'select-all', Row[IdKey]>` | —                   | Called on row or select-all toggle                                       |
+| `expanded`              | `Row[IdKey][]`                                         | —                   | Array of expanded row IDs                                                |
+| `onRowExpand`           | `RowStateChange<'expand', Row[IdKey]>`                 | —                   | Called when a row is expanded or collapsed                               |
+| `expandedContent`       | `ExpandRow<Row>`                                       | —                   | Render function for expanded row content; receives `{ row, onCollapse }` |
+| `variant`               | `'default' \| 'card'`                                  | `'default'`         | Row visual style                                                         |
+| `header`                | `boolean`                                              | —                   | Whether to show a header row                                             |
+| `hideSelectAll`         | `boolean`                                              | `false`             | Hides the select-all checkbox in the header                              |
+| `loading`               | `boolean`                                              | `false`             | Replaces row content with shimmer placeholders                           |
+| `spacing`               | `'condensed' \| 'normal'`                              | `'condensed'`       | Row padding                                                              |
+| `emptyMessage`          | `ReactNode`                                            | default empty state | Rendered when `rows` is empty                                            |
+| `disableContainerQuery` | `boolean`                                              | `false`             | Falls back to media queries instead of container queries                 |
+
+> DataList always sets `scrollable={false}`. For a horizontally scrollable table, use `DataTable`.
+
+## ColumnConfig
+
+Identical to DataTable — see [`gamut-datatable`](../gamut-datatable/SKILL.md#columnconfig) for the full table.
+
+## Basic usage
+
+```tsx
+import { DataList, useLocalQuery } from '@codecademy/gamut';
+
+const columns = [
+  { key: 'title', header: 'Title', size: 'md', type: 'header', fill: true },
+  { key: 'status', header: 'Status', size: 'sm' },
+];
+
+const MyList = ({ data }) => {
+  const queryData = useLocalQuery({ idKey: 'id', rows: data, columns });
+
+  return <DataList id="my-list" idKey="id" columns={columns} {...queryData} />;
+};
+```
+
+## Expandable rows
+
+Pass `expandedContent`, `expanded`, and `onRowExpand` together. The `expandedContent` render function receives `{ row, onCollapse }` — call `onCollapse` from within the expanded panel to close it programmatically.
+
+```tsx
+const [expanded, setExpanded] = useState<string[]>([]);
+
+const handleExpand = ({ type, payload: { rowId, toggle } }) => {
+  if (type === 'expand') {
+    setExpanded((prev) =>
+      toggle ? prev.filter((id) => id !== rowId) : [...prev, rowId]
+    );
+  }
+};
+
+<DataList
+  id="expandable-list"
+  idKey="id"
+  columns={columns}
+  rows={data}
+  expanded={expanded}
+  onRowExpand={handleExpand}
+  expandedContent={({ row, onCollapse }) => (
+    <Box p={16}>
+      <Text>{row.title}: additional details here</Text>
+      <TextButton onClick={onCollapse}>Collapse</TextButton>
+    </Box>
+  )}
+/>;
+```
+
+## Selectable rows
+
+Pass `selected` and `onRowSelect` to enable checkboxes. The callback receives `{ type, payload: { rowId, toggle } }` where `type` is `'select'` or `'select-all'`.
+
+```tsx
+const [selected, setSelected] = useState<string[]>([]);
+
+const handleSelect = ({ type, payload: { rowId, toggle } }) => {
+  if (type === 'select-all') {
+    setSelected((prev) => (prev.length > 0 ? [] : data.map((r) => r.id)));
+  } else if (type === 'select') {
+    setSelected((prev) =>
+      toggle ? prev.filter((id) => id !== rowId) : [...prev, rowId]
+    );
+  }
+};
+
+<DataList
+  id="selectable-list"
+  idKey="id"
+  columns={columns}
+  rows={data}
+  selected={selected}
+  onRowSelect={handleSelect}
+/>;
+```
+
+To hide row checkboxes entirely, omit `onRowSelect` and `selected`. To hide only the select-all checkbox, pass `hideSelectAll`.
+
+## Expansion + selection combined
+
+Both can be active at once — pass all four props together.
+
+```tsx
+<DataList
+  id="full-list"
+  idKey="id"
+  columns={columns}
+  rows={data}
+  selected={selected}
+  onRowSelect={handleSelect}
+  expanded={expanded}
+  onRowExpand={handleExpand}
+  expandedContent={({ row }) => <ExpandedDetail row={row} />}
+/>
+```
+
+## Sorting and filtering with useLocalQuery
+
+`useLocalQuery` handles client-side sort and filter state. Spread its return value directly into `DataList`. Mark columns as `sortable` or provide `filters` in the column config.
+
+```tsx
+const columns = [
+  { key: 'title', header: 'Title', size: 'md', sortable: true },
+  {
+    key: 'type',
+    header: 'Type',
+    size: 'sm',
+    filters: ['Course', 'Path', 'Project'],
+  },
+];
+
+const queryData = useLocalQuery({ idKey: 'id', rows: data, columns });
+
+<DataList id="my-list" idKey="id" columns={columns} {...queryData} />;
+```
+
+For server-side filtering or pagination, manage `query` and `onQueryChange` externally instead of using `useLocalQuery`.
+
+## Empty state
+
+DataList shows a default empty state when `rows` is empty. Override with `emptyMessage`.
+
+```tsx
+<DataList
+  id="my-list"
+  idKey="id"
+  columns={columns}
+  rows={[]}
+  emptyMessage={
+    <Box as="tbody" height="100%" width="100%">
+      <FlexBox
+        as="tr"
+        height="inherit"
+        position="absolute"
+        width="inherit"
+        zIndex={1}
+      >
+        <FlexBox
+          as="th"
+          center
+          column
+          left="calc(50% - 115px)"
+          p={16}
+          position="sticky"
+          top="calc(50% - 115px)"
+          width="fit-content"
+        >
+          <Text>No items yet.</Text>
+        </FlexBox>
+      </FlexBox>
+    </Box>
+  }
+/>
+```
+
+## Loading state
+
+Pass `loading` to replace row content with shimmer placeholders while data fetches.
+
+```tsx
+<DataList id="my-list" idKey="id" columns={columns} rows={[]} loading />
+```
+
+## Container queries
+
+DataList uses CSS container queries by default. Disable only when the list lives in a constrained container or you are managing your own responsive logic.
+
+```tsx
+<div style={{ width: '280px' }}>
+  <DataList
+    disableContainerQuery
+    id="sidebar-list"
+    idKey="id"
+    rows={data}
+    columns={columns}
+  />
+</div>
+```
+
+## Accessibility
+
+- DataList renders as a semantic `<table>` via the underlying `List as="table"`.
+- Expanded rows receive an `aria-live="polite"` region automatically — do not add a duplicate live region.
+- The expanded content region is labeled by the row's header column text; provide meaningful header column values.
+- Selection checkboxes are grouped as a checkbox list; the select-all checkbox is in the `<thead>`.
+- For custom `emptyMessage`, use `tbody > tr > th` structure for valid table semantics.