@seed-design/lynx-react 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"AppBar.d.ts","sourceRoot":"","sources":["../../../src/components/AppBar/AppBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAGxC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EAEtB,aAAa,EAEd,MAAM,aAAa,CAAC;AAoCrB,MAAM,WAAW,eAAgB,SAAQ,kBAAkB,EAAE,sBAAsB;CAAG;AAEtF,eAAO,MAAM,UAAU,iFA2BrB,CAAC;AAKH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB;IAC7D,gBAAgB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACtD;AAED,eAAO,MAAM,UAAU,iFA4BrB,CAAC;AAGH,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,gBAAgB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACtD;AAED,eAAO,MAAM,WAAW,kFA4BtB,CAAC;AAKH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB,EAAE,sBAAsB;CAAG;AAE1F,eAAO,MAAM,UAAU,iFAmCrB,CAAC;AAGH,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;CAAG;AAE7D,eAAO,MAAM,WAAW,kFAYtB,CAAC;AAGH,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;CAAG;AAEhE,eAAO,MAAM,cAAc,qFAYzB,CAAC;AAKH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB,EAAE,kBAAkB;IACjF,IAAI,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC;IAChD,qBAAqB,CAAC,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAC7D,uBAAuB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IACjE,sBAAsB,CAAC,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAC;CAChE;AAED,eAAO,MAAM,gBAAgB,uFA2B3B,CAAC;AAGH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB;CAAG;AAElE,eAAO,MAAM,UAAU,iFAYrB,CAAC"}
1
+ {"version":3,"file":"AppBar.d.ts","sourceRoot":"","sources":["../../../src/components/AppBar/AppBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAGxC,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EAEtB,aAAa,EAEd,MAAM,aAAa,CAAC;AAqCrB,MAAM,WAAW,eAAgB,SAAQ,kBAAkB,EAAE,sBAAsB;CAAG;AAEtF,eAAO,MAAM,UAAU,iFA2BrB,CAAC;AAKH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB;IAC7D,gBAAgB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACtD;AAED,eAAO,MAAM,UAAU,iFA4BrB,CAAC;AAGH,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,gBAAgB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACtD;AAED,eAAO,MAAM,WAAW,kFA4BtB,CAAC;AAKH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB,EAAE,sBAAsB;CAAG;AAE1F,eAAO,MAAM,UAAU,iFAmCrB,CAAC;AAGH,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;CAAG;AAE7D,eAAO,MAAM,WAAW,kFAYtB,CAAC;AAGH,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;CAAG;AAEhE,eAAO,MAAM,cAAc,qFAYzB,CAAC;AAKH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB,EAAE,kBAAkB;IACjF,IAAI,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC;IAChD,qBAAqB,CAAC,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAC7D,uBAAuB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IACjE,sBAAsB,CAAC,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAC;CAChE;AAED,eAAO,MAAM,gBAAgB,uFA6B3B,CAAC;AAGH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB;CAAG;AAElE,eAAO,MAAM,UAAU,iFAYrB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { mergeProps } from "../../utils/merge-props.js";
2
2
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context.jsx";
3
- import { Icon } from "../Icon/Icon.jsx";
3
+ import { Icon, IconSlotProvider } from "../Icon/Icon.jsx";
4
4
  import { AppBarProvider, useAppBarContext } from "./context.js";
5
5
  import { getLayoutWidth, getMainLayoutStyle, useAppBar } from "./useAppBar.js";
6
6
  import * as React from "@lynx-js/react";
@@ -117,7 +117,12 @@ var AppBarIconButton = React.forwardRef((props, ref) => {
117
117
  const classNames = useAppBarClassNames("AppBarIconButton");
118
118
  if (process.env.NODE_ENV !== "production" && accessibilityElement && !accessibilityLabel) console.warn("AppBarIconButton requires `accessibility-label` for accessibility.");
119
119
  return <view {...mergeProps(ref ? { ref } : {}, nativeProps)} accessibility-element={accessibilityElement} accessibility-label={accessibilityLabel} accessibility-traits={accessibilityTraits} className={clsx(classNames.iconButton, className)}>
120
- {icon ? <Icon className={classNames.icon} icon={icon} /> : children}
120
+ <IconSlotProvider value={{
121
+ classNames: { icon: classNames.icon },
122
+ deps: [classNames.icon]
123
+ }}>
124
+ {icon ? <Icon icon={icon} /> : children}
125
+ </IconSlotProvider>
121
126
  </view>;
122
127
  });
123
128
  AppBarIconButton.displayName = "AppBarIconButton";
@@ -1 +1 @@
1
- {"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../../src/components/Icon/Icon.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAe,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAIhE,UAAU,oBAAoB;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAWD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;CAClB,qBAyCA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,qBAEA;AAeD,wBAAgB,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,GAAG,YAAY,GAAG,IAAI,CAI1E;AAaD,MAAM,WAAW,SAAU,SAAQ,sBAAsB;IACvD,IAAI,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC;IACrC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;CAAG;AAErD,MAAM,WAAW,eAAgB,SAAQ,SAAS;CAAG;AA2GrD,eAAO,MAAM,IAAI,2EAA8D,CAAC;AAChF,eAAO,MAAM,UAAU,iFAItB,CAAC;AACF,eAAO,MAAM,UAAU,iFAItB,CAAC;AAIF,MAAM,WAAW,iBAAkB,SAAQ,sBAAsB;IAC/D,IAAI,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAED,eAAO,MAAM,YAAY,mFA6BvB,CAAC"}
1
+ {"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../../src/components/Icon/Icon.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAe,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAIhE,UAAU,oBAAoB;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAWD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;CAClB,qBAyCA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,qBAEA;AAeD,wBAAgB,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,GAAG,YAAY,GAAG,IAAI,CAI1E;AAaD,MAAM,WAAW,SAAU,SAAQ,sBAAsB;IACvD,IAAI,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC;IACrC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;CAAG;AAErD,MAAM,WAAW,eAAgB,SAAQ,SAAS;CAAG;AA+GrD,eAAO,MAAM,IAAI,2EAA8D,CAAC;AAChF,eAAO,MAAM,UAAU,iFAItB,CAAC;AACF,eAAO,MAAM,UAAU,iFAItB,CAAC;AAIF,MAAM,WAAW,iBAAkB,SAAQ,sBAAsB;IAC/D,IAAI,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAED,eAAO,MAAM,YAAY,mFA6BvB,CAAC"}
@@ -101,7 +101,7 @@ var IconSlotBase = React.forwardRef((props, ref) => {
101
101
  slot
102
102
  ]);
103
103
  if (!hasValidIcon) return null;
104
- return <view {...mergeProps({ "main-thread:ref": sourceRef }, ref ? { ref } : {}, nativeProps)} className={clsx(baseClassName, slotClassName, className)} style={mergeWrapperStyle({
104
+ return <view {...mergeProps({ "main-thread:ref": sourceRef }, ref ? { ref } : {}, nativeProps)} className={clsx(slotClassName && baseClassName ? `${baseClassName}-slot` : baseClassName, slotClassName, className)} style={mergeWrapperStyle({
105
105
  size,
106
106
  color,
107
107
  style
@@ -39,7 +39,7 @@ export interface UseScaleFeedbackReturn {
39
39
  * The target's transform is owned by this hook. Use a separate content layer
40
40
  * when the element already uses translate, rotate, or another transform.
41
41
  *
42
- * Supported on Lynx Engine 3.9 and later. Missing or unknown
42
+ * Supported on Lynx Engine 3.6 and later. Missing or unknown
43
43
  * `GlobalProps.motion` values preserve the default motion; only the exact
44
44
  * `"reduced"` value disables scaling.
45
45
  */
@@ -42,7 +42,7 @@ function readScaleFeedbackScale(target) {
42
42
  * The target's transform is owned by this hook. Use a separate content layer
43
43
  * when the element already uses translate, rotate, or another transform.
44
44
  *
45
- * Supported on Lynx Engine 3.9 and later. Missing or unknown
45
+ * Supported on Lynx Engine 3.6 and later. Missing or unknown
46
46
  * `GlobalProps.motion` values preserve the default motion; only the exact
47
47
  * `"reduced"` value disables scaling.
48
48
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/lynx-react",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -34,12 +34,12 @@
34
34
  "peerDependencies": {
35
35
  "@lynx-js/react": ">=0.117.0",
36
36
  "@lynx-js/types": ">=3.9.0",
37
- "@seed-design/lynx-css": "0.0.0 || >=0.11.0 <1.0.0"
37
+ "@seed-design/lynx-css": "0.0.0 || >=0.11.1 <1.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@lynx-js/react": "^0.117.0",
41
41
  "@lynx-js/types": "^3.9.0",
42
- "@seed-design/lynx-css": "0.11.0",
42
+ "@seed-design/lynx-css": "0.11.1",
43
43
  "@testing-library/jest-dom": "^6.9.1",
44
44
  "jsdom": "^29.0.2",
45
45
  "preact": "npm:@lynx-js/internal-preact@10.28.4-ee7bb26",
@@ -88,9 +88,9 @@ describe("ActionButton", () => {
88
88
  const { getByText } = getRenderedQueries();
89
89
  const root = getRenderedRoot();
90
90
 
91
- expect(root.querySelector(".seed-prefix-icon")).toHaveClass("seed-action-button__prefixIcon");
92
- expect(root.querySelector(".seed-suffix-icon")).toHaveClass("seed-action-button__suffixIcon");
93
- expect(root.querySelector(".seed-prefix-icon image")).toHaveStyle({
91
+ expect(root.querySelector(".seed-prefix-icon-slot")).toHaveClass("seed-action-button__prefixIcon");
92
+ expect(root.querySelector(".seed-suffix-icon-slot")).toHaveClass("seed-action-button__suffixIcon");
93
+ expect(root.querySelector(".seed-prefix-icon-slot image")).toHaveStyle({
94
94
  width: "100%",
95
95
  height: "100%",
96
96
  });
@@ -106,8 +106,8 @@ describe("ActionButton", () => {
106
106
 
107
107
  const root = getRenderedRoot();
108
108
 
109
- expect(root.querySelector(".seed-prefix-icon")).toHaveClass("seed-action-button__prefixIcon");
110
- expect(root.querySelector(".seed-suffix-icon")).toHaveClass("seed-action-button__suffixIcon");
109
+ expect(root.querySelector(".seed-prefix-icon-slot")).toHaveClass("seed-action-button__prefixIcon");
110
+ expect(root.querySelector(".seed-suffix-icon-slot")).toHaveClass("seed-action-button__suffixIcon");
111
111
  });
112
112
 
113
113
  it("supports icon-only child slot", () => {
@@ -119,7 +119,7 @@ describe("ActionButton", () => {
119
119
 
120
120
  const root = getRenderedRoot();
121
121
 
122
- expect(root.querySelector(".seed-icon")).toHaveClass("seed-action-button__icon");
122
+ expect(root.querySelector(".seed-icon-slot")).toHaveClass("seed-action-button__icon");
123
123
  });
124
124
 
125
125
  it("maps accessibility props to the root view", () => {
@@ -141,7 +141,7 @@ describe("ActionButton", () => {
141
141
 
142
142
  const root = getRenderedRoot();
143
143
 
144
- expect(root.querySelector(".seed-icon")).toHaveClass("seed-action-button__icon");
144
+ expect(root.querySelector(".seed-icon-slot")).toHaveClass("seed-action-button__icon");
145
145
  });
146
146
 
147
147
  it("throws in development when icon-only layout has no Icon child", () => {
@@ -1,4 +1,7 @@
1
1
  import "@testing-library/jest-dom";
2
+ import * as React from "@lynx-js/react";
3
+ import type { MainThread } from "@lynx-js/types";
4
+ import type { LynxIconElementProps } from "../../types";
2
5
  import {
3
6
  act,
4
7
  fireEvent,
@@ -9,6 +12,7 @@ import {
9
12
  import { afterEach, describe, expect, it, vi } from "vitest";
10
13
 
11
14
  import * as AppBar from "./AppBar.namespace";
15
+ import { Icon } from "../Icon/Icon";
12
16
 
13
17
  type TestSystemInfo = { platform?: string };
14
18
 
@@ -268,3 +272,25 @@ describe("AppBar", () => {
268
272
  });
269
273
  });
270
274
  });
275
+
276
+ describe("AppBar icon recipe ownership", () => {
277
+ const TestIcon = React.forwardRef<MainThread.Element, LynxIconElementProps>((props, ref) => (
278
+ <image {...props} {...(ref ? { "main-thread:ref": ref } : {})} />
279
+ ));
280
+ it.each(["prop", "children"])("connects the %s icon without fallback styles", (mode) => {
281
+ const icon = <TestIcon />;
282
+ const { container } = render(
283
+ <AppBar.Root theme="cupertino">
284
+ <AppBar.Left>
285
+ <AppBar.IconButton accessibility-label="Close" {...(mode === "prop" ? { icon } : {})}>
286
+ {mode === "children" ? <Icon icon={icon} /> : null}
287
+ </AppBar.IconButton>
288
+ </AppBar.Left>
289
+ </AppBar.Root>,
290
+ );
291
+ const wrapper = container.querySelector(".seed-app-bar__icon")!;
292
+ expect(wrapper).not.toBeNull();
293
+ expect(wrapper.classList.contains("seed-icon-slot")).toBe(true);
294
+ expect(wrapper.classList.contains("seed-icon")).toBe(false);
295
+ });
296
+ });
@@ -17,6 +17,7 @@ import type {
17
17
  } from "../../types";
18
18
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
19
19
  import { Icon } from "../Icon";
20
+ import { IconSlotProvider } from "../Icon/Icon";
20
21
  import { AppBarProvider, useAppBarContext } from "./context";
21
22
  import { getLayoutWidth, getMainLayoutStyle, useAppBar } from "./useAppBar";
22
23
  import { mergeProps } from "../../utils/merge-props";
@@ -263,7 +264,9 @@ export const AppBarIconButton = React.forwardRef<unknown, AppBarIconButtonProps>
263
264
  accessibility-traits={accessibilityTraits}
264
265
  className={clsx(classNames.iconButton, className)}
265
266
  >
266
- {icon ? <Icon className={classNames.icon} icon={icon} /> : children}
267
+ <IconSlotProvider value={{ classNames: { icon: classNames.icon }, deps: [classNames.icon] }}>
268
+ {icon ? <Icon icon={icon} /> : children}
269
+ </IconSlotProvider>
267
270
  </view>
268
271
  );
269
272
  });
@@ -1,10 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+
1
4
  import * as React from "@lynx-js/react";
2
5
  import { act, render, waitSchedule } from "@lynx-js/react/testing-library";
3
6
  import type { MainThread } from "@lynx-js/types";
4
7
  import { describe, expect, it } from "vitest";
5
8
 
6
9
  import type { LynxIconElementProps } from "../../types";
7
- import { InternalIcon, PrefixIcon } from "./Icon";
10
+ import { Icon, IconSlotProvider, InternalIcon, PrefixIcon, SuffixIcon } from "./Icon";
8
11
 
9
12
  const TestIcon = React.forwardRef<
10
13
  MainThread.Element,
@@ -101,7 +104,8 @@ describe("InternalIcon", () => {
101
104
  await waitSchedule();
102
105
 
103
106
  expect(frames.size).toBe(1);
104
- expect(image.getAttribute("tint-color")).toBeNull();
107
+ // The immediate read uses the old native style; the next frame must refresh it.
108
+ expect(image.getAttribute("tint-color")).toBe("rgb(134, 139, 148)");
105
109
 
106
110
  computedColor = "rgb(255, 102, 0)";
107
111
  runNextFrame();
@@ -149,3 +153,54 @@ describe("PrefixIcon", () => {
149
153
  expect(getSourceAndImage().image.getAttribute("tint-color")).toBeNull();
150
154
  });
151
155
  });
156
+
157
+ describe("icon recipe ownership", () => {
158
+ it.each([
159
+ [Icon, "icon", "seed-icon"],
160
+ [PrefixIcon, "prefixIcon", "seed-prefix-icon"],
161
+ [SuffixIcon, "suffixIcon", "seed-suffix-icon"],
162
+ ] as const)("keeps fallback styles exclusive to unbound %s", (Component, slot, baseClass) => {
163
+ const { container, rerender } = render(<Component icon={<TestIcon />} />);
164
+ expect(container.querySelector(`.${baseClass}`)).not.toBeNull();
165
+ rerender(
166
+ <IconSlotProvider value={{ classNames: { [slot]: "test-recipe-icon" }, deps: [] }}>
167
+ <Component size={28} color="#2475e8" className="user-icon" icon={<TestIcon />} />
168
+ </IconSlotProvider>,
169
+ );
170
+ const wrapper = container.querySelector(".test-recipe-icon")!;
171
+ expect(wrapper.classList.contains(`${baseClass}-slot`)).toBe(true);
172
+ expect(wrapper.classList.contains("user-icon")).toBe(true);
173
+ expect(wrapper.classList.contains(baseClass)).toBe(false);
174
+ expect(wrapper.getAttribute("style")).toContain("28px");
175
+ });
176
+ });
177
+
178
+ // This checks generated CSS cascade only; native state/theme invalidation is verified separately.
179
+ describe("generated icon CSS import order", () => {
180
+ const { JSDOM } = createRequire(`${process.cwd()}/package.json`)("jsdom") as {
181
+ JSDOM: new (html: string) => { window: Window & typeof globalThis };
182
+ };
183
+ const base = readFileSync("../lynx-css/base.css", "utf8");
184
+ const recipe = readFileSync("../lynx-css/recipes/callout.css", "utf8");
185
+
186
+ it("keeps the legacy standalone fallback selectors", () => {
187
+ const dom = new JSDOM(`<style>${base}</style><div class="seed-suffix-icon"></div>`);
188
+ const style = dom.window.getComputedStyle(dom.window.document.querySelector("div")!);
189
+ expect(style.width).toBe("var(--seed-suffix-icon-size)");
190
+ expect(style.height).toBe("var(--seed-suffix-icon-size)");
191
+ expect(style.color).toBe("var(--seed-suffix-icon-color, currentcolor)");
192
+ dom.window.close();
193
+ });
194
+
195
+ it.each(["base-first", "recipe-first"])("preserves Callout slot styles with %s", (order) => {
196
+ const dom = new JSDOM(`<style>${order === "base-first" ? base + recipe : recipe + base}</style>
197
+ <div class="seed-suffix-icon-slot seed-callout__suffixIcon seed-callout__suffixIcon--tone_neutral"></div>`);
198
+ const icon = dom.window.document.querySelector("div")!;
199
+ const style = dom.window.getComputedStyle(icon);
200
+ expect(style.width).toBe("var(--seed-dimension-x4)");
201
+ expect(style.height).toBe("var(--seed-dimension-x4)");
202
+ expect(style.color).toBe("var(--seed-color-fg-neutral)");
203
+ expect(style.flexShrink).toBe("0");
204
+ dom.window.close();
205
+ });
206
+ });
@@ -205,7 +205,11 @@ const IconSlotBase = React.forwardRef<unknown, IconSlotBaseProps>((props, ref) =
205
205
  ref ? { ref: ref as LynxViewRef } : {},
206
206
  nativeProps,
207
207
  )}
208
- className={clsx(baseClassName, slotClassName, className)}
208
+ className={clsx(
209
+ slotClassName && baseClassName ? `${baseClassName}-slot` : baseClassName,
210
+ slotClassName,
211
+ className,
212
+ )}
209
213
  style={mergeWrapperStyle({ size, color, style })}
210
214
  >
211
215
  {cloneElement(icon, {
@@ -69,7 +69,7 @@ describe("ReactionButton", () => {
69
69
  const queries = getQueriesForElement(getRenderedRoot());
70
70
 
71
71
  expect(root).toHaveClass("seed-reaction-button__root--size_small");
72
- expect(root.querySelector(".seed-prefix-icon")).toHaveClass("seed-reaction-button__prefixIcon");
72
+ expect(root.querySelector(".seed-prefix-icon-slot")).toHaveClass("seed-reaction-button__prefixIcon");
73
73
  expect(queries.getByText("좋아요")).toHaveClass("seed-reaction-button__label");
74
74
  expect(queries.getByText("12")).toHaveClass("seed-count", "seed-reaction-button__count");
75
75
 
@@ -119,7 +119,7 @@ function readScaleFeedbackScale(target: ScaleFeedbackElement | null): number | n
119
119
  * The target's transform is owned by this hook. Use a separate content layer
120
120
  * when the element already uses translate, rotate, or another transform.
121
121
  *
122
- * Supported on Lynx Engine 3.9 and later. Missing or unknown
122
+ * Supported on Lynx Engine 3.6 and later. Missing or unknown
123
123
  * `GlobalProps.motion` values preserve the default motion; only the exact
124
124
  * `"reduced"` value disables scaling.
125
125
  */