@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.
- package/lib/components/ActionButton/ActionButton.d.ts +40 -9
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
- package/lib/components/ActionButton/ActionButton.jsx +132 -14
- package/lib/components/BottomSheet/BottomSheet.d.ts +92 -0
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.jsx +178 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +2 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.js +1 -0
- package/lib/components/BottomSheet/index.d.ts +3 -0
- package/lib/components/BottomSheet/index.d.ts.map +1 -0
- package/lib/components/BottomSheet/index.js +2 -0
- package/lib/components/Checkbox/Checkbox.d.ts +51 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.jsx +121 -0
- package/lib/components/Checkbox/Checkbox.namespace.d.ts +2 -0
- package/lib/components/Checkbox/Checkbox.namespace.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.namespace.js +1 -0
- package/lib/components/Checkbox/index.d.ts +3 -0
- package/lib/components/Checkbox/index.d.ts.map +1 -0
- package/lib/components/Checkbox/index.js +2 -0
- package/lib/components/ProgressCircle/ProgressCircle.d.ts +10 -9
- package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
- package/lib/components/ProgressCircle/ProgressCircle.jsx +8 -14
- package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +2 -0
- package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts.map +1 -0
- package/lib/components/ProgressCircle/ProgressCircle.namespace.js +1 -0
- package/lib/components/ProgressCircle/index.d.ts +2 -1
- package/lib/components/ProgressCircle/index.d.ts.map +1 -1
- package/lib/components/ProgressCircle/index.js +2 -1
- package/lib/components/RadioGroup/RadioGroup.d.ts +50 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.jsx +144 -0
- package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +2 -0
- package/lib/components/RadioGroup/RadioGroup.namespace.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.namespace.js +1 -0
- package/lib/components/RadioGroup/index.d.ts +3 -0
- package/lib/components/RadioGroup/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/index.js +2 -0
- package/lib/components/Switch/Switch.d.ts +27 -0
- package/lib/components/Switch/Switch.d.ts.map +1 -0
- package/lib/components/Switch/Switch.jsx +75 -0
- package/lib/components/Switch/Switch.namespace.d.ts +2 -0
- package/lib/components/Switch/Switch.namespace.d.ts.map +1 -0
- package/lib/components/Switch/Switch.namespace.js +1 -0
- package/lib/components/Switch/index.d.ts +3 -0
- package/lib/components/Switch/index.d.ts.map +1 -0
- package/lib/components/Switch/index.js +2 -0
- package/lib/components/TagGroup/TagGroup.d.ts +40 -0
- package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.jsx +54 -0
- package/lib/components/TagGroup/TagGroup.namespace.d.ts +2 -0
- package/lib/components/TagGroup/TagGroup.namespace.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.namespace.js +1 -0
- package/lib/components/TagGroup/index.d.ts +3 -0
- package/lib/components/TagGroup/index.d.ts.map +1 -0
- package/lib/components/TagGroup/index.js +2 -0
- package/lib/components/index.d.ts +8 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/components/index.js +7 -0
- package/lib/hooks/index.d.ts +4 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +3 -0
- package/lib/hooks/use-controllable-state.d.ts +15 -0
- package/lib/hooks/use-controllable-state.d.ts.map +1 -0
- package/lib/hooks/use-controllable-state.js +27 -0
- package/lib/hooks/use-icon-color.d.ts +15 -0
- package/lib/hooks/use-icon-color.d.ts.map +1 -0
- package/lib/hooks/use-icon-color.js +37 -0
- package/lib/hooks/use-press-tap.d.ts +24 -0
- package/lib/hooks/use-press-tap.d.ts.map +1 -0
- package/lib/hooks/use-press-tap.js +35 -0
- package/lib/index.d.ts +3 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -4
- package/lib/utils/create-slot-recipe-context.d.ts +26 -0
- package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
- package/lib/utils/create-slot-recipe-context.jsx +76 -0
- package/lib/utils/get-seed-class-name.d.ts.map +1 -0
- package/lib/utils/index.d.ts +5 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +4 -0
- package/lib/utils/resolve-recipe-token.d.ts +38 -0
- package/lib/utils/resolve-recipe-token.d.ts.map +1 -0
- package/lib/utils/resolve-recipe-token.js +47 -0
- package/lib/utils/split-multiple-variants-props.d.ts +29 -0
- package/lib/utils/split-multiple-variants-props.d.ts.map +1 -0
- package/lib/utils/split-multiple-variants-props.js +34 -0
- package/package.json +12 -9
- package/src/components/ActionButton/ActionButton.tsx +290 -31
- package/src/components/ActionButton/__tests__/ActionButton.test.tsx +35 -0
- package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
- package/src/components/BottomSheet/BottomSheet.tsx +386 -0
- package/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +119 -0
- package/src/components/BottomSheet/index.ts +27 -0
- package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
- package/src/components/Checkbox/Checkbox.tsx +291 -0
- package/src/components/Checkbox/index.ts +14 -0
- package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +14 -13
- package/src/components/ProgressCircle/index.ts +9 -1
- package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
- package/src/components/RadioGroup/RadioGroup.tsx +338 -0
- package/src/components/RadioGroup/index.ts +14 -0
- package/src/components/Switch/Switch.namespace.ts +10 -0
- package/src/components/Switch/Switch.tsx +191 -0
- package/src/components/Switch/index.ts +12 -0
- package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
- package/src/components/TagGroup/TagGroup.tsx +125 -0
- package/src/components/TagGroup/index.ts +10 -0
- package/src/components/__tests__/ProgressCircle.test.ts +33 -0
- package/src/components/__tests__/exports.test.ts +38 -0
- package/src/components/__tests__/switch.test.ts +34 -0
- package/src/components/index.ts +7 -0
- package/src/hooks/__tests__/use-controllable-state.test.ts +120 -0
- package/src/hooks/__tests__/use-press-tap.test.ts +129 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-controllable-state.ts +38 -0
- package/src/hooks/use-icon-color.ts +51 -0
- package/src/hooks/use-press-tap.ts +54 -0
- package/src/index.ts +3 -4
- package/src/utils/__tests__/create-slot-recipe-context.test.tsx +228 -0
- package/src/utils/__tests__/split-multiple-variants-props.test.ts +61 -0
- package/src/utils/create-slot-recipe-context.tsx +147 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/resolve-recipe-token.ts +47 -0
- package/src/utils/split-multiple-variants-props.ts +59 -0
- package/lib/components/Box/Box.d.ts +0 -10
- package/lib/components/Box/Box.d.ts.map +0 -1
- package/lib/components/Box/Box.jsx +0 -19
- package/lib/components/Box/index.d.ts +0 -2
- package/lib/components/Box/index.d.ts.map +0 -1
- package/lib/components/Box/index.js +0 -1
- package/lib/components/Flex/Flex.d.ts +0 -33
- package/lib/components/Flex/Flex.d.ts.map +0 -1
- package/lib/components/Flex/Flex.jsx +0 -7
- package/lib/components/Flex/index.d.ts +0 -2
- package/lib/components/Flex/index.d.ts.map +0 -1
- package/lib/components/Flex/index.js +0 -1
- package/lib/components/List/List.d.ts +0 -9
- package/lib/components/List/List.d.ts.map +0 -1
- package/lib/components/List/List.jsx +0 -17
- package/lib/components/List/index.d.ts +0 -2
- package/lib/components/List/index.d.ts.map +0 -1
- package/lib/components/List/index.js +0 -1
- package/lib/components/ListItem/ListItem.d.ts +0 -8
- package/lib/components/ListItem/ListItem.d.ts.map +0 -1
- package/lib/components/ListItem/ListItem.jsx +0 -11
- package/lib/components/ListItem/index.d.ts +0 -2
- package/lib/components/ListItem/index.d.ts.map +0 -1
- package/lib/components/ListItem/index.js +0 -1
- package/lib/components/ScrollView/ScrollView.d.ts +0 -9
- package/lib/components/ScrollView/ScrollView.d.ts.map +0 -1
- package/lib/components/ScrollView/ScrollView.jsx +0 -17
- package/lib/components/ScrollView/index.d.ts +0 -2
- package/lib/components/ScrollView/index.d.ts.map +0 -1
- package/lib/components/ScrollView/index.js +0 -1
- package/lib/components/Stack/Stack.d.ts +0 -6
- package/lib/components/Stack/Stack.d.ts.map +0 -1
- package/lib/components/Stack/Stack.jsx +0 -10
- package/lib/components/Stack/index.d.ts +0 -2
- package/lib/components/Stack/index.d.ts.map +0 -1
- package/lib/components/Stack/index.js +0 -1
- package/lib/components/Text/Text.d.ts +0 -30
- package/lib/components/Text/Text.d.ts.map +0 -1
- package/lib/components/Text/Text.jsx +0 -49
- package/lib/components/Text/index.d.ts +0 -2
- package/lib/components/Text/index.d.ts.map +0 -1
- package/lib/components/Text/index.js +0 -1
- package/lib/get-seed-class-name.d.ts.map +0 -1
- package/lib/utils/dynamic-style.d.ts +0 -13
- package/lib/utils/dynamic-style.d.ts.map +0 -1
- package/lib/utils/dynamic-style.js +0 -26
- package/lib/utils/styled.d.ts +0 -120
- package/lib/utils/styled.d.ts.map +0 -1
- package/lib/utils/styled.jsx +0 -131
- package/src/utils/dynamic-style.ts +0 -26
- /package/lib/{get-seed-class-name.d.ts → utils/get-seed-class-name.d.ts} +0 -0
- /package/lib/{get-seed-class-name.js → utils/get-seed-class-name.js} +0 -0
- /package/src/{get-seed-class-name.ts → utils/get-seed-class-name.ts} +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split `props` across multiple recipes' variant extractors in one pass.
|
|
3
|
+
*
|
|
4
|
+
* Given `recipesMap` of `{ [recipeKey]: recipe }`, returns:
|
|
5
|
+
* - A bucket per recipe key with that recipe's variant-only props.
|
|
6
|
+
* - `remainingProps` with every recipe variant key stripped out.
|
|
7
|
+
*
|
|
8
|
+
* Keys that appear in more than one recipe (e.g. shared `size`) are copied
|
|
9
|
+
* into each corresponding bucket. `remainingProps` never includes any key
|
|
10
|
+
* that any recipe claimed as a variant.
|
|
11
|
+
*
|
|
12
|
+
* Mirror of `packages/react/src/utils/splitMultipleVariantsProps` adapted
|
|
13
|
+
* to TypeScript for Lynx. Use when a single compound component composes two
|
|
14
|
+
* or more recipes (e.g. Switch's `switch` + `switchmark`).
|
|
15
|
+
*/
|
|
16
|
+
export function splitMultipleVariantsProps(props, recipesMap) {
|
|
17
|
+
const multipleVariantsProps = {};
|
|
18
|
+
const extractedKeys = new Set();
|
|
19
|
+
const propsRecord = props;
|
|
20
|
+
for (const recipeKey in recipesMap) {
|
|
21
|
+
const [variantProps] = recipesMap[recipeKey].splitVariantProps(propsRecord);
|
|
22
|
+
multipleVariantsProps[recipeKey] = variantProps;
|
|
23
|
+
for (const variantPropKey in variantProps) {
|
|
24
|
+
extractedKeys.add(variantPropKey);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const remainingProps = {};
|
|
28
|
+
for (const propKey in propsRecord) {
|
|
29
|
+
if (extractedKeys.has(propKey))
|
|
30
|
+
continue;
|
|
31
|
+
remainingProps[propKey] = propsRecord[propKey];
|
|
32
|
+
}
|
|
33
|
+
return [multipleVariantsProps, remainingProps];
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/lynx-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/daangn/seed-design.git",
|
|
@@ -15,10 +15,6 @@
|
|
|
15
15
|
"types": "./lib/index.d.ts",
|
|
16
16
|
"import": "./lib/index.js"
|
|
17
17
|
},
|
|
18
|
-
"./dynamic-style": {
|
|
19
|
-
"types": "./lib/utils/dynamic-style.d.ts",
|
|
20
|
-
"import": "./lib/utils/dynamic-style.js"
|
|
21
|
-
},
|
|
22
18
|
"./package.json": "./package.json"
|
|
23
19
|
},
|
|
24
20
|
"files": [
|
|
@@ -28,24 +24,31 @@
|
|
|
28
24
|
"scripts": {
|
|
29
25
|
"clean": "rm -rf lib",
|
|
30
26
|
"build": "tsc",
|
|
31
|
-
"lint:publish": "bun publint"
|
|
27
|
+
"lint:publish": "bun publint",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest"
|
|
32
30
|
},
|
|
33
31
|
"peerDependencies": {
|
|
34
32
|
"@seed-design/css": ">=1.1.17",
|
|
35
|
-
"@seed-design/lynx-css": ">=0.1.1-alpha.0",
|
|
33
|
+
"@seed-design/lynx-css": ">=0.1.1-alpha.0 <0.2.0 || >=0.2.0-alpha.0 <1.0.0",
|
|
36
34
|
"@lynx-js/react": ">=0.100.0"
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
37
|
"@lynx-js/react": "^0.116.3",
|
|
40
38
|
"@lynx-js/types": "^3.7.0",
|
|
39
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
41
40
|
"@types/react": "^19.2.2",
|
|
42
|
-
"
|
|
41
|
+
"jsdom": "^29.0.2",
|
|
42
|
+
"react": "^19.1.0",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
43
44
|
},
|
|
44
45
|
"publishConfig": {
|
|
45
46
|
"access": "public"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"@
|
|
49
|
+
"@lynx-js/lynx-ui-sheet": "^3.130.1",
|
|
50
|
+
"@lynx-js/lynx-ui-common": "^3.130.0",
|
|
51
|
+
"@seed-design/lynx-primitive": "workspace:*",
|
|
49
52
|
"clsx": "^2.1.1"
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -1,62 +1,321 @@
|
|
|
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 { actionButton as actionButtonVars } from "@seed-design/lynx-css/vars/component";
|
|
4
|
+
import type { MainThread } from "@lynx-js/types";
|
|
3
5
|
import clsx from "clsx";
|
|
4
6
|
import * as React from "react";
|
|
7
|
+
import { cloneElement, isValidElement, useMemo, type ReactElement } from "react";
|
|
8
|
+
|
|
9
|
+
import { useIconColor } from "../../hooks/use-icon-color";
|
|
10
|
+
import { usePressTap, type UsePressTapReturn } from "../../hooks/use-press-tap";
|
|
11
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
12
|
+
import { capitalize, resolveRecipeToken } from "../../utils/resolve-recipe-token";
|
|
13
|
+
import {
|
|
14
|
+
ProgressCircleRange,
|
|
15
|
+
ProgressCircleRoot,
|
|
16
|
+
type ProgressCircleRootProps,
|
|
17
|
+
} from "../ProgressCircle";
|
|
18
|
+
|
|
19
|
+
// Root/TextSlot 은 `withProvider("view", ...)` / `withContext("text", ...)` 를 쓰지 않는다.
|
|
20
|
+
// intrinsic string 인자는 `React.createElement("view", ...)` 로 컴파일되어 Lynx 컴파일러의
|
|
21
|
+
// 리터럴 JSX 정적 분석을 우회하고 `BackgroundSnapshot not found: view` 런타임 에러를 유발한다.
|
|
22
|
+
// (자세한 내용: `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"]>;
|
|
28
|
+
|
|
29
|
+
type IconElementProps = {
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: React.CSSProperties;
|
|
32
|
+
ref?: React.Ref<MainThread.Element>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ActionButtonContentProps = {
|
|
36
|
+
children?: React.ReactNode;
|
|
37
|
+
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) => {
|
|
65
|
+
const props = { layout: "withText" as const, ...innerProps };
|
|
66
|
+
const [variantProps, otherProps] = actionButton.splitVariantProps(props);
|
|
67
|
+
const classNames = actionButton(variantProps);
|
|
68
|
+
const {
|
|
69
|
+
className: userClassName,
|
|
70
|
+
children,
|
|
71
|
+
...rest
|
|
72
|
+
} = otherProps as ActionButtonRootOwnProps & Record<string, unknown>;
|
|
73
|
+
const propsForContext = useMemo(
|
|
74
|
+
() => variantProps,
|
|
75
|
+
[
|
|
76
|
+
variantProps.variant,
|
|
77
|
+
variantProps.size,
|
|
78
|
+
variantProps.layout,
|
|
79
|
+
variantProps.pressed,
|
|
80
|
+
variantProps.disabled,
|
|
81
|
+
variantProps.loading,
|
|
82
|
+
],
|
|
83
|
+
);
|
|
84
|
+
return (
|
|
85
|
+
<ClassNamesProvider value={classNames}>
|
|
86
|
+
<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>
|
|
94
|
+
</PropsProvider>
|
|
95
|
+
</ClassNamesProvider>
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
ActionButtonRoot.displayName = "ActionButtonRoot";
|
|
99
|
+
|
|
100
|
+
const ActionButtonTextSlot = React.forwardRef<
|
|
101
|
+
unknown,
|
|
102
|
+
{ children?: React.ReactNode; className?: string }
|
|
103
|
+
>((props, ref) => {
|
|
104
|
+
const { children, className: userClassName, ...rest } = props;
|
|
105
|
+
const classNames = useClassNames();
|
|
106
|
+
return (
|
|
107
|
+
<text
|
|
108
|
+
{...(ref ? { ref: ref as React.Ref<SVGTextElement> } : {})}
|
|
109
|
+
{...rest}
|
|
110
|
+
className={clsx(classNames.text, userClassName)}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</text>
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
ActionButtonTextSlot.displayName = "ActionButtonTextSlot";
|
|
117
|
+
|
|
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
|
+
}
|
|
167
|
+
|
|
168
|
+
function ActionButtonContent({
|
|
169
|
+
children,
|
|
170
|
+
isIconOnly,
|
|
171
|
+
icon,
|
|
172
|
+
prefixIcon,
|
|
173
|
+
suffixIcon,
|
|
174
|
+
}: ActionButtonContentProps) {
|
|
175
|
+
if (isIconOnly) {
|
|
176
|
+
return icon != null && isValidElement(icon) ? (
|
|
177
|
+
<ActionButtonIconSlot icon={icon} slot="icon" />
|
|
178
|
+
) : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
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}
|
|
190
|
+
</>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ActionButtonLoadingContent(props: ActionButtonContentProps) {
|
|
195
|
+
const classNames = useClassNames();
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<view className={classNames.content}>
|
|
199
|
+
<ActionButtonContent {...props} />
|
|
200
|
+
</view>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ActionButtonLoadingIndicator({ size }: { size: ActionButtonSize }) {
|
|
205
|
+
const classNames = useClassNames();
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<view className={classNames.loadingIndicator}>
|
|
209
|
+
<ProgressCircleRoot size={progressCircleSizeMap[size]} tone="inherit">
|
|
210
|
+
<ProgressCircleRange />
|
|
211
|
+
</ProgressCircleRoot>
|
|
212
|
+
</view>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
5
215
|
|
|
6
216
|
/**
|
|
7
217
|
* @platform Lynx
|
|
8
218
|
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
219
|
+
* 웹 대비 차이:
|
|
220
|
+
* - 아이콘 전달 방식: 웹의 `<ActionButton.PrefixIcon svg={...} />` 가 아니라 `prefixIcon` /
|
|
221
|
+
* `suffixIcon` / `icon` prop 으로 ReactElement 를 직접 넘긴다. Lynx `<text>` 가 flex
|
|
222
|
+
* 컨테이너가 아니라 children 전체를 text 로 감싸면 아이콘이 flex item 이 안 되기 때문.
|
|
223
|
+
* - 미지원 prop: `color`, `fontWeight`, `bleedX`, `bleedY` (CSS variable 동적 주입 제한)
|
|
224
|
+
*
|
|
225
|
+
* ```tsx
|
|
226
|
+
* import IconPlusFill from "@karrotmarket/lynx-monochrome-icon/IconPlusFill";
|
|
227
|
+
* import IconChevronDownFill from "@karrotmarket/lynx-monochrome-icon/IconChevronDownFill";
|
|
228
|
+
*
|
|
229
|
+
* // withText (기본)
|
|
230
|
+
* <ActionButton
|
|
231
|
+
* variant="brandSolid"
|
|
232
|
+
* prefixIcon={<IconPlusFill />}
|
|
233
|
+
* suffixIcon={<IconChevronDownFill />}
|
|
234
|
+
* >
|
|
235
|
+
* 라벨
|
|
236
|
+
* </ActionButton>
|
|
13
237
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
238
|
+
* // iconOnly — `icon` prop 과 `aria-label` 필수
|
|
239
|
+
* <ActionButton
|
|
240
|
+
* layout="iconOnly"
|
|
241
|
+
* variant="neutralSolid"
|
|
242
|
+
* icon={<IconPlusFill />}
|
|
243
|
+
* aria-label="추가"
|
|
244
|
+
* />
|
|
245
|
+
* ```
|
|
17
246
|
*/
|
|
18
|
-
export interface ActionButtonProps extends Omit<ActionButtonVariantProps, "
|
|
247
|
+
export interface ActionButtonProps extends Omit<ActionButtonVariantProps, "layout" | "pressed"> {
|
|
19
248
|
children?: React.ReactNode;
|
|
20
|
-
disabled?: boolean;
|
|
21
249
|
className?: string;
|
|
22
250
|
flexGrow?: number;
|
|
251
|
+
layout?: "withText" | "iconOnly";
|
|
252
|
+
icon?: ReactElement<IconElementProps>;
|
|
253
|
+
prefixIcon?: ReactElement<IconElementProps>;
|
|
254
|
+
suffixIcon?: ReactElement<IconElementProps>;
|
|
255
|
+
"aria-label"?: string;
|
|
23
256
|
bindtap?: () => void;
|
|
24
257
|
"main-thread:bindtap"?: () => void;
|
|
25
258
|
}
|
|
26
259
|
|
|
27
260
|
export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props, ref) => {
|
|
28
|
-
const [variantProps, restProps] = actionButton.splitVariantProps(props);
|
|
29
261
|
const {
|
|
30
262
|
children,
|
|
31
|
-
className,
|
|
32
263
|
flexGrow,
|
|
264
|
+
layout,
|
|
265
|
+
icon,
|
|
266
|
+
prefixIcon,
|
|
267
|
+
suffixIcon,
|
|
33
268
|
bindtap,
|
|
34
269
|
"main-thread:bindtap": mainThreadBindtap,
|
|
35
|
-
...
|
|
36
|
-
} =
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
270
|
+
...variantAndRest
|
|
271
|
+
} = props;
|
|
272
|
+
const { disabled = false, loading = false } = variantAndRest;
|
|
273
|
+
const isInteractive = !disabled && !loading;
|
|
274
|
+
const isIconOnly = layout === "iconOnly";
|
|
275
|
+
const size = variantAndRest.size ?? "medium";
|
|
276
|
+
|
|
277
|
+
if (process.env.NODE_ENV !== "production" && isIconOnly && !props["aria-label"]) {
|
|
278
|
+
console.warn('ActionButton: `layout="iconOnly"` requires `aria-label` for accessibility.');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { pressed, ...pressTapHandlers } = usePressTap({
|
|
282
|
+
disabled: !isInteractive,
|
|
283
|
+
onTap: bindtap,
|
|
284
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
43
285
|
});
|
|
44
|
-
const isInteractive = !disabled;
|
|
45
286
|
|
|
46
287
|
return (
|
|
47
|
-
<
|
|
48
|
-
{...
|
|
49
|
-
|
|
288
|
+
<ActionButtonRoot
|
|
289
|
+
{...variantAndRest}
|
|
290
|
+
layout={layout}
|
|
291
|
+
pressed={pressed}
|
|
292
|
+
ref={ref}
|
|
50
293
|
style={flexGrow != null ? { flexGrow } : undefined}
|
|
51
|
-
{...
|
|
52
|
-
{...(isInteractive &&
|
|
53
|
-
mainThreadBindtap && {
|
|
54
|
-
"main-thread:bindtap": mainThreadBindtap,
|
|
55
|
-
})}
|
|
56
|
-
{...nativeProps}
|
|
294
|
+
{...pressTapHandlers}
|
|
57
295
|
>
|
|
58
|
-
|
|
59
|
-
|
|
296
|
+
{loading ? (
|
|
297
|
+
<>
|
|
298
|
+
<ActionButtonLoadingIndicator size={size} />
|
|
299
|
+
<ActionButtonLoadingContent
|
|
300
|
+
isIconOnly={isIconOnly}
|
|
301
|
+
icon={icon}
|
|
302
|
+
prefixIcon={prefixIcon}
|
|
303
|
+
suffixIcon={suffixIcon}
|
|
304
|
+
>
|
|
305
|
+
{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>
|
|
60
319
|
);
|
|
61
320
|
});
|
|
62
321
|
ActionButton.displayName = "ActionButton";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { getQueriesForElement, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { ActionButton } from "../ActionButton";
|
|
6
|
+
|
|
7
|
+
function getRenderedQueries() {
|
|
8
|
+
return getQueriesForElement(getRenderedRoot());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getRenderedRoot() {
|
|
12
|
+
const root = elementTree.root;
|
|
13
|
+
|
|
14
|
+
if (!root) {
|
|
15
|
+
throw new Error("Expected Lynx render root to exist.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("ActionButton", () => {
|
|
22
|
+
it("keeps the original label in the text slot while loading", () => {
|
|
23
|
+
render(<ActionButton loading>Submit</ActionButton>);
|
|
24
|
+
|
|
25
|
+
const { getByText } = getRenderedQueries();
|
|
26
|
+
const label = getByText("Submit");
|
|
27
|
+
const root = getRenderedRoot();
|
|
28
|
+
|
|
29
|
+
expect(label).toHaveClass("seed-action-button__text");
|
|
30
|
+
expect(label).toHaveClass("seed-action-button__text--size_medium-layout_withText");
|
|
31
|
+
expect(root.querySelector(".seed-action-button__content")).toBeInTheDocument();
|
|
32
|
+
expect(root.querySelector(".seed-action-button__loadingIndicator")).toBeInTheDocument();
|
|
33
|
+
expect(root.querySelector(".seed-progress-circle__root")).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
BottomSheetBackdrop as Backdrop,
|
|
3
|
+
BottomSheetBody as Body,
|
|
4
|
+
BottomSheetContent as Content,
|
|
5
|
+
BottomSheetDescription as Description,
|
|
6
|
+
BottomSheetFooter as Footer,
|
|
7
|
+
BottomSheetHandle as Handle,
|
|
8
|
+
BottomSheetHeader as Header,
|
|
9
|
+
BottomSheetPositioner as Positioner,
|
|
10
|
+
BottomSheetRoot as Root,
|
|
11
|
+
BottomSheetTitle as Title,
|
|
12
|
+
BottomSheetTrigger as Trigger,
|
|
13
|
+
type BottomSheetBackdropProps as BackdropProps,
|
|
14
|
+
type BottomSheetBodyProps as BodyProps,
|
|
15
|
+
type BottomSheetContentProps as ContentProps,
|
|
16
|
+
type BottomSheetDescriptionProps as DescriptionProps,
|
|
17
|
+
type BottomSheetFooterProps as FooterProps,
|
|
18
|
+
type BottomSheetHandleProps as HandleProps,
|
|
19
|
+
type BottomSheetHeaderProps as HeaderProps,
|
|
20
|
+
type BottomSheetPositionerProps as PositionerProps,
|
|
21
|
+
type BottomSheetRootProps as RootProps,
|
|
22
|
+
type BottomSheetTitleProps as TitleProps,
|
|
23
|
+
type BottomSheetTriggerProps as TriggerProps,
|
|
24
|
+
} from "./BottomSheet";
|