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

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 (186) hide show
  1. package/lib/_virtual/_rolldown/runtime.js +13 -0
  2. package/lib/components/ActionButton/ActionButton.d.ts +26 -32
  3. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
  4. package/lib/components/ActionButton/ActionButton.jsx +139 -120
  5. package/lib/components/ActionButton/index.d.ts +1 -1
  6. package/lib/components/AppBar/AppBar.d.ts +35 -0
  7. package/lib/components/AppBar/AppBar.d.ts.map +1 -0
  8. package/lib/components/AppBar/AppBar.jsx +132 -0
  9. package/lib/components/AppBar/AppBar.namespace.d.ts +2 -0
  10. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -0
  11. package/lib/components/AppBar/AppBar.namespace.js +15 -0
  12. package/lib/components/AppBar/context.d.ts +13 -0
  13. package/lib/components/AppBar/context.d.ts.map +1 -0
  14. package/lib/components/AppBar/context.js +12 -0
  15. package/lib/components/AppBar/index.d.ts +3 -0
  16. package/lib/components/AppBar/index.d.ts.map +1 -0
  17. package/lib/components/AppBar/useAppBar.d.ts +15 -0
  18. package/lib/components/AppBar/useAppBar.d.ts.map +1 -0
  19. package/lib/components/AppBar/useAppBar.js +72 -0
  20. package/lib/components/Badge/Badge.d.ts +14 -0
  21. package/lib/components/Badge/Badge.d.ts.map +1 -0
  22. package/lib/components/Badge/Badge.jsx +15 -0
  23. package/lib/components/Badge/index.d.ts +2 -0
  24. package/lib/components/Badge/index.d.ts.map +1 -0
  25. package/lib/components/BottomSheet/BottomSheet.d.ts +22 -27
  26. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  27. package/lib/components/BottomSheet/BottomSheet.jsx +127 -142
  28. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +1 -1
  29. package/lib/components/BottomSheet/BottomSheet.namespace.js +18 -1
  30. package/lib/components/BottomSheet/index.d.ts +2 -2
  31. package/lib/components/Box/Box.d.ts +20 -0
  32. package/lib/components/Box/Box.d.ts.map +1 -0
  33. package/lib/components/Box/Box.jsx +14 -0
  34. package/lib/components/Box/index.d.ts +2 -0
  35. package/lib/components/Box/index.d.ts.map +1 -0
  36. package/lib/components/Checkbox/Checkbox.d.ts +14 -30
  37. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
  38. package/lib/components/Checkbox/Checkbox.jsx +107 -98
  39. package/lib/components/Checkbox/Checkbox.namespace.d.ts +1 -1
  40. package/lib/components/Checkbox/Checkbox.namespace.js +12 -1
  41. package/lib/components/Checkbox/index.d.ts +2 -2
  42. package/lib/components/Icon/Icon.d.ts +37 -0
  43. package/lib/components/Icon/Icon.d.ts.map +1 -0
  44. package/lib/components/Icon/Icon.jsx +143 -0
  45. package/lib/components/Icon/index.d.ts +2 -0
  46. package/lib/components/Icon/index.d.ts.map +1 -0
  47. package/lib/components/ProgressCircle/ProgressCircle.d.ts +4 -8
  48. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
  49. package/lib/components/ProgressCircle/ProgressCircle.jsx +255 -260
  50. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +1 -1
  51. package/lib/components/ProgressCircle/ProgressCircle.namespace.js +9 -1
  52. package/lib/components/ProgressCircle/index.d.ts +2 -2
  53. package/lib/components/RadioGroup/RadioGroup.d.ts +12 -28
  54. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  55. package/lib/components/RadioGroup/RadioGroup.jsx +134 -118
  56. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +1 -1
  57. package/lib/components/RadioGroup/RadioGroup.namespace.js +12 -1
  58. package/lib/components/RadioGroup/index.d.ts +2 -2
  59. package/lib/components/Stack/Stack.d.ts +33 -0
  60. package/lib/components/Stack/Stack.d.ts.map +1 -0
  61. package/lib/components/Stack/Stack.jsx +39 -0
  62. package/lib/components/Stack/index.d.ts +2 -0
  63. package/lib/components/Stack/index.d.ts.map +1 -0
  64. package/lib/components/Switch/Switch.d.ts +8 -14
  65. package/lib/components/Switch/Switch.d.ts.map +1 -1
  66. package/lib/components/Switch/Switch.jsx +82 -56
  67. package/lib/components/Switch/Switch.namespace.d.ts +1 -1
  68. package/lib/components/Switch/Switch.namespace.js +11 -1
  69. package/lib/components/Switch/index.d.ts +2 -2
  70. package/lib/components/TagGroup/TagGroup.d.ts +8 -14
  71. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -1
  72. package/lib/components/TagGroup/TagGroup.jsx +42 -39
  73. package/lib/components/TagGroup/TagGroup.namespace.d.ts +1 -1
  74. package/lib/components/TagGroup/TagGroup.namespace.js +10 -1
  75. package/lib/components/TagGroup/index.d.ts +2 -2
  76. package/lib/components/Text/Text.d.ts +7 -0
  77. package/lib/components/Text/Text.d.ts.map +1 -0
  78. package/lib/components/Text/Text.jsx +28 -0
  79. package/lib/components/Text/index.d.ts +2 -0
  80. package/lib/components/Text/index.d.ts.map +1 -0
  81. package/lib/components/index.d.ts +13 -7
  82. package/lib/components/index.d.ts.map +1 -1
  83. package/lib/hooks/index.d.ts +4 -3
  84. package/lib/hooks/index.d.ts.map +1 -1
  85. package/lib/hooks/{use-controllable-state.d.ts → useControllableState.d.ts} +1 -1
  86. package/lib/hooks/useControllableState.d.ts.map +1 -0
  87. package/lib/hooks/useControllableState.js +27 -0
  88. package/lib/hooks/useIconColor.d.ts +19 -0
  89. package/lib/hooks/useIconColor.d.ts.map +1 -0
  90. package/lib/hooks/useIconColor.js +38 -0
  91. package/lib/hooks/{use-press-tap.d.ts → usePressTap.d.ts} +12 -9
  92. package/lib/hooks/usePressTap.d.ts.map +1 -0
  93. package/lib/hooks/usePressTap.js +35 -0
  94. package/lib/hooks/useSafeArea.d.ts +19 -0
  95. package/lib/hooks/useSafeArea.d.ts.map +1 -0
  96. package/lib/hooks/useSafeArea.js +33 -0
  97. package/lib/index.d.ts +4 -3
  98. package/lib/index.d.ts.map +1 -1
  99. package/lib/index.js +29 -3
  100. package/lib/types.d.ts +51 -0
  101. package/lib/types.d.ts.map +1 -0
  102. package/lib/utils/children.d.ts +3 -0
  103. package/lib/utils/children.d.ts.map +1 -0
  104. package/lib/utils/children.js +8 -0
  105. package/lib/utils/create-slot-recipe-context.d.ts +13 -12
  106. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
  107. package/lib/utils/create-slot-recipe-context.jsx +88 -70
  108. package/lib/utils/get-seed-class-name.d.ts +6 -7
  109. package/lib/utils/get-seed-class-name.d.ts.map +1 -1
  110. package/lib/utils/get-seed-class-name.js +26 -37
  111. package/lib/utils/index.d.ts +4 -4
  112. package/lib/utils/resolve-recipe-token.js +45 -43
  113. package/lib/utils/split-multiple-variants-props.js +32 -32
  114. package/lib/utils/styled.d.ts +83 -0
  115. package/lib/utils/styled.d.ts.map +1 -0
  116. package/lib/utils/styled.js +144 -0
  117. package/package.json +9 -9
  118. package/src/components/ActionButton/ActionButton.test.tsx +167 -0
  119. package/src/components/ActionButton/ActionButton.tsx +210 -168
  120. package/src/components/AppBar/AppBar.namespace.ts +18 -0
  121. package/src/components/AppBar/AppBar.test.tsx +270 -0
  122. package/src/components/AppBar/AppBar.tsx +288 -0
  123. package/src/components/AppBar/context.ts +27 -0
  124. package/src/components/AppBar/index.ts +21 -0
  125. package/src/components/AppBar/useAppBar.ts +100 -0
  126. package/src/components/Badge/Badge.test.tsx +54 -0
  127. package/src/components/Badge/Badge.tsx +32 -0
  128. package/src/components/Badge/index.ts +1 -0
  129. package/src/components/BottomSheet/BottomSheet.test.tsx +219 -0
  130. package/src/components/BottomSheet/BottomSheet.tsx +120 -83
  131. package/src/components/Box/Box.test.tsx +85 -0
  132. package/src/components/Box/Box.tsx +39 -0
  133. package/src/components/Box/index.ts +1 -0
  134. package/src/components/Checkbox/Checkbox.tsx +108 -117
  135. package/src/components/Icon/Icon.tsx +265 -0
  136. package/src/components/Icon/index.ts +8 -0
  137. package/src/components/ProgressCircle/ProgressCircle.tsx +39 -25
  138. package/src/components/RadioGroup/RadioGroup.tsx +100 -118
  139. package/src/components/Stack/Stack.tsx +97 -0
  140. package/src/components/Stack/index.ts +1 -0
  141. package/src/components/Switch/Switch.tsx +61 -52
  142. package/src/components/TagGroup/TagGroup.tsx +27 -25
  143. package/src/components/Text/Text.tsx +64 -0
  144. package/src/components/Text/index.ts +1 -0
  145. package/src/components/index.ts +6 -0
  146. package/src/hooks/index.ts +4 -3
  147. package/src/hooks/{__tests__/use-controllable-state.test.ts → useControllableState.test.ts} +1 -1
  148. package/src/hooks/useIconColor.test.ts +13 -0
  149. package/src/hooks/useIconColor.ts +67 -0
  150. package/src/hooks/{__tests__/use-press-tap.test.ts → usePressTap.test.ts} +1 -1
  151. package/src/hooks/{use-press-tap.ts → usePressTap.ts} +14 -10
  152. package/src/hooks/useSafeArea.test.ts +83 -0
  153. package/src/hooks/useSafeArea.ts +56 -0
  154. package/src/index.ts +1 -0
  155. package/src/types.ts +76 -0
  156. package/src/utils/children.ts +13 -0
  157. package/src/utils/create-slot-recipe-context.test.tsx +96 -0
  158. package/src/utils/create-slot-recipe-context.tsx +55 -15
  159. package/src/utils/get-seed-class-name.ts +7 -12
  160. package/src/utils/{__tests__/split-multiple-variants-props.test.ts → split-multiple-variants-props.test.ts} +1 -1
  161. package/src/utils/styled.ts +442 -0
  162. package/lib/components/ActionButton/index.js +0 -1
  163. package/lib/components/BottomSheet/index.js +0 -2
  164. package/lib/components/Checkbox/index.js +0 -2
  165. package/lib/components/ProgressCircle/index.js +0 -2
  166. package/lib/components/RadioGroup/index.js +0 -2
  167. package/lib/components/Switch/index.js +0 -2
  168. package/lib/components/TagGroup/index.js +0 -2
  169. package/lib/components/index.js +0 -7
  170. package/lib/hooks/index.js +0 -3
  171. package/lib/hooks/use-controllable-state.d.ts.map +0 -1
  172. package/lib/hooks/use-controllable-state.js +0 -27
  173. package/lib/hooks/use-icon-color.d.ts +0 -15
  174. package/lib/hooks/use-icon-color.d.ts.map +0 -1
  175. package/lib/hooks/use-icon-color.js +0 -37
  176. package/lib/hooks/use-press-tap.d.ts.map +0 -1
  177. package/lib/hooks/use-press-tap.js +0 -35
  178. package/lib/utils/index.js +0 -4
  179. package/src/components/ActionButton/__tests__/ActionButton.test.tsx +0 -35
  180. package/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +0 -119
  181. package/src/components/__tests__/ProgressCircle.test.ts +0 -33
  182. package/src/components/__tests__/exports.test.ts +0 -38
  183. package/src/components/__tests__/switch.test.ts +0 -34
  184. package/src/hooks/use-icon-color.ts +0 -51
  185. package/src/utils/__tests__/create-slot-recipe-context.test.tsx +0 -228
  186. /package/src/hooks/{use-controllable-state.ts → useControllableState.ts} +0 -0
