@seed-design/lynx-react 0.2.0-alpha.3 → 0.3.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 (179) hide show
  1. package/lib/_virtual/_rolldown/runtime.js +13 -0
  2. package/lib/components/ActionButton/ActionButton.d.ts +26 -33
  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 -31
  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 -29
  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 +5 -3
  84. package/lib/hooks/index.d.ts.map +1 -1
  85. package/lib/hooks/useControllableState.js +23 -23
  86. package/lib/hooks/useIconColor.d.ts +6 -3
  87. package/lib/hooks/useIconColor.d.ts.map +1 -1
  88. package/lib/hooks/useIconColor.js +34 -37
  89. package/lib/hooks/usePressTap.d.ts +11 -8
  90. package/lib/hooks/usePressTap.d.ts.map +1 -1
  91. package/lib/hooks/usePressTap.js +31 -31
  92. package/lib/hooks/useSafeArea.d.ts +19 -0
  93. package/lib/hooks/useSafeArea.d.ts.map +1 -0
  94. package/lib/hooks/useSafeArea.js +33 -0
  95. package/lib/hooks/useSeedClassName.d.ts +27 -0
  96. package/lib/hooks/useSeedClassName.d.ts.map +1 -0
  97. package/lib/hooks/useSeedClassName.js +32 -0
  98. package/lib/index.d.ts +4 -3
  99. package/lib/index.d.ts.map +1 -1
  100. package/lib/index.js +30 -3
  101. package/lib/types.d.ts +51 -0
  102. package/lib/types.d.ts.map +1 -0
  103. package/lib/utils/children.d.ts +3 -0
  104. package/lib/utils/children.d.ts.map +1 -0
  105. package/lib/utils/children.js +8 -0
  106. package/lib/utils/create-slot-recipe-context.d.ts +13 -12
  107. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
  108. package/lib/utils/create-slot-recipe-context.jsx +88 -70
  109. package/lib/utils/get-seed-class-name.d.ts +9 -7
  110. package/lib/utils/get-seed-class-name.d.ts.map +1 -1
  111. package/lib/utils/get-seed-class-name.js +29 -37
  112. package/lib/utils/index.d.ts +4 -4
  113. package/lib/utils/resolve-recipe-token.js +45 -43
  114. package/lib/utils/split-multiple-variants-props.js +32 -32
  115. package/lib/utils/styled.d.ts +83 -0
  116. package/lib/utils/styled.d.ts.map +1 -0
  117. package/lib/utils/styled.js +144 -0
  118. package/package.json +10 -9
  119. package/src/components/ActionButton/ActionButton.test.tsx +167 -0
  120. package/src/components/ActionButton/ActionButton.tsx +210 -169
  121. package/src/components/AppBar/AppBar.namespace.ts +18 -0
  122. package/src/components/AppBar/AppBar.test.tsx +270 -0
  123. package/src/components/AppBar/AppBar.tsx +288 -0
  124. package/src/components/AppBar/context.ts +27 -0
  125. package/src/components/AppBar/index.ts +21 -0
  126. package/src/components/AppBar/useAppBar.ts +100 -0
  127. package/src/components/Badge/Badge.test.tsx +54 -0
  128. package/src/components/Badge/Badge.tsx +32 -0
  129. package/src/components/Badge/index.ts +1 -0
  130. package/src/components/BottomSheet/BottomSheet.test.tsx +219 -0
  131. package/src/components/BottomSheet/BottomSheet.tsx +120 -83
  132. package/src/components/Box/Box.test.tsx +85 -0
  133. package/src/components/Box/Box.tsx +39 -0
  134. package/src/components/Box/index.ts +1 -0
  135. package/src/components/Checkbox/Checkbox.tsx +106 -116
  136. package/src/components/Icon/Icon.tsx +265 -0
  137. package/src/components/Icon/index.ts +8 -0
  138. package/src/components/ProgressCircle/ProgressCircle.tsx +39 -25
  139. package/src/components/RadioGroup/RadioGroup.tsx +98 -117
  140. package/src/components/Stack/Stack.tsx +97 -0
  141. package/src/components/Stack/index.ts +1 -0
  142. package/src/components/Switch/Switch.tsx +59 -50
  143. package/src/components/TagGroup/TagGroup.tsx +27 -25
  144. package/src/components/Text/Text.tsx +64 -0
  145. package/src/components/Text/index.ts +1 -0
  146. package/src/components/index.ts +6 -0
  147. package/src/hooks/index.ts +2 -0
  148. package/src/hooks/{__tests__/useControllableState.test.ts → useControllableState.test.ts} +1 -1
  149. package/src/hooks/{__tests__/useIconColor.test.ts → useIconColor.test.ts} +1 -1
  150. package/src/hooks/useIconColor.ts +22 -12
  151. package/src/hooks/{__tests__/usePressTap.test.ts → usePressTap.test.ts} +1 -1
  152. package/src/hooks/usePressTap.ts +14 -10
  153. package/src/hooks/useSafeArea.test.ts +83 -0
  154. package/src/hooks/useSafeArea.ts +56 -0
  155. package/src/hooks/useSeedClassName.ts +35 -0
  156. package/src/index.ts +1 -0
  157. package/src/types.ts +76 -0
  158. package/src/utils/children.ts +13 -0
  159. package/src/utils/create-slot-recipe-context.test.tsx +96 -0
  160. package/src/utils/create-slot-recipe-context.tsx +55 -15
  161. package/src/utils/get-seed-class-name.ts +10 -12
  162. package/src/utils/{__tests__/split-multiple-variants-props.test.ts → split-multiple-variants-props.test.ts} +1 -1
  163. package/src/utils/styled.ts +442 -0
  164. package/lib/components/ActionButton/index.js +0 -1
  165. package/lib/components/BottomSheet/index.js +0 -2
  166. package/lib/components/Checkbox/index.js +0 -2
  167. package/lib/components/ProgressCircle/index.js +0 -2
  168. package/lib/components/RadioGroup/index.js +0 -2
  169. package/lib/components/Switch/index.js +0 -2
  170. package/lib/components/TagGroup/index.js +0 -2
  171. package/lib/components/index.js +0 -7
  172. package/lib/hooks/index.js +0 -3
  173. package/lib/utils/index.js +0 -4
  174. package/src/components/ActionButton/__tests__/ActionButton.test.tsx +0 -35
  175. package/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +0 -119
  176. package/src/components/__tests__/ProgressCircle.test.ts +0 -33
  177. package/src/components/__tests__/exports.test.ts +0 -38
  178. package/src/components/__tests__/switch.test.ts +0 -34
  179. package/src/utils/__tests__/create-slot-recipe-context.test.tsx +0 -228
