tharaday 0.7.2 → 0.7.3

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 (44) hide show
  1. package/.storybook/main.ts +1 -1
  2. package/.storybook/preview.ts +0 -2
  3. package/.storybook/vitest.setup.ts +2 -0
  4. package/dist/ds.css +1 -1
  5. package/dist/ds.js +873 -805
  6. package/dist/ds.umd.cjs +1 -1
  7. package/dist/src/components/Tree/Tree.d.ts +1 -1
  8. package/dist/src/components/Tree/Tree.stories.d.ts +1 -1
  9. package/dist/src/components/Tree/TreeItem.d.ts +1 -1
  10. package/dist/src/components/Tree/TreeItem.types.d.ts +6 -0
  11. package/package.json +8 -1
  12. package/src/components/Accordion/Accordion.test.tsx +82 -0
  13. package/src/components/Avatar/Avatar.test.tsx +36 -0
  14. package/src/components/Badge/Badge.test.tsx +15 -0
  15. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +96 -0
  16. package/src/components/Checkbox/Checkbox.module.css +7 -7
  17. package/src/components/Checkbox/Checkbox.test.tsx +68 -0
  18. package/src/components/Dropdown/Dropdown.test.tsx +104 -0
  19. package/src/components/Input/Input.test.tsx +61 -0
  20. package/src/components/List/List.module.css +12 -12
  21. package/src/components/List/List.test.tsx +46 -0
  22. package/src/components/Modal/Modal.module.css +5 -5
  23. package/src/components/Modal/Modal.test.tsx +86 -0
  24. package/src/components/NavBar/NavBar.module.css +3 -3
  25. package/src/components/Notification/Notification.module.css +6 -6
  26. package/src/components/Notification/Notification.test.tsx +38 -0
  27. package/src/components/Pagination/Pagination.test.tsx +70 -0
  28. package/src/components/ProgressBar/ProgressBar.test.tsx +58 -0
  29. package/src/components/RadioButton/RadioButton.test.tsx +51 -0
  30. package/src/components/Select/Select.test.tsx +64 -0
  31. package/src/components/Slider/Slider.test.tsx +49 -0
  32. package/src/components/Stepper/Step.module.css +2 -2
  33. package/src/components/Stepper/Stepper.test.tsx +51 -0
  34. package/src/components/Switch/Switch.test.tsx +53 -0
  35. package/src/components/Table/Table.test.tsx +78 -0
  36. package/src/components/Tabs/Tabs.test.tsx +83 -0
  37. package/src/components/Textarea/Textarea.test.tsx +56 -0
  38. package/src/components/Tree/Tree.test.tsx +116 -0
  39. package/src/components/Tree/Tree.tsx +65 -1
  40. package/src/components/Tree/TreeItem.module.css +20 -26
  41. package/src/components/Tree/TreeItem.tsx +144 -79
  42. package/src/components/Tree/TreeItem.types.ts +6 -0
  43. package/src/styles/semantic.css +3 -0
  44. package/src/styles/tokens.css +15 -0
@@ -1,4 +1,6 @@
1
1
  import clsx from 'clsx';
2
+ import { useEffect, useRef } from 'react';
3
+ import type { KeyboardEvent } from 'react';
2
4
  import type { TreeProps } from './Tree.types';
3
5
  import styles from './Tree.module.css';
4
6
  import { TreeItem } from './TreeItem';
