Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
08e8ebc
Add a11y config and audit module to @surfnet/storybook-config
sjoerdbeentjes Jun 22, 2026
3e419ff
Wire WCAG 2.1 AA audit into React and Angular Storybooks
sjoerdbeentjes Jul 6, 2026
70f5703
Add test:a11y workspace script, Turbo task, and CI step
sjoerdbeentjes Jun 22, 2026
045859a
Reorder storybook:serve before test:a11y in package scripts
sjoerdbeentjes Jul 6, 2026
5b62ef6
Write a11y reports to git-ignored .a11y-report dir
sjoerdbeentjes Jun 22, 2026
00341a7
Group a11y comment by component → theme → mode
sjoerdbeentjes Jun 22, 2026
2b2d511
Split component and variation into h4/h5 in a11y comment
sjoerdbeentjes Jun 23, 2026
208c3ff
Collapse theme-independent a11y findings to one per variation
sjoerdbeentjes Jun 23, 2026
9d24ae7
Add link-in-text-block WCAG ref (1.4.1) to a11y comment
sjoerdbeentjes Jul 6, 2026
57ce4b4
Move playwright dep from react to storybook-config, scope CI install …
sjoerdbeentjes Jun 23, 2026
3aac4d8
Stabilise a11y audit and trim comments
sjoerdbeentjes Jul 6, 2026
2a8d411
Adopt @surfnet/curve-* package names; note headless run in a11y CI
sjoerdbeentjes Jul 6, 2026
35bb443
Inline a11y-comment script call in CI, drop a11y:comment workspace sc…
sjoerdbeentjes Jul 6, 2026
afa90ef
Capture and embed a11y violation screenshots
sjoerdbeentjes Jul 6, 2026
4bcd972
Drop fixed width on a11y comment screenshots
sjoerdbeentjes Jul 6, 2026
e51e697
Update playwright to 1.61.1
sjoerdbeentjes Jul 6, 2026
f680b0a
fix: always give progress component a accessible name
anneke Sep 1, 2026
89849d6
fix: use primary-strong for badges to increase contrast
anneke Sep 1, 2026
d1b0852
fix: add aria-attributes to combobox trigger button
anneke Sep 1, 2026
5a8cedb
fix: add aria-attributes to resizable handle
anneke Sep 1, 2026
3930f38
fix: combobox change breaking linter
anneke Sep 1, 2026
cab0874
fix: combobox clear button missing label
anneke Sep 1, 2026
e8dc90d
fix: curve data table needs label on button
anneke Sep 1, 2026
4bb3a1d
fix: Add aria-labels to buttons in input-group
anneke Sep 1, 2026
cf611cb
fix: give nativeSelect accessible name
anneke Sep 1, 2026
86efae7
fix: input-OTP accessibility
anneke Sep 1, 2026
33af2ae
fix: breadcrumb accessibility, removed obsolete attributes
anneke Sep 1, 2026
227fbbd
fix: add roles to command element
anneke Sep 1, 2026
1246471
fix: add tabindex 0 to scroll element
anneke Sep 1, 2026
0c2b18a
fixup fix: add roles to command element
anneke Sep 1, 2026
4b3849a
fix: set label on button
anneke Sep 1, 2026
272610d
fix: improve layout of accessibility feedback
anneke Sep 1, 2026
5c55d9f
fix: remove Command (temporarily?)
anneke Sep 1, 2026
ee0d848
fix: add missing aria-labels
anneke Sep 1, 2026
ee4e60e
fix: improve layout of accessibility feedback
anneke Sep 1, 2026
18f1636
fix: improve layout of accessibility feedback
anneke Sep 1, 2026
a17d357
fix: improve layout of accessibility feedback
anneke Sep 1, 2026
0fa1494
feat: accessibility fixes of shadcn components
anneke Aug 27, 2026
d97a6ad
chore: Update Playwright version
anneke Sep 21, 2026
2aeb81a
Update WCAG2.1 testing to WCAG2.2
anneke Sep 21, 2026
512cdcf
Update storybook/test-runner minor version
anneke Sep 21, 2026
356813c
Add ignore for haste collision issue
anneke Sep 21, 2026
8a70cad
Fix for test-a11y ci crashing
anneke Sep 21, 2026
2844afb
fix: storybook example combobox button name
anneke Sep 21, 2026
dc7e8b8
fix: item component story/roles
anneke Sep 21, 2026
889ed45
fix: native select stories by adding label
anneke Sep 21, 2026
732459c
fix: select trigger and pagination stories
anneke Sep 21, 2026
c5d0886
chore: update comment style for readability
anneke Sep 21, 2026
b349031
feat: apply learnings from accessibility audit on shadcn
anneke Aug 27, 2026
d8c1ab6
fix: add an offset to button focus rings, make them solid
anneke Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .agents/skills/add-component/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ description: Use when adding, scaffolding, or vendoring a component into the des
# Add a component

