From e669d1609e62dcd7c84fed5112f348bf374dee93 Mon Sep 17 00:00:00 2001 From: Anneke Sinnema Date: Tue, 6 Oct 2026 13:43:01 +0200 Subject: [PATCH 1/5] feat: fix parity between frameworks, fix color Signed-off-by: Anneke Sinnema --- .../lib/ui/radio-group/src/lib/hlm-radio-group.css | 14 ++++++++++++-- .../ui/radio-group/radio-group.module.css | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css index 44b76a91..cf0ee5b2 100644 --- a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css +++ b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css @@ -55,12 +55,22 @@ } .curve-radio-indicator-dot { + position: absolute; + top: 50%; + left: 50%; width: calc(var(--spacing, 0.25rem) * 2); height: calc(var(--spacing, 0.25rem) * 2); - border-radius: calc(infinity * 1px); + border-radius: 9999px; + transform: translate(-50%, -50%); background-color: transparent; } -.curve-radio[data-checked='true'] .curve-radio-indicator-dot { +.brn-radio-checked .curve-radio-indicator { + border-color: var(--primary); background-color: var(--primary); + color: var(--primary-foreground); +} + +.curve-radio[data-checked='true'] .curve-radio-indicator-dot { + background-color: var(--background); } diff --git a/packages/react/src/components/ui/radio-group/radio-group.module.css b/packages/react/src/components/ui/radio-group/radio-group.module.css index 0015fa7e..3080751c 100644 --- a/packages/react/src/components/ui/radio-group/radio-group.module.css +++ b/packages/react/src/components/ui/radio-group/radio-group.module.css @@ -78,6 +78,6 @@ width: calc(var(--spacing, 0.25rem) * 2); height: calc(var(--spacing, 0.25rem) * 2); border-radius: 9999px; - background-color: var(--primary-foreground); + background-color: var(--background); transform: translate(-50%, -50%); } From 7107538eb83929629ff9739eca01d89122ffe8fa Mon Sep 17 00:00:00 2001 From: Anneke Sinnema Date: Wed, 7 Oct 2026 12:13:44 +0200 Subject: [PATCH 2/5] Add the "choice card" story Signed-off-by: Anneke Sinnema --- .../src/lib/hlm-radio-group.stories.ts | 47 ++++++++++++++++++ .../ui/radio-group/radio-group.stories.tsx | 48 ++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts index 9aa68567..6ffdc5f4 100644 --- a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts +++ b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts @@ -104,6 +104,53 @@ export const InFieldset: Story = { }), }; +/** + * Choice cards: wrap each option's `hlmField` in a `label[hlmFieldLabel]` and the whole + * label becomes a selectable card. The selected card gets a primary-tinted border and + * background. + */ +export const ChoiceCard: Story = { + render: () => ({ + template: ` + + + + + + `, + }), +}; + /** Disabling a single item versus disabling the whole group. */ export const Disabled: Story = { render: () => ({ diff --git a/packages/react/src/components/ui/radio-group/radio-group.stories.tsx b/packages/react/src/components/ui/radio-group/radio-group.stories.tsx index 645ed6f6..cc8b8984 100644 --- a/packages/react/src/components/ui/radio-group/radio-group.stories.tsx +++ b/packages/react/src/components/ui/radio-group/radio-group.stories.tsx @@ -2,7 +2,15 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { radioGroupContract } from '@surfnet/curve-contracts'; -import { FieldDescription, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'; +import { + Field, + FieldContent, + FieldDescription, + FieldLabel, + FieldLegend, + FieldSet, + FieldTitle, +} from '@/components/ui/field'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from './radio-group'; @@ -83,6 +91,44 @@ export const InFieldset: Story = { ), }; +/** + * Choice cards: wrap each option's `Field` in a `FieldLabel` and the whole label becomes a + * selectable card. The selected card gets a primary-tinted border and background. + */ +export const ChoiceCard: Story = { + render: () => ( + + + + + Plus + For individuals and small teams. + + + + + + + + Pro + For growing businesses. + + + + + + + + Enterprise + For large teams and enterprises. + + + + + + ), +}; + /** Disabling a single item versus disabling the whole group. */ export const Disabled: Story = { render: () => ( From 9b9592cea192ec280ec283d732e97c48dc407e8f Mon Sep 17 00:00:00 2001 From: Anneke Sinnema Date: Wed, 7 Oct 2026 12:20:14 +0200 Subject: [PATCH 3/5] Added styling and changeset file Signed-off-by: Anneke Sinnema --- .changeset/radio-group-choice-card.md | 5 +++ .../src/components/ui/field/field.module.css | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 .changeset/radio-group-choice-card.md diff --git a/.changeset/radio-group-choice-card.md b/.changeset/radio-group-choice-card.md new file mode 100644 index 00000000..1176af16 --- /dev/null +++ b/.changeset/radio-group-choice-card.md @@ -0,0 +1,5 @@ +--- +'@surfnet/curve-react': minor +--- + +`FieldLabel` now becomes a selectable "choice card" when it wraps a `Field` (bordered, with a primary tint when the control inside is checked), matching `@surfnet/curve-angular`. Horizontal `Field`s with `FieldContent` now top-align their checkbox or radio. See the new RadioGroup **ChoiceCard** story. diff --git a/packages/react/src/components/ui/field/field.module.css b/packages/react/src/components/ui/field/field.module.css index ed883eaf..ae78654c 100644 --- a/packages/react/src/components/ui/field/field.module.css +++ b/packages/react/src/components/ui/field/field.module.css @@ -59,6 +59,15 @@ align-items: center; } +.field[data-orientation='horizontal']:has(> [data-slot='field-content']) { + align-items: flex-start; +} + +.field[data-orientation='horizontal']:has(> [data-slot='field-content']) > [role='checkbox'], +.field[data-orientation='horizontal']:has(> [data-slot='field-content']) [role='radio'] { + margin-top: 1px; +} + .fieldContent { display: flex; flex: 1; @@ -74,6 +83,29 @@ line-height: 1.375; } +.fieldLabel:has(:is([data-state='checked'], [data-checked]:not([data-checked='false']))) { + border-color: color-mix(in oklab, var(--primary) 30%, transparent); + background-color: color-mix(in oklab, var(--primary) 5%, transparent); +} + +:global(.dark) + .fieldLabel:has(:is([data-state='checked'], [data-checked]:not([data-checked='false']))) { + border-color: color-mix(in oklab, var(--primary) 20%, transparent); + background-color: color-mix(in oklab, var(--primary) 10%, transparent); +} + +/* A label wrapping a whole field becomes a selectable card. */ +.fieldLabel:has(> [data-slot='field']) { + width: 100%; + flex-direction: column; + border: 1px solid var(--border); + border-radius: var(--radius-md, calc(var(--radius, 0.625rem) * 0.8)); +} + +.fieldLabel > [data-slot='field'] { + padding: calc(var(--spacing, 0.25rem) * 3); +} + .fieldTitle { display: flex; width: fit-content; From 02b63c44570b85f66b195bbb414417c630472052 Mon Sep 17 00:00:00 2001 From: Anneke Sinnema Date: Wed, 7 Oct 2026 12:22:34 +0200 Subject: [PATCH 4/5] Update CSS variable used for bordercolor of fieldlabel Signed-off-by: Anneke Sinnema --- packages/angular/src/lib/ui/field/src/lib/hlm-field.css | 2 +- packages/react/src/components/ui/field/field.module.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field.css b/packages/angular/src/lib/ui/field/src/lib/hlm-field.css index 83ef965e..b9a4abfb 100644 --- a/packages/angular/src/lib/ui/field/src/lib/hlm-field.css +++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field.css @@ -115,7 +115,7 @@ .curve-field-label:has(> [data-slot='field']) { width: 100%; flex-direction: column; - border: 1px solid var(--border); + border: 1px solid var(--input); border-radius: var(--radius-md, calc(var(--radius, 0.625rem) * 0.8)); } diff --git a/packages/react/src/components/ui/field/field.module.css b/packages/react/src/components/ui/field/field.module.css index ae78654c..7c946f25 100644 --- a/packages/react/src/components/ui/field/field.module.css +++ b/packages/react/src/components/ui/field/field.module.css @@ -98,7 +98,7 @@ .fieldLabel:has(> [data-slot='field']) { width: 100%; flex-direction: column; - border: 1px solid var(--border); + border: 1px solid var(--input); border-radius: var(--radius-md, calc(var(--radius, 0.625rem) * 0.8)); } From 6ba6e721736c39dde6159efed212d964491a03fd Mon Sep 17 00:00:00 2001 From: Anneke Sinnema Date: Wed, 7 Oct 2026 15:17:35 +0200 Subject: [PATCH 5/5] Fix accessibility (faulty role), add label Signed-off-by: Anneke Sinnema --- .changeset/field-drop-group-role.md | 6 ++ .../src/lib/ui/field/src/lib/hlm-field.ts | 3 +- .../src/lib/hlm-radio-group.stories.ts | 84 +++++++++++-------- .../react/src/components/ui/field/field.tsx | 3 +- .../ui/radio-group/radio-group.stories.tsx | 83 +++++++++++------- 5 files changed, 114 insertions(+), 65 deletions(-) create mode 100644 .changeset/field-drop-group-role.md diff --git a/.changeset/field-drop-group-role.md b/.changeset/field-drop-group-role.md new file mode 100644 index 00000000..97d61aab --- /dev/null +++ b/.changeset/field-drop-group-role.md @@ -0,0 +1,6 @@ +--- +'@surfnet/curve-react': patch +'@surfnet/curve-angular': patch +--- + +`Field` / `hlmField` no longer renders `role="group"`. A single field isn't a group, and inside a choice-card label the role caused the radio or checkbox to lose its accessible name (axe "Form elements must have labels"). To group related fields, use `FieldSet` / `fieldset[hlmFieldSet]`. diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts index df20cbd6..d186b8a8 100644 --- a/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts +++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts @@ -14,8 +14,9 @@ export type FieldVariants = { orientation?: FieldOrientationName | null }; @Directive({ selector: '[hlmField],hlm-field', hostDirectives: [{ directive: BrnField, inputs: ['data-invalid', 'forceInvalid'] }], + // CURVE: a11y — no role="group" (upstream adds it): a single field isn't a group, and inside a + // choice-card label it blanks the label's text for axe. Group fields with hlmFieldSet instead. host: { - role: 'group', 'data-slot': 'field', '[attr.data-orientation]': 'orientation()', }, diff --git a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts index 6ffdc5f4..a4e6ab3e 100644 --- a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts +++ b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts @@ -107,46 +107,64 @@ export const InFieldset: Story = { /** * Choice cards: wrap each option's `hlmField` in a `label[hlmFieldLabel]` and the whole * label becomes a selectable card. The selected card gets a primary-tinted border and - * background. + * background. The group is named by a visible `label[hlmLabel]` via `aria-labelledby`. */ export const ChoiceCard: Story = { render: () => ({ template: ` - -