@seed-design/lynx-react 0.1.1-alpha.0 → 0.2.0-alpha.2

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.
Files changed (180) hide show
  1. package/lib/components/ActionButton/ActionButton.d.ts +40 -9
  2. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
  3. package/lib/components/ActionButton/ActionButton.jsx +132 -14
  4. package/lib/components/BottomSheet/BottomSheet.d.ts +92 -0
  5. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
  6. package/lib/components/BottomSheet/BottomSheet.jsx +178 -0
  7. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +2 -0
  8. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -0
  9. package/lib/components/BottomSheet/BottomSheet.namespace.js +1 -0
  10. package/lib/components/BottomSheet/index.d.ts +3 -0
  11. package/lib/components/BottomSheet/index.d.ts.map +1 -0
  12. package/lib/components/BottomSheet/index.js +2 -0
  13. package/lib/components/Checkbox/Checkbox.d.ts +51 -0
  14. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
  15. package/lib/components/Checkbox/Checkbox.jsx +121 -0
  16. package/lib/components/Checkbox/Checkbox.namespace.d.ts +2 -0
  17. package/lib/components/Checkbox/Checkbox.namespace.d.ts.map +1 -0
  18. package/lib/components/Checkbox/Checkbox.namespace.js +1 -0
  19. package/lib/components/Checkbox/index.d.ts +3 -0
  20. package/lib/components/Checkbox/index.d.ts.map +1 -0
  21. package/lib/components/Checkbox/index.js +2 -0
  22. package/lib/components/ProgressCircle/ProgressCircle.d.ts +10 -9
  23. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
  24. package/lib/components/ProgressCircle/ProgressCircle.jsx +8 -14
  25. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +2 -0
  26. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts.map +1 -0
  27. package/lib/components/ProgressCircle/ProgressCircle.namespace.js +1 -0
  28. package/lib/components/ProgressCircle/index.d.ts +2 -1
  29. package/lib/components/ProgressCircle/index.d.ts.map +1 -1
  30. package/lib/components/ProgressCircle/index.js +2 -1
  31. package/lib/components/RadioGroup/RadioGroup.d.ts +50 -0
  32. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  33. package/lib/components/RadioGroup/RadioGroup.jsx +144 -0
  34. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +2 -0
  35. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts.map +1 -0
  36. package/lib/components/RadioGroup/RadioGroup.namespace.js +1 -0
  37. package/lib/components/RadioGroup/index.d.ts +3 -0
  38. package/lib/components/RadioGroup/index.d.ts.map +1 -0
  39. package/lib/components/RadioGroup/index.js +2 -0
  40. package/lib/components/Switch/Switch.d.ts +27 -0
  41. package/lib/components/Switch/Switch.d.ts.map +1 -0
  42. package/lib/components/Switch/Switch.jsx +75 -0
  43. package/lib/components/Switch/Switch.namespace.d.ts +2 -0
  44. package/lib/components/Switch/Switch.namespace.d.ts.map +1 -0
  45. package/lib/components/Switch/Switch.namespace.js +1 -0
  46. package/lib/components/Switch/index.d.ts +3 -0
  47. package/lib/components/Switch/index.d.ts.map +1 -0
  48. package/lib/components/Switch/index.js +2 -0
  49. package/lib/components/TagGroup/TagGroup.d.ts +40 -0
  50. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
  51. package/lib/components/TagGroup/TagGroup.jsx +54 -0
  52. package/lib/components/TagGroup/TagGroup.namespace.d.ts +2 -0
  53. package/lib/components/TagGroup/TagGroup.namespace.d.ts.map +1 -0
  54. package/lib/components/TagGroup/TagGroup.namespace.js +1 -0
  55. package/lib/components/TagGroup/index.d.ts +3 -0
  56. package/lib/components/TagGroup/index.d.ts.map +1 -0
  57. package/lib/components/TagGroup/index.js +2 -0
  58. package/lib/components/index.d.ts +8 -0
  59. package/lib/components/index.d.ts.map +1 -0
  60. package/lib/components/index.js +7 -0
  61. package/lib/hooks/index.d.ts +4 -0
  62. package/lib/hooks/index.d.ts.map +1 -0
  63. package/lib/hooks/index.js +3 -0
  64. package/lib/hooks/use-controllable-state.d.ts +15 -0
  65. package/lib/hooks/use-controllable-state.d.ts.map +1 -0
  66. package/lib/hooks/use-controllable-state.js +27 -0
  67. package/lib/hooks/use-icon-color.d.ts +15 -0
  68. package/lib/hooks/use-icon-color.d.ts.map +1 -0
  69. package/lib/hooks/use-icon-color.js +37 -0
  70. package/lib/hooks/use-press-tap.d.ts +24 -0
  71. package/lib/hooks/use-press-tap.d.ts.map +1 -0
  72. package/lib/hooks/use-press-tap.js +35 -0
  73. package/lib/index.d.ts +3 -4
  74. package/lib/index.d.ts.map +1 -1
  75. package/lib/index.js +3 -4
  76. package/lib/utils/create-slot-recipe-context.d.ts +26 -0
  77. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
  78. package/lib/utils/create-slot-recipe-context.jsx +76 -0
  79. package/lib/utils/get-seed-class-name.d.ts.map +1 -0
  80. package/lib/utils/index.d.ts +5 -0
  81. package/lib/utils/index.d.ts.map +1 -0
  82. package/lib/utils/index.js +4 -0
  83. package/lib/utils/resolve-recipe-token.d.ts +38 -0
  84. package/lib/utils/resolve-recipe-token.d.ts.map +1 -0
  85. package/lib/utils/resolve-recipe-token.js +47 -0
  86. package/lib/utils/split-multiple-variants-props.d.ts +29 -0
  87. package/lib/utils/split-multiple-variants-props.d.ts.map +1 -0
  88. package/lib/utils/split-multiple-variants-props.js +34 -0
  89. package/package.json +12 -9
  90. package/src/components/ActionButton/ActionButton.tsx +290 -31
  91. package/src/components/ActionButton/__tests__/ActionButton.test.tsx +35 -0
  92. package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
  93. package/src/components/BottomSheet/BottomSheet.tsx +386 -0
  94. package/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +119 -0
  95. package/src/components/BottomSheet/index.ts +27 -0
  96. package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
  97. package/src/components/Checkbox/Checkbox.tsx +291 -0
  98. package/src/components/Checkbox/index.ts +14 -0
  99. package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
  100. package/src/components/ProgressCircle/ProgressCircle.tsx +14 -13
  101. package/src/components/ProgressCircle/index.ts +9 -1
  102. package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
  103. package/src/components/RadioGroup/RadioGroup.tsx +338 -0
  104. package/src/components/RadioGroup/index.ts +14 -0
  105. package/src/components/Switch/Switch.namespace.ts +10 -0
  106. package/src/components/Switch/Switch.tsx +191 -0
  107. package/src/components/Switch/index.ts +12 -0
  108. package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
  109. package/src/components/TagGroup/TagGroup.tsx +125 -0
  110. package/src/components/TagGroup/index.ts +10 -0
  111. package/src/components/__tests__/ProgressCircle.test.ts +33 -0
  112. package/src/components/__tests__/exports.test.ts +38 -0
  113. package/src/components/__tests__/switch.test.ts +34 -0
  114. package/src/components/index.ts +7 -0
  115. package/src/hooks/__tests__/use-controllable-state.test.ts +120 -0
  116. package/src/hooks/__tests__/use-press-tap.test.ts +129 -0
  117. package/src/hooks/index.ts +3 -0
  118. package/src/hooks/use-controllable-state.ts +38 -0
  119. package/src/hooks/use-icon-color.ts +51 -0
  120. package/src/hooks/use-press-tap.ts +54 -0
  121. package/src/index.ts +3 -4
  122. package/src/utils/__tests__/create-slot-recipe-context.test.tsx +228 -0
  123. package/src/utils/__tests__/split-multiple-variants-props.test.ts +61 -0
  124. package/src/utils/create-slot-recipe-context.tsx +147 -0
  125. package/src/utils/index.ts +4 -0
  126. package/src/utils/resolve-recipe-token.ts +47 -0
  127. package/src/utils/split-multiple-variants-props.ts +59 -0
  128. package/lib/components/Box/Box.d.ts +0 -10
  129. package/lib/components/Box/Box.d.ts.map +0 -1
  130. package/lib/components/Box/Box.jsx +0 -19
  131. package/lib/components/Box/index.d.ts +0 -2
  132. package/lib/components/Box/index.d.ts.map +0 -1
  133. package/lib/components/Box/index.js +0 -1
  134. package/lib/components/Flex/Flex.d.ts +0 -33
  135. package/lib/components/Flex/Flex.d.ts.map +0 -1
  136. package/lib/components/Flex/Flex.jsx +0 -7
  137. package/lib/components/Flex/index.d.ts +0 -2
  138. package/lib/components/Flex/index.d.ts.map +0 -1
  139. package/lib/components/Flex/index.js +0 -1
  140. package/lib/components/List/List.d.ts +0 -9
  141. package/lib/components/List/List.d.ts.map +0 -1
  142. package/lib/components/List/List.jsx +0 -17
  143. package/lib/components/List/index.d.ts +0 -2
  144. package/lib/components/List/index.d.ts.map +0 -1
  145. package/lib/components/List/index.js +0 -1
  146. package/lib/components/ListItem/ListItem.d.ts +0 -8
  147. package/lib/components/ListItem/ListItem.d.ts.map +0 -1
  148. package/lib/components/ListItem/ListItem.jsx +0 -11
  149. package/lib/components/ListItem/index.d.ts +0 -2
  150. package/lib/components/ListItem/index.d.ts.map +0 -1
  151. package/lib/components/ListItem/index.js +0 -1
  152. package/lib/components/ScrollView/ScrollView.d.ts +0 -9
  153. package/lib/components/ScrollView/ScrollView.d.ts.map +0 -1
  154. package/lib/components/ScrollView/ScrollView.jsx +0 -17
  155. package/lib/components/ScrollView/index.d.ts +0 -2
  156. package/lib/components/ScrollView/index.d.ts.map +0 -1
  157. package/lib/components/ScrollView/index.js +0 -1
  158. package/lib/components/Stack/Stack.d.ts +0 -6
  159. package/lib/components/Stack/Stack.d.ts.map +0 -1
  160. package/lib/components/Stack/Stack.jsx +0 -10
  161. package/lib/components/Stack/index.d.ts +0 -2
  162. package/lib/components/Stack/index.d.ts.map +0 -1
  163. package/lib/components/Stack/index.js +0 -1
  164. package/lib/components/Text/Text.d.ts +0 -30
  165. package/lib/components/Text/Text.d.ts.map +0 -1
  166. package/lib/components/Text/Text.jsx +0 -49
  167. package/lib/components/Text/index.d.ts +0 -2
  168. package/lib/components/Text/index.d.ts.map +0 -1
  169. package/lib/components/Text/index.js +0 -1
  170. package/lib/get-seed-class-name.d.ts.map +0 -1
  171. package/lib/utils/dynamic-style.d.ts +0 -13
  172. package/lib/utils/dynamic-style.d.ts.map +0 -1
  173. package/lib/utils/dynamic-style.js +0 -26
  174. package/lib/utils/styled.d.ts +0 -120
  175. package/lib/utils/styled.d.ts.map +0 -1
  176. package/lib/utils/styled.jsx +0 -131
  177. package/src/utils/dynamic-style.ts +0 -26
  178. /package/lib/{get-seed-class-name.d.ts → utils/get-seed-class-name.d.ts} +0 -0
  179. /package/lib/{get-seed-class-name.js → utils/get-seed-class-name.js} +0 -0
  180. /package/src/{get-seed-class-name.ts → utils/get-seed-class-name.ts} +0 -0
