Skip to content

Add missing components to close the Figma component library gap#99

Open
sjoerdbeentjes wants to merge 11 commits into
mainfrom
feat/additional-components
Open

Add missing components to close the Figma component library gap#99
sjoerdbeentjes wants to merge 11 commits into
mainfrom
feat/additional-components

Conversation

@sjoerdbeentjes

@sjoerdbeentjes sjoerdbeentjes commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes the gap identified against the Figma component library (16 present / 41 missing) by adding components to both @surfnet/curve-react and @surfnet/curve-angular.

  • Added to both frameworks: accordion, alert, alert-dialog, aspect-ratio, badge, button-group, calendar, carousel, combobox, command, context-menu, date-picker, dialog, empty, hover-card, input-otp, item, kbd, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, sheet, skeleton, slider, sonner, spinner, switch, tabs, toggle, toggle-group, tooltip
  • Deferred (no Spartan/Angular equivalent yet): chart and drawer — tracked in ADR-019 (docs/decision-log.md) rather than shipped React-only, so we don't quietly break framework parity
  • Excluded (per scope decision, also tracked in ADR-019): Login and Top navigation (custom "maatwerk" flows, not vendorable primitives) and Typography (not a real shadcn/Spartan registry component)
  • sheet, skeleton, and tooltip existed internally in @surfnet/curve-angular (as Sidebar dependencies) but weren't exported — they're now part of the public API

Every component has a @surfnet/curve-contracts entry (description-only, or with a variant/size/orientation axis enforced via satisfies Record<...> on both frameworks) and a Storybook story covering its full surface. Along the way, also de-duplicated sibling files the shadcn CLI vendors redundantly into new component folders (now reusing the existing top-level button/input/separator/etc.), and replaced Spartan's default Lucide icons with Phosphor across the newly vendored Angular files to match this repo's existing icon convention.

A couple of framework-surface gaps are documented in code rather than papered over: Angular's scroll-area (wraps ngx-scrollbar, which auto-detects orientation) has no matching orientation control, and Angular's vendored navigation-menu is missing align/navOffset inputs that upstream Spartan docs now expose.

Test plan

  • pnpm --filter @surfnet/curve-contracts lint / build
  • pnpm --filter @surfnet/curve-react lint / build
  • pnpm --filter @surfnet/curve-angular lint / build
  • pnpm format
  • build-storybook for both packages
  • Manually verified in the browser: Dialog, the hand-built DatePicker (open/select flow), Badge variants, and Angular Alert
  • Changeset added (minor bump for both packages)

