Skip to content

feat(lynx-react): add ActionButton.PrefixIcon/SuffixIcon#1507

Merged
junghyeonsu merged 5 commits intolynxfrom
lynx-action-button-icon
Apr 24, 2026
Merged

feat(lynx-react): add ActionButton.PrefixIcon/SuffixIcon#1507
junghyeonsu merged 5 commits intolynxfrom
lynx-action-button-icon

Conversation

@junghyeonsu
Copy link
Copy Markdown
Contributor

Summary

Implements ActionButton.PrefixIcon / ActionButton.SuffixIcon on Lynx by introducing a main-thread color sync pattern that fits Lynx's <image> tint semantics.

Measured during POC (page included in lynx-spa):

  • Lynx does not resolve CSS var() anywhere for image tint — not in tint-color attribute, not in tint-color CSS property. Only concrete hex/rgb works.
  • Main-thread getComputedStyleProperty("color") does return the resolved hex, so we set it as the tint-color attribute from there.

What changed

  • Recipe packages/qvism-preset/src/recipes/lynx/action-button.ts — Lynx fork of action-button as a slot recipe with root/text/prefixIcon/suffixIcon. Color goes on slot class (color: var(--seed-color-...)), size on size×layout compound variants.
  • Hook packages/lynx-react/src/hooks/use-icon-color.tsuseIconColor(depKey) returns a main-thread ref; on mount and when depKey changes, mirrors resolved color to tint-color attribute via runOnMainThread.
  • Util packages/lynx-react/src/utils/resolve-recipe-token.ts — generic resolveRecipeToken(vars, path[]) + capitalize for cross-component rootage vars lookup. Used by ActionButton for icon size; ready for Checkbox/ActionChip/etc. later.
  • Component packages/lynx-react/src/components/ActionButton/ActionButton.tsx — new ActionButton.PrefixIcon / SuffixIcon slot components that cloneElement @karrotmarket/lynx-monochrome-icon 1.9.0+ icons, injecting className (for recipe CSS), ref (for the hook), and style.width/height (to override the icon's inline default). ActionButtonRoot now wraps with PropsProvider so slot subcomponents observe variant state.
  • Infra createSlotRecipeContext.withProvider and withRootProvider both wrap children with PropsProvider now (previously only ClassNamesProvider).
  • postcss-lynx-compat adds tint-color to supportedProperties so generated CSS isn't stripped.
  • Icon package upgrade to @karrotmarket/lynx-monochrome-icon@1.9.0 / -multicolor-icon@1.12.0 (forwardRef + className + optional color). bunfig.toml minimumReleaseAgeExcludes extended.
  • Examples Adapt Foundation pages to new React.ComponentType icon signature; add IconColorPOC page documenting each approach tested.

Test plan

  • bun install
  • bun run ecosystem:build && bun packages:build && bun generate:all
  • bun --filter @seed-design/lynx-react test → 37/37 pass
  • bun --filter lynx-spa dev → ActionButtonPage → "Prefix / Suffix Icon" section
    • variant-appropriate tint applies on mount (brandSolid=white, neutralSolid=white, brandOutline=brand-orange, disabled=disabled-gray)
    • state transitions re-tint correctly
  • Icon sizing will pick up the recipe (width/height via style) once @karrotmarket/lynx-monochrome-icon@1.10.0 ships with the style-merge fix (consumer style overrides the icon's inline default 24px). Color behavior is independent and works today.

🤖 Generated with Claude Code

junghyeonsu and others added 3 commits April 22, 2026 11:36
# Conflicts:
#	packages/lynx-react/src/components/ActionButton/ActionButton.tsx
Implements prefix/suffix icon slots using @karrotmarket/lynx-monochrome-icon
1.9.0+ icons via cloneElement injection.

Lynx <image> does not resolve var() anywhere — neither as tint-color
attribute nor as a CSS property (see POC page for measured evidence). The
working path is: recipe sets slot color: var(--...) → useIconColor hook
reads the resolved hex in the main thread via getComputedStyleProperty
("color") → sets it as the tint-color attribute. variant/disabled/loading
changes re-sync via a JSON dep key.

- Add `useIconColor` hook (packages/lynx-react/src/hooks/use-icon-color.ts)
- Add generic `resolveRecipeToken` util for cross-component rootage vars
  lookup (capitalize + vars path traversal). ActionButton uses it to
  compute prefix/suffix icon size and injects via style prop so it wins
  over the icon package's inline default 24px once the icon package's
  style-merge fix ships in 1.10.0.
- `createSlotRecipeContext.withProvider` and `withRootProvider` also wrap
  with PropsProvider so slot subcomponents can observe variant state.
- Fork Lynx action-button recipe with `root`/`text`/`prefixIcon`/
  `suffixIcon` slots; color lives on slot class (via CSS var), size on
  size×layout compound variants.
- `postcss-lynx-compat`: allow `tint-color` as a supported CSS property
  (runtime ignores it but keeps the generator from filtering it out).
- Bump `@karrotmarket/lynx-monochrome-icon` / `-multicolor-icon` alpha
  versions; register them in bunfig `minimumReleaseAgeExcludes`.
- Adapt existing Foundation icon pages to the new icon component signature
  (React.ComponentType).
- Add Icon Color POC page in lynx-spa documenting A/B/C/D measurements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

⚠️ No Changeset found

Latest commit: c86cc58

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 86f8cb75-a359-4cd0-a47d-764ab95696aa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lynx-action-button-icon

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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

Alpha Preview (Stackflow SPA)

@github-actions
Copy link
Copy Markdown
Contributor

Alpha Preview (Storybook)

@github-actions
Copy link
Copy Markdown
Contributor

Alpha Preview (Docs)

junghyeonsu and others added 2 commits April 22, 2026 18:28
…ound children

Lynx `<text>` is not a flex container and raw text nodes must live inside a
`<text>` element, so wrapping children in TextSlot meant icons ended up inside
that `<text>` — flex gap on root couldn't separate icon/label. The
splitSlottedChildren workaround (identity-based children filtering) was
fragile and noisy.

Switch to prop-based icon slots. Children is always label-only, wrapped
unconditionally in `<ActionButtonTextSlot>`. `prefixIcon` / `suffixIcon`
accept a ReactElement; internal `ActionButtonIconSlot` handles slot className +
size (style) + main-thread ref injection. Same pattern applies cleanly to
future Checkbox / Chip / ListItem / ActionChip etc.

Also rolls in /simplify review fixes:
- Hoist `syncTintColor` worklet to module scope so it isn't recreated every
  effect run (Lynx re-marshals the worklet across threads on each call)
- `useIconColor(deps: DependencyList)` — drop the JSON.stringify dep key
  indirection; pass primitives directly
- Memoize `propsForContext` in ActionButtonRoot so PropsContext isn't
  invalidated every render; also pick only disabled/loading instead of full
  innerProps spread
- Drop `as unknown as Record<string, unknown>` cast by widening
  `resolveRecipeToken`'s param to `unknown`
- Factor ActionButtonIconSlot as a single internal component; call hooks
  unconditionally (the former PrefixIcon/SuffixIcon components returned null
  before hooks in the invalid-children path — rule-of-hooks hazard)

Namespace exports (`ActionButton.PrefixIcon`, `ActionButton.SuffixIcon`) are
removed — they were never published, so BC-safe within this PR window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `icon` slot to the Lynx action-button recipe and expose an `icon` prop +
`layout="iconOnly"` branch on ActionButton, matching the web counterpart API
shape (aria-label required on iconOnly).

Recipe changes (packages/qvism-preset/src/recipes/lynx/action-button.ts):
- `slots` gains `"icon"` with `base.icon.flexShrink: 0`
- Each of 7 variants now defines `icon: { color, [disabled]: color }`,
  pulling from rootage `vars.variantXxx.{enabled,disabled}.icon.color`
- Each of 4 size×layout=iconOnly compoundVariants now emits
  `icon: { width, height }` using `vars.sizeXxxLayoutIconOnly.enabled.icon.size`
- Generated CSS includes `.seed-action-button__icon--...` rules (color per
  variant×state, size per size+iconOnly)

Component changes (packages/lynx-react/src/components/ActionButton/ActionButton.tsx):
- `IconSlotKey` widened to `"prefixIcon" | "suffixIcon" | "icon"`
- `resolveIconSize` branches: when slot is `"icon"` it uses the
  `sizeXxxLayoutIconOnly` vars path (irrespective of `variantProps.layout`),
  otherwise the `Layout{Xxx}` path used by prefixIcon/suffixIcon
- Adds `layout`, `icon`, `aria-label` to `ActionButtonProps`
- Render branches on `layout === "iconOnly"`: renders only the single icon
  slot and skips prefixIcon / TextSlot / suffixIcon
- Dev warn when `layout="iconOnly"` is set without `aria-label` (matches web
  ActionButton behavior)
- JSDoc gains an iconOnly usage example; removes the stale "iconOnly is
  unsupported" note

Example / doc:
- ActionButtonPage adds an "Icon Only" section with 4 cases
  (brandSolid, neutralSolid/small, brandOutline, brandSolid+disabled)
- TIER-B-svg-blocked.md notes that ActionButton's prefixIcon/suffixIcon/icon
  are implemented via WebP + tint-color main-thread sync and calls out the
  same pattern for future ToggleButton / ReactionButton /
  ContextualFloatingButton work

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@junghyeonsu junghyeonsu merged commit 6330a02 into lynx Apr 24, 2026
1 check passed
@junghyeonsu junghyeonsu deleted the lynx-action-button-icon branch April 24, 2026 08:21
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