The design system ships components per framework. Decide the scope first, then follow the
matching playbook(s) in this skill's subfiles:
matching playbook(s) in this skill's subfiles.

**Already in the repo?** Do not re-run this add flow. Use
[`update-component`](../update-component/SKILL.md) so Curve design and accessibility
edits are merged, not overwritten. Changes can have an effect on accessibility —
see that playbook before restyling or re-vendoring.

- **React only** → follow [`react.md`](react.md) (shadcn/Base UI flow for `@surfnet/curve-react`).
- **Angular only** → follow [`angular.md`](angular.md) (Spartan flow for `@surfnet/curve-angular`).
Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/add-component/angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ pnpm format
the contract object.
- `pnpm build`, `pnpm format`, and `build-storybook` all pass.

## Updating an existing component

Do not re-run `ng g @spartan-ng/cli:ui` on a component that is already vendored
— the generator overwrites helm. Follow
[`update-component/angular.md`](../update-component/angular.md). Restyling or
merging upstream can have an effect on accessibility.

## Notes

- The Spartan CLI always vendors cross-component imports through the `@spartan-ng/helm/*`
Expand Down
6 changes: 6 additions & 0 deletions .agents/skills/add-component/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ pnpm format
the contract object.
- `pnpm build`, `pnpm lint`, `pnpm format`, and `build-storybook` all pass.

## Updating an existing component

Do not re-run `shadcn add` (or `--overwrite`) on a component that is already
vendored. Follow [`update-component/react.md`](../update-component/react.md).
Restyling or merging upstream can have an effect on accessibility.

## Notes