Adds a @surfnet/curve-contracts entry for every component in this batch
(description-only where there's no variant/size/orientation axis), so both
frameworks have a shared source of truth for names and docs before vendoring.
Vendors accordion, alert, alert-dialog, aspect-ratio, badge, button-group,
calendar, carousel, chart, combobox, command, context-menu, date-picker,
dialog, drawer, empty, hover-card, input-otp, item, kbd, native-select,
navigation-menu, pagination, popover, progress, radio-group, resizable,
scroll-area, sheet, skeleton, slider, sonner, spinner, switch, tabs, toggle,
toggle-group, and tooltip via the shadcn CLI. date-picker is hand-composed
from Calendar + Popover since shadcn has no registry item for it.

Each component is wired to its @surfnet/curve-contracts entry, ships its own
barrel and Storybook story, and is exported from the package entry. Also
de-duplicates sibling files (button, input, textarea, input-group, separator,
dialog, toggle) that the CLI vendored redundantly into new component folders
instead of reusing the existing top-level ones, and promotes sidebar's
internal sheet/skeleton/tooltip copies to the new top-level components.
Vendors accordion, alert, alert-dialog, aspect-ratio, badge, button-group,
calendar, carousel, combobox, command, context-menu, date-picker, dialog,
empty, hover-card, input-otp, item, kbd, native-select, navigation-menu,
pagination, popover, progress, radio-group, resizable, scroll-area, slider,
sonner, spinner, switch, tabs, toggle, and toggle-group via the Spartan CLI,
plus stories for the already-vendored-but-unexported sheet, skeleton, and
tooltip (previously internal Sidebar dependencies, now part of the public
API). No Spartan equivalent exists yet for chart or drawer.

Each component is wired to its @surfnet/curve-contracts entry and exported
from public-api.ts. Replaces the Spartan CLI's default Lucide icons with
Phosphor across the newly vendored files to match this package's existing
icon convention, and drops the now-unused @ng-icons/lucide dependency.
Minor bump for @surfnet/curve-react and @surfnet/curve-angular.
Neither has a Spartan counterpart — chart has no generator at all, and
Spartan's closest primitive to drawer is sheet, a different pattern already
vendored as its own component. Keeping a React-only component breaks the
in-parity contract this design system is built on (ADR-016); see ADR-019 for
the follow-up plan.
Records why chart and drawer were removed (no Spartan equivalent) and tracks
the components still fully out of scope from the original gap analysis —
login, top navigation, and typography — so the follow-up work doesn't get
lost.
@sjoerdbeentjes sjoerdbeentjes changed the title Add 38 missing components to close the Figma component library gap Add 36 missing components to close the Figma component library gap Jul 13, 2026
@sjoerdbeentjes sjoerdbeentjes changed the title Add 36 missing components to close the Figma component library gap Add missing components to close the Figma component library gap Jul 14, 2026
…nt set

The demo app's initial bundle now sits at ~1.08MB after this PR adds 36
components to @surfnet/curve-angular, tripping the CLI's 1MB hard budget in
CI. Raise it to 1.5MB (warn at 1MB, was 500kB) to give headroom as the
design system keeps growing, while still catching genuine bloat regressions.
…nents

Restore the `'use client'` directive on the 33 newly vendored React
component files that shadcn ships it on but the vendoring stripped. The
library build preserves directives per module, so hook/context users
(calendar, carousel, input-otp, sonner, ...) threw when imported into a
Next.js server component.

Close four contract-axis gaps where a real styled axis lived only in the
component, not the shared @surfnet/curve-contracts spec:

- context-menu item `variant` (ContextMenuItemVariantName)
- alert-dialog content `size` (AlertDialogSizeName)
- toggle-group `orientation` (ToggleGroupOrientationName)
- sheet content `side` (SheetSideName)

Each prop is now typed from the contract in both frameworks (sheet's
Angular side stays Brain-delegated, matching slider/resizable), so a
stray value fails the type-check instead of drifting silently.
@sjoerdbeentjes
sjoerdbeentjes requested review from loofpc and oharsta July 14, 2026 11:44
@sjoerdbeentjes sjoerdbeentjes linked an issue Jul 14, 2026 that may be closed by this pull request
@@ -0,0 +1,7 @@
import { defineContract } from './define-contract.js';

export const kbdContract = defineContract({

@loofpc loofpc Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestie: keyboard gewoon uitschrijven i.p.v. afkorten naar kdb in de bestandsnaam en contractnaam want dat is niet een algemeen bekende afkorting. Vooral ook toepassen in naam van storybook component en selector die consumers uiteindelijk afnemen. Of misschien beter KeyboardKey.

@@ -0,0 +1,7 @@
import { defineContract } from './define-contract.js';

export const inputOtpContract = defineContract({

@loofpc loofpc Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestie: gebruik een volledig uitgeschreven term i.p.v. de afkorting otp om duidelijker te maken wat dit is. Belangrijkst is vooral hoe we de selector noemen die consumers afnemen en hoe de component heet in storybook.
Wel lastig om een duidelijke term te vinden die ook kort is. Het wordt typisch gebruikt voor 2FA maar zou ook werken voor andere toepassingen, dus de naam hoeft niet verplicht zo specifiek te zijn. Ideeën voor component-naam: InputCode, InputPassCode, InputSecurityCode, InputCharBoxes, ...

export const Default: Story = {
render: (args) => ({
props: args,
template: `

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik weet niet of het bewust is, maar je ziet altijd 6 boxes, ook als je met de interactieve control de lengte aanpast. Een kleiner instelbare lengte zorgt alleen dat hij de extra boxes leeg laat. Ik had verwacht dat het aantal boxes mee zou aanpassen.

description: 'Whether one or multiple items can be expanded at the same time.',
table: { defaultValue: { summary: 'single' } },
},
orientation: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In storybook lijkt de orientatie niks te doen, en de story 'Horizontal' lijkt ook precies hetzelfde als de default.

type Story = StoryObj<HlmCarousel>;

/** Interactive playground — flip `orientation` in the controls to see both directions. */
export const Default: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

De knoppen van de carousel staan vlak naast elkaar in Angular, in plaats van links/rechts of boven/onder de items. In React ziet het er een stuk beter uit.

};

/** Checkbox items for toggling options. */
export const WithCheckboxItems: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithCheckboxItems en WithRadioItems toont een foutmelding als je rechts klikt, alleen in React storybook.

type Story = StoryObj<typeof meta>;

/** A single-date picker — click the trigger to open the calendar popover. */
export const Default: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storybook toont 4 controls die allemaal niet instelbaar zijn.

* `showCloseButton={false}` on `DialogContent` hides the corner close icon; pair it with
* `DialogFooter`'s own `showCloseButton` to give the user a single, explicit way to dismiss.
*/
export const WithoutCloseButton: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deze toont toch wel een close button. Bij Angular werkt het wel zoals verwacht.

* which collides with Storybook's own `render` story field when spread through
* `{...args}`, so this story hardcodes its props instead.
*/
export const Default: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hier staan drie instelbare parameters in storybook die je niet kunt instellen.

@@ -0,0 +1,19 @@
import { defineContract } from './define-contract.js';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er is niet veel verschil tussen de gewone Select en NativeSelect en dan heb je daarnaast ook nog de Combobox, die heel anders heet maar eigenlijk een geavanceerdere select is of een multiselect. Als je dit allemaal ontsluit dan vraagt dat om verwarring en/of inconsistentie bij de afnemers.

Wellicht een idee voor op de backlog dat Curve de beschikbare opties enigszins inperkt en op versimpelde manier aanbiedt. Bijvoorbeeld één Select met een parameter of je input kan filteren of niet, en één MultiSelect. Het kan onderdeel zijn van de vervolgstory om te bepalen wat Curve wil aanbieden en waar je de splitsing wilt tussen aparte componenten of parametrisering.

};

/** The `align` prop controls where the popup is anchored relative to its trigger. */
export const Alignment: Story = {

@loofpc loofpc Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deze story toont alignment-opties en dat is niet aanwezig bij Angular. Bij Angular heb je weer een Vertical story die ik hier niet zie. Verschil in functionaliteit, en willen we die opties dan niet weghalen als ze niet door beide frameworks ondersteund worden?

Daarnaast wordt de title en description van elke nav-optie naast elkaar getoond bij de meeste story's van deze component. Bij Angular ziet het er netter uit omdat ze daar steeds boven elkaar staan. Verder heeft React pijltjes op de nav-buttons en Angular niet.

* The composed numbered-pagination block: page links, previous/next controls, and a
* page-size selector, driven by two-way `currentPage` / `itemsPerPage` bindings.
*/
export const NumberedPagination: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deze story toont opties die ik in React niet terug zie. Bij React is er in plaats van deze een Interactive story.

};

/** The popup can open on any side of the trigger, flipping to stay in view. */
export const Placement: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deze story/functionaliteit ontbreekt in Angular. Willen we dit dan wel ontsluiten voor React?

Functioneel zie ik weinig verschil met de Dialog component. Is het alleen dat deze door de knop getriggerd wordt en naast de knop verschijnt, terwijl de dialog algemeen midden in het scherm komt? Ik zou verwachten dat deze component alleen de trigger en locatie hoeft te regelen en dat de inhoud van de popup een dialog is of een gedeelde dialog-content o.i.d.

};

/** Several tasks at different points of completion, each labelled with its percentage. */
export const Values: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bij React is het percentage van deze story verkeerd uitgelijnd.

type Story = StoryObj<typeof meta>;

/** Two panels split side by side; tweak `orientation` via the controls. */
export const Default: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alle stories van React van deze component tonen een veel kleinere height dan die van Angular.

* `showCloseButton={false}` on `SheetContent` hides the corner close icon — pair it with an
* explicit `SheetClose` action in the footer so the user still has a way to dismiss.
*/
export const WithoutCloseButton: Story = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zowel bij Angular als React is de close-button er nog gewoon en werkt die ook.

@@ -0,0 +1,7 @@
import { defineContract } from './define-contract.js';

export const sonnerContract = defineContract({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik heb nog nooit van een sonner gehoord, zullen we het component gewoon Toast noemen of iets nog explicieter zoals Notification, BriefNotification, ToastMessage o.i.d.?

@@ -0,0 +1,19 @@
import { defineContract } from './define-contract.js';

export const switchContract = defineContract({

@loofpc loofpc Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nog een backlog-idee voor naamgeving: een toggle en switch klinkt voor mij als bijna hetzelfde en je kunt ze ook interchangeably gebruiken. Bij een kort woord of symbool wordt het vaak een button en bij een form field of langere tekst zet je een switch naast de tekst. Misschien kan de naamgeving van de componenten worden aangepast naar ToggleField/ToggleInput, ToggleButton en ToggleButtonGroup o.i.d. zodat afnemers beter zien wat de vergelijkbare opties zijn.

@@ -0,0 +1,8 @@
import { defineContract } from './define-contract.js';

export const tooltipContract = defineContract({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik zie veel overlap in functionaliteit tussen de HoverCard en Tooltip, de tooltip is eigenlijk een HoverCard met alleen (rich) tekst. Dus puntje voor de backlog om vanuit Curve te besluiten of dit niet als één gecombineerde component ontsloten moet worden.

Daarnaast zie ik ook heel veel overlap in verschillende vormen van popups. Buiten de logica van hoe het triggert (click of hover), locatie van een popup (bij element of centraal) en of het de achtergrond wel of niet blurred, is de popup zelf eigenlijk gewoon een Card die hergebruikt zou kunnen worden in ContextMenu, DropdownMenu, NavigationMenu, PopOver, HoverCard en Tooltip (en misschien zelfs in Combobox en Select). Dat zou ook veel kunnen helpen om consistente layout te krijgen (paddings, margins etc) tussen verschillende vormen van popups.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Component set compleet maken

3 participants