@@ -1,68 +1,88 @@
1
1
  import { actionButton } from "@seed-design/lynx-css/recipes/action-button";
2
2
  import type { ActionButtonVariantProps } from "@seed-design/lynx-css/recipes/action-button";
3
+ import { progressCircleVariantMap } from "@seed-design/lynx-css/recipes/progress-circle";
3
4
  import { actionButton as actionButtonVars } from "@seed-design/lynx-css/vars/component";
4
- import type { MainThread } from "@lynx-js/types";
5
5
  import clsx from "clsx";
6
- import * as React from "react";
7
- import { cloneElement, isValidElement, useMemo, type ReactElement } from "react";
6
+ import * as React from "@lynx-js/react";
7
+ import { isValidElement, useMemo } from "@lynx-js/react";
8
8
 
9
- import { useIconColor } from "../../hooks/useIconColor";
10
- import { usePressTap, type UsePressTapReturn } from "../../hooks/usePressTap";
9
+ import { usePressTap } from "../../hooks/usePressTap";
10
+ import type {
11
+ LynxElementProps,
12
+ LynxPressableProps,
13
+ LynxStyledElementProps,
14
+ LynxTextRef,
15
+ LynxTouchProps,
16
+ LynxViewProps,
17
+ LynxViewRef,
18
+ } from "../../types";
11
19
  import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
20
+ import { toArray } from "../../utils/children";
12
21
  import { capitalize, resolveRecipeToken } from "../../utils/resolve-recipe-token";
22
+ import { resolveFlexValue, type StyleProps } from "../../utils/styled";
13
23
  import {
14
24
  ProgressCircleRange,
15
25
  ProgressCircleRoot,
16
26
  type ProgressCircleRootProps,
17
27
  } from "../ProgressCircle";
28
+ import {
29
+ Icon,
30
+ IconRequired,
31
+ IconSlotProvider,
32
+ PrefixIcon,
33
+ SuffixIcon,
34
+ getIconSlotName,
35
+ type IconProps,
36
+ type PrefixIconProps,
37
+ type SuffixIconProps,
38
+ } from "../Icon/Icon";
18
39
 
