@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
@@ -1,67 +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/use-icon-color";
10
- import { usePressTap, type UsePressTapReturn } from "../../hooks/use-press-tap";
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
- };
46
+ ////////////////////////////////////////////////////////////////////////////////////
34
47
 
35
- type ActionButtonContentProps = {
36
- children?: React.ReactNode;
48
+ interface ActionButtonContentProps extends LynxElementProps {
37
49
  isIconOnly: boolean;
38
- icon?: ReactElement<IconElementProps>;
39
- prefixIcon?: ReactElement<IconElementProps>;
40
- suffixIcon?: ReactElement<IconElementProps>;
41
- };
42
-
43
- type ActionButtonRootOwnProps = {
44
- className?: string;
45
- style?: React.CSSProperties;
46
- children?: React.ReactNode;
47
- bindtouchstart?: UsePressTapReturn["bindtouchstart"];
48
- bindtouchend?: UsePressTapReturn["bindtouchend"];
49
- bindtouchcancel?: UsePressTapReturn["bindtouchcancel"];
50
- bindtap?: UsePressTapReturn["bindtap"];
51
- "main-thread:bindtap"?: UsePressTapReturn["main-thread:bindtap"];
52
- };
53
-
54
- const progressCircleSizeMap = {
55
- xsmall: "14",
56
- small: "14",
57
- medium: "16",
58
- large: "18",
59
- } as const satisfies Record<ActionButtonSize, NonNullable<ProgressCircleRootProps["size"]>>;
60
-
61
- const ActionButtonRoot = React.forwardRef<
62
- unknown,
63
- ActionButtonVariantProps & ActionButtonRootOwnProps
64
- >((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) => {
65
86
  const props = { layout: "withText" as const, ...innerProps };
66
87
  const [variantProps, otherProps] = actionButton.splitVariantProps(props);
67
88
  const classNames = actionButton(variantProps);
@@ -81,31 +102,58 @@ const ActionButtonRoot = React.forwardRef<
81
102
  variantProps.loading,
82
103
  ],
83
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
+ );
84
129
  return (
85
130
  <ClassNamesProvider value={classNames}>
86
131
  <PropsProvider value={propsForContext}>
87
- <view
88
- {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
89
- {...rest}
90
- className={clsx(classNames.root, userClassName)}
91
- >
92
- {children as React.ReactNode}
93
- </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>
94
141
  </PropsProvider>
95
142
  </ClassNamesProvider>
96
143
  );
97
144
  });
98
145
  ActionButtonRoot.displayName = "ActionButtonRoot";
99
146
 