@@ -11,14 +13,76 @@ export const Tree = ({
11
13
  collapseIcon,
12
14
  ...props
13
15
  }: TreeProps) => {
16
+ const treeRef = useRef<HTMLDivElement>(null);
17
+
18
+ useEffect(() => {
19
+ const first = treeRef.current?.querySelector<HTMLElement>('[role="treeitem"]');
20
+ if (first) {
21
+ first.tabIndex = 0;
22
+ }
23
+ }, []);
24
+
25
+ const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
26
+ const tree = treeRef.current;
27
+ if (!tree) {
28
+ return;
29
+ }
30
+ const items = Array.from(tree.querySelectorAll<HTMLElement>('[role="treeitem"]'));
31
+ if (items.length === 0) {
32
+ return;
33
+ }
34
+ const current = items.find((el) => el.tabIndex === 0) ?? items[0];
35
+ const idx = items.indexOf(current);
36
+
37
+ switch (e.key) {
38
+ case 'ArrowDown':
39
+ e.preventDefault();
40
+ if (idx < items.length - 1) {
41
+ current.tabIndex = -1;
42
+ items[idx + 1].tabIndex = 0;
43
+ items[idx + 1].focus();
44
+ }
45
+ break;
46
+ case 'ArrowUp':
47
+ e.preventDefault();
48
+ if (idx > 0) {
49
+ current.tabIndex = -1;
50
+ items[idx - 1].tabIndex = 0;
51
+ items[idx - 1].focus();
52
+ }
53
+ break;
54
+ case 'Home':
55
+ e.preventDefault();
56
+ current.tabIndex = -1;
57
+ items[0].tabIndex = 0;
58
+ items[0].focus();
59
+ break;
60
+ case 'End':
61
+ e.preventDefault();
62
+ current.tabIndex = -1;
63
+ items[items.length - 1].tabIndex = 0;
64
+ items[items.length - 1].focus();
65
+ break;
66
+ }
67
+ };
68
+
14
69
  return (
15
- <div className={clsx(styles.root, className)} {...props}>
70
+ <div
71
+ ref={treeRef}
72
+ role="tree"
73
+ className={clsx(styles.root, className)}
74
+ onKeyDown={handleKeyDown}
75
+ {...props}
76
+ >
16
77
  <TreeItem
17
78
  data={data}
18
79
  defaultExpanded={defaultExpanded}
19
80
  expandIcon={expandIcon}
20
81
  collapseIcon={collapseIcon}
21
82
  isRoot
83
+ level={1}
84
+ setSize={1}
85
+ posInSet={1}
22
86
  />
23
87
  </div>
24
88
  );