19
40
  // Root/TextSlot 은 `withProvider("view", ...)` / `withContext("text", ...)` 를 쓰지 않는다.
20
41
  // intrinsic string 인자는 `React.createElement("view", ...)` 로 컴파일되어 Lynx 컴파일러의
21
42
  // 리터럴 JSX 정적 분석을 우회하고 `BackgroundSnapshot not found: view` 런타임 에러를 유발한다.
22
43
  // (자세한 내용: `packages/lynx-react/AGENTS.md` 의 "Native tag literal JSX constraint" 섹션)
23
- const { ClassNamesProvider, useClassNames, PropsProvider, useProps } =
24
- createSlotRecipeContext(actionButton);
25
-
26
- type IconSlotKey = "prefixIcon" | "suffixIcon" | "icon";
27
- type ActionButtonSize = NonNullable<ActionButtonVariantProps["size"]>;
44
+ const { ClassNamesProvider, useClassNames, PropsProvider } = createSlotRecipeContext(actionButton);
28
45
 
29
- type IconElementProps = {
30
- className?: string;
31
- style?: React.CSSProperties;
32
- ref?: React.Ref<MainThread.Element>;
33
- "main-thread:binduiappear"?: () => void;
34
- };
46
+ ////////////////////////////////////////////////////////////////////////////////////
35
47
 
