Skip to content

feat(ui): add field composition primitives - #629

Merged
zacjones93 merged 30 commits into
codex/series-event-movements-form-context-fixfrom
codex/ui-field-primitives
Jul 15, 2026
Merged

feat(ui): add field composition primitives#629
zacjones93 merged 30 commits into
codex/series-event-movements-form-context-fixfrom
codex/ui-field-primitives

Conversation

@zacjones93

@zacjones93 zacjones93 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add children-driven Field and FieldGroup compounds at the direct @repo/ui/field subpath
  • preserve Start and Crew import surfaces with identity-only adapters; migrate no feature routes in this slice
  • add exhaustive semantic/unit coverage and Storybook stories for default, described, invalid transition, disabled, slotted custom control, and fieldset group states
  • refresh the generated UI inventory and LAT boundary documentation

Stack

Validation

  • pnpm --filter @repo/ui test (55 tests)
  • pnpm --filter @repo/ui type-check
  • pnpm --filter @repo/ui build
  • Start and Crew type-checks
  • monorepo pre-push lint and type-check
  • Storybook static build
  • Storybook semantic contrast/a11y browser suite (2/2)
  • UI inventory freshness
  • page coverage freshness
  • lat check
  • GitNexus impact/detect-changes: low risk, zero affected processes

Summary by cubic

Added Field, FieldGroup, EmptyState, and Metric primitives in @repo/ui to standardize forms, empty states, and summary metrics, improve accessibility, and tighten mobile responsiveness; adopted across Crew and Start via thin re-exports.

  • New Features

    • Introduced @repo/ui/empty-state and @repo/ui/metric with Card/Root composition; Storybook showcases typical patterns.
    • Hardened @repo/ui/field (ignores empty fragment metadata) and composed FieldGroup for grouped controls.
    • Adopted shared primitives in settings pages, scaling/judge rotation dialogs, import previews, and organizer panels.
    • Preserved app import paths through thin re-exports; refreshed UI inventory docs and coverage evidence.
  • Bug Fixes

    • Improved ARIA and semantics: named navigation landmarks, fieldset/legend via FieldGroup, and preserved empty-state actions.
    • Contained mobile header navigation; fixed print/export table sizing.
    • Crew demo seed dates now derive from the event timezone across UTC/DST boundaries.
    • Stabilized admin sidebar hydration and heading hierarchy; aligned token routes and public forms with accessibility checks.

Written for commit d6316c1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added reusable Field and FieldGroup components for accessible form layouts.
    • Supports labels, descriptions, validation errors, disabled controls, custom controls, and fieldsets.
    • Added public UI library access and application-level integration.
  • Documentation

    • Updated UI library inventory and accessibility guidance.
    • Added Storybook examples covering common field states and compositions.
  • Tests

    • Added coverage for accessibility attributes, validation states, custom controls, stable IDs, and grouped fields.

@zacjones93

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds shared Field and FieldGroup components to @repo/ui, exposes them through app-local re-exports, and adds tests, Storybook stories, inventory updates, compatibility checks, and documentation for their accessibility and composition contracts.

Changes

Field component extraction

Layer / File(s) Summary
Shared Field and FieldGroup implementation
packages/ui/src/components/field.tsx, packages/ui/package.json
Implements context-based field and fieldset compounds with labels, controls, descriptions, errors, stable IDs, accessibility metadata, and slotted controls; adds the ./field package export.
App re-exports and inventory integration
apps/wodsmith-start/src/components/ui/field.tsx, apps/crew/src/components/ui/field.tsx, apps/wodsmith-start/scripts/..., apps/wodsmith-start/docs/...
Adds app-local re-exports, classifies field as a foundation primitive, and updates inventory counts, consumers, and migration text.
Storybook field scenarios
apps/wodsmith-start/src/components/ui/field.stories.tsx
Adds stories for descriptions, validation transitions, disabled and custom controls, and grouped checkbox fields.
Compatibility and contract validation
packages/ui/test/field.test.tsx, packages/ui/test/compatibility.test.ts, lat.md/ui-library.md
Tests composition, accessibility metadata, slotted controls, fieldset semantics, context errors, and cross-app exports; documents the corresponding contracts.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant FieldRoot
  participant FieldControl
  participant DescriptionError
  App->>FieldRoot: render field with id and metadata
  FieldRoot->>FieldControl: provide shared field context
  FieldControl->>DescriptionError: reference description and error ids
  DescriptionError-->>FieldControl: render accessible metadata
  FieldControl-->>App: render control with aria attributes