@@ -1,25 +1,32 @@
1
- .list {
2
- list-style: none;
3
- padding-left: var(--ds-space-4);
4
- margin: 0;
1
+ .item {
2
+ outline: none;
5
3
  }
6
4
 
7
- .item {
8
- margin: 0;
5
+ .item:focus-visible > .itemHeader {
6
+ box-shadow:
7
+ 0 0 0 2px var(--ds-ring-offset),
8
+ 0 0 0 4px var(--ds-ring);
9
+ border-radius: var(--ds-radius-sm);
9
10
  }
10
11
 
11
12
  .itemHeader {
12
13
  display: flex;
13
14
  align-items: center;
14
15
  min-height: var(--ds-space-6);
16
+ padding: 0 var(--ds-space-1);
17
+ border-radius: var(--ds-radius-sm);
18
+ cursor: default;
15
19
  }
16
20
 
17
- .toggleButton {
18
- background: none;
19
- border: none;
20
- padding: 0;
21
- margin: 0;
21
+ .branch > .itemHeader {
22
22
  cursor: pointer;
23
+ }
24
+
25
+ .itemHeader:hover {
26
+ background-color: var(--ds-surface-1);
27
+ }
28
+
29
+ .toggleIcon {
23
30
  display: inline-flex;
24
31
  align-items: center;
25
32
  justify-content: center;
@@ -27,11 +34,6 @@
27
34
  width: var(--ds-space-5);
28
35
  height: var(--ds-space-5);
29
36
  flex-shrink: 0;
30
- transition: transform var(--ds-transition-fast);
31
- }
32
-
33
- .toggleButton:hover {
34
- color: var(--ds-text-1);
35
37
  }
36
38
 
37
39
  .key {
@@ -50,14 +52,6 @@
50
52
  margin-left: var(--ds-space-1);
51
53
  }
52
54
 
53
- .collapsibleContent {
54
- display: none;
55
- }
56
-
57
- .expanded {
58
- display: block;
59
- }
60
-
61
- .rootList {
62
- padding-left: 0;
55
+ .childGroup {
56
+ padding-left: var(--ds-space-4);
63
57
  }
@@ -1,8 +1,21 @@
1
1
  import clsx from 'clsx';
2
- import { useState } from 'react';
2
+ import { useRef, useState } from 'react';
3
+ import type { FocusEvent, KeyboardEvent, MouseEvent } from 'react';
3
4
  import type { TreeItemProps } from './TreeItem.types';
4
5
  import styles from './TreeItem.module.css';
5
6
 
7
+ function focusTreeItem(item: HTMLElement) {
8
+ const tree = item.closest('[role="tree"]');
9
+ if (!tree) {
10
+ return;
11
+ }
12
+ tree.querySelectorAll<HTMLElement>('[role="treeitem"]').forEach((el) => {
13
+ el.tabIndex = -1;
14
+ });
15
+ item.tabIndex = 0;
16
+ item.focus();
17
+ }
18
+
6
19
  export const TreeItem = ({
7
20
  data,
8
21
  label,
@@ -10,14 +23,91 @@ export const TreeItem = ({
10
23
  expandIcon,
11
24
  collapseIcon,
12
25
  isRoot,
26
+ level,
27
+ setSize,
28
+ posInSet,
13
29
  }: TreeItemProps) => {
14
30
  const [isExpanded, setIsExpanded] = useState(isRoot ? true : (defaultExpanded ?? false));
31
+ const itemRef = useRef<HTMLDivElement>(null);
15
32
 
16
33
  const isObject = data !== null && typeof data === 'object';
17
34
  const hasChildren =
18
- isObject && (Array.isArray(data) ? data.length > 0 : Object.keys(data).length > 0);
35
+ isObject && (Array.isArray(data) ? data.length > 0 : Object.keys(data as object).length > 0);
36
+
37
+ const childLevel = isRoot ? level : level + 1;
38
+
39
+ const getChildren = (): { key: string; value: unknown }[] => {
40
+ if (!isObject) {
41
+ return [];
42
+ }
43
+ if (Array.isArray(data)) {
44
+ return (data as unknown[]).map((v, i) => ({ key: String(i), value: v }));
45
+ }
46
+ return Object.entries(data as Record<string, unknown>).map(([k, v]) => ({ key: k, value: v }));
47
+ };
19
48
 
20
- const toggle = () => setIsExpanded(!isExpanded);
49
+ const children = getChildren();
50
+
51
+ const handleClick = (e: MouseEvent) => {
52
+ e.stopPropagation();
53
+ if (hasChildren) {
54
+ setIsExpanded((prev) => !prev);
55
+ }
56
+ if (itemRef.current) {
57
+ focusTreeItem(itemRef.current);
58
+ }
59
+ };
60
+
61
+ const handleFocus = (e: FocusEvent) => {
62
+ e.stopPropagation();
63
+ if (!itemRef.current) {
64
+ return;
65
+ }
66
+ const tree = itemRef.current.closest('[role="tree"]');
67
+ if (!tree) {
68
+ return;
69
+ }
70
+ tree.querySelectorAll<HTMLElement>('[role="treeitem"]').forEach((el) => {
71
+ el.tabIndex = -1;
72
+ });
73
+ itemRef.current.tabIndex = 0;
74
+ };
75
+
76
+ const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
77
+ switch (e.key) {
78
+ case 'Enter':
79
+ case ' ':
80
+ e.preventDefault();
81
+ if (hasChildren) {
82
+ setIsExpanded((prev) => !prev);
83
+ }
84
+ break;
85
+ case 'ArrowRight':
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+ if (hasChildren && !isExpanded) {
89
+ setIsExpanded(true);
90
+ } else if (hasChildren && isExpanded) {
91
+ const firstChild = itemRef.current?.querySelector<HTMLElement>('[role="treeitem"]');
92
+ if (firstChild) {
93
+ focusTreeItem(firstChild);
94
+ }
95
+ }
96
+ break;
97
+ case 'ArrowLeft':
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+ if (hasChildren && isExpanded) {
101
+ setIsExpanded(false);
102
+ } else {
103
+ const parent = itemRef.current?.parentElement?.closest<HTMLElement>('[role="treeitem"]');
104
+ if (parent) {
105
+ focusTreeItem(parent);
106
+ }
107
+ }
108
+ break;
109
+ }
110
+ };
21
111
 
22
112
  const defaultExpandIcon = (
23
113
  <svg
@@ -49,98 +139,73 @@ export const TreeItem = ({
49
139
  </svg>
50
140
  );
51
141
 
52
- const renderContent = () => {
142
+ const renderLeafContent = () => {
53
143
  if (data === undefined) {
54
144
  return <span className={styles.empty}>undefined</span>;
55
145
  }
56
-
57
146
  if (data === null) {
58
147
  return <span className={styles.empty}>null</span>;
59
148
  }
60
-
61
149
  if (typeof data !== 'object') {
62
150
  return <span className={styles.value}>{String(data)}</span>;
63
151
  }
64
-
65
- if (Array.isArray(data)) {
66
- if (data.length === 0) {
67
- return <span className={styles.empty}>[]</span>;
68
- }
69
- return (
70
- <ul
71
- className={clsx(
72
- styles.list,
73
- styles.collapsibleContent,
74
- isExpanded && styles.expanded,
75
- isRoot && styles.rootList
76
- )}
77
- >
78
- {data.map((item, index) => (
79
- <li key={index} className={styles.item}>
80
- <TreeItem
81
- data={item}
82
- defaultExpanded={defaultExpanded}
83
- expandIcon={expandIcon}
84
- collapseIcon={collapseIcon}
85
- />
86
- </li>
87
- ))}
88
- </ul>
89
- );
152
+ if (Array.isArray(data) && (data as unknown[]).length === 0) {
153
+ return <span className={styles.empty}>[]</span>;
90
154
  }
91
-
92
- if (Object.keys(data).length === 0) {
93
- return <span className={styles.empty}>{}</span>;
155
+ if (!Array.isArray(data) && Object.keys(data as object).length === 0) {
156
+ return <span className={styles.empty}>{'{}'}</span>;
94
157
  }
95
-
96
- return (
97
- <ul
98
- className={clsx(
99
- styles.list,
100
- styles.collapsibleContent,
101
- isExpanded && styles.expanded,
102
- isRoot && styles.rootList
103
- )}
104
- >
105
- {Object.entries(data).map(([key, value]) => (
106
- <li key={key} className={styles.item}>
107
- <TreeItem
108
- label={key}
109
- data={value}
110
- defaultExpanded={defaultExpanded}
111
- expandIcon={expandIcon}
112
- collapseIcon={collapseIcon}
113
- />
114
- </li>
115
- ))}
116
- </ul>
117
- );
158
+ return null;
118
159
  };
119
160
 
161
+ const childrenNodes = children.map((child, index) => (
162
+ <TreeItem
163
+ key={child.key}
164
+ label={Array.isArray(data) ? undefined : child.key}
165
+ data={child.value}
166
+ defaultExpanded={defaultExpanded}
167
+ expandIcon={expandIcon}
168
+ collapseIcon={collapseIcon}
169
+ level={childLevel}
170
+ setSize={children.length}
171
+ posInSet={index + 1}
172
+ />
173
+ ));
174
+
175
+ if (isRoot) {
176
+ return hasChildren ? <>{childrenNodes}</> : <>{renderLeafContent()}</>;
177
+ }
178
+
120
179
  return (
121
- <div>
122
- {!isRoot && (
123
- <div className={styles.itemHeader}>
124
- {hasChildren ? (
125
- <button
126
- type="button"
127
- className={styles.toggleButton}
128
- onClick={toggle}
129
- aria-expanded={isExpanded}
130
- >
131
- {isExpanded
132
- ? (collapseIcon ?? defaultCollapseIcon)
133
- : (expandIcon ?? defaultExpandIcon)}
134
- </button>
135
- ) : (
136
- <div className={styles.toggleButton} aria-hidden="true" />
137
- )}
138
- {label && <span className={styles.key}>{label}:</span>}
139
- {!hasChildren && renderContent()}
180
+ <div
181
+ ref={itemRef}
182
+ role="treeitem"
183
+ aria-expanded={hasChildren ? isExpanded : undefined}
184
+ aria-level={level}
185
+ aria-setsize={setSize}
186
+ aria-posinset={posInSet}
187
+ tabIndex={-1}
188
+ className={clsx(styles.item, hasChildren && styles.branch)}
189
+ onClick={handleClick}
190
+ onFocus={handleFocus}
191
+ onKeyDown={handleKeyDown}
192
+ >
193
+ <div className={styles.itemHeader}>
194
+ <span className={styles.toggleIcon} aria-hidden="true">
195
+ {hasChildren
196
+ ? isExpanded
197
+ ? (collapseIcon ?? defaultCollapseIcon)
198
+ : (expandIcon ?? defaultExpandIcon)
199
+ : null}
200
+ </span>
201
+ {label !== undefined && <span className={styles.key}>{label}:</span>}
202
+ {!hasChildren && renderLeafContent()}
203
+ </div>
204
+ {hasChildren && isExpanded && (
205
+ <div role="group" className={styles.childGroup}>
206
+ {childrenNodes}
140
207
  </div>
141
208
  )}
142
- {hasChildren && renderContent()}
143
- {isRoot && !hasChildren && <div className={styles.itemHeader}>{renderContent()}</div>}
144
209
  </div>
145
210
  );
146
211
  };
@@ -13,4 +13,10 @@ export interface TreeItemProps {
13
13
  collapseIcon?: ReactNode;
14
14
  /** Whether the tree item is a root element */
15
15
  isRoot?: boolean;
16
+ /** ARIA level (depth in the tree, 1-based) */
17
+ level: number;
18
+ /** Total number of siblings at this level */
19
+ setSize: number;
20
+ /** 1-based position among siblings */
21
+ posInSet: number;
16
22
  }
@@ -50,6 +50,9 @@
50
50
  --ds-border-default: var(--ds-border-1);
51
51
  --ds-surface-disabled: var(--ds-ref-surface-disabled);
52
52
 
53
+ /* Overlay */
54
+ --ds-overlay: rgba(0, 0, 0, 0.5);
55
+
53
56
  /* Skeleton */
54
57
  --ds-skeleton: var(--ds-ref-skeleton);
55
58
  --ds-skeleton-highlight: var(--ds-ref-skeleton-highlight);
@@ -258,8 +258,19 @@
258
258
  --ds-space-14: 3.5rem; /* 56px */
259
259
  --ds-space-16: 4rem; /* 64px */
260
260
 
261
+ /* --- CONTENT WIDTH SCALE --- */
262
+
263
+ --ds-content-width-sm: 25rem; /* 400px — notifications, small modals */
264
+ --ds-content-width-md: 37.5rem; /* 600px — default modal */
265
+ --ds-content-width-lg: 50rem; /* 800px — large modal */
266
+ --ds-content-width-xl: 62.5rem; /* 1000px — xl modal */
267
+ --ds-content-width-2xl: 75rem; /* 1200px — page/nav max-width */
268
+
261
269
  /* --- BORDERS & SHADOWS --- */
262
270
 
271
+ --ds-border-width: 0.0625rem; /* 1px */
272
+ --ds-border-width-2: 0.125rem; /* 2px */
273
+
263
274
  --ds-radius-sm: 0.25rem;
264
275
  --ds-radius-md: 0.5rem;
265
276
  --ds-radius-lg: 1rem;
@@ -269,6 +280,10 @@
269
280
  --ds-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
270
281
  --ds-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
271
282
 
283
+ /* --- COMPONENT TOKENS --- */
284
+
285
+ --ds-checkbox-size: 1.125rem; /* 18px — checkbox and radio control box */
286
+
272
287
  /* --- ANIMATION --- */
273
288
 
274
289
  --ds-transition-fast: 150ms ease;