36
- type ActionButtonContentProps = {
37
- children?: React.ReactNode;
48
+ interface ActionButtonContentProps extends LynxElementProps {
38
49
  isIconOnly: boolean;
39
- icon?: ReactElement<IconElementProps>;
40
- prefixIcon?: ReactElement<IconElementProps>;
41
- suffixIcon?: ReactElement<IconElementProps>;
42
- };
43
-
44
- type ActionButtonRootOwnProps = {
45
- className?: string;
46
- style?: React.CSSProperties;
47
- children?: React.ReactNode;
48
- bindtouchstart?: UsePressTapReturn["bindtouchstart"];
49
- bindtouchend?: UsePressTapReturn["bindtouchend"];
50
- bindtouchcancel?: UsePressTapReturn["bindtouchcancel"];
51
- bindtap?: UsePressTapReturn["bindtap"];
52
- "main-thread:bindtap"?: UsePressTapReturn["main-thread:bindtap"];
53
- };
54
-
55
- const progressCircleSizeMap = {
56
- xsmall: "14",
57
- small: "14",
58
- medium: "16",
59
- large: "18",
60
- } as const satisfies Record<ActionButtonSize, NonNullable<ProgressCircleRootProps["size"]>>;
61
-
62
- const ActionButtonRoot = React.forwardRef<
63
- unknown,
64
- ActionButtonVariantProps & ActionButtonRootOwnProps
65
- >((innerProps, ref) => {
50
+ icon?: IconProps["icon"];
51
+ prefixIcon?: PrefixIconProps["icon"];
52
+ suffixIcon?: SuffixIconProps["icon"];
53
+ }
54
+
55
+ interface ActionButtonAccessibilityProps {
56
+ "accessibility-label"?: LynxViewProps["accessibility-label"];
57
+ "accessibility-element"?: LynxViewProps["accessibility-element"];
58
+ "accessibility-traits"?: LynxViewProps["accessibility-traits"];
59
+ }
60
+
61
+ interface ActionButtonRootOwnProps
62
+ extends LynxStyledElementProps,
63
+ LynxTouchProps,
64
+ ActionButtonAccessibilityProps {}
65
+
66
+ interface ActionButtonRootProps extends ActionButtonVariantProps, ActionButtonRootOwnProps {}
67
+
68
+ function resolveProgressCircleSize(
69
+ actionButtonSize: ActionButtonVariantProps["size"],
70
+ ): ProgressCircleRootProps["size"] {
71
+ const size = actionButtonSize ?? "medium";
72
+ const token = resolveRecipeToken(actionButtonVars, [
73
+ `size${capitalize(size)}`,
74
+ "enabled",
75
+ "progressCircle",
76
+ "size",
77
+ ]);
78
+ const normalized = token?.endsWith("px") ? token.slice(0, -2) : token;
79
+
80
+ return progressCircleVariantMap.size.find((size) => size === normalized) ?? "16";
81
+ }
82
+
83
+ ////////////////////////////////////////////////////////////////////////////////////
84
+
85
+ const ActionButtonRoot = React.forwardRef<unknown, ActionButtonRootProps>((innerProps, ref) => {
66
86
  const props = { layout: "withText" as const, ...innerProps };
67
87
  const [variantProps, otherProps] = actionButton.splitVariantProps(props);
68
88
  const classNames = actionButton(variantProps);
@@ -82,31 +102,58 @@ const ActionButtonRoot = React.forwardRef<
82
102
  variantProps.loading,
83
103
  ],
84
104
  );
105
+ const iconSlotContextValue = useMemo(
106
+ () => ({
107
+ classNames: {
108
+ icon: classNames.icon,
109
+ prefixIcon: classNames.prefixIcon,
110
+ suffixIcon: classNames.suffixIcon,
111
+ },
112
+ deps: [
113
+ variantProps.variant ?? null,
114
+ variantProps.disabled ?? false,
115
+ variantProps.loading ?? false,
116
+ variantProps.pressed ?? false,
117
+ ],
118
+ }),
119
+ [
120
+ classNames.icon,
121
+ classNames.prefixIcon,
122
+ classNames.suffixIcon,
123
+ variantProps.variant,
124
+ variantProps.disabled,
125
+ variantProps.loading,
126
+ variantProps.pressed,
127
+ ],
128
+ );
85
129
  return (
86
130
  <ClassNamesProvider value={classNames}>
87
131
  <PropsProvider value={propsForContext}>
88
- <view
89
- {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
90
- {...rest}
91
- className={clsx(classNames.root, userClassName)}
92
- >
93
- {children as React.ReactNode}
94
- </view>
132
+ <IconSlotProvider value={iconSlotContextValue}>
133
+ <view
134
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
135
+ {...rest}
136
+ className={clsx(classNames.root, userClassName)}
137
+ >
138
+ {children as React.ReactNode}
139
+ </view>
140
+ </IconSlotProvider>
95
141
  </PropsProvider>
96
142
  </ClassNamesProvider>
97
143
  );
98
144
  });
99
145
  ActionButtonRoot.displayName = "ActionButtonRoot";
100
146
 
101
- const ActionButtonTextSlot = React.forwardRef<
102
- unknown,
103
- { children?: React.ReactNode; className?: string }
104
- >((props, ref) => {
147
+ ////////////////////////////////////////////////////////////////////////////////////
148
+
149
+ interface ActionButtonTextSlotProps extends LynxElementProps {}
150
+
151
+ const ActionButtonTextSlot = React.forwardRef<unknown, ActionButtonTextSlotProps>((props, ref) => {
105
152
  const { children, className: userClassName, ...rest } = props;
106
153
  const classNames = useClassNames();
107
154
  return (
108
155
  <text
109
- {...(ref ? { ref: ref as React.Ref<SVGTextElement> } : {})}
156
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
110
157
  {...rest}
111
158
  className={clsx(classNames.text, userClassName)}
112
159
  >
@@ -116,55 +163,7 @@ const ActionButtonTextSlot = React.forwardRef<
116
163
  });
117
164
  ActionButtonTextSlot.displayName = "ActionButtonTextSlot";
118
165
 
119
- /**
120
- * rootage vars 에서 현재 variant/size/layout 조합의 slot `size` 토큰
121
- * (예: `"var(--seed-dimension-x4)"`) 을 꺼낸다. 아이콘 컴포넌트가 inline
122
- * `style={{ width, height }}` 를 박기 때문에 style prop 으로 덮어 씌워야 recipe 사이즈가 적용된다.
123
- */
124
- function resolveIconSize(
125
- variantProps: ActionButtonVariantProps | null,
126
- slot: IconSlotKey,
127
- ): string | undefined {
128
- const size = variantProps?.size ?? "medium";
129
- // `icon` slot is only rendered under `layout="iconOnly"` and keyed at
130
- // `sizeXxxLayoutIconOnly.enabled.icon.size`. prefixIcon/suffixIcon follow
131
- // the current `layout` (`withText` by default) path.
132
- const layout = slot === "icon" ? "iconOnly" : (variantProps?.layout ?? "withText");
133
- return resolveRecipeToken(actionButtonVars, [
134
- `size${capitalize(size)}Layout${capitalize(layout)}`,
135
- "enabled",
136
- slot,
137
- "size",
138
- ]);
139
- }
140
-
141
- /**
142
- * `prefixIcon` / `suffixIcon` prop 으로 전달된 아이콘 element 에 slot className +
143
- * size(style) + main-thread tint-color ref 를 주입한다.
144
- */
145
- function ActionButtonIconSlot({
146
- icon,
147
- slot,
148
- }: {
149
- icon: ReactElement<IconElementProps>;
150
- slot: IconSlotKey;
151
- }) {
152
- const classNames = useClassNames();
153
- const variantProps = useProps() as ActionButtonVariantProps | null;
154
- const iconColor = useIconColor([
155
- variantProps?.variant ?? null,
156
- variantProps?.disabled ?? false,
157
- variantProps?.loading ?? false,
158
- ]);
159
- const sizeVar = resolveIconSize(variantProps, slot);
160
- const childProps = icon.props;
161
- return cloneElement(icon, {
162
- ...iconColor,
163
- className: clsx(classNames[slot], childProps.className),
164
- style:
165
- sizeVar != null ? { width: sizeVar, height: sizeVar, ...childProps.style } : childProps.style,
166
- });
167
- }
166
+ ////////////////////////////////////////////////////////////////////////////////////
168
167
 
169
168
  function ActionButtonContent({
170
169
  children,
@@ -173,25 +172,49 @@ function ActionButtonContent({
173
172
  prefixIcon,
174
173
  suffixIcon,
175
174
  }: ActionButtonContentProps) {
175
+ const childArray = toArray(children);
176
+ const prefixIconChildren: React.ReactNode[] = [];
177
+ const suffixIconChildren: React.ReactNode[] = [];
178
+ const iconChildren: React.ReactNode[] = [];
179
+ const textChildren: React.ReactNode[] = [];
180
+
181
+ for (const child of childArray) {
182
+ const slotName = getIconSlotName(child);
183
+
184
+ if (slotName === "prefixIcon") {
185
+ prefixIconChildren.push(child);
186
+ continue;
187
+ }
188
+ if (slotName === "suffixIcon") {
189
+ suffixIconChildren.push(child);
190
+ continue;
191
+ }
192
+ if (slotName === "icon") {
193
+ iconChildren.push(child);
194
+ continue;
195
+ }
196
+
197
+ textChildren.push(child);
198
+ }
199
+
176
200
  if (isIconOnly) {
177
- return icon != null && isValidElement(icon) ? (
178
- <ActionButtonIconSlot icon={icon} slot="icon" />
179
- ) : null;
201
+ if (icon != null && isValidElement(icon)) return <Icon icon={icon} />;
202
+ return iconChildren.length > 0 ? <>{iconChildren}</> : null;
180
203
  }
181
204
 
182
205
  return (
183
206
  <>
184
- {prefixIcon != null && isValidElement(prefixIcon) ? (
185
- <ActionButtonIconSlot icon={prefixIcon} slot="prefixIcon" />
186
- ) : null}
187
- <ActionButtonTextSlot>{children}</ActionButtonTextSlot>
188
- {suffixIcon != null && isValidElement(suffixIcon) ? (
189
- <ActionButtonIconSlot icon={suffixIcon} slot="suffixIcon" />
190
- ) : null}
207
+ {prefixIcon != null && isValidElement(prefixIcon) ? <PrefixIcon icon={prefixIcon} /> : null}
208
+ {prefixIconChildren}
209
+ {textChildren.length > 0 ? <ActionButtonTextSlot>{textChildren}</ActionButtonTextSlot> : null}
210
+ {suffixIconChildren}
211
+ {suffixIcon != null && isValidElement(suffixIcon) ? <SuffixIcon icon={suffixIcon} /> : null}
191
212
  </>
192
213
  );
193
214
  }
194
215
 
216
+ ////////////////////////////////////////////////////////////////////////////////////
217
+
195
218
  function ActionButtonLoadingContent(props: ActionButtonContentProps) {
196
219
  const classNames = useClassNames();
197
220
 
@@ -202,60 +225,63 @@ function ActionButtonLoadingContent(props: ActionButtonContentProps) {
202
225
  );
203
226
  }
204
227
 
205
- function ActionButtonLoadingIndicator({ size }: { size: ActionButtonSize }) {
228
+ ////////////////////////////////////////////////////////////////////////////////////
229
+
230
+ function ActionButtonLoadingIndicator({ size }: { size: ActionButtonVariantProps["size"] }) {
206
231
  const classNames = useClassNames();
232
+ const progressCircleSize = resolveProgressCircleSize(size);
207
233
 
208
234
  return (
209
235
  <view className={classNames.loadingIndicator}>
210
- <ProgressCircleRoot size={progressCircleSizeMap[size]} tone="inherit">
236
+ <ProgressCircleRoot size={progressCircleSize} tone="inherit">
211
237
  <ProgressCircleRange />
212
238
  </ProgressCircleRoot>
213
239
  </view>
214
240
  );
215
241
  }
216
242
 
243
+ ////////////////////////////////////////////////////////////////////////////////////
244
+
217
245
  /**
218
246
  * @platform Lynx
219
247
  *
220
248
  * 웹 대비 차이:
221
- * - 아이콘 전달 방식: 웹의 `<ActionButton.PrefixIcon svg={...} />` 가 아니라 `prefixIcon` /
222
- * `suffixIcon` / `icon` prop 으로 ReactElement 를 직접 넘긴다. Lynx `<text>` 가 flex
223
- * 컨테이너가 아니라 children 전체를 text 로 감싸면 아이콘이 flex item 이 안 되기 때문.
249
+ * - 아이콘 렌더링: 웹의 SVG `currentColor` 대신 Lynx `<image>` 의 `tint-color` 를
250
+ * `Icon` / `PrefixIcon` / `SuffixIcon` wrapper 가 동기화한다.
251
+ * - 호환 API: 기존 `prefixIcon` / `suffixIcon` / `icon` prop 도 유지한다.
224
252
  * - 미지원 prop: `color`, `fontWeight`, `bleedX`, `bleedY` (CSS variable 동적 주입 제한)
225
253
  *
226
254
  * ```tsx
227
255
  * import IconPlusFill from "@karrotmarket/lynx-monochrome-icon/IconPlusFill";
228
256
  * import IconChevronDownFill from "@karrotmarket/lynx-monochrome-icon/IconChevronDownFill";
257
+ * import { Icon, PrefixIcon, SuffixIcon } from "@seed-design/lynx-react";
229
258
  *
230
259
  * // withText (기본)
231
- * <ActionButton
232
- * variant="brandSolid"
233
- * prefixIcon={<IconPlusFill />}
234
- * suffixIcon={<IconChevronDownFill />}
235
- * >
260
+ * <ActionButton variant="brandSolid">
261
+ * <PrefixIcon icon={<IconPlusFill />} />
236
262
  * 라벨
263
+ * <SuffixIcon icon={<IconChevronDownFill />} />
237
264
  * </ActionButton>
238
265
  *
239
- * // iconOnly — `icon` prop 과 `aria-label` 필수
266
+ * // iconOnly — `<Icon />` child 와 `accessibility-label` 필수
240
267
  * <ActionButton
241
268
  * layout="iconOnly"
242
269
  * variant="neutralSolid"
243
- * icon={<IconPlusFill />}
244
- * aria-label="추가"
245
- * />
270
+ * accessibility-label="추가"
271
+ * >
272
+ * <Icon icon={<IconPlusFill />} />
273
+ * </ActionButton>
246
274
  * ```
247
275
  */
248
- export interface ActionButtonProps extends Omit<ActionButtonVariantProps, "layout" | "pressed"> {
249
- children?: React.ReactNode;
250
- className?: string;
251
- flexGrow?: number;
252
- layout?: "withText" | "iconOnly";
253
- icon?: ReactElement<IconElementProps>;
254
- prefixIcon?: ReactElement<IconElementProps>;
255
- suffixIcon?: ReactElement<IconElementProps>;
256
- "aria-label"?: string;
257
- bindtap?: () => void;
258
- "main-thread:bindtap"?: () => void;
276
+ export interface ActionButtonProps
277
+ extends Omit<ActionButtonVariantProps, "pressed">,
278
+ Pick<StyleProps, "flexGrow">,
279
+ LynxElementProps,
280
+ LynxPressableProps,
281
+ ActionButtonAccessibilityProps {
282
+ icon?: IconProps["icon"];
283
+ prefixIcon?: PrefixIconProps["icon"];
284
+ suffixIcon?: SuffixIconProps["icon"];
259
285
  }
260
286
 
261
287
  export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props, ref) => {
@@ -268,15 +294,25 @@ export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props,
268
294
  suffixIcon,
269
295
  bindtap,
270
296
  "main-thread:bindtap": mainThreadBindtap,
297
+ "accessibility-element": accessibilityElement = true,
298
+ "accessibility-label": accessibilityLabel,
299
+ "accessibility-traits": accessibilityTraits = "button",
271
300
  ...variantAndRest
272
301
  } = props;
273
302
  const { disabled = false, loading = false } = variantAndRest;
274
303
  const isInteractive = !disabled && !loading;
275
304
  const isIconOnly = layout === "iconOnly";
276
- const size = variantAndRest.size ?? "medium";
305
+ const size = variantAndRest.size;
277
306
 
278
- if (process.env.NODE_ENV !== "production" && isIconOnly && !props["aria-label"]) {
279
- console.warn('ActionButton: `layout="iconOnly"` requires `aria-label` for accessibility.');
307
+ if (
308
+ process.env.NODE_ENV !== "production" &&
309
+ isIconOnly &&
310
+ accessibilityElement &&
311
+ !accessibilityLabel
312
+ ) {
313
+ console.warn(
314
+ 'ActionButton: `layout="iconOnly"` requires `accessibility-label` for accessibility.',
315
+ );
280
316
  }
281
317
 
282
318
  const { pressed, ...pressTapHandlers } = usePressTap({
@@ -286,37 +322,42 @@ export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props,
286
322
  });
287
323
 
288
324
  return (
289
- <ActionButtonRoot
290
- {...variantAndRest}
291
- layout={layout}
292
- pressed={pressed}
293
- ref={ref}
294
- style={flexGrow != null ? { flexGrow } : undefined}
295
- {...pressTapHandlers}
296
- >
297
- {loading ? (
298
- <>
299
- <ActionButtonLoadingIndicator size={size} />
300
- <ActionButtonLoadingContent
325
+ <IconRequired enabled={isIconOnly}>
326
+ <ActionButtonRoot
327
+ {...variantAndRest}
328
+ layout={layout}
329
+ pressed={pressed}
330
+ ref={ref}
331
+ style={flexGrow != null ? { flexGrow: resolveFlexValue(flexGrow) } : undefined}
332
+ accessibility-element={accessibilityElement}
333
+ accessibility-label={accessibilityLabel}
334
+ accessibility-traits={accessibilityTraits}
335
+ {...pressTapHandlers}
336
+ >
337
+ {loading ? (
338
+ <>
339
+ <ActionButtonLoadingIndicator size={size} />
340
+ <ActionButtonLoadingContent
341
+ isIconOnly={isIconOnly}
342
+ icon={icon}
343
+ prefixIcon={prefixIcon}
344
+ suffixIcon={suffixIcon}
345
+ >
346
+ {children}
347
+ </ActionButtonLoadingContent>
348
+ </>
349
+ ) : (
350
+ <ActionButtonContent
301
351
  isIconOnly={isIconOnly}
302
352
  icon={icon}
303
353
  prefixIcon={prefixIcon}
304
354
  suffixIcon={suffixIcon}
305
355
  >
306
356
  {children}
307
- </ActionButtonLoadingContent>
308
- </>
309
- ) : (
310
- <ActionButtonContent
311
- isIconOnly={isIconOnly}
312
- icon={icon}
313
- prefixIcon={prefixIcon}
314
- suffixIcon={suffixIcon}
315
- >
316
- {children}
317
- </ActionButtonContent>
318
- )}
319
- </ActionButtonRoot>
357
+ </ActionButtonContent>
358
+ )}
359
+ </ActionButtonRoot>
360
+ </IconRequired>
320
361
  );
321
362
  });
322
363
  ActionButton.displayName = "ActionButton";
@@ -0,0 +1,18 @@
1
+ export {
2
+ AppBarIconButton as IconButton,
3
+ AppBarLeft as Left,
4
+ AppBarMain as Main,
5
+ AppBarRight as Right,
6
+ AppBarRoot as Root,
7
+ AppBarSlot as Slot,
8
+ AppBarSubtitle as Subtitle,
9
+ AppBarTitle as Title,
10
+ type AppBarIconButtonProps as IconButtonProps,
11
+ type AppBarLeftProps as LeftProps,
12
+ type AppBarMainProps as MainProps,
13
+ type AppBarRightProps as RightProps,
14
+ type AppBarRootProps as RootProps,
15
+ type AppBarSlotProps as SlotProps,
16
+ type AppBarSubtitleProps as SubtitleProps,
17
+ type AppBarTitleProps as TitleProps,
18
+ } from "./AppBar";