From 9628a30c7026a0c84a412d7d2fa624d5b67ef99a Mon Sep 17 00:00:00 2001 From: Edwin Tantawi Date: Thu, 10 Sep 2026 01:03:36 +0700 Subject: [PATCH 1/2] feat(core): add the Accordion component A stack of disclosure rows in the same token-driven shape as the rest of the set. `contained` groups the rows on one surface and divides them; `separated` gives each its own outline. `exclusive` opens one row at a time, and a row can be disabled. Underneath it is `
` and ``, so the keyboard, the screen reader and find-in-page behaviour are the platform's rather than ours - and `exclusive` is nothing but the shared `name` attribute, so no state tracks which row is open. Four decisions worth writing down: - Rows arrive as an `items` array rather than as children. A compound `` would have to read the group's variant and its `exclusive` group name out of React context on every render, and the package README advertises that nothing here does a context lookup in the render path. That paragraph is extended rather than quietly falsified. - The chevron mirrors the element's own open state through `onToggle` instead of turning in CSS. `stylex.when.ancestor('[open]')` compiles to `:where(.marker[open] *)`, which reaches every chevron under an open row and would rotate the closed ones of a nested Accordion with it. - `contained` draws its dividers by painting the group in the border token and letting a `gap` of the same width show through between opaque rows. No `:first-child` rule to exempt the first one, and a theme that thickens its outlines thickens the dividers to match. - A disabled row takes the pointer away from its summary and drops it out of the tab order rather than cancelling the click. Enter on a focused summary *is* a click, so both routes had to go; the `not-allowed` cursor sits on the row, since a summary that ignores the pointer cannot set one. The example app gains an Accordion section showing both variants, the exclusive group and the disabled row. --- .changeset/tall-pillows-shake.md | 12 + apps/example/src/showcase.tsx | 78 ++++++- packages/core/README.md | 9 +- packages/core/src/components/accordion.tsx | 245 +++++++++++++++++++++ packages/core/src/index.ts | 6 + 5 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 .changeset/tall-pillows-shake.md create mode 100644 packages/core/src/components/accordion.tsx diff --git a/.changeset/tall-pillows-shake.md b/.changeset/tall-pillows-shake.md new file mode 100644 index 0000000..64337ff --- /dev/null +++ b/.changeset/tall-pillows-shake.md @@ -0,0 +1,12 @@ +--- +'@batik-prototype/core': minor +--- + +Add `Accordion`, a stack of disclosure rows in the same token-driven shape as the rest of +the set: `contained` groups the rows on one surface and divides them with the border token, +`separated` gives each row its own outline. + +It is `
` and `` underneath, so the keyboard, the screen reader and +find-in-page behaviour are the platform's. `exclusive` names the group rather than tracking +which row is open, which is why the rows arrive as an `items` array instead of as children: +nothing has to read the group's name or variant out of React context to render. diff --git a/apps/example/src/showcase.tsx b/apps/example/src/showcase.tsx index 8fb64d1..60b0ba4 100644 --- a/apps/example/src/showcase.tsx +++ b/apps/example/src/showcase.tsx @@ -1,6 +1,7 @@ -import { Badge, Button, Card, Input } from '@batik-prototype/core'; +import { Accordion, Badge, Button, Card, Input, type AccordionItem } from '@batik-prototype/core'; import { color } from '@batik-prototype/core/tokens/color.stylex'; import { font } from '@batik-prototype/core/tokens/font.stylex'; +import { radius } from '@batik-prototype/core/tokens/shape.stylex'; import { space } from '@batik-prototype/core/tokens/space.stylex'; import * as stylex from '@stylexjs/stylex'; import { useId, type ReactNode } from 'react'; @@ -33,8 +34,73 @@ const styles = stylex.create({ cardTitle: { fontSize: font.sizeMd, fontWeight: font.weightSemibold, margin: 0 }, cardBody: { color: color.muted, fontSize: font.sizeSm, margin: 0 }, + + code: { + backgroundColor: color.neutralSurface, + borderRadius: radius.sm, + color: color.onNeutralSurface, + fontFamily: font.familyMono, + fontSize: '0.85em', + paddingInline: space.xs, + }, }); +const FAQ: readonly AccordionItem[] = [ + { + id: 'tokens', + title: 'Where do the colours come from?', + content: ( +

+ Every one of them is a StyleX variable declared by{' '} + @batik-prototype/core. A theme overrides the + variable; this row never learns which theme won. +

+ ), + defaultOpen: true, + }, + { + id: 'platform', + title: 'What is this built on?', + content: ( +

+ A <details> and a{' '} + <summary>, so the keyboard and the screen + reader work without a line of JavaScript. Opening one row closes the others because they + share a name, which is a browser feature rather + than a component one. +

+ ), + }, + { + id: 'locked', + title: 'A row that cannot be opened', + content:

Unreachable, by design.

, + disabled: true, + }, +]; + +const SEPARATED: readonly AccordionItem[] = [ + { + id: 'radius', + title: 'Ocean rounds the corners', + content: ( +

+ The radius token moves and every row follows, in both variants at once. +

+ ), + }, + { + id: 'divider', + title: 'Sunset squares them off', + content: ( +

+ In the contained variant the divider between rows is the border token, so a theme that + thickens its outlines thickens these too. +

+ ), + }, +]; + function Section({ title, caption, @@ -172,6 +238,16 @@ export function Showcase() { + +
+
+ + +
+
); } diff --git a/packages/core/README.md b/packages/core/README.md index c43339f..1ef5a86 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -36,14 +36,17 @@ anyway to compile your own StyleX. See [Setting up the compiler](#setting-up-the | `Input` | Three sizes, `invalid`, disabled, themed placeholder | | `Card` | `elevated` / `outlined`, four padding steps | | `Badge` | Five tones: neutral, accent, success, warning, danger | +| `Accordion` | `contained` / `separated`, optional `exclusive` rows | | `ThemeProvider` | Applies a theme, resolves the colour scheme | | `useTheme` / `useColorScheme` | Read the active theme and the resolved scheme | | `usePreferredColorScheme` | The OS preference on its own, kept live | | `defineTheme` / `resolveTheme` | The theme contract, also at `@batik-prototype/core/theme` | -Components are plain elements underneath — `