@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.
Files changed (88) hide show
  1. package/dist/components/Icon/_utils/createIcon.d.ts.map +1 -1
  2. package/dist/components/Icon/_utils/createIcon.js +1 -0
  3. package/dist/components/Icon/_utils/createIcon.js.map +1 -1
  4. package/dist/components/Icon/_utils/createIcon.mjs +1 -0
  5. package/dist/components/Icon/_utils/createIcon.mjs.map +1 -1
  6. package/dist/components/Loaders/PageLoader.d.ts.map +1 -1
  7. package/dist/components/Loaders/PageLoader.js +2 -2
  8. package/dist/components/Loaders/PageLoader.js.map +1 -1
  9. package/dist/components/Loaders/PageLoader.mjs +2 -2
  10. package/dist/components/Loaders/PageLoader.mjs.map +1 -1
  11. package/dist/components/Loaders/SectionLoader.d.ts.map +1 -1
  12. package/dist/components/Loaders/SectionLoader.js +2 -2
  13. package/dist/components/Loaders/SectionLoader.js.map +1 -1
  14. package/dist/components/Loaders/SectionLoader.mjs +2 -2
  15. package/dist/components/Loaders/SectionLoader.mjs.map +1 -1
  16. package/dist/components/Tag/Tag.d.ts.map +1 -1
  17. package/dist/components/Tag/Tag.js +1 -1
  18. package/dist/components/Tag/Tag.js.map +1 -1
  19. package/dist/components/Tag/Tag.mjs +1 -1
  20. package/dist/components/Tag/Tag.mjs.map +1 -1
  21. package/dist/components/Tag/Tag.styles.d.ts.map +1 -1
  22. package/dist/components/Tag/Tag.styles.js +22 -16
  23. package/dist/components/Tag/Tag.styles.js.map +1 -1
  24. package/dist/components/Tag/Tag.styles.mjs +22 -16
  25. package/dist/components/Tag/Tag.styles.mjs.map +1 -1
  26. package/dist/components/Tree/Tree.d.ts +5 -1
  27. package/dist/components/Tree/Tree.d.ts.map +1 -1
  28. package/dist/components/Tree/Tree.types.d.ts +29 -1
  29. package/dist/components/Tree/Tree.types.d.ts.map +1 -1
  30. package/dist/components/Tree/TreeBranchIndentGuide.d.ts +5 -1
  31. package/dist/components/Tree/TreeBranchIndentGuide.d.ts.map +1 -1
  32. package/dist/components/Tree/TreeBranchIndentGuide.js +107 -8
  33. package/dist/components/Tree/TreeBranchIndentGuide.js.map +1 -1
  34. package/dist/components/Tree/TreeBranchIndentGuide.mjs +107 -8
  35. package/dist/components/Tree/TreeBranchIndentGuide.mjs.map +1 -1
  36. package/dist/components/Typography/Link.d.ts.map +1 -1
  37. package/dist/components/Typography/Link.js +6 -1
  38. package/dist/components/Typography/Link.js.map +1 -1
  39. package/dist/components/Typography/Link.mjs +6 -1
  40. package/dist/components/Typography/Link.mjs.map +1 -1
  41. package/dist/index.d.ts +0 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +0 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/index.mjs +0 -1
  46. package/dist/index.mjs.map +1 -1
  47. package/dist/theme/global.d.ts.map +1 -1
  48. package/dist/theme/global.js +4 -1
  49. package/dist/theme/global.js.map +1 -1
  50. package/dist/theme/global.mjs +4 -1
  51. package/dist/theme/global.mjs.map +1 -1
  52. package/dist/theme/index.d.ts +1 -1
  53. package/dist/theme/index.d.ts.map +1 -1
  54. package/dist/theme/index.js +8 -4
  55. package/dist/theme/index.js.map +1 -1
  56. package/dist/theme/index.mjs +8 -4
  57. package/dist/theme/index.mjs.map +1 -1
  58. package/package.json +1 -3
  59. package/src/components/Icon/ICON_MANAGEMENT.md +1 -0
  60. package/src/components/Icon/Icon.stories.tsx +6 -2
  61. package/src/components/Icon/_utils/createIcon.tsx +1 -0
  62. package/src/components/Loaders/PageLoader.tsx +3 -5
  63. package/src/components/Loaders/SectionLoader.tsx +3 -2
  64. package/src/components/Tag/Tag.styles.ts +22 -16
  65. package/src/components/Tag/Tag.tsx +1 -0
  66. package/src/components/Tree/Tree.GuideElbow.stories.tsx +220 -0
  67. package/src/components/Tree/Tree.types.ts +29 -1
  68. package/src/components/Tree/TreeBranchIndentGuide.test.tsx +213 -0
  69. package/src/components/Tree/TreeBranchIndentGuide.tsx +137 -11
  70. package/src/components/Typography/Link.tsx +5 -1
  71. package/src/index.ts +0 -1
  72. package/src/theme/global.ts +4 -1
  73. package/src/theme/index.ts +8 -4
  74. package/dist/components/MDXEditor/MDXEditor.d.ts +0 -6
  75. package/dist/components/MDXEditor/MDXEditor.d.ts.map +0 -1
  76. package/dist/components/MDXEditor/MDXEditor.js +0 -237
  77. package/dist/components/MDXEditor/MDXEditor.js.map +0 -1
  78. package/dist/components/MDXEditor/MDXEditor.mjs +0 -235
  79. package/dist/components/MDXEditor/MDXEditor.mjs.map +0 -1
  80. package/dist/components/MDXEditor/MDXEditor.types.d.ts +0 -7
  81. package/dist/components/MDXEditor/MDXEditor.types.d.ts.map +0 -1
  82. package/dist/components/MDXEditor/index.d.ts +0 -3
  83. package/dist/components/MDXEditor/index.d.ts.map +0 -1
  84. package/src/components/MDXEditor/MDXEditor.css +0 -40
  85. package/src/components/MDXEditor/MDXEditor.stories.tsx +0 -56
  86. package/src/components/MDXEditor/MDXEditor.tsx +0 -337
  87. package/src/components/MDXEditor/MDXEditor.types.ts +0 -7
  88. package/src/components/MDXEditor/index.ts +0 -2