@@ -0,0 +1,228 @@
1
+ import { forwardRef } from "@lynx-js/react";
2
+ import { render, renderHook } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { createSlotRecipeContext } from "../create-slot-recipe-context";
6
+
7
+ type MockVariantProps = {
8
+ variant?: "a" | "b";
9
+ };
10
+
11
+ type MockClassnames = {
12
+ root: string;
13
+ label: string;
14
+ };
15
+
16
+ const mockSlotRecipe = Object.assign(
17
+ (props?: MockVariantProps): MockClassnames => ({
18
+ root: `root-${props?.variant ?? "a"}`,
19
+ label: `label-${props?.variant ?? "a"}`,
20
+ }),
21
+ {
22
+ splitVariantProps: <T extends MockVariantProps>(props: T) => {
23
+ const { variant, ...rest } = props;
24
+ return [{ variant }, rest] as [MockVariantProps, Omit<T, keyof MockVariantProps>];
25
+ },
26
+ },
27
+ );
28
+
29
+ type MockReceived = { props?: Record<string, unknown>; refValue?: unknown };
30
+
31
+ function createMock(name: string) {
32
+ const received: MockReceived = {};
33
+ const Mock = forwardRef<{ tag: "mock" }, Record<string, unknown>>((props, ref) => {
34
+ received.props = props;
35
+ received.refValue = ref;
36
+ const { children } = props as { children?: unknown };
37
+ return <>{children as never}</>;
38
+ });
39
+ Mock.displayName = name;
40
+ return { Mock, received };
41
+ }
42
+
43
+ describe("createSlotRecipeContext", () => {
44
+ describe("useClassNames", () => {
45
+ it("throws when used outside a ClassNamesProvider", () => {
46
+ const { useClassNames } = createSlotRecipeContext(mockSlotRecipe);
47
+
48
+ expect(() => renderHook(() => useClassNames())).toThrow(
49
+ /useClassNames must be used within a ClassNamesProvider/,
50
+ );
51
+ });
52
+
53
+ it("returns the provided classnames inside ClassNamesProvider", () => {
54
+ const { useClassNames, ClassNamesProvider } = createSlotRecipeContext(mockSlotRecipe);
55
+ const value: MockClassnames = { root: "custom-root", label: "custom-label" };
56
+
57
+ const { result } = renderHook(() => useClassNames(), {
58
+ wrapper: ({ children }) => (
59
+ <ClassNamesProvider value={value}>{children}</ClassNamesProvider>
60
+ ),
61
+ });
62
+
63
+ expect(result.current).toEqual(value);
64
+ });
65
+ });
66
+
67
+ describe("useProps", () => {
68
+ it("returns null outside of PropsProvider", () => {
69
+ const { useProps } = createSlotRecipeContext(mockSlotRecipe);
70
+ const { result } = renderHook(() => useProps());
71
+
72
+ expect(result.current).toBeNull();
73
+ });
74
+
75
+ it("returns the provided value inside PropsProvider", () => {
76
+ const { useProps, PropsProvider } = createSlotRecipeContext(mockSlotRecipe);
77
+ const { result } = renderHook(() => useProps(), {
78
+ wrapper: ({ children }) => (
79
+ <PropsProvider value={{ variant: "b" }}>{children}</PropsProvider>
80
+ ),
81
+ });
82
+
83
+ expect(result.current).toEqual({ variant: "b" });
84
+ });
85
+ });
86
+
87
+ describe("withRootProvider", () => {
88
+ it("injects classnames into context without applying className on the root element", () => {
89
+ const { Mock: Root, received: rootReceived } = createMock("Root");
90
+ const { Mock: Child, received: childReceived } = createMock("Child");
91
+ const { withRootProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
92
+
93
+ const StyledRoot = withRootProvider<MockVariantProps>(Root);
94
+ const StyledLabel = withContext<{ tag: "mock" }, Record<string, unknown>>(Child, "label");
95
+
96
+ render(
97
+ <StyledRoot variant="b">
98
+ <StyledLabel />
99
+ </StyledRoot>,
100
+ );
101
+
102
+ expect(rootReceived.props).not.toHaveProperty("className");
103
+ expect(childReceived.props?.className).toBe("label-b");
104
+ });
105
+ });
106
+
107
+ describe("withProvider", () => {
108
+ it("applies slot className to the provider element and injects context", () => {
109
+ const { Mock: Root, received: rootReceived } = createMock("Root");
110
+ const { Mock: Child, received: childReceived } = createMock("Child");
111
+ const { withProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
112
+
113
+ const StyledRoot = withProvider<{ tag: "mock" }, MockVariantProps>(Root, "root");
114
+ const StyledLabel = withContext<{ tag: "mock" }, Record<string, unknown>>(Child, "label");
115
+
116
+ render(
117
+ <StyledRoot variant="a">
118
+ <StyledLabel />
119
+ </StyledRoot>,
120
+ );
121
+
122
+ expect(rootReceived.props?.className).toBe("root-a");
123
+ expect(childReceived.props?.className).toBe("label-a");
124
+ });
125
+
126
+ it("merges user-provided className with the slot className", () => {
127
+ const { Mock: Root, received: rootReceived } = createMock("Root");
128
+ const { withProvider } = createSlotRecipeContext(mockSlotRecipe);
129
+ type StyledProps = MockVariantProps & { className?: string };
130
+ const StyledRoot = withProvider<{ tag: "mock" }, StyledProps>(Root, "root");
131
+
132
+ render(<StyledRoot variant="a" className="extra" />);
133
+
134
+ expect(rootReceived.props?.className).toBe("root-a extra");
135
+ });
136
+
137
+ it("forwards the ref to the provider element", () => {
138
+ const { Mock: Root, received } = createMock("Root");
139
+ const { withProvider } = createSlotRecipeContext(mockSlotRecipe);
140
+ const StyledRoot = withProvider<{ tag: "mock" }, MockVariantProps>(Root, "root");
141
+ const refFn = (node: { tag: "mock" } | null) => {
142
+ received.refValue = node;
143
+ };
144
+
145
+ render(<StyledRoot ref={refFn} variant="a" />);
146
+
147
+ expect(received.refValue).toBe(refFn);
148
+ });
149
+
150
+ it("prefers innerProps over PropsProvider and defaultProps", () => {
151
+ const { Mock: Root, received } = createMock("Root");
152
+ const { withProvider, PropsProvider } = createSlotRecipeContext(mockSlotRecipe);
153
+ const StyledRoot = withProvider<{ tag: "mock" }, MockVariantProps>(Root, "root", {
154
+ defaultProps: { variant: "a" },
155
+ });
156
+
157
+ render(
158
+ <PropsProvider value={{ variant: "a" }}>
159
+ <StyledRoot variant="b" />
160
+ </PropsProvider>,
161
+ );
162
+
163
+ expect(received.props?.className).toBe("root-b");
164
+ });
165
+ });
166
+
167
+ describe("withContext", () => {
168
+ it("reads slot className from ClassNamesProvider", () => {
169
+ const { Mock: Label, received } = createMock("Label");
170
+ const { ClassNamesProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
171
+ const StyledLabel = withContext<{ tag: "mock" }, Record<string, unknown>>(Label, "label");
172
+ const classNames: MockClassnames = { root: "r", label: "custom-label" };
173
+
174
+ render(
175
+ <ClassNamesProvider value={classNames}>
176
+ <StyledLabel />
177
+ </ClassNamesProvider>,
178
+ );
179
+
180
+ expect(received.props?.className).toBe("custom-label");
181
+ });
182
+
183
+ it("merges user-provided className with slot className", () => {
184
+ const { Mock: Label, received } = createMock("Label");
185
+ const { ClassNamesProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
186
+ type StyledProps = { className?: string };
187
+ const StyledLabel = withContext<{ tag: "mock" }, StyledProps>(Label, "label");
188
+
189
+ render(
190
+ <ClassNamesProvider value={{ root: "r", label: "l" }}>
191
+ <StyledLabel className="extra" />
192
+ </ClassNamesProvider>,
193
+ );
194
+
195
+ expect(received.props?.className).toBe("l extra");
196
+ });
197
+
198
+ it("passes through native props like data attributes", () => {
199
+ const { Mock: Label, received } = createMock("Label");
200
+ const { ClassNamesProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
201
+ type StyledProps = { "data-testid"?: string };
202
+ const StyledLabel = withContext<{ tag: "mock" }, StyledProps>(Label, "label");
203
+
204
+ render(
205
+ <ClassNamesProvider value={{ root: "r", label: "l" }}>
206
+ <StyledLabel data-testid="hello" />
207
+ </ClassNamesProvider>,
208
+ );
209
+
210
+ expect(received.props?.["data-testid"]).toBe("hello");
211
+ });
212
+
213
+ it("renders without a slot className when slot argument is omitted", () => {
214
+ const { Mock: Label, received } = createMock("Label");
215
+ const { ClassNamesProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
216
+ const StyledLabel = withContext<{ tag: "mock" }, Record<string, unknown>>(Label);
217
+
218
+ render(
219
+ <ClassNamesProvider value={{ root: "r", label: "l" }}>
220
+ <StyledLabel />
221
+ </ClassNamesProvider>,
222
+ );
223
+
224
+ // clsx(undefined, undefined) → ""
225
+ expect(received.props?.className).toBe("");
226
+ });
227
+ });
228
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { splitMultipleVariantsProps } from "../split-multiple-variants-props";
3
+
4
+ function makeRecipe<V extends Record<string, unknown>>(variantKeys: (keyof V)[]) {
5
+ return {
6
+ splitVariantProps: (props: Record<string, unknown>) => {
7
+ const variantProps: Record<string, unknown> = {};
8
+ const rest: Record<string, unknown> = {};
9
+ for (const key in props) {
10
+ if (variantKeys.includes(key as keyof V)) {
11
+ variantProps[key] = props[key];
12
+ } else {
13
+ rest[key] = props[key];
14
+ }
15
+ }
16
+ return [variantProps, rest] as [V, Omit<Record<string, unknown>, keyof V>];
17
+ },
18
+ };
19
+ }
20
+
21
+ describe("splitMultipleVariantsProps", () => {
22
+ it("splits props across disjoint recipes", () => {
23
+ const a = makeRecipe<{ size: string }>(["size"]);
24
+ const b = makeRecipe<{ tone: string }>(["tone"]);
25
+
26
+ const [{ a: av, b: bv }, rest] = splitMultipleVariantsProps(
27
+ { size: "32", tone: "brand", onTap: () => {}, children: "x" },
28
+ { a, b },
29
+ );
30
+
31
+ expect(av).toEqual({ size: "32" });
32
+ expect(bv).toEqual({ tone: "brand" });
33
+ expect(rest).toEqual({ onTap: expect.any(Function), children: "x" });
34
+ });
35
+
36
+ it("puts shared variant keys into every recipe bucket", () => {
37
+ const a = makeRecipe<{ size: string }>(["size"]);
38
+ const b = makeRecipe<{ size: string; tone: string }>(["size", "tone"]);
39
+
40
+ const [{ a: av, b: bv }, rest] = splitMultipleVariantsProps(
41
+ { size: "32", tone: "brand", disabled: true },
42
+ { a, b },
43
+ );
44
+
45
+ expect(av).toEqual({ size: "32" });
46
+ expect(bv).toEqual({ size: "32", tone: "brand" });
47
+ expect(rest).toEqual({ disabled: true });
48
+ });
49
+
50
+ it("returns empty variant buckets when no recipe claims a prop", () => {
51
+ const a = makeRecipe<{ size: string }>(["size"]);
52
+
53
+ const [{ a: av }, rest] = splitMultipleVariantsProps(
54
+ { onTap: () => {}, className: "x" },
55
+ { a },
56
+ );
57
+
58
+ expect(av).toEqual({});
59
+ expect(rest).toEqual({ onTap: expect.any(Function), className: "x" });
60
+ });
61
+ });
@@ -0,0 +1,147 @@
1
+ import { createContext, forwardRef, useContext } from "@lynx-js/react";
2
+ import clsx from "clsx";
3
+
4
+ type SlotRecipe<
5
+ Props extends Record<string, string | boolean | undefined>,
6
+ Classnames extends Record<string, string>,
7
+ > = ((props?: Props) => Classnames) & {
8
+ splitVariantProps: <T extends Props>(props: T) => [Props, Omit<T, keyof Props>];
9
+ };
10
+
11
+ export function createSlotRecipeContext<
12
+ Props extends Record<string, string | boolean | undefined>,
13
+ Classnames extends Record<string, string>,
14
+ >(recipe: SlotRecipe<Props, Classnames>) {
15
+ const ClassNamesContext = createContext<Classnames | null>(null);
16
+ const PropsContext = createContext<Props | null>(null);
17
+
18
+ const ClassNamesProvider = ({
19
+ children,
20
+ value,
21
+ }: {
22
+ children: React.ReactNode;
23
+ value: Classnames;
24
+ }) => {
25
+ return <ClassNamesContext.Provider value={value}>{children}</ClassNamesContext.Provider>;
26
+ };
27
+
28
+ const PropsProvider = ({ children, value }: { children: React.ReactNode; value: Props }) => {
29
+ return <PropsContext.Provider value={value}>{children}</PropsContext.Provider>;
30
+ };
31
+
32
+ function useClassNames() {
33
+ const context = useContext(ClassNamesContext);
34
+ if (context === null) {
35
+ throw new Error(
36
+ "useClassNames must be used within a ClassNamesProvider. Did you forget to wrap your component in a ClassNamesProvider?",
37
+ );
38
+ }
39
+
40
+ return context;
41
+ }
42
+
43
+ function useProps() {
44
+ return useContext(PropsContext);
45
+ }
46
+
47
+ const withRootProvider = <P,>(
48
+ Component: React.ElementType<any>,
49
+ options?: {
50
+ defaultProps?: Partial<P>;
51
+ },
52
+ ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P>> => {
53
+ const { defaultProps } = options ?? {};
54
+
55
+ const StyledComponent = (innerProps: any) => {
56
+ const props = { ...(defaultProps ?? {}), ...useProps(), ...innerProps } as Props &
57
+ Record<string, unknown>;
58
+ const [variantProps, otherProps] = recipe.splitVariantProps(props);
59
+ const classNames = recipe(variantProps);
60
+
61
+ return (
62
+ <ClassNamesProvider value={classNames}>
63
+ <PropsProvider value={variantProps}>
64
+ <Component {...otherProps} />
65
+ </PropsProvider>
66
+ </ClassNamesProvider>
67
+ );
68
+ };
69
+
70
+ // @ts-expect-error — assigning displayName to a plain function component
71
+ StyledComponent.displayName = Component.displayName || Component.name;
72
+
73
+ return StyledComponent as any;
74
+ };
75
+
76
+ const withProvider = <T, P>(
77
+ Component: React.ElementType<any>,
78
+ slot: keyof Classnames,
79
+ options?: {
80
+ defaultProps?: Partial<P>;
81
+ },
82
+ ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
83
+ const { defaultProps } = options ?? {};
84
+
85
+ const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
86
+ const props: any = { ...(defaultProps ?? {}), ...useProps(), ...innerProps };
87
+ const [variantProps, otherProps] = recipe.splitVariantProps(props as Props);
88
+ const classNames = recipe(variantProps);
89
+ const slotClassName: string | undefined = classNames[slot];
90
+ const userClassName: string | undefined = props["className"];
91
+
92
+ return (
93
+ <ClassNamesProvider value={classNames}>
94
+ <PropsProvider value={variantProps}>
95
+ <Component
96
+ {...(ref ? { ref } : {})}
97
+ {...otherProps}
98
+ className={clsx(slotClassName, userClassName)}
99
+ />
100
+ </PropsProvider>
101
+ </ClassNamesProvider>
102
+ );
103
+ });
104
+
105
+ StyledComponent.displayName = (Component as any).displayName || (Component as any).name;
106
+
107
+ return StyledComponent as any;
108
+ };
109
+
110
+ const withContext = <T, P>(
111
+ Component: React.ElementType<any>,
112
+ slot?: keyof Classnames,
113
+ options?: {
114
+ defaultProps?: Partial<P>;
115
+ },
116
+ ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
117
+ const { defaultProps } = options ?? {};
118
+
119
+ const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
120
+ const props: any = { ...(defaultProps ?? {}), ...innerProps };
121
+ const classNames = useClassNames();
122
+ const slotClassName: string | undefined = slot ? classNames[slot] : undefined;
123
+ const userClassName: string | undefined = props["className"];
124
+
125
+ return (
126
+ <Component
127
+ {...(ref ? { ref } : {})}
128
+ {...props}
129
+ className={clsx(slotClassName, userClassName)}
130
+ />
131
+ );
132
+ });
133
+
134
+ StyledComponent.displayName = (Component as any).displayName || (Component as any).name;
135
+ return StyledComponent as any;
136
+ };
137
+
138
+ return {
139
+ ClassNamesProvider,
140
+ PropsProvider,
141
+ useClassNames,
142
+ useProps,
143
+ withRootProvider,
144
+ withProvider,
145
+ withContext,
146
+ };
147
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./create-slot-recipe-context";
2
+ export * from "./get-seed-class-name";
3
+ export * from "./resolve-recipe-token";
4
+ export * from "./split-multiple-variants-props";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Rootage vars 객체에서 `path` 를 따라 token 값(주로 `"var(--seed-...)"` 스트링)을 꺼낸다.
3
+ *
4
+ * 쓰임새: Lynx `<image>` 의 `tint-color` / `width` / `height` 처럼 CSS 만으로는
5
+ * override 할 수 없거나(아이콘 패키지가 inline style 을 박는 경우 등) React 가
6
+ * recipe 와 같은 소스에서 꺼내 inline attribute 로 주입해야 하는 속성을 다룰 때.
7
+ *
8
+ * ```ts
9
+ * import { actionButton as actionButtonVars } from "@seed-design/lynx-css/vars/component";
10
+ * import { capitalize, resolveRecipeToken } from "@seed-design/lynx-react/utils/resolve-recipe-token";
11
+ *
12
+ * // variant × state × slot → color
13
+ * const color = resolveRecipeToken(actionButtonVars, [
14
+ * `variant${capitalize(variant)}`,
15
+ * state, // "enabled" | "disabled" | "loading" | "pressed"
16
+ * "prefixIcon",
17
+ * "color",
18
+ * ]);
19
+ *
20
+ * // size × layout → icon size
21
+ * const size = resolveRecipeToken(actionButtonVars, [
22
+ * `size${capitalize(size)}Layout${capitalize(layout)}`,
23
+ * "enabled",
24
+ * "prefixIcon",
25
+ * "size",
26
+ * ]);
27
+ * ```
28
+ *
29
+ * recipe token 경로가 컴포넌트마다 조금씩 다르므로(variant/size/layout 조합 여부 등)
30
+ * 이 유틸은 "vars + path" 조합만 제공하고, 각 컴포넌트가 자기 path 를 만들어 호출한다.
31
+ */
32
+ export function resolveRecipeToken(vars: unknown, path: readonly string[]): string | undefined {
33
+ let cursor: unknown = vars;
34
+ for (const key of path) {
35
+ if (cursor == null || typeof cursor !== "object") return undefined;
36
+ cursor = (cursor as Record<string, unknown>)[key];
37
+ }
38
+ return typeof cursor === "string" ? cursor : undefined;
39
+ }
40
+
41
+ /**
42
+ * 첫 글자 대문자. recipe variant key 조립용
43
+ * (e.g. `variant` + `brandSolid` → `variantBrandSolid`).
44
+ */
45
+ export function capitalize<T extends string>(value: T): Capitalize<T> {
46
+ return (value.charAt(0).toUpperCase() + value.slice(1)) as Capitalize<T>;
47
+ }
@@ -0,0 +1,59 @@
1
+ type RecipeLike = {
2
+ splitVariantProps: (props: Record<string, unknown>) => [Record<string, unknown>, unknown];
3
+ };
4
+
5
+ type ExtractVariantProps<T> = T extends {
6
+ splitVariantProps: (...args: never[]) => [infer V, unknown];
7
+ }
8
+ ? V
9
+ : never;
10
+
11
+ type ExtractAllVariantKeys<R> = {
12
+ [K in keyof R]: ExtractVariantProps<R[K]> extends infer V
13
+ ? V extends Record<string, unknown>
14
+ ? keyof V
15
+ : never
16
+ : never;
17
+ }[keyof R];
18
+
19
+ /**
20
+ * Split `props` across multiple recipes' variant extractors in one pass.
21
+ *
22
+ * Given `recipesMap` of `{ [recipeKey]: recipe }`, returns:
23
+ * - A bucket per recipe key with that recipe's variant-only props.
24
+ * - `remainingProps` with every recipe variant key stripped out.
25
+ *
26
+ * Keys that appear in more than one recipe (e.g. shared `size`) are copied
27
+ * into each corresponding bucket. `remainingProps` never includes any key
28
+ * that any recipe claimed as a variant.
29
+ *
30
+ * Mirror of `packages/react/src/utils/splitMultipleVariantsProps` adapted
31
+ * to TypeScript for Lynx. Use when a single compound component composes two
32
+ * or more recipes (e.g. Switch's `switch` + `switchmark`).
33
+ */
34
+ export function splitMultipleVariantsProps<R extends Record<string, RecipeLike>, P>(
35
+ props: P,
36
+ recipesMap: R,
37
+ ): [{ [K in keyof R]: ExtractVariantProps<R[K]> }, Omit<P, ExtractAllVariantKeys<R>>] {
38
+ const multipleVariantsProps = {} as { [K in keyof R]: ExtractVariantProps<R[K]> };
39
+ const extractedKeys = new Set<string>();
40
+ const propsRecord = props as Record<string, unknown>;
41
+
42
+ for (const recipeKey in recipesMap) {
43
+ const [variantProps] = recipesMap[recipeKey].splitVariantProps(propsRecord);
44
+ multipleVariantsProps[recipeKey] = variantProps as ExtractVariantProps<R[keyof R]>;
45
+
46
+ for (const variantPropKey in variantProps) {
47
+ extractedKeys.add(variantPropKey);
48
+ }
49
+ }
50
+
51
+ const remainingProps = {} as Record<string, unknown>;
52
+
53
+ for (const propKey in propsRecord) {
54
+ if (extractedKeys.has(propKey)) continue;
55
+ remainingProps[propKey] = propsRecord[propKey];
56
+ }
57
+
58
+ return [multipleVariantsProps, remainingProps as Omit<P, ExtractAllVariantKeys<R>>];
59
+ }
@@ -1,10 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { type StyleProps } from "../../utils/styled";
3
- export interface BoxProps extends StyleProps {
4
- asChild?: boolean;
5
- className?: string;
6
- style?: React.CSSProperties;
7
- children?: ReactNode;
8
- }
9
- export declare const Box: import("react").ForwardRefExoticComponent<BoxProps & import("react").RefAttributes<any>>;
10
- //# sourceMappingURL=Box.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Box.d.ts","sourceRoot":"","sources":["../../../src/components/Box/Box.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEpE,MAAM,WAAW,QAAS,SAAQ,UAAU;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,eAAO,MAAM,GAAG,0FAgBd,CAAC"}
@@ -1,19 +0,0 @@
1
- import clsx from "clsx";
2
- import { forwardRef } from "react";
3
- import { Slot } from "@seed-design/lynx-primitive";
4
- import { useStyleProps } from "../../utils/styled";
5
- export const Box = forwardRef((props, ref) => {
6
- const { style, restProps } = useStyleProps(props);
7
- const { asChild = false, className, children, ...nativeProps } = restProps;
8
- const mergedProps = {
9
- ...(ref ? { ref } : {}),
10
- className: clsx("seed-box", className),
11
- style: style,
12
- ...nativeProps,
13
- };
14
- if (asChild) {
15
- return <Slot {...mergedProps}>{children}</Slot>;
16
- }
17
- return <view {...mergedProps}>{children}</view>;
18
- });
19
- Box.displayName = "Box";
@@ -1,2 +0,0 @@
1
- export { Box, type BoxProps } from "./Box";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Box/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAC"}
@@ -1 +0,0 @@
1
- export { Box } from "./Box";
@@ -1,33 +0,0 @@
1
- import { type BoxProps } from "../Box/Box";
2
- export interface FlexProps extends Omit<BoxProps, "display"> {
3
- /**
4
- * @default "flex"
5
- */
6
- display?: "flex" | "none";
7
- /**
8
- * Shorthand for `flexDirection`.
9
- */
10
- direction?: BoxProps["flexDirection"];
11
- /**
12
- * Shorthand for `flexWrap`.
13
- */
14
- wrap?: BoxProps["flexWrap"];
15
- /**
16
- * Shorthand for `alignItems`.
17
- */
18
- align?: BoxProps["alignItems"];
19
- /**
20
- * Shorthand for `justifyContent`.
21
- */
22
- justify?: BoxProps["justifyContent"];
23
- /**
24
- * Shorthand for `flexGrow`.
25
- */
26
- grow?: BoxProps["flexGrow"];
27
- /**
28
- * Shorthand for `flexShrink`.
29
- */
30
- shrink?: BoxProps["flexShrink"];
31
- }
32
- export declare const Flex: import("react").ForwardRefExoticComponent<FlexProps & import("react").RefAttributes<any>>;
33
- //# sourceMappingURL=Flex.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Flex.d.ts","sourceRoot":"","sources":["../../../src/components/Flex/Flex.tsx"],"names":[],"mappings":"AACA,OAAO,EAAO,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC1D;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAE1B;;OAEG;IACH,SAAS,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAEtC;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAE/B;;OAEG;IACH,OAAO,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAErC;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;CACjC;AAED,eAAO,MAAM,IAAI,2FAgBf,CAAC"}
@@ -1,7 +0,0 @@
1
- import { forwardRef } from "react";
2
- import { Box } from "../Box/Box";
3
- export const Flex = forwardRef((props, ref) => {
4
- const { direction, wrap, align, justify, grow, shrink, ...rest } = props;
5
- return (<Box ref={ref} display="flex" flexDirection={direction} flexWrap={wrap} alignItems={align} justifyContent={justify} flexGrow={grow} flexShrink={shrink} {...rest}/>);
6
- });
7
- Flex.displayName = "Flex";
@@ -1,2 +0,0 @@
1
- export { Flex, type FlexProps } from "./Flex";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Flex/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC"}
@@ -1 +0,0 @@
1
- export { Flex } from "./Flex";
@@ -1,9 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { type StyleProps } from "../../utils/styled";
3
- import type { ListProps as LynxListProps } from "@lynx-js/types";
4
- export interface ListProps extends StyleProps, Omit<LynxListProps, "style"> {
5
- asChild?: boolean;
6
- children?: ReactNode;
7
- }
8
- export declare const List: import("react").NamedExoticComponent<Omit<ListProps, "ref"> & import("react").RefAttributes<any>>;
9
- //# sourceMappingURL=List.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"List.d.ts","sourceRoot":"","sources":["../../../src/components/List/List.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,WAAW,SAAU,SAAQ,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAuBD,eAAO,MAAM,IAAI,mGAAiB,CAAC"}