feat(lynx-react): add ActionButton.PrefixIcon/SuffixIcon#1507
feat(lynx-react): add ActionButton.PrefixIcon/SuffixIcon#1507junghyeonsu merged 5 commits intolynxfrom
Conversation
# 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>
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Alpha Preview (Stackflow SPA)
|
Alpha Preview (Storybook)
|
Alpha Preview (Docs)
|
…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>
Summary
Implements
ActionButton.PrefixIcon/ActionButton.SuffixIconon Lynx by introducing a main-thread color sync pattern that fits Lynx's<image>tint semantics.Measured during POC (page included in lynx-spa):
var()anywhere for image tint — not intint-colorattribute, not intint-colorCSS property. Only concrete hex/rgb works.getComputedStyleProperty("color")does return the resolved hex, so we set it as thetint-colorattribute from there.What changed
packages/qvism-preset/src/recipes/lynx/action-button.ts— Lynx fork of action-button as a slot recipe withroot/text/prefixIcon/suffixIcon. Color goes on slot class (color: var(--seed-color-...)), size on size×layout compound variants.packages/lynx-react/src/hooks/use-icon-color.ts—useIconColor(depKey)returns a main-thread ref; on mount and whendepKeychanges, mirrors resolvedcolortotint-colorattribute viarunOnMainThread.packages/lynx-react/src/utils/resolve-recipe-token.ts— genericresolveRecipeToken(vars, path[])+capitalizefor cross-component rootage vars lookup. Used by ActionButton for icon size; ready for Checkbox/ActionChip/etc. later.packages/lynx-react/src/components/ActionButton/ActionButton.tsx— newActionButton.PrefixIcon/SuffixIconslot components thatcloneElement@karrotmarket/lynx-monochrome-icon1.9.0+ icons, injectingclassName(for recipe CSS),ref(for the hook), andstyle.width/height(to override the icon's inline default).ActionButtonRootnow wraps withPropsProviderso slot subcomponents observe variant state.createSlotRecipeContext.withProviderandwithRootProviderboth wrap children withPropsProvidernow (previously onlyClassNamesProvider).tint-colortosupportedPropertiesso generated CSS isn't stripped.@karrotmarket/lynx-monochrome-icon@1.9.0/-multicolor-icon@1.12.0(forwardRef + className + optional color).bunfig.tomlminimumReleaseAgeExcludesextended.React.ComponentTypeicon signature; addIconColorPOCpage documenting each approach tested.Test plan
bun installbun run ecosystem:build && bun packages:build && bun generate:allbun --filter @seed-design/lynx-react test→ 37/37 passbun --filter lynx-spa dev→ ActionButtonPage → "Prefix / Suffix Icon" section@karrotmarket/lynx-monochrome-icon@1.10.0ships 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