@@ -78,6 +78,7 @@ Override per usage: `<MyIcon color="primary.main" />`.
78
78
 
79
79
  | boxSize | px |
80
80
  |---------|-----|
81
+ | `2xs` | 14px |
81
82
  | `xs` | 16px |
82
83
  | `sm` | 20px |
83
84
  | `md` | 24px |
@@ -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 (16px ~ 40px)</Text>
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>
@@ -7,6 +7,7 @@ export type IconProps = Partial<SVGProps<SVGElement>> &
7
7
 
8
8
  // Static size mapping (SSR-safe)
9
9
  const sizeMapping: Record<string, string> = {
10
+ '2xs': '3.5',
10
11
  xs: '4',
11
12
  sm: '5',
12
13
  md: '6',
@@ -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
- <BounceLoader
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
- <DotLoader color="var(--chakra-colors-primary-main)" />
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.dark',
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.main',
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.dark',
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.main',
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.dark',
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.main',
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.dark',
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.main',
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.dark',
88
+ color: 'warning.darker',
89
89
  },
90
90
  solid: {
91
- bgColor: 'gold.500',
92
- borderColor: 'gold.500',
93
- color: 'gold.900', // dark text: white/gold.500 was 2.39:1; gold.900 = 6.73:1
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
- color: 'warning.main',
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.muted text stays legible
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.muted',
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.subtle',
124
+ color: 'fg.muted',
119
125
  },
120
126
  },
121
127
  };
@@ -36,6 +36,7 @@ export const Tag = forwardRef(
36
36
  borderRadius="md"
37
37
  boxShadow="none"
38
38
  fontSize="sm"
39
+ fontWeight="semibold"
39
40
  {...styles}
40
41
  {...rest}
41
42
  ref={ref}
@@ -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
+ });