@@ -0,0 +1,56 @@
1
+ import { useMemo } from "@lynx-js/react";
2
+
3
+ type SafeAreaEdge = "top" | "bottom";
4
+
5
+ declare module "@lynx-js/types" {
6
+ interface GlobalProps {
7
+ safeAreaInsetTop?: number;
8
+ safeAreaInsetBottom?: number;
9
+ }
10
+ }
11
+
12
+ const safeAreaProp: Record<SafeAreaEdge, "safeAreaInsetTop" | "safeAreaInsetBottom"> = {
13
+ top: "safeAreaInsetTop",
14
+ bottom: "safeAreaInsetBottom",
15
+ };
16
+
17
+ function getSafeAreaEnvInset(edge: SafeAreaEdge): string {
18
+ return `env(safe-area-inset-${edge})`;
19
+ }
20
+
21
+ function normalizeSafeAreaValue(value: number | undefined): string | undefined {
22
+ if (typeof value !== "number" || value <= 0 || !Number.isFinite(value)) {
23
+ return undefined;
24
+ }
25
+
26
+ return `${value}px`;
27
+ }
28
+
29
+ function resolveSafeAreaInset(edge: SafeAreaEdge, value: number | undefined): string {
30
+ return normalizeSafeAreaValue(value) ?? getSafeAreaEnvInset(edge);
31
+ }
32
+
33
+ export interface UseSafeAreaReturn {
34
+ safeAreaInsetTop: string;
35
+ safeAreaInsetBottom: string;
36
+ }
37
+
38
+ /**
39
+ * Lynx 앱에서 top/bottom safe area inset 값을 반환합니다.
40
+ *
41
+ * `lynx.__globalProps.safeAreaInsetTop`과 `safeAreaInsetBottom`을 우선 사용하고,
42
+ * host가 값을 제공하지 않으면 Lynx CSS `env(safe-area-inset-*)` 값을 fallback으로
43
+ * 반환합니다.
44
+ */
45
+ export function useSafeArea(): UseSafeAreaReturn {
46
+ const safeAreaInsetTop = lynx.__globalProps?.[safeAreaProp.top];
47
+ const safeAreaInsetBottom = lynx.__globalProps?.[safeAreaProp.bottom];
48
+
49
+ return useMemo(
50
+ () => ({
51
+ safeAreaInsetTop: resolveSafeAreaInset("top", safeAreaInsetTop),
52
+ safeAreaInsetBottom: resolveSafeAreaInset("bottom", safeAreaInsetBottom),
53
+ }),
54
+ [safeAreaInsetTop, safeAreaInsetBottom],
55
+ );
56
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./components";
2
2
  export * from "./hooks";
3
+ export type * from "./types";
3
4
  export * from "./utils";
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { ReactNode, Ref } from "@lynx-js/react";
2
+ import type {
3
+ CSSProperties,
4
+ IntrinsicElements,
5
+ MainThread,
6
+ NodesRef,
7
+ TextProps,
8
+ ViewProps,
9
+ } from "@lynx-js/types";
10
+
11
+ export type LynxViewProps = IntrinsicElements["view"] & ViewProps;
12
+ export type LynxTextProps = IntrinsicElements["text"] & TextProps;
13
+ export type LynxViewRef = Ref<NodesRef>;
14
+ export type LynxTextRef = Ref<NodesRef>;
15
+ export type LynxMainThreadRef = Ref<MainThread.Element>;
16
+ export type LynxStyle = CSSProperties;
17
+
18
+ export interface LynxElementProps {
19
+ children?: ReactNode;
20
+ className?: LynxViewProps["className"];
21
+ }
22
+
23
+ export interface LynxStyledElementProps extends LynxElementProps {
24
+ style?: LynxStyle;
25
+ }
26
+
27
+ export interface LynxPressableProps {
28
+ bindtap?: LynxViewProps["bindtap"];
29
+ "main-thread:bindtap"?: LynxViewProps["main-thread:bindtap"];
30
+ }
31
+
32
+ export interface LynxTouchProps {
33
+ bindtap?: LynxViewProps["bindtap"];
34
+ bindtouchstart?: LynxViewProps["bindtouchstart"];
35
+ bindtouchend?: LynxViewProps["bindtouchend"];
36
+ bindtouchcancel?: LynxViewProps["bindtouchcancel"];
37
+ "main-thread:bindtap"?: LynxViewProps["main-thread:bindtap"];
38
+ }
39
+
40
+ export interface LynxIconElementProps {
41
+ className?: IntrinsicElements["image"]["className"];
42
+ style?: LynxStyle;
43
+ ref?: LynxMainThreadRef;
44
+ "main-thread:binduiappear"?: IntrinsicElements["image"]["main-thread:binduiappear"];
45
+ }
46
+
47
+ /**
48
+ * @platform Lynx
49
+ *
50
+ * native element(`<view>`/`<text>`/`<image>` 등)가 공유하는 접근성 속성 모음.
51
+ * `@lynx-js/types`의 StandardProps 접근성 속성을 표준화한 타입으로, 컴포넌트가
52
+ * 확장해 native element에 패스스루한다. 모든 native tag가 동일한 공통 속성을
53
+ * 가지며 tag별 차이는 없다.
54
+ *
55
+ * web ARIA 대응:
56
+ * - `accessibility-label` ← `aria-label`
57
+ * - `accessibility-role-description` ← `role` (switch/checkbox/progressbar)
58
+ * - `accessibility-value` ← `aria-checked` / `aria-valuenow`
59
+ * - `accessibility-elements-hidden` ← `aria-hidden`
60
+ *
61
+ * `accessibility-traits`는 단일 값이라 `selected`+`disabled` 동시 표현이 안 된다.
62
+ * 역할은 `accessibility-role-description`, 상태는 `accessibility-value`로 분담한다.
63
+ */
64
+ export type LynxAccessibilityProps = Pick<
65
+ LynxViewProps,
66
+ | "accessibility-label"
67
+ | "accessibility-traits"
68
+ | "accessibility-element"
69
+ | "accessibility-value"
70
+ | "accessibility-role-description"
71
+ | "accessibility-elements-hidden"
72
+ | "accessibility-heading"
73
+ | "accessibility-actions"
74
+ | "accessibility-exclusive-focus"
75
+ | "ios-platform-accessibility-id"
76
+ >;
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "@lynx-js/react";
2
+
3
+ export function toArray(children: ReactNode): ReactNode[] {
4
+ if (children == null || typeof children === "boolean") {
5
+ return [];
6
+ }
7
+
8
+ if (Array.isArray(children)) {
9
+ return children.flatMap(toArray);
10
+ }
11
+
12
+ return [children];
13
+ }
@@ -0,0 +1,96 @@
1
+ import { forwardRef } from "@lynx-js/react";
2
+ import { render } 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 MockProps = {
30
+ children?: unknown;
31
+ className?: string;
32
+ };
33
+
34
+ type MockReceived = { props?: MockProps };
35
+
36
+ function createMock(name: string) {
37
+ const received: MockReceived = {};
38
+ const Mock = forwardRef<{ tag: "mock" }, MockProps>((props) => {
39
+ received.props = props;
40
+ return <>{props.children as never}</>;
41
+ });
42
+ Mock.displayName = name;
43
+ return { Mock, received };
44
+ }
45
+
46
+ describe("createSlotRecipeContext", () => {
47
+ it("rejects intrinsic native tags to avoid Lynx BackgroundSnapshot errors", () => {
48
+ const { withRootProvider, withProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
49
+
50
+ expect(() => withRootProvider<MockVariantProps>("view")).toThrow(
51
+ /literal JSX.*BackgroundSnapshot/,
52
+ );
53
+ expect(() => withProvider<unknown, MockVariantProps>("view", "root")).toThrow(
54
+ /literal JSX.*BackgroundSnapshot/,
55
+ );
56
+ expect(() => withContext<unknown, Record<string, unknown>>("text", "label")).toThrow(
57
+ /literal JSX.*BackgroundSnapshot/,
58
+ );
59
+ });
60
+
61
+ it("injects root classnames into context without applying className on the root element", () => {
62
+ const { Mock: Root, received: rootReceived } = createMock("Root");
63
+ const { Mock: Child, received: childReceived } = createMock("Child");
64
+ const { withRootProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
65
+
66
+ const StyledRoot = withRootProvider<MockVariantProps & MockProps>(Root);
67
+ const StyledLabel = withContext<{ tag: "mock" }, MockProps>(Child, "label");
68
+
69
+ render(
70
+ <StyledRoot variant="b">
71
+ <StyledLabel />
72
+ </StyledRoot>,
73
+ );
74
+
75
+ expect(rootReceived.props).not.toHaveProperty("className");
76
+ expect(childReceived.props?.className).toBe("label-b");
77
+ });
78
+
79
+ it("applies and merges provider slot classnames", () => {
80
+ const { Mock: Root, received: rootReceived } = createMock("Root");
81
+ const { Mock: Child, received: childReceived } = createMock("Child");
82
+ const { withProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
83
+
84
+ const StyledRoot = withProvider<{ tag: "mock" }, MockVariantProps & MockProps>(Root, "root");
85
+ const StyledLabel = withContext<{ tag: "mock" }, MockProps>(Child, "label");
86
+
87
+ render(
88
+ <StyledRoot variant="a" className="extra">
89
+ <StyledLabel className="child-extra" />
90
+ </StyledRoot>,
91
+ );
92
+
93
+ expect(rootReceived.props?.className).toBe("root-a extra");
94
+ expect(childReceived.props?.className).toBe("label-a child-extra");
95
+ });
96
+ });
@@ -1,4 +1,12 @@
1
1
  import { createContext, forwardRef, useContext } from "@lynx-js/react";
2
+ import type {
3
+ ElementType,
4
+ ForwardRefExoticComponent,
5
+ PropsWithoutRef,
6
+ ReactElement,
7
+ ReactNode,
8
+ RefAttributes,
9
+ } from "@lynx-js/react";
2
10
  import clsx from "clsx";
3
11
 
4
12
  type SlotRecipe<
@@ -8,24 +16,53 @@ type SlotRecipe<
8
16
  splitVariantProps: <T extends Props>(props: T) => [Props, Omit<T, keyof Props>];
9
17
  };
10
18
 
19
+ function assertNotIntrinsicComponent(Component: ElementType<any>, caller: string) {
20
+ if (typeof Component !== "string") return;
21
+
22
+ throw new Error(
23
+ `${caller} cannot wrap intrinsic tag "${Component}" in Lynx. Native <view>/<text> slots must be rendered as literal JSX in the component file; passing intrinsic strings through createSlotRecipeContext can cause BackgroundSnapshot not found errors.`,
24
+ );
25
+ }
26
+
11
27
  export function createSlotRecipeContext<
12
28
  Props extends Record<string, string | boolean | undefined>,
13
29
  Classnames extends Record<string, string>,
14
- >(recipe: SlotRecipe<Props, Classnames>) {
30
+ >(
31
+ recipe: SlotRecipe<Props, Classnames>,
32
+ ): {
33
+ ClassNamesProvider: (props: { children: ReactNode; value: Classnames }) => ReactElement;
34
+ PropsProvider: (props: { children: ReactNode; value: Props }) => ReactElement;
35
+ useClassNames: () => Classnames;
36
+ useProps: () => Props | null;
37
+ withRootProvider: <P>(
38
+ Component: ElementType<any>,
39
+ options?: {
40
+ defaultProps?: Partial<P>;
41
+ },
42
+ ) => ForwardRefExoticComponent<PropsWithoutRef<P>>;
43
+ withProvider: <T, P>(
44
+ Component: ElementType<any>,
45
+ slot: keyof Classnames,
46
+ options?: {
47
+ defaultProps?: Partial<P>;
48
+ },
49
+ ) => ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
50
+ withContext: <T, P>(
51
+ Component: ElementType<any>,
52
+ slot?: keyof Classnames,
53
+ options?: {
54
+ defaultProps?: Partial<P>;
55
+ },
56
+ ) => ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
57
+ } {
15
58
  const ClassNamesContext = createContext<Classnames | null>(null);
16
59
  const PropsContext = createContext<Props | null>(null);
17
60
 
18
- const ClassNamesProvider = ({
19
- children,
20
- value,
21
- }: {
22
- children: React.ReactNode;
23
- value: Classnames;
24
- }) => {
61
+ const ClassNamesProvider = ({ children, value }: { children: ReactNode; value: Classnames }) => {
25
62
  return <ClassNamesContext.Provider value={value}>{children}</ClassNamesContext.Provider>;
26
63
  };
27
64
 
28
- const PropsProvider = ({ children, value }: { children: React.ReactNode; value: Props }) => {
65
+ const PropsProvider = ({ children, value }: { children: ReactNode; value: Props }) => {
29
66
  return <PropsContext.Provider value={value}>{children}</PropsContext.Provider>;
30
67
  };
31
68
 
@@ -45,11 +82,12 @@ export function createSlotRecipeContext<
45
82
  }
46
83
 
47
84
  const withRootProvider = <P,>(
48
- Component: React.ElementType<any>,
85
+ Component: ElementType<any>,
49
86
  options?: {
50
87
  defaultProps?: Partial<P>;
51
88
  },
52
- ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P>> => {
89
+ ): ForwardRefExoticComponent<PropsWithoutRef<P>> => {
90
+ assertNotIntrinsicComponent(Component, "withRootProvider");
53
91
  const { defaultProps } = options ?? {};
54
92
 
55
93
  const StyledComponent = (innerProps: any) => {
@@ -74,12 +112,13 @@ export function createSlotRecipeContext<
74
112
  };
75
113
 
76
114
  const withProvider = <T, P>(
77
- Component: React.ElementType<any>,
115
+ Component: ElementType<any>,
78
116
  slot: keyof Classnames,
79
117
  options?: {
80
118
  defaultProps?: Partial<P>;
81
119
  },
82
- ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
120
+ ): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> => {
121
+ assertNotIntrinsicComponent(Component, "withProvider");
83
122
  const { defaultProps } = options ?? {};
84
123
 
85
124
  const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
@@ -108,12 +147,13 @@ export function createSlotRecipeContext<
108
147
  };
109
148
 
110
149
  const withContext = <T, P>(
111
- Component: React.ElementType<any>,
150
+ Component: ElementType<any>,
112
151
  slot?: keyof Classnames,
113
152
  options?: {
114
153
  defaultProps?: Partial<P>;
115
154
  },
116
- ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
155
+ ): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> => {
156
+ assertNotIntrinsicComponent(Component, "withContext");
117
157
  const { defaultProps } = options ?? {};
118
158
 
119
159
  const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
@@ -4,17 +4,16 @@ declare const lynx: {
4
4
  __globalProps?: Record<string, unknown>;
5
5
  };
6
6
 
7
- declare const SystemInfo: {
8
- platform?: "Android" | "iOS" | "Harmony" | "windows" | "macOS";
9
- };
7
+ export type ColorMode = "system" | "light-only" | "dark-only";
10
8
 
11
- type ColorMode = "system" | "light-only" | "dark-only";
9
+ export interface GetSeedClassNameOptions {
10
+ colorMode?: ColorMode;
11
+ }
12
12
 
13
13
  /**
14
14
  * Lynx 앱의 root `<page>` 요소에 적용할 SEED Design className을 반환한다.
15
15
  *
16
- * 테마(dark/light)와 플랫폼(iOS/Android)을 자동 감지하여
17
- * 렌더링 전에 올바른 CSS 변수가 적용되도록 한다.
16
+ * 테마(dark/light)를 자동 감지하여 렌더링 전에 올바른 CSS 변수가 적용되도록 한다.
18
17
  *
19
18
  * @example
20
19
  * ```tsx
@@ -27,7 +26,7 @@ type ColorMode = "system" | "light-only" | "dark-only";
27
26
  * );
28
27
  * ```
29
28
  */
30
- export function getSeedClassName(options?: { colorMode?: ColorMode }): string {
29
+ export function getSeedClassName(options?: GetSeedClassNameOptions): string {
31
30
  const { colorMode = "system" } = options ?? {};
32
31
 
33
32
  // Theme — lynx.__globalProps.theme에서 시스템 테마 읽기
@@ -44,9 +43,5 @@ export function getSeedClassName(options?: { colorMode?: ColorMode }): string {
44
43
  : "seed-user-color-scheme-light";
45
44
  }
46
45
 
47
- // Platform — SystemInfo.platform에서 디바이스 플랫폼 읽기
48
- const platform = SystemInfo?.platform;
49
- const platformClass = platform === "iOS" ? "seed-platform-ios" : "seed-platform-android";
50
-
51
- return `${themeClass} ${platformClass}`;
46
+ return themeClass;
52
47
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { splitMultipleVariantsProps } from "../split-multiple-variants-props";
2
+ import { splitMultipleVariantsProps } from "./split-multiple-variants-props";
3
3
 
4
4
  function makeRecipe<V extends Record<string, unknown>>(variantKeys: (keyof V)[]) {
5
5
  return {