100
- const ActionButtonTextSlot = React.forwardRef<
101
- unknown,
102
- { children?: React.ReactNode; className?: string }
103
- >((props, ref) => {
147
+ ////////////////////////////////////////////////////////////////////////////////////
148
+
149
+ interface ActionButtonTextSlotProps extends LynxElementProps {}
150
+
151
+ const ActionButtonTextSlot = React.forwardRef<unknown, ActionButtonTextSlotProps>((props, ref) => {
104
152
  const { children, className: userClassName, ...rest } = props;
105
153
  const classNames = useClassNames();
106
154
  return (
107
155
  <text
108
- {...(ref ? { ref: ref as React.Ref<SVGTextElement> } : {})}
156
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
109
157
  {...rest}
110
158
  className={clsx(classNames.text, userClassName)}
111
159
  >
@@ -115,55 +163,7 @@ const ActionButtonTextSlot = React.forwardRef<
115
163
  });
116
164
  ActionButtonTextSlot.displayName = "ActionButtonTextSlot";
117
165
 
118
- /**
119
- * rootage vars 에서 현재 variant/size/layout 조합의 slot `size` 토큰
120
- * (예: `"var(--seed-dimension-x4)"`) 을 꺼낸다. 아이콘 컴포넌트가 inline
121
- * `style={{ width, height }}` 를 박기 때문에 style prop 으로 덮어 씌워야 recipe 사이즈가 적용된다.
122
- */
123
- function resolveIconSize(
124
- variantProps: ActionButtonVariantProps | null,
125
- slot: IconSlotKey,
126
- ): string | undefined {
127
- const size = variantProps?.size ?? "medium";
128
- // `icon` slot is only rendered under `layout="iconOnly"` and keyed at
129
- // `sizeXxxLayoutIconOnly.enabled.icon.size`. prefixIcon/suffixIcon follow
130
- // the current `layout` (`withText` by default) path.
131
- const layout = slot === "icon" ? "iconOnly" : (variantProps?.layout ?? "withText");
132
- return resolveRecipeToken(actionButtonVars, [
133
- `size${capitalize(size)}Layout${capitalize(layout)}`,
134
- "enabled",
135
- slot,
136
- "size",
137
- ]);
138
- }
139
-
140
- /**
141
- * `prefixIcon` / `suffixIcon` prop 으로 전달된 아이콘 element 에 slot className +
142
- * size(style) + main-thread tint-color ref 를 주입한다.
143
- */
144
- function ActionButtonIconSlot({
145
- icon,
146
- slot,
147
- }: {
148
- icon: ReactElement<IconElementProps>;
149
- slot: IconSlotKey;
150
- }) {
151
- const classNames = useClassNames();
152
- const variantProps = useProps() as ActionButtonVariantProps | null;
153
- const { ref } = useIconColor([
154
- variantProps?.variant ?? null,
155
- variantProps?.disabled ?? false,
156
- variantProps?.loading ?? false,
157
- ]);
158
- const sizeVar = resolveIconSize(variantProps, slot);
159
- const childProps = icon.props;
160
- return cloneElement(icon, {
161
- className: clsx(classNames[slot], childProps.className),
162
- style:
163
- sizeVar != null ? { width: sizeVar, height: sizeVar, ...childProps.style } : childProps.style,
164
- ref: ref as React.Ref<MainThread.Element>,
165
- });
166
- }
166
+ ////////////////////////////////////////////////////////////////////////////////////
167
167
 
168
168
  function ActionButtonContent({
169
169
  children,
@@ -172,25 +172,49 @@ function ActionButtonContent({
172
172
  prefixIcon,
173
173
  suffixIcon,
174
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
+
175
200
  if (isIconOnly) {
176
- return icon != null && isValidElement(icon) ? (
177
- <ActionButtonIconSlot icon={icon} slot="icon" />
178
- ) : null;
201
+ if (icon != null && isValidElement(icon)) return <Icon icon={icon} />;
202
+ return iconChildren.length > 0 ? <>{iconChildren}</> : null;
179
203
  }
180
204
 
181
205
  return (
182
206
  <>
183
- {prefixIcon != null && isValidElement(prefixIcon) ? (
184
- <ActionButtonIconSlot icon={prefixIcon} slot="prefixIcon" />
185
- ) : null}
186
- <ActionButtonTextSlot>{children}</ActionButtonTextSlot>
187
- {suffixIcon != null && isValidElement(suffixIcon) ? (
188
- <ActionButtonIconSlot icon={suffixIcon} slot="suffixIcon" />
189
- ) : 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}
190
212
  </>
191
213
  );
192
214
  }
193
215
 
216
+ ////////////////////////////////////////////////////////////////////////////////////
217
+
194
218
  function ActionButtonLoadingContent(props: ActionButtonContentProps) {
195
219
  const classNames = useClassNames();
196
220
 
@@ -201,60 +225,63 @@ function ActionButtonLoadingContent(props: ActionButtonContentProps) {
201
225
  );
202
226
  }
203
227
 
204
- function ActionButtonLoadingIndicator({ size }: { size: ActionButtonSize }) {
228
+ ////////////////////////////////////////////////////////////////////////////////////
229
+
230
+ function ActionButtonLoadingIndicator({ size }: { size: ActionButtonVariantProps["size"] }) {
205
231
  const classNames = useClassNames();
232
+ const progressCircleSize = resolveProgressCircleSize(size);
206
233
 
207
234
  return (
208
235
  <view className={classNames.loadingIndicator}>
209
- <ProgressCircleRoot size={progressCircleSizeMap[size]} tone="inherit">
236
+ <ProgressCircleRoot size={progressCircleSize} tone="inherit">
210
237
  <ProgressCircleRange />
211
238
  </ProgressCircleRoot>
212
239
  </view>
213
240
  );
214
241
  }
215
242
 
243
+ ////////////////////////////////////////////////////////////////////////////////////
244
+
216
245
  /**
217
246
  * @platform Lynx
218
247
  *
219
248
  * 웹 대비 차이:
220
- * - 아이콘 전달 방식: 웹의 `<ActionButton.PrefixIcon svg={...} />` 가 아니라 `prefixIcon` /
221
- * `suffixIcon` / `icon` prop 으로 ReactElement 를 직접 넘긴다. Lynx `<text>` 가 flex
222
- * 컨테이너가 아니라 children 전체를 text 로 감싸면 아이콘이 flex item 이 안 되기 때문.
249
+ * - 아이콘 렌더링: 웹의 SVG `currentColor` 대신 Lynx `<image>` 의 `tint-color` 를
250
+ * `Icon` / `PrefixIcon` / `SuffixIcon` wrapper 가 동기화한다.
251
+ * - 호환 API: 기존 `prefixIcon` / `suffixIcon` / `icon` prop 도 유지한다.
223
252
  * - 미지원 prop: `color`, `fontWeight`, `bleedX`, `bleedY` (CSS variable 동적 주입 제한)
224
253
  *
225
254
  * ```tsx
226
255
  * import IconPlusFill from "@karrotmarket/lynx-monochrome-icon/IconPlusFill";
227
256
  * import IconChevronDownFill from "@karrotmarket/lynx-monochrome-icon/IconChevronDownFill";
257
+ * import { Icon, PrefixIcon, SuffixIcon } from "@seed-design/lynx-react";
228
258
  *
229
259
  * // withText (기본)
230
- * <ActionButton
231
- * variant="brandSolid"
232
- * prefixIcon={<IconPlusFill />}
233
- * suffixIcon={<IconChevronDownFill />}
234
- * >
260
+ * <ActionButton variant="brandSolid">
261
+ * <PrefixIcon icon={<IconPlusFill />} />
235
262
  * 라벨
263
+ * <SuffixIcon icon={<IconChevronDownFill />} />
236
264
  * </ActionButton>
237
265
  *
238
- * // iconOnly — `icon` prop 과 `aria-label` 필수
266
+ * // iconOnly — `<Icon />` child 와 `accessibility-label` 필수
239
267
  * <ActionButton
240
268
  * layout="iconOnly"
241
269
  * variant="neutralSolid"
242
- * icon={<IconPlusFill />}
243
- * aria-label="추가"
244
- * />
270
+ * accessibility-label="추가"
271
+ * >
272
+ * <Icon icon={<IconPlusFill />} />
273
+ * </ActionButton>
245
274
  * ```
246
275
  */
247
- export interface ActionButtonProps extends Omit<ActionButtonVariantProps, "layout" | "pressed"> {
248
- children?: React.ReactNode;
249
- className?: string;
250
- flexGrow?: number;
251
- layout?: "withText" | "iconOnly";
252
- icon?: ReactElement<IconElementProps>;
253
- prefixIcon?: ReactElement<IconElementProps>;
254
- suffixIcon?: ReactElement<IconElementProps>;
255
- "aria-label"?: string;
256
- bindtap?: () => void;
257
- "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"];
258
285
  }
259
286
 
260
287
  export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props, ref) => {
@@ -267,15 +294,25 @@ export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props,
267
294
  suffixIcon,
268
295
  bindtap,
269
296
  "main-thread:bindtap": mainThreadBindtap,
297
+ "accessibility-element": accessibilityElement = true,
298
+ "accessibility-label": accessibilityLabel,
299
+ "accessibility-traits": accessibilityTraits = "button",
270
300
  ...variantAndRest
271
301
  } = props;
272
302
  const { disabled = false, loading = false } = variantAndRest;
273
303
  const isInteractive = !disabled && !loading;
274
304
  const isIconOnly = layout === "iconOnly";
275
- const size = variantAndRest.size ?? "medium";
305
+ const size = variantAndRest.size;
276
306
 
277
- if (process.env.NODE_ENV !== "production" && isIconOnly && !props["aria-label"]) {
278
- 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
+ );
279
316
  }
280
317
 
281
318
  const { pressed, ...pressTapHandlers } = usePressTap({
@@ -285,37 +322,42 @@ export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props,
285
322
  });
286
323
 
287
324
  return (
288
- <ActionButtonRoot
289
- {...variantAndRest}
290
- layout={layout}
291
- pressed={pressed}
292
- ref={ref}
293
- style={flexGrow != null ? { flexGrow } : undefined}
294
- {...pressTapHandlers}
295
- >
296
- {loading ? (
297
- <>
298
- <ActionButtonLoadingIndicator size={size} />
299
- <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
300
351
  isIconOnly={isIconOnly}
301
352
  icon={icon}
302
353
  prefixIcon={prefixIcon}
303
354
  suffixIcon={suffixIcon}
304
355
  >
305
356
  {children}
306
- </ActionButtonLoadingContent>
307
- </>
308
- ) : (
309
- <ActionButtonContent
310
- isIconOnly={isIconOnly}
311
- icon={icon}
312
- prefixIcon={prefixIcon}
313
- suffixIcon={suffixIcon}
314
- >
315
- {children}
316
- </ActionButtonContent>
317
- )}
318
- </ActionButtonRoot>
357
+ </ActionButtonContent>
358
+ )}
359
+ </ActionButtonRoot>
360
+ </IconRequired>
319
361
  );
320
362
  });
321
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";