Loading

Poem

I’m a bunny with a field to share,
Labels and errors hop through the air.
A grouped little set, both neat and bright,
Stories and tests keep semantics right.
Squeak—stable IDs take flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding field composition primitives in the UI package.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/ui-field-primitives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/ui/src/components/field.tsx (3)

65-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Self-closing JSX with children passed via prop spread works but is unusual.

<div {...props} /> and <fieldset {...props} /> render children because children remains in ...props. This is valid React 19 behavior and confirmed by tests, but the self-closing syntax may confuse readers expecting explicit children. Consider adding a brief comment or rendering children explicitly for clarity.

Also applies to: 165-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/field.tsx` around lines 65 - 86, Clarify the
implicit children rendering in FieldRoot and the analogous fieldset component by
either rendering props.children explicitly inside the div/fieldset or adding a
brief comment explaining that children are preserved through the prop spread;
keep the existing FieldContext and prop behavior unchanged.

14-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer interface for Metadata per coding guidelines.

The guidelines for **/*.{ts,tsx} state to "prefer interfaces over types." Metadata is a simple object shape with no union or intersection, so it can be an interface. The other type aliases (FieldRootProps, FieldGroupRootProps, MetadataProps) use intersection/Omit and correctly remain as type.

♻️ Proposed refactor
-type Metadata = {
+interface Metadata {
   id: string
   description: ReactNode | undefined
   error: ReactNode | undefined
   descriptionId: string
   errorId: string
-}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/field.tsx` around lines 14 - 20, Change the simple
object-shaped Metadata declaration to an interface, keeping its existing
properties and types unchanged; leave FieldRootProps, FieldGroupRootProps, and
MetadataProps as type aliases because they use intersections or Omit.

Source: Coding guidelines


128-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting a shared Description/Error renderer to reduce duplication.

FieldDescription/FieldGroupDescription and FieldError/FieldGroupError are near-identical pairs differing only in the context hook call. A shared internal component that accepts pre-resolved metadata would eliminate ~30 lines of duplication while preserving the same behavior.

Also applies to: 206-235

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/field.tsx` around lines 128 - 157, Extract a
shared internal metadata renderer for the duplicated
FieldDescription/FieldGroupDescription and FieldError/FieldGroupError markup.
Have each component resolve its context-specific values via useField or the
group hook, then pass the description/error text, ID, className, and alert-role
requirement into the shared renderer while preserving current styling, props,
IDs, and null behavior.
apps/wodsmith-start/src/components/ui/field.stories.tsx (2)

181-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a play function to verify error and aria-invalid state.

InvalidGroup renders with an error but doesn't verify aria-invalid="true" or that the alert is visible. CompositeGroup has a play function, so adding one here would ensure the invalid group state is covered.

✨ Suggested play function
 export const InvalidGroup: Story = {
   render: () => (
     <FieldGroup.Root
       id="heat-days"
       description="At least one day is required."
       error="Select Saturday or Sunday."
       className="max-w-sm"
     >
       <FieldGroup.Legend>Competition days</FieldGroup.Legend>
       <FieldGroup.Description />
       <div className="space-y-2">
         <label
           htmlFor="heat-saturday"
           className="flex items-center gap-2 text-sm"
         >
           <Checkbox id="heat-saturday" /> Saturday
         </label>
         <label
           htmlFor="heat-sunday"
           className="flex items-center gap-2 text-sm"
         >
           <Checkbox id="heat-sunday" /> Sunday
         </label>
       </div>
       <FieldGroup.Error />
     </FieldGroup.Root>
   ),
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement)
+    const group = canvas.getByRole("group", {
+      name: "Competition days",
+    })
+    await expect(group).toHaveAccessibleDescription(
+      "At least one day is required. Select Saturday or Sunday.",
+    )
+    await expect(group).toHaveAttribute("aria-invalid", "true")
+    await expect(canvas.getByRole("alert")).toBeVisible()
+  },
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/wodsmith-start/src/components/ui/field.stories.tsx` around lines 181 -
208, Add a play function to the InvalidGroup story that queries the rendered
group and verifies it has aria-invalid="true", then confirms the error alert is
visible. Follow the existing CompositeGroup play-function pattern and use the
story's testing utilities.

