@mindlogic-ai/logician-ui 3.2.0-alpha.5 → 4.0.0-alpha.11
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/dist/components/Icon/_utils/createIcon.d.ts.map +1 -1
- package/dist/components/Icon/_utils/createIcon.js +1 -0
- package/dist/components/Icon/_utils/createIcon.js.map +1 -1
- package/dist/components/Icon/_utils/createIcon.mjs +1 -0
- package/dist/components/Icon/_utils/createIcon.mjs.map +1 -1
- package/dist/components/Loaders/PageLoader.d.ts.map +1 -1
- package/dist/components/Loaders/PageLoader.js +2 -2
- package/dist/components/Loaders/PageLoader.js.map +1 -1
- package/dist/components/Loaders/PageLoader.mjs +2 -2
- package/dist/components/Loaders/PageLoader.mjs.map +1 -1
- package/dist/components/Loaders/SectionLoader.d.ts.map +1 -1
- package/dist/components/Loaders/SectionLoader.js +2 -2
- package/dist/components/Loaders/SectionLoader.js.map +1 -1
- package/dist/components/Loaders/SectionLoader.mjs +2 -2
- package/dist/components/Loaders/SectionLoader.mjs.map +1 -1
- package/dist/components/Tag/Tag.d.ts.map +1 -1
- package/dist/components/Tag/Tag.js +1 -1
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Tag/Tag.mjs +1 -1
- package/dist/components/Tag/Tag.mjs.map +1 -1
- package/dist/components/Tag/Tag.styles.d.ts.map +1 -1
- package/dist/components/Tag/Tag.styles.js +22 -16
- package/dist/components/Tag/Tag.styles.js.map +1 -1
- package/dist/components/Tag/Tag.styles.mjs +22 -16
- package/dist/components/Tag/Tag.styles.mjs.map +1 -1
- package/dist/components/Tree/Tree.d.ts +5 -1
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/Tree.types.d.ts +29 -1
- package/dist/components/Tree/Tree.types.d.ts.map +1 -1
- package/dist/components/Tree/TreeBranchIndentGuide.d.ts +5 -1
- package/dist/components/Tree/TreeBranchIndentGuide.d.ts.map +1 -1
- package/dist/components/Tree/TreeBranchIndentGuide.js +107 -8
- package/dist/components/Tree/TreeBranchIndentGuide.js.map +1 -1
- package/dist/components/Tree/TreeBranchIndentGuide.mjs +107 -8
- package/dist/components/Tree/TreeBranchIndentGuide.mjs.map +1 -1
- package/dist/components/Typography/Link.d.ts.map +1 -1
- package/dist/components/Typography/Link.js +6 -1
- package/dist/components/Typography/Link.js.map +1 -1
- package/dist/components/Typography/Link.mjs +6 -1
- package/dist/components/Typography/Link.mjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +0 -1
- package/dist/index.mjs.map +1 -1
- package/dist/theme/global.d.ts.map +1 -1
- package/dist/theme/global.js +4 -1
- package/dist/theme/global.js.map +1 -1
- package/dist/theme/global.mjs +4 -1
- package/dist/theme/global.mjs.map +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +8 -4
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +8 -4
- package/dist/theme/index.mjs.map +1 -1
- package/package.json +1 -3
- package/src/components/Icon/ICON_MANAGEMENT.md +1 -0
- package/src/components/Icon/Icon.stories.tsx +6 -2
- package/src/components/Icon/_utils/createIcon.tsx +1 -0
- package/src/components/Loaders/PageLoader.tsx +3 -5
- package/src/components/Loaders/SectionLoader.tsx +3 -2
- package/src/components/Tag/Tag.styles.ts +22 -16
- package/src/components/Tag/Tag.tsx +1 -0
- package/src/components/Tree/Tree.GuideElbow.stories.tsx +220 -0
- package/src/components/Tree/Tree.types.ts +29 -1
- package/src/components/Tree/TreeBranchIndentGuide.test.tsx +213 -0
- package/src/components/Tree/TreeBranchIndentGuide.tsx +137 -11
- package/src/components/Typography/Link.tsx +5 -1
- package/src/index.ts +0 -1
- package/src/theme/global.ts +4 -1
- package/src/theme/index.ts +8 -4
- package/dist/components/MDXEditor/MDXEditor.d.ts +0 -6
- package/dist/components/MDXEditor/MDXEditor.d.ts.map +0 -1
- package/dist/components/MDXEditor/MDXEditor.js +0 -237
- package/dist/components/MDXEditor/MDXEditor.js.map +0 -1
- package/dist/components/MDXEditor/MDXEditor.mjs +0 -235
- package/dist/components/MDXEditor/MDXEditor.mjs.map +0 -1
- package/dist/components/MDXEditor/MDXEditor.types.d.ts +0 -7
- package/dist/components/MDXEditor/MDXEditor.types.d.ts.map +0 -1
- package/dist/components/MDXEditor/index.d.ts +0 -3
- package/dist/components/MDXEditor/index.d.ts.map +0 -1
- package/src/components/MDXEditor/MDXEditor.css +0 -40
- package/src/components/MDXEditor/MDXEditor.stories.tsx +0 -56
- package/src/components/MDXEditor/MDXEditor.tsx +0 -337
- package/src/components/MDXEditor/MDXEditor.types.ts +0 -7
- package/src/components/MDXEditor/index.ts +0 -2
|
@@ -155,7 +155,7 @@ export const Sizes: Story = {
|
|
|
155
155
|
<VStack maxW="1400px" gap={4} align="start">
|
|
156
156
|
<Text fontWeight="bold">크기 조절</Text>
|
|
157
157
|
<HStack gap={4} align="center">
|
|
158
|
-
{['xs', 'sm', 'md', 'lg', 'xl'].map((size) => (
|
|
158
|
+
{['2xs', 'xs', 'sm', 'md', 'lg', 'xl'].map((size) => (
|
|
159
159
|
<VStack key={size}>
|
|
160
160
|
<SampleIcon boxSize={size} color="blue.500" />
|
|
161
161
|
<Text textStyle="subtext">{size}</Text>
|
|
@@ -223,6 +223,10 @@ const CustomBuildingIcon = createIcon(
|
|
|
223
223
|
<Box borderTop="1px solid" borderColor="gray.200" pt={4}>
|
|
224
224
|
<Text fontWeight="semibold" mb={3}>결과:</Text>
|
|
225
225
|
<HStack gap={6}>
|
|
226
|
+
<VStack>
|
|
227
|
+
<CustomBuildingIcon boxSize="2xs" color="blue.500" />
|
|
228
|
+
<Text textStyle="subtext">2xs (14px)</Text>
|
|
229
|
+
</VStack>
|
|
226
230
|
<VStack>
|
|
227
231
|
<CustomBuildingIcon boxSize="xs" color="blue.500" />
|
|
228
232
|
<Text textStyle="subtext">xs (16px)</Text>
|
|
@@ -305,7 +309,7 @@ const MyCustomIcon = createIcon(
|
|
|
305
309
|
<Box w="100%" bg="blue.50" p={4} borderRadius="md" borderLeft="4px solid" borderColor="blue.500">
|
|
306
310
|
<Text fontWeight="bold" mb={2}>✨ 주요 기능</Text>
|
|
307
311
|
<VStack align="start" gap={2} textStyle="subtext">
|
|
308
|
-
<Text>• <strong>boxSize 지원:</strong> xs, sm, md, lg, xl (
|
|
312
|
+
<Text>• <strong>boxSize 지원:</strong> 2xs, xs, sm, md, lg, xl (14px ~ 40px)</Text>
|
|
309
313
|
<Text>• <strong>Chakra UI 스타일링:</strong> color, bg, margin, padding 등 모든 Box props 사용 가능</Text>
|
|
310
314
|
<Text>• <strong>Tree shaking:</strong> 사용하지 않는 아이콘은 번들에서 제외됨</Text>
|
|
311
315
|
<Text>• <strong>TypeScript 지원:</strong> 완전한 타입 안전성 제공</Text>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import BounceLoader from 'react-spinners/BounceLoader';
|
|
2
1
|
import { Box, Flex, FlexProps } from '@chakra-ui/react';
|
|
3
2
|
|
|
3
|
+
import { Spinner } from '@/components/Spinner';
|
|
4
|
+
|
|
4
5
|
export const PageLoader = ({
|
|
5
6
|
isLoading,
|
|
6
7
|
...rest
|
|
@@ -21,10 +22,7 @@ export const PageLoader = ({
|
|
|
21
22
|
{...rest}
|
|
22
23
|
>
|
|
23
24
|
<Box position="relative">
|
|
24
|
-
<
|
|
25
|
-
color="var(--chakra-colors-primary-main)"
|
|
26
|
-
style={{ zIndex: 999 }}
|
|
27
|
-
/>
|
|
25
|
+
<Spinner boxSize="60px" borderWidth="6px" css={{ zIndex: 999 }} />
|
|
28
26
|
</Box>
|
|
29
27
|
</Flex>
|
|
30
28
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import DotLoader from 'react-spinners/DotLoader';
|
|
2
1
|
import { Flex, FlexProps } from '@chakra-ui/react';
|
|
3
2
|
|
|
3
|
+
import { Spinner } from '@/components/Spinner';
|
|
4
|
+
|
|
4
5
|
export const SectionLoader = ({
|
|
5
6
|
isLoading,
|
|
6
7
|
...rest
|
|
@@ -20,7 +21,7 @@ export const SectionLoader = ({
|
|
|
20
21
|
zIndex={9999}
|
|
21
22
|
{...rest}
|
|
22
23
|
>
|
|
23
|
-
<
|
|
24
|
+
<Spinner boxSize="60px" borderWidth="6px" />
|
|
24
25
|
</Flex>
|
|
25
26
|
);
|
|
26
27
|
};
|
|
@@ -17,7 +17,7 @@ export const tagColorPaletteStyles: Record<
|
|
|
17
17
|
soft: {
|
|
18
18
|
bgColor: 'primary.extralight',
|
|
19
19
|
borderColor: 'primary.light',
|
|
20
|
-
color: 'primary.
|
|
20
|
+
color: 'primary.darker',
|
|
21
21
|
},
|
|
22
22
|
solid: {
|
|
23
23
|
bgColor: 'blue.500',
|
|
@@ -27,14 +27,14 @@ export const tagColorPaletteStyles: Record<
|
|
|
27
27
|
outline: {
|
|
28
28
|
bgColor: 'transparent',
|
|
29
29
|
borderColor: 'primary.main',
|
|
30
|
-
color: 'primary.
|
|
30
|
+
color: 'primary.dark',
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
secondary: {
|
|
34
34
|
soft: {
|
|
35
35
|
bgColor: 'secondary.extralight',
|
|
36
36
|
borderColor: 'secondary.light',
|
|
37
|
-
color: 'secondary.
|
|
37
|
+
color: 'secondary.darker',
|
|
38
38
|
},
|
|
39
39
|
solid: {
|
|
40
40
|
bgColor: 'violet.500',
|
|
@@ -44,14 +44,14 @@ export const tagColorPaletteStyles: Record<
|
|
|
44
44
|
outline: {
|
|
45
45
|
bgColor: 'transparent',
|
|
46
46
|
borderColor: 'secondary.main',
|
|
47
|
-
color: 'secondary.
|
|
47
|
+
color: 'secondary.dark',
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
danger: {
|
|
51
51
|
soft: {
|
|
52
52
|
bgColor: 'danger.extralight',
|
|
53
53
|
borderColor: 'danger.light',
|
|
54
|
-
color: 'danger.
|
|
54
|
+
color: 'danger.darker',
|
|
55
55
|
},
|
|
56
56
|
solid: {
|
|
57
57
|
bgColor: 'rose.500',
|
|
@@ -61,14 +61,14 @@ export const tagColorPaletteStyles: Record<
|
|
|
61
61
|
outline: {
|
|
62
62
|
bgColor: 'transparent',
|
|
63
63
|
borderColor: 'danger.main',
|
|
64
|
-
color: 'danger.
|
|
64
|
+
color: 'danger.dark',
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
success: {
|
|
68
68
|
soft: {
|
|
69
69
|
bgColor: 'success.extralight',
|
|
70
70
|
borderColor: 'success.light',
|
|
71
|
-
color: 'success.
|
|
71
|
+
color: 'success.darker',
|
|
72
72
|
},
|
|
73
73
|
solid: {
|
|
74
74
|
bgColor: 'green.600',
|
|
@@ -78,34 +78,40 @@ export const tagColorPaletteStyles: Record<
|
|
|
78
78
|
outline: {
|
|
79
79
|
bgColor: 'transparent',
|
|
80
80
|
borderColor: 'success.main',
|
|
81
|
-
color: 'success.
|
|
81
|
+
color: 'success.dark',
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
84
|
warning: {
|
|
85
85
|
soft: {
|
|
86
86
|
bgColor: 'warning.extralight',
|
|
87
87
|
borderColor: 'warning.light',
|
|
88
|
-
color: 'warning.
|
|
88
|
+
color: 'warning.darker',
|
|
89
89
|
},
|
|
90
90
|
solid: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
// Deepened from gold.500 so white text clears AA and the chip matches the
|
|
92
|
+
// other solids (white on a saturated fill). White/gold.500 was only
|
|
93
|
+
// 2.39:1; white/gold.700 = 5.86:1 ✓ AA. Bright gold + near-black text read
|
|
94
|
+
// harsh on the dark canvas.
|
|
95
|
+
bgColor: 'gold.700',
|
|
96
|
+
borderColor: 'gold.700',
|
|
97
|
+
color: 'white',
|
|
94
98
|
},
|
|
95
99
|
outline: {
|
|
96
100
|
bgColor: 'transparent',
|
|
97
101
|
borderColor: 'warning.main',
|
|
98
|
-
|
|
102
|
+
// warning.dark (gold.700, 5.8:1 AA) over warning.main (gold.500, 3.0:1) —
|
|
103
|
+
// darker text also lifts this off the palette's AA-risk step.
|
|
104
|
+
color: 'warning.dark',
|
|
99
105
|
},
|
|
100
106
|
},
|
|
101
107
|
neutral: {
|
|
102
108
|
soft: {
|
|
103
|
-
// Flip surface + border with the mode so the fg.
|
|
109
|
+
// Flip surface + border with the mode so the fg.default text stays legible
|
|
104
110
|
// in dark. Light values preserved (bg.muted → gray.100, border.default →
|
|
105
111
|
// gray.300).
|
|
106
112
|
bgColor: 'bg.muted',
|
|
107
113
|
borderColor: 'border.default',
|
|
108
|
-
color: 'fg.
|
|
114
|
+
color: 'fg.default',
|
|
109
115
|
},
|
|
110
116
|
solid: {
|
|
111
117
|
bgColor: 'gray.800',
|
|
@@ -115,7 +121,7 @@ export const tagColorPaletteStyles: Record<
|
|
|
115
121
|
outline: {
|
|
116
122
|
bgColor: 'transparent',
|
|
117
123
|
borderColor: 'border.strong', // gray.500 / gray.900 (_dark)
|
|
118
|
-
color: 'fg.
|
|
124
|
+
color: 'fg.muted',
|
|
119
125
|
},
|
|
120
126
|
},
|
|
121
127
|
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Box, HStack, Stack, Text } from '@chakra-ui/react';
|
|
2
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import { Tree } from './Tree';
|
|
5
|
+
import { createTreeCollection } from './index';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Stories for the `elbow` option on `Tree.BranchIndentGuide`.
|
|
9
|
+
*
|
|
10
|
+
* `elbow` turns the plain vertical rails into `├` / `└` guide lines:
|
|
11
|
+
* every row gets a horizontal foot joining it to its parent rail, and
|
|
12
|
+
* the last row of each group stops at its centre (`└`). `guideColor`
|
|
13
|
+
* customizes the line color (default `border.default`, which flips
|
|
14
|
+
* with color mode). Pass both through the `indentGuide` element:
|
|
15
|
+
*
|
|
16
|
+
* <Tree.Node indentGuide={<Tree.BranchIndentGuide elbow />} ... />
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface Node {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
children?: Node[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const orgSample: Node = {
|
|
26
|
+
id: 'ROOT',
|
|
27
|
+
name: '',
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
id: 'hq',
|
|
31
|
+
name: '본사',
|
|
32
|
+
children: [
|
|
33
|
+
{
|
|
34
|
+
id: 'hq-dev',
|
|
35
|
+
name: '개발본부',
|
|
36
|
+
children: [
|
|
37
|
+
{
|
|
38
|
+
id: 'hq-dev-backend',
|
|
39
|
+
name: '백엔드그룹',
|
|
40
|
+
children: [
|
|
41
|
+
{ id: 'hq-dev-backend-api', name: 'API팀' },
|
|
42
|
+
{ id: 'hq-dev-backend-db', name: 'DB팀' },
|
|
43
|
+
{ id: 'hq-dev-backend-platform', name: '플랫폼팀' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'hq-dev-frontend',
|
|
48
|
+
name: '프론트엔드그룹',
|
|
49
|
+
children: [
|
|
50
|
+
{ id: 'hq-dev-frontend-web', name: '웹팀' },
|
|
51
|
+
{ id: 'hq-dev-frontend-mobile', name: '모바일팀' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'hq-sales',
|
|
58
|
+
name: '영업본부',
|
|
59
|
+
children: [
|
|
60
|
+
{ id: 'hq-sales-domestic', name: '국내영업팀' },
|
|
61
|
+
{ id: 'hq-sales-global', name: '해외영업팀' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'rnd',
|
|
68
|
+
name: 'R&D센터',
|
|
69
|
+
children: [
|
|
70
|
+
{ id: 'rnd-ml', name: '머신러닝팀' },
|
|
71
|
+
{ id: 'rnd-data', name: '데이터팀' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const orgCollection = createTreeCollection<Node>({
|
|
78
|
+
rootNode: orgSample,
|
|
79
|
+
nodeToValue: (node) => node.id,
|
|
80
|
+
nodeToString: (node) => node.name,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const allBranchIds = (function collect(n: Node): string[] {
|
|
84
|
+
if (!n.children) return [];
|
|
85
|
+
return n.children.flatMap((c) => (c.children ? [c.id, ...collect(c)] : []));
|
|
86
|
+
})(orgSample);
|
|
87
|
+
|
|
88
|
+
const renderNode = ({
|
|
89
|
+
node,
|
|
90
|
+
nodeState,
|
|
91
|
+
}: {
|
|
92
|
+
node: Node;
|
|
93
|
+
nodeState: { isBranch: boolean };
|
|
94
|
+
}) =>
|
|
95
|
+
nodeState.isBranch ? (
|
|
96
|
+
<Tree.BranchControl>
|
|
97
|
+
<Tree.BranchIndicator />
|
|
98
|
+
<Tree.BranchText>{node.name}</Tree.BranchText>
|
|
99
|
+
</Tree.BranchControl>
|
|
100
|
+
) : (
|
|
101
|
+
<Tree.Item>
|
|
102
|
+
<Tree.ItemText>{node.name}</Tree.ItemText>
|
|
103
|
+
</Tree.Item>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const meta = {
|
|
107
|
+
title: 'Components/Tree',
|
|
108
|
+
component: Tree.Root,
|
|
109
|
+
parameters: { layout: 'padded' },
|
|
110
|
+
} satisfies Meta<typeof Tree.Root>;
|
|
111
|
+
|
|
112
|
+
export default meta;
|
|
113
|
+
|
|
114
|
+
type Story = StoryObj<typeof meta>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `elbow` guides — the innermost rail grows a horizontal foot so each
|
|
118
|
+
* row connects to its parent column like a classic file-tree.
|
|
119
|
+
*/
|
|
120
|
+
export const WithElbowGuides: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
collection: orgCollection,
|
|
123
|
+
'aria-label': '조직',
|
|
124
|
+
defaultExpandedValue: allBranchIds,
|
|
125
|
+
defaultSelectedValue: ['hq-dev-frontend-web'],
|
|
126
|
+
},
|
|
127
|
+
render: (args) => (
|
|
128
|
+
<Box maxWidth="360px">
|
|
129
|
+
<Tree.Root {...args}>
|
|
130
|
+
<Tree.Tree>
|
|
131
|
+
<Tree.Node
|
|
132
|
+
indentGuide={<Tree.BranchIndentGuide elbow />}
|
|
133
|
+
render={renderNode}
|
|
134
|
+
/>
|
|
135
|
+
</Tree.Tree>
|
|
136
|
+
</Tree.Root>
|
|
137
|
+
</Box>
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `guideColor` — customize the guide line color with any Chakra color
|
|
143
|
+
* token. The default `border.default` flips with color mode; pick a
|
|
144
|
+
* stronger token (or any palette color) for more contrast.
|
|
145
|
+
*/
|
|
146
|
+
export const GuideColor: Story = {
|
|
147
|
+
args: {
|
|
148
|
+
collection: orgCollection,
|
|
149
|
+
'aria-label': '조직',
|
|
150
|
+
defaultExpandedValue: allBranchIds,
|
|
151
|
+
},
|
|
152
|
+
render: (args) => (
|
|
153
|
+
<HStack align="flex-start" gap={8}>
|
|
154
|
+
{(
|
|
155
|
+
[
|
|
156
|
+
['border.default (기본)', undefined],
|
|
157
|
+
['border.strong', 'border.strong'],
|
|
158
|
+
['primary.light', 'primary.light'],
|
|
159
|
+
] as const
|
|
160
|
+
).map(([label, guideColor]) => (
|
|
161
|
+
<Stack key={label} gap={2} width="280px">
|
|
162
|
+
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
|
|
163
|
+
{label}
|
|
164
|
+
</Text>
|
|
165
|
+
<Tree.Root {...args}>
|
|
166
|
+
<Tree.Tree>
|
|
167
|
+
<Tree.Node
|
|
168
|
+
indentGuide={
|
|
169
|
+
<Tree.BranchIndentGuide elbow guideColor={guideColor} />
|
|
170
|
+
}
|
|
171
|
+
render={renderNode}
|
|
172
|
+
/>
|
|
173
|
+
</Tree.Tree>
|
|
174
|
+
</Tree.Root>
|
|
175
|
+
</Stack>
|
|
176
|
+
))}
|
|
177
|
+
</HStack>
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Plain vertical rails (default) vs. `elbow` guides, side by side.
|
|
183
|
+
*/
|
|
184
|
+
export const ElbowGuideComparison: Story = {
|
|
185
|
+
args: {
|
|
186
|
+
collection: orgCollection,
|
|
187
|
+
'aria-label': '조직',
|
|
188
|
+
defaultExpandedValue: allBranchIds,
|
|
189
|
+
},
|
|
190
|
+
render: (args) => (
|
|
191
|
+
<HStack align="flex-start" gap={8}>
|
|
192
|
+
<Stack gap={2} width="320px">
|
|
193
|
+
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
|
|
194
|
+
기본 — 수직 레일
|
|
195
|
+
</Text>
|
|
196
|
+
<Tree.Root {...args}>
|
|
197
|
+
<Tree.Tree>
|
|
198
|
+
<Tree.Node
|
|
199
|
+
indentGuide={<Tree.BranchIndentGuide />}
|
|
200
|
+
render={renderNode}
|
|
201
|
+
/>
|
|
202
|
+
</Tree.Tree>
|
|
203
|
+
</Tree.Root>
|
|
204
|
+
</Stack>
|
|
205
|
+
<Stack gap={2} width="320px">
|
|
206
|
+
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
|
|
207
|
+
elbow — ├ / └ 가이드 라인
|
|
208
|
+
</Text>
|
|
209
|
+
<Tree.Root {...args}>
|
|
210
|
+
<Tree.Tree>
|
|
211
|
+
<Tree.Node
|
|
212
|
+
indentGuide={<Tree.BranchIndentGuide elbow />}
|
|
213
|
+
render={renderNode}
|
|
214
|
+
/>
|
|
215
|
+
</Tree.Tree>
|
|
216
|
+
</Tree.Root>
|
|
217
|
+
</Stack>
|
|
218
|
+
</HStack>
|
|
219
|
+
),
|
|
220
|
+
};
|
|
@@ -23,7 +23,35 @@ export type TreeBranchTriggerProps = TreeViewBranchTriggerProps;
|
|
|
23
23
|
export type TreeBranchIndicatorProps = TreeViewBranchIndicatorProps;
|
|
24
24
|
export type TreeBranchTextProps = TreeViewBranchTextProps;
|
|
25
25
|
export type TreeBranchContentProps = TreeViewBranchContentProps;
|
|
26
|
-
export type TreeBranchIndentGuideProps = TreeViewBranchIndentGuideProps
|
|
26
|
+
export type TreeBranchIndentGuideProps = TreeViewBranchIndentGuideProps & {
|
|
27
|
+
/**
|
|
28
|
+
* Draw `├` / `└` guide lines instead of a plain vertical rail: every
|
|
29
|
+
* row gets a horizontal foot joining it to its parent rail, middle
|
|
30
|
+
* rows keep the vertical (`├`), and the last row of each group stops
|
|
31
|
+
* at its centre (`└`). Off by default to preserve the existing
|
|
32
|
+
* plain-vertical-rail look. See `TreeBranchIndentGuide`.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
elbow?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Length of the elbow foot — the horizontal cross-stroke that joins
|
|
38
|
+
* the rail to the row content, drawn from the parent rail column
|
|
39
|
+
* toward the text. In elbow mode each level indents 4px further than
|
|
40
|
+
* the plain-rail default, and since the default foot length doesn't
|
|
41
|
+
* grow with it, the foot ends 8px short of the row text. Any Chakra
|
|
42
|
+
* width value. Only applies when `elbow` is set.
|
|
43
|
+
* @default 'calc(var(--tree-indentation) - 4px)'
|
|
44
|
+
*/
|
|
45
|
+
footLength?: string | number;
|
|
46
|
+
/**
|
|
47
|
+
* Color of the guide lines (rail, elbow foot and vertical segments).
|
|
48
|
+
* Any Chakra color token. The default is a mode-flipping semantic
|
|
49
|
+
* token, so it darkens in light mode and lightens in dark mode
|
|
50
|
+
* automatically.
|
|
51
|
+
* @default 'border.default'
|
|
52
|
+
*/
|
|
53
|
+
guideColor?: string;
|
|
54
|
+
};
|
|
27
55
|
export type TreeItemProps = TreeViewItemProps;
|
|
28
56
|
export type TreeItemTextProps = TreeViewItemTextProps;
|
|
29
57
|
export type TreeItemIndicatorProps = TreeViewItemIndicatorProps;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ChakraProvider } from '@chakra-ui/react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import { system } from '../../theme';
|
|
5
|
+
import { createTreeCollection } from './index';
|
|
6
|
+
import { Tree } from './Tree';
|
|
7
|
+
|
|
8
|
+
interface N {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
children?: N[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sample: N = {
|
|
15
|
+
id: 'ROOT',
|
|
16
|
+
name: '',
|
|
17
|
+
children: [{ id: 'a', name: 'A', children: [{ id: 'a1', name: 'A1' }] }],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const collection = createTreeCollection<N>({
|
|
21
|
+
rootNode: sample,
|
|
22
|
+
nodeToValue: (n) => n.id,
|
|
23
|
+
nodeToString: (n) => n.name,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const renderNode = ({
|
|
27
|
+
node,
|
|
28
|
+
nodeState,
|
|
29
|
+
}: {
|
|
30
|
+
node: N;
|
|
31
|
+
nodeState: { isBranch: boolean };
|
|
32
|
+
}) =>
|
|
33
|
+
nodeState.isBranch ? (
|
|
34
|
+
<Tree.BranchControl>
|
|
35
|
+
<Tree.BranchText>{node.name}</Tree.BranchText>
|
|
36
|
+
</Tree.BranchControl>
|
|
37
|
+
) : (
|
|
38
|
+
<Tree.Item>
|
|
39
|
+
<Tree.ItemText>{node.name}</Tree.ItemText>
|
|
40
|
+
</Tree.Item>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function renderGuide(elbow: boolean, extraProps: { guideColor?: string } = {}) {
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<ChakraProvider value={system}>
|
|
46
|
+
<Tree.Root
|
|
47
|
+
collection={collection}
|
|
48
|
+
aria-label="t"
|
|
49
|
+
defaultExpandedValue={['a']}
|
|
50
|
+
>
|
|
51
|
+
<Tree.Tree>
|
|
52
|
+
<Tree.Node
|
|
53
|
+
indentGuide={
|
|
54
|
+
elbow ? (
|
|
55
|
+
<Tree.BranchIndentGuide elbow {...extraProps} />
|
|
56
|
+
) : (
|
|
57
|
+
<Tree.BranchIndentGuide {...extraProps} />
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
render={renderNode}
|
|
61
|
+
/>
|
|
62
|
+
</Tree.Tree>
|
|
63
|
+
</Tree.Root>
|
|
64
|
+
</ChakraProvider>
|
|
65
|
+
);
|
|
66
|
+
const guide = container.querySelector<HTMLElement>(
|
|
67
|
+
'[data-part="branch-indent-guide"]'
|
|
68
|
+
);
|
|
69
|
+
if (!guide) throw new Error('no indent guide rendered');
|
|
70
|
+
return guide.className;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// All Chakra/emotion-injected CSS, concatenated.
|
|
74
|
+
const sheetText = () =>
|
|
75
|
+
Array.from(document.querySelectorAll('style'))
|
|
76
|
+
.map((s) => s.textContent ?? '')
|
|
77
|
+
.join('\n');
|
|
78
|
+
|
|
79
|
+
// The foot rule is anchored on the emotion-generated `css-*` class
|
|
80
|
+
// (not the static `chakra-*` slot class), so target that one.
|
|
81
|
+
const styleAnchor = (className: string) => {
|
|
82
|
+
const emotion = className
|
|
83
|
+
.trim()
|
|
84
|
+
.split(/\s+/)
|
|
85
|
+
.find((c) => /^css-/.test(c));
|
|
86
|
+
if (!emotion) throw new Error(`no emotion class in: ${className}`);
|
|
87
|
+
return '\\.' + emotion;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Regression guard for the `elbow` foot. Chakra renders ONE indent guide
|
|
92
|
+
* per `branchContent` (its first child), followed by that branch's rows
|
|
93
|
+
* as siblings — so the foot must be drawn on those sibling rows, scoped
|
|
94
|
+
* to the elbow guide's own class. An earlier `:last-of-type::after`
|
|
95
|
+
* attempt silently matched nothing (the guide is never the last sibling
|
|
96
|
+
* of its type), which is exactly what these tests pin down.
|
|
97
|
+
*/
|
|
98
|
+
describe('Tree.BranchIndentGuide elbow', () => {
|
|
99
|
+
it('draws a ::before foot on the guide’s sibling rows when elbow is set', () => {
|
|
100
|
+
const cls = renderGuide(true);
|
|
101
|
+
const anchor = styleAnchor(cls);
|
|
102
|
+
const css = sheetText();
|
|
103
|
+
|
|
104
|
+
// Foot rule is scoped to THIS guide's class via the sibling combinator,
|
|
105
|
+
// for both leaf rows and branch rows — never applied globally.
|
|
106
|
+
expect(css).toMatch(
|
|
107
|
+
new RegExp(`${anchor}\\s*~\\s*\\[data-part="item"\\]::before`)
|
|
108
|
+
);
|
|
109
|
+
expect(css).toMatch(
|
|
110
|
+
new RegExp(
|
|
111
|
+
`${anchor}\\s*~\\s*\\[data-part="branch"\\]\\s*>\\s*\\[data-part="branch-control"\\]::before`
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// The foot paints a 1px cross-stroke pinned to the row's vertical
|
|
116
|
+
// centre, starting at the parent rail column.
|
|
117
|
+
const footRule =
|
|
118
|
+
css.match(
|
|
119
|
+
new RegExp(
|
|
120
|
+
`${anchor}\\s*~\\s*\\[data-part="item"\\]::before\\s*\\{[^}]*\\}`
|
|
121
|
+
)
|
|
122
|
+
)?.[0] ?? '';
|
|
123
|
+
expect(footRule).toContain('content:""');
|
|
124
|
+
expect(footRule).toContain('position:absolute');
|
|
125
|
+
expect(footRule).toContain('inset-block-start:50%');
|
|
126
|
+
expect(footRule).toContain('height:1px');
|
|
127
|
+
expect(footRule).toMatch(
|
|
128
|
+
/inset-inline-start:calc\(var\(--tree-offset\) - var\(--tree-elbow-indentation\)\)/
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Elbow mode widens per-level indentation: the sibling rows redefine
|
|
132
|
+
// the recipe's indentation-offset from the widened value, pushing the
|
|
133
|
+
// row text right, away from the elbow foot.
|
|
134
|
+
const rowVarsRule =
|
|
135
|
+
css.match(
|
|
136
|
+
new RegExp(`${anchor}\\s*~\\s*\\[data-part="item"\\]\\s*\\{[^}]*\\}`)
|
|
137
|
+
)?.[0] ?? '';
|
|
138
|
+
expect(rowVarsRule).toContain(
|
|
139
|
+
'--tree-elbow-indentation:calc(var(--tree-indentation) + 4px)'
|
|
140
|
+
);
|
|
141
|
+
expect(rowVarsRule).toContain(
|
|
142
|
+
'--tree-indentation-offset:calc(var(--tree-elbow-indentation) * var(--tree-depth))'
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('draws no foot for a plain guide (no elbow)', () => {
|
|
147
|
+
const cls = renderGuide(false);
|
|
148
|
+
const anchor = styleAnchor(cls);
|
|
149
|
+
// This specific guide's class must not anchor any ::before foot rule.
|
|
150
|
+
expect(sheetText()).not.toMatch(new RegExp(`${anchor}\\s*~[^{]*::before`));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('stops the vertical segment at the centre of the last row (└)', () => {
|
|
154
|
+
const cls = renderGuide(true);
|
|
155
|
+
const anchor = styleAnchor(cls);
|
|
156
|
+
const css = sheetText();
|
|
157
|
+
|
|
158
|
+
// Middle rows: full-height vertical (├) on the row itself.
|
|
159
|
+
const middleRule =
|
|
160
|
+
css.match(
|
|
161
|
+
new RegExp(
|
|
162
|
+
`${anchor}\\s*~\\s*\\[data-part="item"\\]:not\\(:last-child\\)::after\\s*\\{[^}]*\\}`
|
|
163
|
+
)
|
|
164
|
+
)?.[0] ?? '';
|
|
165
|
+
expect(middleRule).toContain('inset-block-start:0');
|
|
166
|
+
expect(middleRule).toContain('inset-block-end:0');
|
|
167
|
+
expect(middleRule).toContain('width:1px');
|
|
168
|
+
|
|
169
|
+
// Last leaf row: segment ends at 50% so the foot forms a └.
|
|
170
|
+
const lastItemRule =
|
|
171
|
+
css.match(
|
|
172
|
+
new RegExp(
|
|
173
|
+
`${anchor}\\s*~\\s*\\[data-part="item"\\]:last-child::after\\s*\\{[^}]*\\}`
|
|
174
|
+
)
|
|
175
|
+
)?.[0] ?? '';
|
|
176
|
+
expect(lastItemRule).toContain('inset-block-start:0');
|
|
177
|
+
expect(lastItemRule).toContain('height:50%');
|
|
178
|
+
|
|
179
|
+
// Last branch row: half-height segment on its control only, so an
|
|
180
|
+
// expanded last branch has no rail running past its elbow.
|
|
181
|
+
const lastBranchRule =
|
|
182
|
+
css.match(
|
|
183
|
+
new RegExp(
|
|
184
|
+
`${anchor}\\s*~\\s*\\[data-part="branch"\\]:last-child\\s*>\\s*\\[data-part="branch-control"\\]::after\\s*\\{[^}]*\\}`
|
|
185
|
+
)
|
|
186
|
+
)?.[0] ?? '';
|
|
187
|
+
expect(lastBranchRule).toContain('height:50%');
|
|
188
|
+
|
|
189
|
+
// Non-last branch: pass-through rail spans the whole expanded subtree,
|
|
190
|
+
// painted above row hover backgrounds like the original slot rail.
|
|
191
|
+
const passThroughRule =
|
|
192
|
+
css.match(
|
|
193
|
+
new RegExp(
|
|
194
|
+
`${anchor}\\s*~\\s*\\[data-part="branch"\\]:not\\(:last-child\\)::before\\s*\\{[^}]*\\}`
|
|
195
|
+
)
|
|
196
|
+
)?.[0] ?? '';
|
|
197
|
+
expect(passThroughRule).toContain('inset-block-end:0');
|
|
198
|
+
expect(passThroughRule).toContain('z-index:1');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('paints the guide with the custom guideColor token', () => {
|
|
202
|
+
const cls = renderGuide(true, { guideColor: 'red.500' });
|
|
203
|
+
const anchor = styleAnchor(cls);
|
|
204
|
+
const css = sheetText();
|
|
205
|
+
const footRule =
|
|
206
|
+
css.match(
|
|
207
|
+
new RegExp(
|
|
208
|
+
`${anchor}\\s*~\\s*\\[data-part="item"\\]::before\\s*\\{[^}]*\\}`
|
|
209
|
+
)
|
|
210
|
+
)?.[0] ?? '';
|
|
211
|
+
expect(footRule).toMatch(/--chakra-colors-red-500|red\.500/);
|
|
212
|
+
});
|
|
213
|
+
});
|