Skip to content

feat: Add Expanding Action and Fluid Tooltip components - #79

Merged
Dinil-Thilakarathne merged 4 commits into
masterfrom
dev
Aug 2, 2026
Merged

Dinil-Thilakarathne merged 4 commits into
masterfrom
dev

Conversation

@Dinil-Thilakarathne

@Dinil-Thilakarathne Dinil-Thilakarathne commented Aug 2, 2026 •

Copy link
Copy Markdown
Owner
  • Add expanding-action component and demo
  • Add fluid-tooltip component and demo
  • Add anatomy and promotion agent skills
  • Update documentation and registry configuration

Summary by CodeRabbit

  • New Features
    • Added Fluid Tooltip, Expanding Action, Lightbox, and Avatar Showcase components.
    • Added interactive prototypes for action rails, radial sliders, component anatomy, avatar showcases, and lightboxes.
    • Added accessible keyboard, responsive, and reduced-motion interactions.
  • Documentation
    • Added component installation, usage, props, previews, and playground guides.
    • Added guidance for creating, auditing, promoting, installing, and validating skills.
  • Improvements
    • Improved component navigation categories and source syntax highlighting.
    • Enhanced registry validation and provider setup guidance.

- Add `expanding-action` component and demo
- Add `fluid-tooltip` component and demo
- Add anatomy and promotion agent skills
- Update documentation and registry configuration
- Add `AvatarShowcase` for rendering dynamic, recording-friendly
  follower
  lists with staggered lanes and deterministic sampling.
- Add `Lightbox` for accessible, spatially expanding image previews.
- Include registry entries, documentation, and example implementations
  for both components.
@vercel

vercel Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sona-ui Ready Ready Preview Aug 2, 2026 12:57pm

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds four UI components, prototype routes for action rails, radial sliders, component anatomy, lightboxes, and avatar data, plus registry integration, documentation, navigation updates, and agent skills.

Changes

Sona UI expansion