- Icons: import from `@phosphor-icons/react` (components are suffixed `Icon`, e.g.
Expand Down
117 changes: 117 additions & 0 deletions .agents/skills/update-component/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
name: update-component
description: >-
Merge upstream shadcn or Spartan changes into an already-vendored Curve
component without overwriting local design or accessibility edits. Use when
updating, upgrading, re-vendoring, re-adding, or refreshing an existing
component; when running shadcn add or ng g on a component that is already in
the repo; or when preserving CURVE customizations against upstream.
---

# Update a component

Use this playbook for a component that **already lives in the repo**. For a
component that is not vendored yet, use [`add-component`](../add-component/SKILL.md)
instead.

- **React only** → [`react.md`](react.md) (`@surfnet/curve-react` / shadcn).
- **Angular only** → [`angular.md`](angular.md) (`@surfnet/curve-angular` / Spartan).
- **Both** → do each playbook, then keep variants, sizes, stories, and a11y
behaviour in parity.

**Never treat a re-vendor as an install.** The CLIs copy source into the package;
running them again on an existing component can wipe Curve edits. Git is the
source of truth. Diff, then merge.

## Accessibility

**Changes can have an effect on accessibility.** Styling, markup, ARIA, focus
order, contrast, and keyboard behaviour are coupled. A visual tweak (hiding a
label, restyling a focus ring, swapping an icon-only control, changing
hover-only state) can make the component unusable with a keyboard or screen
reader. An upstream merge can silently drop a Curve a11y fix.

Before you finish any update or local edit:

1. Re-read the component as assistive tech would: name, role, state, keyboard
path, visible focus, contrast.
2. Check the Storybook **Accessibility** addon (`@storybook/addon-a11y`) on the
Playground and on state stories (disabled, invalid, open, icon-only).
3. Keep or update story tags: `a11y-gap` (known WCAG gap) and `a11y-minor`
(smaller concern). They come from the shared config in
`@surfnet/curve-storybook-config`. If you **fix** an issue, remove or
downgrade the tag. If you **confirm** or **introduce** one, add the matching
tag. Do not drop these tags during a merge “to match upstream.”
4. Prefer putting durable a11y fixes in markup / ARIA / Brain-adjacent helm
behaviour, and covering them with a story or test so a later re-vendor
fails loudly.

## Where to put the change

| Kind of change | Put it here | Survives a re-vendor? |
| --- | --- | --- |
| Color, radius, type, spacing | Tokens (`packages/tokens`) + `@theme` mapping | Yes |
| Variant / size / axis **names** | `@surfnet/curve-contracts` + `satisfies` on both frameworks | Compile error if upstream reintroduces a stray name |
| Extra look on top of upstream | Compose via `cn()` / `hlm()`, keep the extra classes Curve-owned | Only if you re-apply them in the merge |
| Missing `aria-*`, roles, focus, names, keyboard | Vendored TS / templates (helm, not Brain) | No — mark with `CURVE:` and cover with a story/test |
| Headless behaviour (Angular) | Stay on `@spartan-ng/brain` via npm. Do **not** edit `node_modules`. Compensate in helm or wrap. | Brain updates with `pnpm update` + `healthcheck` |

Design that only exists as “we rewrote 40 Tailwind classes in the vendored
file” will fight every upstream update. Design that lives in tokens will not.

## Mark Curve deviations

Every intentional difference from upstream gets a durable marker next to the
code, so a `--diff` / `git diff` review can keep it:

```ts
// CURVE: a11y — icon-only buttons need an accessible name (do not drop on re-vendor)
// CURVE: design — focus ring uses ring-offset-background to match tokens
```

Use `a11y` or `design` (or both) and a short why. Same idea in `cva` maps and
Angular templates (`<!-- CURVE: a11y — … -->`).

Audit markers with:

```bash
rg "CURVE:" packages/react/src/components/ui packages/angular/src/lib/ui
```

Do **not** add a `CURVE:` comment for contract `satisfies` wiring, barrels,
stories, or `fix-helm-imports` rewrites — those are standard Curve scaffolding,
not deviations.

## Hard rules (agents and humans)

1. **Do not** run `shadcn add --overwrite`, `shadcn add --all`, or
`ng g @spartan-ng/cli:ui` on an existing component as a refresh. Follow the
per-framework playbook.
2. **Do not** use the shadcn MCP `add` tool to update an existing component — it
writes files flat and skips the `--path` layout.
3. Preserve every `CURVE:` hunk unless the user explicitly drops that
customization. Preserve contract `satisfies` / `*Name` typing.
4. After Angular helm is regenerated, run
`pnpm --filter @surfnet/curve-angular fix-helm-imports` and confirm `dist`
has no `@spartan-ng/helm` imports.
5. Stop and ask if the upstream diff is large, renames the public API, or
disagrees with the contract.

## After both frameworks (parity)

If the component exists in both packages, confirm after the merge:

- Variant / size / axis **names** still match the contract (`pnpm lint`).
- Story names still match across Storybooks.
- A11y behaviour matches: the same issue is not fixed on one side and left
tagged `a11y-gap` on the other without a documented reason.

## Definition of done

- Upstream hunks merged; Curve `CURVE:` edits and contract wiring kept.
- Story `a11y-gap` / `a11y-minor` tags match the current state.
- Storybook a11y addon checked for the stories you touched.
- `pnpm build`, `pnpm lint`, and `pnpm format` pass for the packages you
touched; rebuild Storybook if stories changed.
- A changeset added (`pnpm changeset`) if a publishable package changed.
- Any React/Angular gap left after the update is documented, not hidden.
124 changes: 124 additions & 0 deletions .agents/skills/update-component/angular.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Update an Angular component (`@surfnet/curve-angular`)

Part of the **update-component** skill — see [`SKILL.md`](SKILL.md) for shared
rules, `CURVE:` markers, and accessibility.

Spartan splits each component in two:

| Layer | Where | How it updates |
| --- | --- | --- |
| **Brain** | `@spartan-ng/brain/<name>` in `node_modules` | `pnpm update` + `ng g @spartan-ng/cli:healthcheck --autoFix` |
| **Helm** | copied into `src/lib/ui/<name>/` | You own it. Re-running the `ui` generator **overwrites** helm. There is no `--diff`. |

Do **not** edit Brain sources. Put accessibility and design fixes in helm (or a
wrapper). If the bug is in Brain itself, compensate in helm and document the
gap rather than patching `node_modules`.

## Brain (npm)

After bumping `@spartan-ng/brain` / `@spartan-ng/cli`:

```bash
cd packages/angular
pnpm exec ng g @spartan-ng/cli:healthcheck
pnpm exec ng g @spartan-ng/cli:healthcheck --autoFix
```

Healthcheck reconciles deprecated Brain APIs. It is not a helm merge. Review
the diff; keep `CURVE:` helm edits.

## Helm (copied source)

The `ui` generator copies helm into `componentsPath` and may also pull
**dependent** components. Treat every already-vendored directory it would
touch as in-scope.

### Steps

1. **Confirm it is already vendored.** If `src/lib/ui/<name>/` does not exist,
stop and use [`add-component/angular.md`](../add-component/angular.md).

2. **See what is installed:**

```bash
cd packages/angular
pnpm exec ng g @spartan-ng/cli:info --json
```

3. **Working tree must be clean** for the helm dirs you are about to
regenerate (commit or stash Curve work first). You need `HEAD` as “ours.”

4. **Regenerate, then immediately inspect — do not commit the dump:**

```bash
cd packages/angular
pnpm exec ng g @spartan-ng/cli:ui <name> --defaults
git diff -- packages/angular/src/lib/ui/<name>/
```

`git diff` is **ours (HEAD, Curve) vs theirs (new helm)**. Dependent
components the generator also copied show up in the same diff — merge those
too, or restore them with `git checkout -- <path>` if they were collateral.

5. **Restore ours and merge by hand** (do not leave the generator output as
the new source):

```bash
# keep a copy of theirs, then restore Curve
cp -R src/lib/ui/<name> /tmp/curve-helm-<name>-theirs
git checkout -- src/lib/ui/<name>
```

Apply upstream hunks from `/tmp/curve-helm-<name>-theirs` onto the restored
Curve files. Keep every `CURVE:` hunk, contract `satisfies` / typed inputs,
story files, and Phosphor / `NgIcon` conventions.

6. **Always re-run import rewrite** after any generator write:

```bash
pnpm --filter @surfnet/curve-angular fix-helm-imports
```

Confirm the published bundle will not leak the alias:

```bash
pnpm --filter @surfnet/curve-angular build
grep -r "@spartan-ng/helm" packages/angular/dist
```

(`grep` must print nothing.)

7. **Re-apply Curve scaffolding** the generator does not know about:

- Contract import + `satisfies Record<*Name, string>` (or typed inputs).
- Export from `src/public-api.ts`.
- New runtime deps in `ng-package.json` → `allowedNonPeerDependencies`.
- Stories still source docs from the contract. Do not let the generator
delete `*.stories.ts`.

8. **Accessibility pass** — see [SKILL.md](SKILL.md#accessibility). Check
`@storybook/addon-a11y`. Update `a11y-gap` / `a11y-minor` tags if the merge
fixed, confirmed, or reintroduced an issue. Put back any dropped
`CURVE: a11y` hunks.

There is no approved `--overwrite` equivalent. If the user wants stock Spartan
helm back, say that explicitly, take the generator output, then still run
`fix-helm-imports` and re-apply contract wiring / stories / a11y markers.

## Verify

```bash
pnpm --filter @surfnet/curve-contracts lint
pnpm --filter @surfnet/curve-angular build
pnpm format
```

Rebuild Storybook if stories changed:
`pnpm --filter @surfnet/curve-angular build-storybook`.

## Notes

- Do not re-vendor `hlm-icon`. Phosphor + `NgIcon` sizing is a Curve
convention; see [add-component/angular.md](../add-component/angular.md).
- Theme CSS lives in `@surfnet/curve-tokens`, not in a `ui-theme` regenerate.
Do not run `ui-theme` as part of a component update.
84 changes: 84 additions & 0 deletions .agents/skills/update-component/react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Update a React component (`@surfnet/curve-react`)

Part of the **update-component** skill — see [`SKILL.md`](SKILL.md) for shared
rules, `CURVE:` markers, and accessibility.

Components are vendored with the shadcn CLI into
`packages/react/src/components/ui/<name>/`. Updating means a **3-way-style
merge** of upstream registry source into that folder, not a re-install.

Use the CLI from `packages/react` so `components.json` (Base UI, Phosphor,
`--path` layout) applies. **Never fetch raw files from GitHub** — the CLI
resolves the registry and CSS.

## Steps

1. **Confirm it is already vendored.** If `src/components/ui/<name>/` does not
exist, stop and use [`add-component/react.md`](../add-component/react.md).

2. **List what the CLI would touch** (no writes):

```bash
cd packages/react
pnpm dlx shadcn@latest add <name> --dry-run
```

Note sibling files (the CLI may want to update dependencies of this
component). Each already-vendored sibling needs the same merge treatment —
do not overwrite them as collateral.

3. **Diff each affected file against the local copy:**

```bash
pnpm dlx shadcn@latest add <name> --diff
pnpm dlx shadcn@latest add <name> --diff <file>
```

`--diff` implies `--dry-run`. Prefer `--diff` on a specific file when the
combined output is truncated.

4. **Merge per file:**

| Diff | Action |
| --- | --- |
| No local Curve edits (file matches a stock vendor copy aside from standard scaffolding) | Safe to take upstream, then **re-apply** contract `satisfies` / `*Name` typing, the barrel, and story docs sourcing. |
| Has `CURVE:` markers or other local edits | Keep those hunks. Apply upstream hunks around them by editing the local file. |
| User explicitly wants to discard Curve customizations | Only then `pnpm dlx shadcn@latest add <name> --overwrite`, and say so. Re-apply contract wiring, barrel, and stories afterwards. |

**Never pass `--overwrite` without explicit approval.**

5. **Re-apply Curve scaffolding the CLI does not know about:**

- Contract import + `satisfies Record<*Name, string>` (or typed inline-union
props) from `@surfnet/curve-contracts`.
- `index.ts` barrel and `src/index.ts` export (do not flatten the directory).
- Story still sources description / axes from the contract object.
- Phosphor icons (`*Icon` from `@phosphor-icons/react`), not whatever
upstream defaulted to.

6. **Accessibility pass** — see [SKILL.md](SKILL.md#accessibility). Check
`@storybook/addon-a11y` on the component stories. Update `a11y-gap` /
`a11y-minor` tags if the merge fixed, confirmed, or reintroduced an issue.
If upstream dropped a `CURVE: a11y` hunk, put it back.

## Verify

```bash
pnpm --filter @surfnet/curve-contracts lint
pnpm --filter @surfnet/curve-react lint
pnpm --filter @surfnet/curve-react build
pnpm format
```

Rebuild Storybook if stories changed:
`pnpm --filter @surfnet/curve-react build-storybook`.

## Notes

- The published `styles.css` is compiled Tailwind. Consumers do not need to
re-run shadcn; only this package's source merge changes what they get after
the next Curve release.
- CSS entry updates (`src/index.css`, `@theme`) from `--diff globals.css` /
the dry-run CSS section: take token mappings only when they are new Curve
needs. **Do not** paste shadcn's `:root` / `.dark` blocks — tokens come from
`@surfnet/curve-tokens`.
6 changes: 6 additions & 0 deletions .changeset/badge-link-primary-strong.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@surfnet/curve-react': patch
'@surfnet/curve-angular': patch
---

Use `--primary-strong` for link-style text (Badge `link`, and inline links in Field, Empty, and Item) so it meets contrast on the page background. Filled Primary is unchanged.
6 changes: 6 additions & 0 deletions .changeset/combobox-trigger-aria.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@surfnet/curve-angular': patch
'@surfnet/curve-react': patch
---

Wire combobox input trigger button `aria-expanded`, `aria-haspopup="listbox"`, and `aria-controls` to the listbox id. Add default `aria-label="Clear"` on the React clear button.
5 changes: 5 additions & 0 deletions .changeset/command-accessible-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@surfnet/curve-react': patch
---

Give Command a default accessible label ("Type a command or search...") so cmdk no longer renders an empty visually hidden label.
6 changes: 6 additions & 0 deletions .changeset/command-empty-live-region.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@surfnet/curve-angular': patch
'@surfnet/curve-react': patch
---

Keep Command empty state mounted in the DOM and announce it with `aria-live="polite"` and `aria-atomic`. Use `role="option"` (`aria-disabled`, `aria-selected="false"`) so the empty message is a valid listbox child, and mark separators as presentational so listboxes only expose `option` / `group` children.
Loading
Loading