40-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a play function to verify aria-describedby wiring.

The Default story asserts aria-describedby is absent, but WithDescription doesn't verify it's present and points to the correct description element. Adding a play function here would close the coverage loop.

✨ Suggested play function
 export const WithDescription: Story = {
   render: () => (
     <Field.Root
       id="team-name"
       description="This name appears on public leaderboards."
       className="max-w-sm"
     >
       <Field.Label>Team name</Field.Label>
       <Field.Control>
         <Input defaultValue="Downtown Strength" />
       </Field.Control>
       <Field.Description />
       <Field.Error />
     </Field.Root>
   ),
+  play: async ({ canvasElement }) => {
+    const input = within(canvasElement).getByRole("textbox", {
+      name: "Team name",
+    })
+    await expect(input).toHaveAccessibleDescription(
+      "This name appears on public leaderboards.",
+    )
+    await expect(input).not.toHaveAttribute("aria-invalid")
+  },
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/wodsmith-start/src/components/ui/field.stories.tsx` around lines 40 -
55, Add a play function to the WithDescription story that queries the rendered
input and description elements, then asserts the input’s aria-describedby
references the description element’s ID. Use the existing Field.Root,
Field.Control, Field.Description, and WithDescription symbols to keep the
accessibility wiring coverage consistent with the Default story.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/test/field.test.tsx`:
- Around line 18-63: Add exactly one `@lat`: comment immediately adjacent to the
test in the “Field composition” describe block, referencing
[[lat.md/ui-library.md#Field composition]]. Apply the same requirement to every
it(...) test in both describe blocks, using the appropriate spec section for
each test and never placing comments at file scope.

---

Nitpick comments:
In `@apps/wodsmith-start/src/components/ui/field.stories.tsx`:
- Around line 181-208: Add a play function to the InvalidGroup story that
queries the rendered group and verifies it has aria-invalid="true", then
confirms the error alert is visible. Follow the existing CompositeGroup
play-function pattern and use the story's testing utilities.
- Around line 40-55: Add a play function to the WithDescription story that
queries the rendered input and description elements, then asserts the input’s
aria-describedby references the description element’s ID. Use the existing
Field.Root, Field.Control, Field.Description, and WithDescription symbols to
keep the accessibility wiring coverage consistent with the Default story.

In `@packages/ui/src/components/field.tsx`:
- Around line 65-86: Clarify the implicit children rendering in FieldRoot and
the analogous fieldset component by either rendering props.children explicitly
inside the div/fieldset or adding a brief comment explaining that children are
preserved through the prop spread; keep the existing FieldContext and prop
behavior unchanged.
- Around line 14-20: Change the simple object-shaped Metadata declaration to an
interface, keeping its existing properties and types unchanged; leave
FieldRootProps, FieldGroupRootProps, and MetadataProps as type aliases because
they use intersections or Omit.
- Around line 128-157: Extract a shared internal metadata renderer for the
duplicated FieldDescription/FieldGroupDescription and FieldError/FieldGroupError
markup. Have each component resolve its context-specific values via useField or
the group hook, then pass the description/error text, ID, className, and
alert-role requirement into the shared renderer while preserving current
styling, props, IDs, and null behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2501433c-5a92-4d8a-8b2c-500c271192cb

📥 Commits

Reviewing files that changed from the base of the PR and between aa3bc60 and 3d0693e.

📒 Files selected for processing (10)
  • apps/crew/src/components/ui/field.tsx
  • apps/wodsmith-start/docs/ui-library-inventory.md
  • apps/wodsmith-start/scripts/generate-ui-library-inventory.mjs
  • apps/wodsmith-start/src/components/ui/field.stories.tsx
  • apps/wodsmith-start/src/components/ui/field.tsx
  • lat.md/ui-library.md
  • packages/ui/package.json
  • packages/ui/src/components/field.tsx
  • packages/ui/test/compatibility.test.ts
  • packages/ui/test/field.test.tsx

Comment thread packages/ui/test/field.test.tsx

Copy link
Copy Markdown
Contributor Author

Review pinned to exact head 3d0693ec58edf708e948892ecf5968fdcfc4c987.

Findings:

  1. [P1] Field.Control metadata is overridden by slotted child props (packages/ui/src/components/field.tsx:102-123). Radix Slot merges ordinary props with child props winning. If the child supplies id, aria-describedby, or aria-invalid, it silently replaces the root id and computed description/error state. This breaks label association and can drop error help during the intended consumer migrations. Merge child and control described-by tokens explicitly, then force the root id/error state on the cloned control. Add a regression test with conflicting child props.

  2. [P2] Non-rendering ReactNode metadata is treated as present (packages/ui/src/components/field.tsx:65-86, 102-156, 165-235). The public props accept ReactNode, but the presence check only excludes null/undefined. Common JSX such as error={hasError && "Required"} passes false, which currently produces aria-invalid=true, an empty alert, and an inconsistent non-destructive label; descriptions similarly create empty referenced nodes. Normalize renderable presence consistently for Field and FieldGroup, or narrow the accepted type, and cover false/empty conditional values.

  3. [P2] A Fragment satisfies the one-child guard but receives the control props instead of its input (packages/ui/src/components/field.tsx:121). Children.only(<><Input /></>) succeeds, after which Slot clones the Fragment; the actual control never receives the id/ARIA contract and React warns about invalid Fragment props. Reject Fragment/non-control children explicitly and add a focused misuse test.

Scope otherwise looks clean: the delta is the expected 10 files, contains identity-only Start/Crew adapters and no feature migrations, package exports/adapters are covered, native fieldset/legend usage is sound, and the Storybook state matrix is representative. GitNexus reports LOW blast radius with zero affected processes, which matches the no-consumer extraction. At this head all 14 GitHub checks are green, including app builds, lint, tests, typechecks, E2E, security, and CodeRabbit. Local package suite reported 55/55 and lat check passed, but the cases above are absent from that suite.

@zacjones93

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

Follow-up review pinned to exact head 04f29558e9c43e20c99be521b36bb9d186d0cc34.

Finding:

  1. [P2] Empty Fragment metadata is still classified as renderable (packages/ui/src/components/field.tsx:58-62). The new predicate fixes false, nullish values, and blank strings, but Children.toArray(<></>) and Children.toArray(<>{false}</>) each return a Fragment element. hasRenderableContent therefore returns true, so these non-rendering ReactNode values still create empty description/error nodes and, for errors, aria-invalid=true. Recursively inspect Fragment children (or narrow the metadata contract) and add Field/FieldGroup coverage for an empty Fragment.

The other two prior findings are fully fixed: normalized child props now win after Radix Slot merging while described-by tokens are ordered and deduplicated, and Field.Control explicitly rejects Fragment controls. The focused tests exercise conflicting child id/ARIA state and the Fragment misuse path.

LAT now has nine distinct field leaf specs with one adjacent test reference each; lat check passes. The added Storybook plays verify described field wiring and invalid group description/state/alert. Remediation scope is limited to the expected four files, git diff --check is clean, and the focused package suite passes 55/55.

Live GitHub state at review time: lint, stack, security, and the rate-limited CodeRabbit status are complete; builds, typechecks/tests, and E2E are still running.

Copy link
Copy Markdown
Contributor Author

Final follow-up review pinned to exact head e7ba19fa32e0c95b49793e4afbfcf2cbe302fdd7.

LGTM — no findings.

The sole residual is fully fixed. hasRenderableContent now recursively inspects Fragment children, so empty Fragments and false-only Fragments are absent metadata. Focused Field and FieldGroup tests verify that no description/error nodes, aria-describedby, or aria-invalid state is emitted.

All earlier findings remain cleared:

  • normalized child props preserve the root id, ordered/deduplicated described-by metadata, and root-owned invalid state after Radix Slot merging;
  • Field.Control rejects Fragment controls;
  • native props, classes, and refs remain preserved;
  • nine unique LAT leaf specs still have one adjacent test reference each;
  • described-field and invalid-group Storybook plays remain present;
  • adapters, exports, inventory, and no-feature-migration scope are unchanged.

The final remediation delta touches only packages/ui/src/components/field.tsx and packages/ui/test/field.test.tsx; git diff --check is clean. At this exact head the package suite passes 55/55 and lat check passes. GitHub stack/security/CodeRabbit statuses are complete; app CI and E2E jobs are still running at review time.

@zacjones93
zacjones93 merged commit d6316c1 into codex/series-event-movements-form-context-fix Jul 15, 2026
53 of 144 checks passed
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.

1 participant