Layer / File(s) Summary
Agent workflow definitions
.agents/skills/*, .claude/skills/*, skills-lock.json, src/registry/sonaui/agent-skill/*, scripts/validate-agent-skill.ts
Adds component anatomy, portfolio promotion, and skill-authoring workflows. Adds provider setup, structured validation, registry checks, and skill metadata.
Registered component implementations
src/registry/sonaui/*, public/r/*, registry.json
Adds Avatar Showcase, Expanding Action, Fluid Tooltip, and Lightbox implementations with animation, accessibility, state, and registry contracts.
Registry, playground, and documentation integration
src/registry/*, src/content/docs/*, src/config/*, src/components/common/*, src/components/component-sidebar/*
Adds examples, playground controls, generated metadata, documentation, component categories, syntax highlighting, and sidebar navigation updates.
Action rail prototype flow
src/app/prototypes/action-rail/*, src/components/prototypes/action-rail/*
Adds three action-rail variants, URL and keyboard navigation, workspace interactions, replay controls, responsive behavior, and picker styling.
Radial slider prototype flow
src/app/prototypes/radial-slider/*, src/components/prototypes/radial-slider/*
Adds radial slider pointer and keyboard state, geometry utilities, three visual variants, picker navigation, replay controls, and styling.
Inspection and lightbox prototypes
src/app/prototypes/fluid-tabs-anatomy/*, src/components/prototypes/component-anatomy/*, src/components/prototypes/lightbox/*
Adds DOM measurement and annotation overlays, a Fluid Tabs inspection board, and an animated lightbox prototype.
Avatar showcase data route
src/app/prototypes/avatar-showcase/*, src/lib/github-followers.ts
Adds typed GitHub follower retrieval with pagination and a dynamic avatar showcase route.
Registry build and dependency support
scripts/build-registry.ts, scripts/check-production-registry-install.ts, public/agent/*
Adds CSS registry-file scanning, updates registry installation checks, and updates Hold To Delete Button dependencies.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies two major components added by the pull request and is concise and specific.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/content/docs/changelog.mdx (1)

843-843: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Restore the removed 1.1.0 release section.

This change deletes historical release notes. A changelog is a permanent record. Restore the section, or move it to an explicit archive, instead of removing it.

🤖 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 `@src/content/docs/changelog.mdx` at line 843, Restore the historical 1.1.0
release section in the changelog, preserving its original release notes and
placement among the versioned sections. Do not remove or rewrite the existing
historical content; if the section cannot remain here, move it to an explicit
archive while keeping it accessible.
🧹 Nitpick comments (9)
src/components/prototypes/action-rail/shared/action-rail-workspace.tsx (2)

521-545: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the duplicated Archive branches.

Both branches render the same ActionButton with the same icon, label, and click handler. Only the labelled value differs. Use one call, as lines 508-520 already do.

♻️ Proposed refactor
-              {variant === "labelled" ? (
-                <ActionButton
-                  icon={<Archive aria-hidden="true" size={15} />}
-                  label="Archive"
-                  labelled
-                  onClick={() => {
-                    setHoverPreviewSuppressed(true);
-                    setHoveredId(null);
-                    setFocusedId(null);
-                    onArchive(visibleId);
-                  }}
-                />
-              ) : (
-                <ActionButton
-                  icon={<Archive aria-hidden="true" size={15} />}
-                  label="Archive"
-                  labelled={false}
-                  onClick={() => {
-                    setHoverPreviewSuppressed(true);
-                    setHoveredId(null);
-                    setFocusedId(null);
-                    onArchive(visibleId);
-                  }}
-                />
-              )}
+              <ActionButton
+                icon={<Archive aria-hidden="true" size={15} />}
+                label="Archive"
+                labelled={variant === "labelled"}
+                onClick={() => {
+                  setHoverPreviewSuppressed(true);
+                  setHoveredId(null);
+                  setFocusedId(null);
+                  onArchive(visibleId);
+                }}
+              />
🤖 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 `@src/components/prototypes/action-rail/shared/action-rail-workspace.tsx`
around lines 521 - 545, Collapse the duplicated Archive branches in the
action-rail rendering into a single ActionButton, preserving the shared icon,
label, and click handler while setting its labelled prop from the variant
comparison, consistent with the existing pattern around the preceding action.

484-495: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

role="toolbar" implies arrow-key navigation.

The rail declares role="toolbar", but the three action buttons have no roving tabindex and no arrow-key handler. Keyboard users therefore tab through each button, which contradicts the announced pattern.

Two options: implement roving focus with ArrowLeft/ArrowRight (or ArrowUp/ArrowDown for the dock variant), or drop the toolbar role and keep aria-label on a plain group.

Note that the global ArrowLeft/ArrowRight shortcut in action-rail-prototype-picker.tsx would need to yield to the rail if you add roving focus.

🤖 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 `@src/components/prototypes/action-rail/shared/action-rail-workspace.tsx`
around lines 484 - 495, Resolve the accessibility mismatch in the action rail
rendered by the motion.div with id "prototype-action-rail": either implement
roving tabindex and directional arrow-key focus movement for its three action
buttons, using horizontal keys or dock-appropriate vertical keys, while ensuring
the global picker shortcut yields to rail handling, or remove role="toolbar" and
retain the aria-label on a plain group.
src/app/prototypes/action-rail/prototype-picker.css (1)

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

Consider a lower stacking value.

2147483647 is the maximum 32-bit integer. No later overlay, such as a modal or a toast, can stack above the picker without matching this exact value. A value like 9999 keeps the picker on top of prototype content and leaves headroom.

🤖 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 `@src/app/prototypes/action-rail/prototype-picker.css` at line 6, Lower the
z-index value in the prototype picker styles from the maximum 32-bit integer to
a smaller value such as 9999, keeping the picker above prototype content while
leaving room for modals, toasts, and other overlays to stack above it.
src/components/prototypes/action-rail/action-rail-prototype-picker.tsx (1)

71-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider scoping the arrow-key shortcuts.

The handler listens on document. ArrowLeft and ArrowRight therefore change the variant while the user navigates inside the workspace, and the key change on <main> then remounts the workspace and drops focus. The workspace also binds its own keydown for Escape and Shift+F10, so the shortcut surfaces overlap.

Two low-cost options: require the picker to hold focus for arrow navigation, or ignore events when event.shiftKey is set and the focus target is inside <main>.

♻️ Optional: ignore modified keys
       if (
         /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) ||
         target.isContentEditable ||
         event.metaKey ||
         event.ctrlKey ||
-        event.altKey
+        event.altKey ||
+        event.shiftKey
       ) {
         return;
       }
🤖 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 `@src/components/prototypes/action-rail/action-rail-prototype-picker.tsx`
around lines 71 - 96, Scope the ArrowLeft and ArrowRight handling in the
useEffect keydown listener so workspace navigation does not switch variants or
drop focus. Either require the picker to own focus before processing arrow
shortcuts, or ignore modified-key events and targets contained within the
workspace’s main element; preserve the existing number and replay shortcuts.
.agents/skills/component-anatomy/SKILL.md (1)

118-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Separate static checks from rendered verification.

The checklist asks the agent to confirm keyboard behavior, pointer behavior, and target attachment. Line 129 says the user owns browser review unless requested. Mark browser-dependent checks as pending when they were not executed. Do not let the completion criterion imply runtime verification without rendered evidence.

🤖 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 @.agents/skills/component-anatomy/SKILL.md around lines 118 - 131, Update the
Verify checklist and completion criterion to distinguish static checks from
rendered browser verification. In the guidance around “The user owns visual
browser review,” require keyboard behavior, pointer interaction, target
attachment, and other browser-dependent checks to be reported as pending when
not executed, and ensure completion is not described as confirmed without
rendered evidence.
src/components/prototypes/component-anatomy/fluid-tabs-inspection-board.tsx (2)

54-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive last-trigger from the node list instead of index 2.

The index 2 is tied to the current three-entry tabs array. If a tab is added, maxTravel silently reports the travel to the third tab, not the last tab.

♻️ Proposed change
   {
     id: "last-trigger",
-    resolve: (root) => root.querySelectorAll('[role="tab"]')[2] ?? null,
+    resolve: (root) => {
+      const triggers = root.querySelectorAll('[role="tab"]');
+      return triggers[triggers.length - 1] ?? null;
+    },
   },
🤖 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 `@src/components/prototypes/component-anatomy/fluid-tabs-inspection-board.tsx`
around lines 54 - 57, Update the last-trigger resolve logic to derive the final
tab from the queried node list rather than hard-coding index 2. In the
last-trigger entry, select the node at the list’s last position so maxTravel
continues targeting the actual last tab when the tabs array changes.

266-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The transition and padding readouts duplicate values from fluid-tabs.tsx.

"320 / 40 / 0.9" mirrors the spring stiffness: 320, damping: 40, mass: 0.9 in src/registry/sonaui/fluid-tabs/fluid-tabs.tsx. "4 px pad" mirrors the p-1 on Tabs.List in the same file. Both match today. If the component transition or padding changes, this board reports stale numbers and nothing fails.

Consider exporting the spring config from fluid-tabs.tsx and importing it here, or deriving the padding from the measured list and first-trigger rects that the board already collects.

🤖 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 `@src/components/prototypes/component-anatomy/fluid-tabs-inspection-board.tsx`
around lines 266 - 276, Remove the hardcoded transition and padding readouts in
the inspection board near the inputMode and formatPixels usages. Reuse the
fluid-tabs spring configuration exported from fluid-tabs.tsx for the transition
display, and derive the list padding from the existing list and first-trigger
measurements instead of the literal “4 px pad”, so these readouts stay
synchronized with the component.
src/registry/prop-types.ts (1)

967-1004: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The fluid-tooltip prop table documents only FluidTooltipGroupProps.

FluidTooltip is a compound component. The table omits FluidTooltip.Root (id, side, align, sideOffset, disabled), FluidTooltip.Trigger (asChild, keepOpenOnClick), and FluidTooltip.Content (className, showArrow). The id prop on Root is required, and the demo depends on it, so consumers need it in the reference.

Add the remaining part props to this entry. Prefix each name with its part, for example Root.id, to keep the flat PropMeta shape.

🤖 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 `@src/registry/prop-types.ts` around lines 967 - 1004, Expand the
“fluid-tooltip” entry in the prop-types registry to include the compound parts
omitted from the current FluidTooltipGroupProps table: Root.id (required),
Root.side, Root.align, Root.sideOffset, Root.disabled, Trigger.asChild,
Trigger.keepOpenOnClick, Content.className, and Content.showArrow. Prefix each
property name with its part name, such as Root.id, while preserving the existing
group props and flat PropMeta structure.
src/lib/github-followers.ts (1)

119-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test coverage for pagination, limit handling, and error paths.

getGitHubFollowers implements pagination via Link-header parsing, a limit/"all" distinction, and two error paths (GitHubFollowersError from the profile call and from paginated calls). None of this logic has visible test coverage in the reviewed files. The fetcher option is designed for injection, which makes this straightforward to unit test with a mock implementation.

Add tests for: limit smaller than a page, limit spanning multiple pages, limit: "all", a non-OK profile response, and a non-OK paginated response.

🤖 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 `@src/lib/github-followers.ts` around lines 119 - 206, Add unit tests for
getGitHubFollowers using an injected fetcher mock, covering a limit smaller than
one page, a limit spanning multiple pages, limit: "all", a non-OK profile
response, and a non-OK paginated response. Assert pagination requests and
returned followers/hasMore values, and verify both failures throw
GitHubFollowersError with the expected status.
🤖 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 @.agents/skills/promote-portfolio-component/SKILL.md:
- Around line 10-14: Constrain repository discovery in both
.agents/skills/promote-portfolio-component/SKILL.md (lines 10-14) and
.agents/skills/component-anatomy/SKILL.md (lines 10-12): validate and reject
supplied paths under protected locations, scope the initial rg --files search to
the supplied path, and exclude personal/journal/** from all broader searches
while locating components, demos, and local dependencies.

In `@src/app/prototypes/avatar-showcase/page.tsx`:
- Around line 4-13: Enable caching for the static GitHub query in
AvatarShowcasePrototypePage by removing the force-dynamic behavior or
configuring an appropriate route revalidate window, and pass the corresponding
cache/revalidation options through getGitHubFollowers. Preserve the existing
username and limit, and ensure production uses GITHUB_TOKEN for the higher
GitHub API quota.
- Around line 34-42: Update the catch block in the avatar showcase page to
capture the thrown error and log it before returning the existing fallback UI.
Preserve the current GitHub followers error handling and distinguish
GitHubFollowersError from unexpected errors when choosing the logging context.

In `@src/components/prototypes/action-rail/shared/action-rail-workspace.tsx`:
- Around line 170-180: Update the archive function so that when projects.length
=== 1, it sets an explanatory message before returning. Preserve the existing
no-op behavior for the final project and the current archive flow for other
valid targets.

In `@src/components/prototypes/component-anatomy/inspection-board.tsx`:
- Around line 101-127: Replace the unconditional requestAnimationFrame recursion
in the useLayoutEffect with a bounded measurement loop: start tracking only
during pointer/keyboard interaction or active-tab changes, stop after rects
remain stable for several frames, and pause while document.hidden. Preserve
ResizeObserver, font-readiness, and window resize measurement behavior, and
ensure cleanup cancels any pending frame and listeners.

In `@src/components/prototypes/lightbox/spatial-expand-lightbox.tsx`:
- Around line 49-63: Update the motion.img element in the lightbox trigger to
use an empty alt value, keeping the existing sr-only span as the sole accessible
name source with the image description.
- Around line 66-86: Update the lightbox render flow around Dialog.Portal to
keep the portal mounted independently of the open state, while preserving the
dialog’s close behavior and allowing exit animations to complete before any
React-level unmount. Remove the open-based conditional wrapping Dialog.Portal
and rely on the dialog’s state/animation lifecycle for visibility.

In `@src/components/prototypes/radial-slider/radial-slider-prototype-picker.tsx`:
- Around line 59-84: Update the global keydown handler in the useEffect to
ignore ArrowLeft and ArrowRight events originating from the focused radial
slider, identified by the target’s role="slider", after allowing the slider’s
own handler to process them. Preserve existing shortcuts for other targets and
keep numeric, replay, and non-slider variant switching behavior unchanged.

In `@src/content/docs/avatar-showcase.mdx`:
- Around line 1-39: Remove the disabled Avatar Showcase documentation page,
including its usage content and component references. Delete avatar-showcase.mdx
while the navigation entry remains disabled, and do not modify unrelated
registry or playground code.

In `@src/content/docs/expanding-action.mdx`:
- Around line 11-18: Update the Features documentation for ExpandingAction to
remove or revise the claim about accessible focus management so it accurately
reflects the implemented behavior. Do not imply that focus is restored or
managed unless the component implementation is changed to provide that behavior.

In `@src/lib/github-followers.ts`:
- Around line 119-206: Update getGitHubFollowers so both the profile request and
each pagination request enforce an internal timeout even when no caller-supplied
signal is provided. Combine the caller’s signal with the timeout signal without
overriding caller cancellation, and ensure timeout resources are cleaned up
after each fetch; alternatively use supported fetch timeout options consistently
for both requests.

In `@src/registry/index.ts`:
- Line 2859: Update the registry extraction prologue for expanding-action so it
skips the "use client"; directive before locating the first import statement.
Preserve the generated imports in the registry’s imports section rather than
moving them into anatomy, ensuring Usage imports are populated.

In `@src/registry/playground/index.tsx`:
- Around line 134-141: Update the playgroundAvatars data to reference locally
bundled avatar assets under public/ instead of the external i.pravatar.cc URLs.
Preserve the 24-item generation and existing identifiers/names, ensuring each
imageUrl resolves to a valid local asset so the showcase remains visible without
network access.

In `@src/registry/registry.json`:
- Around line 220-235: Add "registryDependencies": ["`@sona-ui/sona-utils`"] to
the fluid-tooltip manifest entry and the lightbox and avatar-showcase entries in
src/registry/registry.json. Then regenerate src/registry/index.ts so the
corresponding generated blocks include the same registryDependencies metadata.

In `@src/registry/sonaui/avatar-showcase/avatar-showcase.tsx`:
- Around line 213-221: Update the shouldReduceMotion branch in the avatar
showcase component to invoke onComplete once from an effect when reduced motion
is active, while preserving the staticItems rendering and avoiding repeated
calls on rerenders.
- Around line 154-179: Update the effect containing the ResizeObserver and
measure callback to depend on the measurement values as well as measurementKey,
so it re-observes the remounted track. Guard setMeasurement by comparing the
next container and track dimensions with the current measurement, preventing
unchanged measurements from triggering a render loop while preserving the
existing zero-width handling.

In `@src/registry/sonaui/expanding-action/expanding-action.tsx`:
- Around line 95-190: Update the expanding-action component around the trigger,
choices row, and AnimatePresence to manage keyboard focus: retain refs for the
trigger and Back button, focus the Back button when isOpen becomes true, and
restore focus to the trigger when it becomes false. Add aria-expanded to the
trigger and handle Escape while open by collapsing the control and restoring
focus to the trigger, while preserving existing disabled and selection behavior.

In `@src/registry/sonaui/fluid-tooltip/fluid-tooltip.tsx`:
- Around line 390-402: Move tooltip content, className, and showArrow storage
from refs to state in FluidTooltipRoot, and expose the state through the payload
consumed by FluidTooltipGroup. Update FluidTooltipContent to propagate changes
via the root’s state update mechanism so open tooltips re-render with current
values, and remove render-time reads of the mutable content refs.
- Around line 348-361: Update handleFocus so it calls
group.registerKeyboardTarget() only when
event.currentTarget.matches(":focus-visible"), while still invoking onFocus for
every focus event; leave handlePointerEnter and handleKeyDown behavior
unchanged.

In `@src/registry/sonaui/lightbox/lightbox.tsx`:
- Line 65: Update the Dialog.Root invocation to remove the redundant defaultOpen
prop, keeping open={open} and onOpenChange={setOpen} as the sole state controls.
Preserve the existing initialization of internalOpen from defaultOpen at line
49.

---

Outside diff comments:
In `@src/content/docs/changelog.mdx`:
- Line 843: Restore the historical 1.1.0 release section in the changelog,
preserving its original release notes and placement among the versioned
sections. Do not remove or rewrite the existing historical content; if the
section cannot remain here, move it to an explicit archive while keeping it
accessible.

---

Nitpick comments:
In @.agents/skills/component-anatomy/SKILL.md:
- Around line 118-131: Update the Verify checklist and completion criterion to
distinguish static checks from rendered browser verification. In the guidance
around “The user owns visual browser review,” require keyboard behavior, pointer
interaction, target attachment, and other browser-dependent checks to be
reported as pending when not executed, and ensure completion is not described as
confirmed without rendered evidence.

In `@src/app/prototypes/action-rail/prototype-picker.css`:
- Line 6: Lower the z-index value in the prototype picker styles from the
maximum 32-bit integer to a smaller value such as 9999, keeping the picker above
prototype content while leaving room for modals, toasts, and other overlays to
stack above it.

In `@src/components/prototypes/action-rail/action-rail-prototype-picker.tsx`:
- Around line 71-96: Scope the ArrowLeft and ArrowRight handling in the
useEffect keydown listener so workspace navigation does not switch variants or
drop focus. Either require the picker to own focus before processing arrow
shortcuts, or ignore modified-key events and targets contained within the
workspace’s main element; preserve the existing number and replay shortcuts.

In `@src/components/prototypes/action-rail/shared/action-rail-workspace.tsx`:
- Around line 521-545: Collapse the duplicated Archive branches in the
action-rail rendering into a single ActionButton, preserving the shared icon,
label, and click handler while setting its labelled prop from the variant
comparison, consistent with the existing pattern around the preceding action.
- Around line 484-495: Resolve the accessibility mismatch in the action rail
rendered by the motion.div with id "prototype-action-rail": either implement
roving tabindex and directional arrow-key focus movement for its three action
buttons, using horizontal keys or dock-appropriate vertical keys, while ensuring
the global picker shortcut yields to rail handling, or remove role="toolbar" and
retain the aria-label on a plain group.

In `@src/components/prototypes/component-anatomy/fluid-tabs-inspection-board.tsx`:
- Around line 54-57: Update the last-trigger resolve logic to derive the final
tab from the queried node list rather than hard-coding index 2. In the
last-trigger entry, select the node at the list’s last position so maxTravel
continues targeting the actual last tab when the tabs array changes.
- Around line 266-276: Remove the hardcoded transition and padding readouts in
the inspection board near the inputMode and formatPixels usages. Reuse the
fluid-tabs spring configuration exported from fluid-tabs.tsx for the transition
display, and derive the list padding from the existing list and first-trigger
measurements instead of the literal “4 px pad”, so these readouts stay
synchronized with the component.

In `@src/lib/github-followers.ts`:
- Around line 119-206: Add unit tests for getGitHubFollowers using an injected
fetcher mock, covering a limit smaller than one page, a limit spanning multiple
pages, limit: "all", a non-OK profile response, and a non-OK paginated response.
Assert pagination requests and returned followers/hasMore values, and verify
both failures throw GitHubFollowersError with the expected status.

In `@src/registry/prop-types.ts`:
- Around line 967-1004: Expand the “fluid-tooltip” entry in the prop-types
registry to include the compound parts omitted from the current
FluidTooltipGroupProps table: Root.id (required), Root.side, Root.align,
Root.sideOffset, Root.disabled, Trigger.asChild, Trigger.keepOpenOnClick,
Content.className, and Content.showArrow. Prefix each property name with its
part name, such as Root.id, while preserving the existing group props and flat
PropMeta structure.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 68f3fd95-38e1-4bed-9dea-cfe9446afde6

📥 Commits

Reviewing files that changed from the base of the PR and between 214d331 and 9a1493d.

📒 Files selected for processing (61)
  • .agents/skills/component-anatomy/SKILL.md
  • .agents/skills/component-anatomy/agents/openai.yaml
  • .agents/skills/promote-portfolio-component/SKILL.md
  • .agents/skills/promote-portfolio-component/agents/openai.yaml
  • .agents/skills/writing-great-skills/GLOSSARY.md
  • .agents/skills/writing-great-skills/SKILL.md
  • .agents/skills/writing-great-skills/agents/openai.yaml
  • .claude/skills/writing-great-skills
  • public/agent/catalog.json
  • public/agent/components/hold-to-delete-button.json
  • public/r/avatar-showcase.json
  • public/r/expanding-action.json
  • public/r/fluid-tooltip.json
  • public/r/lightbox.json
  • public/r/registry.json
  • registry.json
  • scripts/build-registry.ts
  • skills-lock.json
  • src/app/prototypes/action-rail/page.tsx
  • src/app/prototypes/action-rail/prototype-picker.css
  • src/app/prototypes/avatar-showcase/page.tsx
  • src/app/prototypes/fluid-tabs-anatomy/page.tsx
  • src/app/prototypes/radial-slider/page.tsx
  • src/app/prototypes/radial-slider/prototype-picker.css
  • src/components/common/component-installation.tsx
  • src/components/common/sidebar-link.tsx
  • src/components/component-sidebar/index.tsx
  • src/components/prototypes/action-rail/action-rail-prototype-picker.tsx
  • src/components/prototypes/action-rail/edge-dock-prototype.tsx
  • src/components/prototypes/action-rail/floating-tools-prototype.tsx
  • src/components/prototypes/action-rail/labelled-bar-prototype.tsx
  • src/components/prototypes/action-rail/shared/action-rail-workspace.tsx
  • src/components/prototypes/component-anatomy/fluid-tabs-inspection-board.tsx
  • src/components/prototypes/component-anatomy/inspection-board.tsx
  • src/components/prototypes/lightbox/spatial-expand-lightbox.tsx
  • src/components/prototypes/radial-slider/instrument-cluster-prototype.tsx
  • src/components/prototypes/radial-slider/mechanical-detents-prototype.tsx
  • src/components/prototypes/radial-slider/quiet-gauge-prototype.tsx
  • src/components/prototypes/radial-slider/radial-slider-prototype-picker.tsx
  • src/components/prototypes/radial-slider/use-radial-slider.ts
  • src/config/components.ts
  • src/content/docs/avatar-showcase.mdx
  • src/content/docs/changelog.mdx
  • src/content/docs/expanding-action.mdx
  • src/content/docs/fluid-tooltip.mdx
  • src/content/docs/lightbox.mdx
  • src/lib/github-followers.ts
  • src/lib/types.ts
  • src/registry/examples/avatar-showcase/avatar-showcase-demo.tsx
  • src/registry/examples/button/button-demo.tsx
  • src/registry/examples/expanding-action/expanding-action-demo.tsx
  • src/registry/examples/fluid-tooltip/fluid-tooltip-demo.tsx
  • src/registry/examples/lightbox/lightbox-demo.tsx
  • src/registry/index.ts
  • src/registry/playground/index.tsx
  • src/registry/prop-types.ts
  • src/registry/registry.json
  • src/registry/sonaui/avatar-showcase/avatar-showcase.tsx
  • src/registry/sonaui/expanding-action/expanding-action.tsx
  • src/registry/sonaui/fluid-tooltip/fluid-tooltip.tsx
  • src/registry/sonaui/lightbox/lightbox.tsx
💤 Files with no reviewable changes (1)
  • src/registry/examples/button/button-demo.tsx

Comment on lines +10 to +14
## Required input

Require a path to the source component, demo, or Craft. Resolve the real files with `rg --files` before forming a plan. Ask only for information that cannot be discovered from the supplied path and its local imports.

Do not assume that a visually finished experiment is library-ready. Do not modify or remove the portfolio source during promotion unless the user explicitly asks for that separate change.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Constrain repository discovery before scanning the repository.

Both workflows leave the search root and protected-path exclusion implicit. A repository-root scan can enumerate personal/journal/, which the promotion workflow explicitly forbids.

  • .agents/skills/promote-portfolio-component/SKILL.md#L10-L14: reject protected paths, scope the initial search to the supplied path, and exclude personal/journal/** from broader searches.
  • .agents/skills/component-anatomy/SKILL.md#L10-L12: apply the same scope and exclusion before locating components, demos, and local dependencies.
🧰 Tools
🪛 LanguageTool

[style] ~10-~10: The double modal “Required input” is nonstandard (only accepted in certain dialects). Consider “to be input”.
Context: ...usable distribution layer. ## Required input Require a path to the source component...

(NEEDS_FIXED)

🪛 SkillSpector (2.4.4)

[warning] 155: [EA2] Autonomous Decision Making: Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Remediation: Add human-in-the-loop confirmation for destructive, irreversible, or high-impact operations. Never auto-execute commands that modify files, send data, or alter system state.

(Excessive Agency (EA2))

📍 Affects 2 files
  • .agents/skills/promote-portfolio-component/SKILL.md#L10-L14 (this comment)
  • .agents/skills/component-anatomy/SKILL.md#L10-L12
🤖 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 @.agents/skills/promote-portfolio-component/SKILL.md around lines 10 - 14,
Constrain repository discovery in both
.agents/skills/promote-portfolio-component/SKILL.md (lines 10-14) and
.agents/skills/component-anatomy/SKILL.md (lines 10-12): validate and reject
supplied paths under protected locations, scope the initial rg --files search to
the supplied path, and exclude personal/journal/** from all broader searches
while locating components, demos, and local dependencies.

Comment on lines +4 to +13
export const dynamic = "force-dynamic";

const githubUsername = "Dinil-Thilakarathne";

export default async function AvatarShowcasePrototypePage() {
try {
const result = await getGitHubFollowers({
username: githubUsername,
limit: 320,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Route re-fetches the same GitHub data on every request with no caching, risking rate-limit exhaustion.

dynamic = "force-dynamic" (Line 4) disables static caching for this route, and getGitHubFollowers (defined in src/lib/github-followers.ts) defaults its fetch cache option to "no-store". The query parameters here are constant (username: "Dinil-Thilakarathne", limit: 320), so every page load re-issues 1 profile request plus 4 pagination requests (320 followers ÷ 100 per page) against the GitHub REST API. GitHub's unauthenticated primary rate limit is 60 requests per hour per IP address, so roughly 12 page loads per hour from a single IP will exhaust it (5,000/hour if GITHUB_TOKEN is set). Once exhausted, the page falls into the generic error state for all visitors sharing that IP or token until the limit resets.

Since the query is static, this data is a strong caching candidate. Enable caching (for example, pass cache: "force-cache" with a next.revalidate window, or drop force-dynamic in favor of a route revalidate export) and ensure GITHUB_TOKEN is configured in production to raise the quota.

🤖 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 `@src/app/prototypes/avatar-showcase/page.tsx` around lines 4 - 13, Enable
caching for the static GitHub query in AvatarShowcasePrototypePage by removing
the force-dynamic behavior or configuring an appropriate route revalidate
window, and pass the corresponding cache/revalidation options through
getGitHubFollowers. Preserve the existing username and limit, and ensure
production uses GITHUB_TOKEN for the higher GitHub API quota.

Comment on lines +34 to +42
} catch {
return (
<main className="grid min-h-screen place-items-center bg-background px-6 py-12">
<p className="text-muted-foreground text-sm">
Unable to load GitHub followers right now.
</p>
</main>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Log the error before returning the fallback UI.

The catch {} block (Lines 34-42) discards the error entirely. It does not distinguish a GitHubFollowersError (network/auth/rate-limit failure) from a programming error, and nothing is logged. This makes production issues silent and hard to diagnose.

🔧 Proposed fix
-  } catch {
+  } catch (error) {
+    console.error("Failed to load GitHub followers for avatar showcase", error);
     return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
return (
<main className="grid min-h-screen place-items-center bg-background px-6 py-12">
<p className="text-muted-foreground text-sm">
Unable to load GitHub followers right now.
</p>
</main>
);
}
} catch (error) {
console.error("Failed to load GitHub followers for avatar showcase", error);
return (
<main className="grid min-h-screen place-items-center bg-background px-6 py-12">
<p className="text-muted-foreground text-sm">
Unable to load GitHub followers right now.
</p>
</main>
);
}
🤖 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 `@src/app/prototypes/avatar-showcase/page.tsx` around lines 34 - 42, Update the
catch block in the avatar showcase page to capture the thrown error and log it
before returning the existing fallback UI. Preserve the current GitHub followers
error handling and distinguish GitHubFollowersError from unexpected errors when
choosing the logging context.

Comment on lines +170 to +180
const archive = (id: string) => {
const target = projects.find((project) => project.id === id);
if (!target || projects.length === 1) return;
const index = projects.findIndex((project) => project.id === target.id);
setArchived({ project: target, index });
setProjects((current) =>
current.filter((project) => project.id !== target.id),
);
setActiveId((current) => (current === target.id ? null : current));
setMessage(`Archived ${target.name}`);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give feedback when archive is blocked.

archive returns early when only one project remains. No state changes and message is not updated. The Archive button then appears unresponsive.

Set an explanatory message before the early return.

🐛 Proposed fix
   const archive = (id: string) => {
     const target = projects.find((project) => project.id === id);
-    if (!target || projects.length === 1) return;
+    if (!target) return;
+    if (projects.length === 1) {
+      setMessage("Keep at least one project");
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const archive = (id: string) => {
const target = projects.find((project) => project.id === id);
if (!target || projects.length === 1) return;
const index = projects.findIndex((project) => project.id === target.id);
setArchived({ project: target, index });
setProjects((current) =>
current.filter((project) => project.id !== target.id),
);
setActiveId((current) => (current === target.id ? null : current));
setMessage(`Archived ${target.name}`);
};
const archive = (id: string) => {
const target = projects.find((project) => project.id === id);
if (!target) return;
if (projects.length === 1) {
setMessage("Keep at least one project");
return;
}
const index = projects.findIndex((project) => project.id === target.id);
setArchived({ project: target, index });
setProjects((current) =>
current.filter((project) => project.id !== target.id),
);
setActiveId((current) => (current === target.id ? null : current));
setMessage(`Archived ${target.name}`);
};
🤖 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 `@src/components/prototypes/action-rail/shared/action-rail-workspace.tsx`
around lines 170 - 180, Update the archive function so that when projects.length
=== 1, it sets an explanatory message before returning. Preserve the existing
no-op behavior for the final project and the current archive flow for other
valid targets.

Comment on lines +101 to +127
useLayoutEffect(() => {
const root = rootRef.current;
if (!root) return;

let frame = 0;
const track = () => {
measure();
frame = requestAnimationFrame(track);
};
frame = requestAnimationFrame(track);

const observer = new ResizeObserver(measure);
observer.observe(root);
for (const target of targets) {
const element = target.resolve(root);
if (element instanceof HTMLElement) observer.observe(element);
}

document.fonts.ready.then(measure);
window.addEventListener("resize", measure);

return () => {
cancelAnimationFrame(frame);
observer.disconnect();
window.removeEventListener("resize", measure);
};
}, [measure, rootRef, targets]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

The requestAnimationFrame loop measures every frame for the page lifetime.

track reschedules itself unconditionally. Each frame calls measure, which runs getBoundingClientRect on the root and on every target. This forces layout on every frame and keeps the CPU busy even when nothing moves. The ResizeObserver, document.fonts.ready, and resize handlers already cover static geometry changes, so the loop is only needed while the Motion layout animation runs.

Consider gating the loop: start it on pointer/keyboard interaction and stop it after the rects stay stable for a few frames. Also stop it when the document is hidden.

♻️ Sketch: stop the loop once rects settle
     let frame = 0;
+    let stableFrames = 0;
+    let previous = "";
     const track = () => {
       measure();
-      frame = requestAnimationFrame(track);
+      const snapshot = JSON.stringify(rectsSnapshot());
+      stableFrames = snapshot === previous ? stableFrames + 1 : 0;
+      previous = snapshot;
+      if (stableFrames < 30) frame = requestAnimationFrame(track);
     };
     frame = requestAnimationFrame(track);

A simpler alternative is to restart a short-lived loop from the board component whenever the active tab changes.

🤖 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 `@src/components/prototypes/component-anatomy/inspection-board.tsx` around
lines 101 - 127, Replace the unconditional requestAnimationFrame recursion in
the useLayoutEffect with a bounded measurement loop: start tracking only during
pointer/keyboard interaction or active-tab changes, stop after rects remain
stable for several frames, and pause while document.hidden. Preserve
ResizeObserver, font-readiness, and window resize measurement behavior, and
ensure cleanup cancels any pending frame and listeners.

Comment on lines +213 to +221
{shouldReduceMotion ? (
<div
className="flex items-center justify-center -space-x-3"
aria-hidden="true"
>
{staticItems.map((item) => (
<ShowcaseAvatar item={item} key={item.id} size={safeAvatarSize} />
))}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Call onComplete in the reduced-motion branch.

When shouldReduceMotion is true the component renders the static stack and never runs onAnimationComplete. onComplete then never fires, so a consumer that gates the next step on the callback stalls. Call onComplete once from an effect when reduced motion is active.

🤖 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 `@src/registry/sonaui/avatar-showcase/avatar-showcase.tsx` around lines 213 -
221, Update the shouldReduceMotion branch in the avatar showcase component to
invoke onComplete once from an effect when reduced motion is active, while
preserving the staticItems rendering and avoiding repeated calls on rerenders.

Comment on lines +95 to +190
return (
<MotionConfig reducedMotion="user">
<LayoutGroup id={`${instanceId}-expanding-action`}>
<AnimatePresence initial={false} mode="popLayout">
{!isOpen ? (
<motion.button
key="trigger"
type="button"
disabled={disabled || !hasEnabledItem}
onClick={() => setOpen(true)}
className={cn(
"relative flex h-12 cursor-pointer items-center gap-2 rounded-full px-5 text-sm font-medium text-(--expanding-action-foreground) active:scale-[0.97] disabled:pointer-events-none disabled:opacity-45",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--expanding-action-ring)",
className,
triggerClassName,
)}
style={tokenStyle}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={contentTransition}
>
<motion.span
layoutId={`${instanceId}-surface`}
aria-hidden="true"
className="absolute inset-0 rounded-full border border-(--expanding-action-border) bg-(--expanding-action-surface)/70 shadow-sm"
transition={surfaceTransition}
/>
{triggerIcon ? (
<span
aria-hidden="true"
className="relative grid size-4 shrink-0 place-items-center"
>
{triggerIcon}
</span>
) : null}
<span className="relative whitespace-nowrap">{trigger}</span>
</motion.button>
) : (
<motion.div
key="choices"
className={cn(
"relative flex max-w-full items-center overflow-x-auto rounded-full p-1",
className,
)}
style={tokenStyle}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={contentTransition}
>
<motion.span
layoutId={`${instanceId}-surface`}
aria-hidden="true"
className="absolute inset-0 rounded-full border border-(--expanding-action-border) bg-(--expanding-action-surface)/70 shadow-sm"
transition={surfaceTransition}
/>
<div className="relative flex items-center gap-1">
<button
type="button"
disabled={disabled}
onClick={() => setOpen(false)}
aria-label={backLabel}
className="grid size-10 shrink-0 cursor-pointer place-items-center rounded-full text-(--expanding-action-muted) transition-colors hover:bg-(--expanding-action-hover) hover:text-(--expanding-action-foreground) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--expanding-action-ring) active:scale-[0.97] disabled:pointer-events-none disabled:opacity-45"
>
<ChevronLeft
aria-hidden="true"
className="size-4"
strokeWidth={1.75}
/>
</button>
<span
aria-hidden="true"
className="h-5 w-px shrink-0 bg-(--expanding-action-border)"
/>
{items.map((item) => (
<button
key={item.value}
type="button"
disabled={disabled || item.disabled}
onClick={() => {
onValueSelect?.(item.value);
setOpen(false);
}}
className={cn(
"h-10 shrink-0 cursor-pointer whitespace-nowrap rounded-full px-3 text-sm text-(--expanding-action-foreground) transition-colors hover:bg-(--expanding-action-hover) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--expanding-action-ring) active:scale-[0.97] disabled:pointer-events-none disabled:opacity-45",
optionClassName,
)}
>
{item.label}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Restore focus when the control expands and collapses.

AnimatePresence removes the trigger button when isOpen becomes true, and removes the choices row when the user selects an item or presses Back. In both cases the focused element leaves the DOM, so focus returns to document.body. A keyboard user loses their position and must tab from the start of the page. The registry description promises accessible focus management.

Move focus to the Back button when the control expands. Move focus back to the trigger when it collapses. Add aria-expanded to the trigger and collapse on Escape.

♿ Proposed fix outline
+  const triggerRef = useRef<HTMLButtonElement>(null);
+  const backRef = useRef<HTMLButtonElement>(null);
+  const shouldRestoreFocus = useRef(false);
+
+  useEffect(() => {
+    if (!shouldRestoreFocus.current) return;
+    if (isOpen) backRef.current?.focus();
+    else triggerRef.current?.focus();
+  }, [isOpen]);
+
   const setOpen = (nextOpen: boolean) => {
+    shouldRestoreFocus.current = true;
     if (open === undefined) setInternalOpen(nextOpen);
     onOpenChange?.(nextOpen);
   };
             <motion.button
               key="trigger"
               type="button"
+              ref={triggerRef}
+              aria-expanded={isOpen}
               disabled={disabled || !hasEnabledItem}
             <motion.div
               key="choices"
+              onKeyDown={(event) => {
+                if (event.key === "Escape") setOpen(false);
+              }}
                 <button
                   type="button"
+                  ref={backRef}
                   disabled={disabled}
                   onClick={() => setOpen(false)}
🤖 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 `@src/registry/sonaui/expanding-action/expanding-action.tsx` around lines 95 -
190, Update the expanding-action component around the trigger, choices row, and
AnimatePresence to manage keyboard focus: retain refs for the trigger and Back
button, focus the Back button when isOpen becomes true, and restore focus to the
trigger when it becomes false. Add aria-expanded to the trigger and handle
Escape while open by collapsing the control and restoring focus to the trigger,
while preserving existing disabled and selection behavior.

Comment on lines +348 to +361
const handlePointerEnter: PointerEventHandler<HTMLElement> = (event) => {
if (event.pointerType !== "touch") {
group.registerPointerTarget(event.currentTarget);
}
onPointerEnter?.(event);
};
const handleFocus: FocusEventHandler<HTMLElement> = (event) => {
group.registerKeyboardTarget();
onFocus?.(event);
};
const handleKeyDown: KeyboardEventHandler<HTMLElement> = (event) => {
group.registerKeyboardTarget();
onKeyDown?.(event);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat click focus as pointer input, not keyboard input.

A mouse click on a trigger fires focus, so handleFocus calls registerKeyboardTarget(). The group then sets keyboardNavigation and disables directional motion and transitions for the rest of the interaction. Gate the keyboard path on :focus-visible.

🐛 Proposed fix
   const handleFocus: FocusEventHandler<HTMLElement> = (event) => {
-    group.registerKeyboardTarget();
+    if (event.currentTarget.matches(":focus-visible")) {
+      group.registerKeyboardTarget();
+    }
     onFocus?.(event);
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePointerEnter: PointerEventHandler<HTMLElement> = (event) => {
if (event.pointerType !== "touch") {
group.registerPointerTarget(event.currentTarget);
}
onPointerEnter?.(event);
};
const handleFocus: FocusEventHandler<HTMLElement> = (event) => {
group.registerKeyboardTarget();
onFocus?.(event);
};
const handleKeyDown: KeyboardEventHandler<HTMLElement> = (event) => {
group.registerKeyboardTarget();
onKeyDown?.(event);
};
const handlePointerEnter: PointerEventHandler<HTMLElement> = (event) => {
if (event.pointerType !== "touch") {
group.registerPointerTarget(event.currentTarget);
}
onPointerEnter?.(event);
};
const handleFocus: FocusEventHandler<HTMLElement> = (event) => {
if (event.currentTarget.matches(":focus-visible")) {
group.registerKeyboardTarget();
}
onFocus?.(event);
};
const handleKeyDown: KeyboardEventHandler<HTMLElement> = (event) => {
group.registerKeyboardTarget();
onKeyDown?.(event);
};
🤖 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 `@src/registry/sonaui/fluid-tooltip/fluid-tooltip.tsx` around lines 348 - 361,
Update handleFocus so it calls group.registerKeyboardTarget() only when
event.currentTarget.matches(":focus-visible"), while still invoking onFocus for
every focus event; leave handlePointerEnter and handleKeyDown behavior
unchanged.

Comment on lines +390 to +402
export function FluidTooltipContent({
children,
className,
showArrow = true,
}: FluidTooltipContentProps) {
const root = useRootContext("FluidTooltip.Content");
useLayoutEffect(() => {
root.contentRef.current = children;
root.contentClassNameRef.current = className;
root.showArrowRef.current = showArrow;
}, [children, className, root, showArrow]);
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Store tooltip content in state so open tooltips update.

FluidTooltipContent writes children, className, and showArrow into refs. Ref writes do not schedule a render. FluidTooltipGroup reads those refs during render on Lines 212, 248, and 252. If the content changes while the tooltip is open, for example a label that toggles between "Play" and "Pause", the popup keeps the previous value. Reading mutable refs during render is also unsafe under concurrent rendering.

Hold these values in state on FluidTooltipRoot and pass the state through the payload.

🤖 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 `@src/registry/sonaui/fluid-tooltip/fluid-tooltip.tsx` around lines 390 - 402,
Move tooltip content, className, and showArrow storage from refs to state in
FluidTooltipRoot, and expose the state through the payload consumed by
FluidTooltipGroup. Update FluidTooltipContent to propagate changes via the
root’s state update mechanism so open tooltips re-render with current values,
and remove render-time reads of the mutable content refs.


return (
<LayoutGroup id={instanceId}>
<Dialog.Root defaultOpen={defaultOpen} onOpenChange={setOpen} open={open}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not pass defaultOpen and open to the same Dialog.Root.

open is always defined, because it falls back to internalOpen. The dialog is therefore always controlled and defaultOpen is redundant. Line 49 already seeds internalOpen with defaultOpen. Remove the prop to avoid a controlled/uncontrolled conflict.

🐛 Proposed fix
-      <Dialog.Root defaultOpen={defaultOpen} onOpenChange={setOpen} open={open}>
+      <Dialog.Root onOpenChange={setOpen} open={open}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Dialog.Root defaultOpen={defaultOpen} onOpenChange={setOpen} open={open}>
<Dialog.Root onOpenChange={setOpen} open={open}>
🤖 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 `@src/registry/sonaui/lightbox/lightbox.tsx` at line 65, Update the Dialog.Root
invocation to remove the redundant defaultOpen prop, keeping open={open} and
onOpenChange={setOpen} as the sole state controls. Preserve the existing
initialization of internalOpen from defaultOpen at line 49.

- Add `provider-setup.md` reference to clarify client-side
  configuration.
- Enhance validation, selection, and design principles references.
- Update `SKILL.md` structure and metadata.
- Improve installation instructions and add smoke test validation for
  skill files.
@Dinil-Thilakarathne
Dinil-Thilakarathne merged commit 4b58d67 into master Aug 2, 2026
5 of 6 checks passed
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 2.18.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@scripts/validate-agent-skill.ts`:
- Around line 45-59: Update parseFrontmatter to parse the matched frontmatter
body with the project’s YAML parser before applying schema checks, and treat
parser errors—including unclosed quoted values—as invalid frontmatter. Build the
returned field map from the parsed YAML object while preserving the existing
validation behavior for top-level fields and unsupported structures.

In `@src/content/docs/agent-skill.mdx`:
- Line 64: Update the sample prompt in the agent-skill documentation so it
explicitly prohibits installation by changing “Before installing anything” to
“Do not install anything.” Preserve the instructions to inspect the consumer
project and Sona catalog, recommend a relevant component, and explain the
decision.

In `@src/registry/sonaui/agent-skill/references/provider-setup.md`:
- Around line 9-15: Update the Codex setup instruction in
src/registry/sonaui/agent-skill/references/provider-setup.md to state that the
TOML block belongs in ~/.codex/config.toml, then regenerate
public/r/agent-skill.json so its corresponding registry payload contains the
same path.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9ac6eac-7f1a-4a2b-8e62-9e0d09e03184

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1493d and 818d036.

📒 Files selected for processing (13)
  • public/r/agent-skill.json
  • public/r/registry.json
  • registry.json
  • scripts/check-production-registry-install.ts
  • scripts/validate-agent-skill.ts
  • src/content/docs/agent-skill.mdx
  • src/registry/index.ts
  • src/registry/registry.json
  • src/registry/sonaui/agent-skill/SKILL.md
  • src/registry/sonaui/agent-skill/references/component-selection.md
  • src/registry/sonaui/agent-skill/references/consumer-validation.md
  • src/registry/sonaui/agent-skill/references/design-principles.md
  • src/registry/sonaui/agent-skill/references/provider-setup.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/registry/registry.json

Comment on lines +45 to +59
function parseFrontmatter(source: string) {
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!match) return null;

const values = new Map<string, string>();
for (const line of match[1].split(/\r?\n/)) {
if (/^\s/.test(line) || line.trim() === "") continue;
const field = line.match(/^([a-z][a-z0-9-]*):\s*(.*)$/);
if (!field) {
errors.push(`invalid top-level frontmatter line: ${line}`);
continue;
}
values.set(field[1], field[2].trim().replace(/^['"]|['"]$/g, ""));
}
return values;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Parse frontmatter with a YAML parser.

Line 45 accepts invalid YAML as valid frontmatter. For example, an unclosed quoted description can pass these checks and cause skill discovery to fail in the client. Parse YAML before applying the schema checks.

🤖 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 `@scripts/validate-agent-skill.ts` around lines 45 - 59, Update
parseFrontmatter to parse the matched frontmatter body with the project’s YAML
parser before applying schema checks, and treat parser errors—including unclosed
quoted values—as invalid frontmatter. Build the returned field map from the
parsed YAML object while preserving the existing validation behavior for
top-level fields and unsupported structures.

/>

The agent should inspect the Sona catalog, recommend a relevant component, and wait before making a project change.
The agent should inspect the consumer project and Sona catalog, recommend a relevant component, explain the decision, and wait because the prompt explicitly says not to install.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the sample prompt prohibit installation.

Line 64 is inaccurate. “Before installing anything” permits installation after the explanation. Change the prompt to “Do not install anything” or remove the instruction that the agent must wait.

🤖 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 `@src/content/docs/agent-skill.mdx` at line 64, Update the sample prompt in the
agent-skill documentation so it explicitly prohibits installation by changing
“Before installing anything” to “Do not install anything.” Preserve the
instructions to inspect the consumer project and Sona catalog, recommend a
relevant component, and explain the decision.

Comment on lines +9 to +15
Add the shadcn server to the user's Codex configuration:

```toml
[mcp_servers.shadcn]
command = "npx"
args = ["shadcn@latest", "mcp"]
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

shadcn MCP configuration ~/.codex/config.toml mcp_servers codex

💡 Result:

To configure the shadcn MCP server for Codex, you must manually add the configuration to your ~/.codex/config.toml file, as the shadcn CLI cannot update this file automatically [1][2]. Add the following block to your ~/.codex/config.toml: [mcp_servers.shadcn] command = "npx" args = ["shadcn@latest", "mcp"] After saving the file, restart Codex to load the MCP server [1][2][3]. Alternatively, you can use the Codex CLI to manage your configuration. The codex mcp subcommand provides tools to list, add, remove, and manage your MCP server launchers directly [4][5]. For example, running codex mcp add can help you generate the necessary configuration entries, though you may still need to verify the final TOML structure in your config file [4][6]. Note that Codex uses TOML format for its configuration, not JSON [6].

Citations:


State the Codex configuration file path.

The Codex setup instruction currently omits where the TOML block belongs, and the generated registry payload repeats that omission. Add ~/.codex/config.toml to the Codex setup instruction and regenerate the public registry payload.

📍 Affects 2 files
  • src/registry/sonaui/agent-skill/references/provider-setup.md#L9-L15 (this comment)
  • public/r/agent-skill.json#L36-L36
🤖 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 `@src/registry/sonaui/agent-skill/references/provider-setup.md` around lines 9
- 15, Update the Codex setup instruction in
src/registry/sonaui/agent-skill/references/provider-setup.md to state that the
TOML block belongs in ~/.codex/config.toml, then regenerate
public/r/agent-skill.json so its corresponding registry payload contains the
same path.

This branch was successfully deployed

1 active deployment
Preview — 818d036c Deployed Aug 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant