tharaday 0.5.11 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/components/List/List.d.ts +5 -0
  2. package/dist/components/List/List.stories.d.ts +25 -0
  3. package/dist/components/List/List.types.d.ts +42 -0
  4. package/dist/components/List/ListItem.d.ts +2 -0
  5. package/dist/components/List/ListItem.types.d.ts +9 -0
  6. package/dist/components/Slider/Slider.d.ts +1 -1
  7. package/dist/components/Slider/Slider.stories.d.ts +2 -1
  8. package/dist/components/Slider/Slider.types.d.ts +1 -0
  9. package/dist/components/Tree/Tree.d.ts +5 -0
  10. package/dist/components/Tree/Tree.stories.d.ts +20 -0
  11. package/dist/components/Tree/Tree.types.d.ts +14 -0
  12. package/dist/components/Tree/TreeItem.d.ts +2 -0
  13. package/dist/components/Tree/TreeItem.types.d.ts +15 -0
  14. package/dist/ds.css +1 -1
  15. package/dist/ds.js +1557 -1254
  16. package/dist/ds.umd.cjs +1 -1
  17. package/dist/index.d.ts +8 -0
  18. package/package.json +3 -3
  19. package/src/components/List/List.module.css +85 -0
  20. package/src/components/List/List.stories.tsx +92 -0
  21. package/src/components/List/List.tsx +93 -0
  22. package/src/components/List/List.types.ts +45 -0
  23. package/src/components/List/ListItem.module.css +12 -0
  24. package/src/components/List/ListItem.tsx +12 -0
  25. package/src/components/List/ListItem.types.ts +10 -0
  26. package/src/components/Slider/Slider.module.css +19 -0
  27. package/src/components/Slider/Slider.stories.tsx +19 -0
  28. package/src/components/Slider/Slider.tsx +101 -2
  29. package/src/components/Slider/Slider.types.ts +1 -0
  30. package/src/components/Tree/Tree.module.css +5 -0
  31. package/src/components/Tree/Tree.stories.tsx +113 -0
  32. package/src/components/Tree/Tree.tsx +27 -0
  33. package/src/components/Tree/Tree.types.ts +16 -0
  34. package/src/components/Tree/TreeItem.module.css +63 -0
  35. package/src/components/Tree/TreeItem.tsx +146 -0
  36. package/src/components/Tree/TreeItem.types.ts +16 -0
  37. package/src/index.ts +8 -0
@@ -0,0 +1,93 @@
1
+ import clsx from 'clsx';
2
+ import type { CSSProperties } from 'react';
3
+
4
+ import styles from './List.module.css';
5
+ import type { ListProps } from './List.types';
6
+ import { getSpacingStyles } from '../Box/helpers/getSpacingStyles';
7
+ import { ListItem } from './ListItem';
8
+
9
+ export const List = ({
10
+ children,
11
+ variant = 'unordered',
12
+ spacing = 0,
13
+ className,
14
+ margin,
15
+ marginX,
16
+ marginY,
17
+ marginTop,
18
+ marginBottom,
19
+ marginLeft,
20
+ marginRight,
21
+ padding,
22
+ paddingX,
23
+ paddingY,
24
+ paddingTop,
25
+ paddingBottom,
26
+ paddingLeft,
27
+ paddingRight,
28
+ style,
29
+ ...props
30
+ }: ListProps) => {
31
+ const Component = variant === 'ordered' ? 'ol' : 'ul';
32
+
33
+ const listStyles: CSSProperties = {
34
+ ...style,
35
+ ...getSpacingStyles(
36
+ 'padding',
37
+ padding,
38
+ paddingX,
39
+ paddingY,
40
+ paddingTop,
41
+ paddingBottom,
42
+ paddingLeft,
43
+ paddingRight
44
+ ),
45
+ ...getSpacingStyles(
46
+ 'margin',
47
+ margin,
48
+ marginX,
49
+ marginY,
50
+ marginTop,
51
+ marginBottom,
52
+ marginLeft,
53
+ marginRight
54
+ ),
55
+ '--list-spacing': typeof spacing === 'number' ? `${spacing * 0.25}rem` : spacing,
56
+ } as CSSProperties;
57
+
58
+ return (
59
+ <Component
60
+ className={clsx(
61
+ styles.root,
62
+ styles[variant],
63
+ typeof spacing === 'number' && styles[`gap-${spacing}`],
64
+ // If it's 'none' variant or has an icon, we might want to use flex to handle alignment
65
+ (variant === 'none' ||
66
+ (typeof spacing === 'number' && spacing > 0) ||
67
+ typeof spacing === 'string') &&
68
+ styles.flex,
69
+ typeof padding === 'number' && styles[`p-${padding}`],
70
+ typeof paddingX === 'number' && styles[`px-${paddingX}`],
71
+ typeof paddingY === 'number' && styles[`py-${paddingY}`],
72
+ typeof paddingTop === 'number' && styles[`pt-${paddingTop}`],
73
+ typeof paddingBottom === 'number' && styles[`pb-${paddingBottom}`],
74
+ typeof paddingLeft === 'number' && styles[`pl-${paddingLeft}`],
75
+ typeof paddingRight === 'number' && styles[`pr-${paddingRight}`],
76
+ typeof margin === 'number' && styles[`margin-${margin}`],
77
+ typeof marginX === 'number' && styles[`marginX-${marginX}`],
78
+ typeof marginY === 'number' && styles[`marginY-${marginY}`],
79
+ typeof marginTop === 'number' && styles[`marginTop-${marginTop}`],
80
+ typeof marginBottom === 'number' && styles[`marginBottom-${marginBottom}`],
81
+ typeof marginLeft === 'number' && styles[`marginLeft-${marginLeft}`],
82
+ typeof marginRight === 'number' && styles[`marginRight-${marginRight}`],
83
+ className
84
+ )}
85
+ style={listStyles}
86
+ {...props}
87
+ >
88
+ {children}
89
+ </Component>
90
+ );
91
+ };
92
+
93
+ List.Item = ListItem;
@@ -0,0 +1,45 @@
1
+ import type { ReactNode, HTMLAttributes } from 'react';
2
+ import type { BoxPadding, BoxMargin } from '../Box/Box.types';
3
+
4
+ export type ListVariant = 'unordered' | 'ordered' | 'none';
5
+
6
+ export interface ListProps extends HTMLAttributes<HTMLUListElement | HTMLOListElement> {
7
+ /** The content of the list, usually ListItem components */
8
+ children: ReactNode;
9
+ /** The visual variant of the list */
10
+ variant?: ListVariant;
11
+ /** Spacing between list items */
12
+ spacing?: BoxPadding;
13
+ /** Additional class name */
14
+ className?: string;
15
+ /** Margin for the entire list */
16
+ margin?: BoxMargin;
17
+ /** Horizontal margin */
18
+ marginX?: BoxMargin;
19
+ /** Vertical margin */
20
+ marginY?: BoxMargin;
21
+ /** Top margin */
22
+ marginTop?: BoxMargin;
23
+ /** Bottom margin */
24
+ marginBottom?: BoxMargin;
25
+ /** Left margin */
26
+ marginLeft?: BoxMargin;
27
+ /** Right margin */
28
+ marginRight?: BoxMargin;
29
+ /** Padding for the entire list */
30
+ padding?: BoxPadding;
31
+ /** Horizontal padding */
32
+ paddingX?: BoxPadding;
33
+ /** Vertical padding */
34
+ paddingY?: BoxPadding;
35
+ /** Top padding */
36
+ paddingTop?: BoxPadding;
37
+ /** Bottom padding */
38
+ paddingBottom?: BoxPadding;
39
+ /** Left padding */
40
+ paddingLeft?: BoxPadding;
41
+ /** Right padding */
42
+ paddingRight?: BoxPadding;
43
+ }
44
+
45
+ export * from './ListItem.types';
@@ -0,0 +1,12 @@
1
+ .item {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ }
5
+
6
+ .iconWrapper {
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ margin-right: 0.5rem;
11
+ flex-shrink: 0;
12
+ }
@@ -0,0 +1,12 @@
1
+ import clsx from 'clsx';
2
+ import type { ListItemProps } from './ListItem.types';
3
+ import styles from './ListItem.module.css';
4
+
5
+ export const ListItem = ({ children, icon, className, ...props }: ListItemProps) => {
6
+ return (
7
+ <li className={clsx(styles.item, className)} {...props}>
8
+ {icon && <span className={styles.iconWrapper}>{icon}</span>}
9
+ <div className={styles.content}>{children}</div>
10
+ </li>
11
+ );
12
+ };
@@ -0,0 +1,10 @@
1
+ import type { ReactNode, HTMLAttributes } from 'react';
2
+
3
+ export interface ListItemProps extends HTMLAttributes<HTMLLIElement> {
4
+ /** The content of the list item */
5
+ children: ReactNode;
6
+ /** Additional class name */
7
+ className?: string;
8
+ /** Optional icon or element to display before the content */
9
+ icon?: ReactNode;
10
+ }
@@ -175,3 +175,22 @@
175
175
  font-size: var(--ds-font-size-xs);
176
176
  color: var(--ds-text-2);
177
177
  }
178
+
179
+ .inputsRow {
180
+ display: grid;
181
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
182
+ align-items: center;
183
+ gap: var(--ds-space-3);
184
+ width: 100%;
185
+ }
186
+
187
+ .singleInputRow {
188
+ grid-template-columns: minmax(0, 1fr);
189
+ }
190
+
191
+ .separator {
192
+ font-family: var(--ds-font-family-base);
193
+ font-size: var(--ds-font-size-sm);
194
+ color: var(--ds-text-2);
195
+ line-height: 1;
196
+ }
@@ -79,3 +79,22 @@ export const DualValue: Story = {
79
79
  showValue: true,
80
80
  },
81
81
  };
82
+
83
+ export const WithInputs: Story = {
84
+ args: {
85
+ min: 0,
86
+ max: 500,
87
+ step: 10,
88
+ defaultValue: [100, 350],
89
+ label: 'Budget range',
90
+ helperText: 'Drag the handles or type exact values.',
91
+ showValue: true,
92
+ showInputs: true,
93
+ fullWidth: true,
94
+ },
95
+ render: (args) => (
96
+ <Box width="420px">
97
+ <Slider {...args} />
98
+ </Box>
99
+ ),
100
+ };
@@ -1,6 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { useId, useMemo, useState } from 'react';
3
3
 
4
+ import { Input } from '../Input/Input.tsx';
4
5
  import styles from './Slider.module.css';
5
6
  import type { SliderProps, SliderValue } from './Slider.types.ts';
6
7
 
@@ -15,6 +16,20 @@ const toNumber = (value: number | string | undefined, fallback: number): number
15
16
  const clamp = (value: number, min: number, max: number): number =>
16
17
  Math.min(Math.max(value, min), max);
17
18
 
19
+ const alignToStep = (
20
+ value: number,
21
+ min: number,
22
+ max: number,
23
+ step: number | 'any' | undefined
24
+ ): number => {
25
+ if (step == null || step === 'any' || step <= 0) {
26
+ return clamp(value, min, max);
27
+ }
28
+
29
+ const steps = Math.round((value - min) / step);
30
+ return clamp(min + steps * step, min, max);
31
+ };
32
+
18
33
  const normalizeToPair = (
19
34
  value: SliderValue | undefined,
20
35
  min: number,
@@ -36,6 +51,7 @@ export const Slider = ({
36
51
  helperText,
37
52
  fullWidth = false,
38
53
  showValue = false,
54
+ showInputs = false,
39
55
  className,
40
56
  id,
41
57
  value,
@@ -48,6 +64,8 @@ export const Slider = ({
48
64
  const helperId = helperText ? `${componentId}-help` : undefined;
49
65
  const min = toNumber(props.min as number | string | undefined, 0);
50
66
  const max = toNumber(props.max as number | string | undefined, 100);
67
+ const step =
68
+ props.step === 'any' ? 'any' : toNumber(props.step as number | string | undefined, 1);
51
69
  const isDual = Array.isArray(value) || Array.isArray(defaultValue);
52
70
 
53
71
  const initialPair = useMemo(
@@ -69,6 +87,36 @@ export const Slider = ({
69
87
  onValueChange?.(isDual ? nextPair : nextPair[0]);
70
88
  };
71
89
 
90
+ const commitStartInput = (rawValue: string) => {
91
+ if (!rawValue.trim()) {
92
+ return;
93
+ }
94
+
95
+ const parsed = Number(rawValue);
96
+
97
+ if (Number.isNaN(parsed)) {
98
+ return;
99
+ }
100
+
101
+ const next = alignToStep(parsed, min, isDual ? endValue : max, step);
102
+ emitChange([next, endValue]);
103
+ };
104
+
105
+ const commitEndInput = (rawValue: string) => {
106
+ if (!rawValue.trim()) {
107
+ return;
108
+ }
109
+
110
+ const parsed = Number(rawValue);
111
+
112
+ if (Number.isNaN(parsed)) {
113
+ return;
114
+ }
115
+
116
+ const next = alignToStep(parsed, startValue, max, step);
117
+ emitChange([startValue, next]);
118
+ };
119
+
72
120
  return (
73
121
  <div className={clsx(styles.wrapper, fullWidth && styles.fullWidth, className)}>
74
122
  {(label || showValue) && (
@@ -100,9 +148,10 @@ export const Slider = ({
100
148
  {...props}
101
149
  min={min}
102
150
  max={max}
151
+ step={step}
103
152
  value={startValue}
104
153
  onChange={(event) => {
105
- const next = clamp(toNumber(event.target.value, min), min, endValue);
154
+ const next = alignToStep(toNumber(event.target.value, min), min, endValue, step);
106
155
  emitChange([next, endValue]);
107
156
  }}
108
157
  />
@@ -116,14 +165,64 @@ export const Slider = ({
116
165
  {...props}
117
166
  min={min}
118
167
  max={max}
168
+ step={step}
119
169
  value={endValue}
120
170
  onChange={(event) => {
121
- const next = clamp(toNumber(event.target.value, max), startValue, max);
171
+ const next = alignToStep(toNumber(event.target.value, max), startValue, max, step);
122
172
  emitChange([startValue, next]);
123
173
  }}
124
174
  />
125
175
  )}
126
176
  </div>
177
+ {showInputs && (
178
+ <div className={clsx(styles.inputsRow, !isDual && styles.singleInputRow)}>
179
+ <Input
180
+ key={`slider-start-${startValue}-${endValue}`}
181
+ type="number"
182
+ inputMode="decimal"
183
+ size={size}
184
+ defaultValue={startValue}
185
+ min={min}
186
+ max={isDual ? endValue : max}
187
+ step={step}
188
+ aria-label={
189
+ label ? `${label} minimum input` : isDual ? 'Slider minimum input' : 'Slider input'
190
+ }
191
+ fullWidth
192
+ onBlur={(event) => {
193
+ commitStartInput(event.target.value);
194
+ }}
195
+ onKeyDown={(event) => {
196
+ if (event.key === 'Enter') {
197
+ commitStartInput(event.currentTarget.value);
198
+ }
199
+ }}
200
+ />
201
+ {isDual && <span className={styles.separator}>-</span>}
202
+ {isDual && (
203
+ <Input
204
+ key={`slider-end-${startValue}-${endValue}`}
205
+ type="number"
206
+ inputMode="decimal"
207
+ size={size}
208
+ defaultValue={endValue}
209
+ min={startValue}
210
+ max={max}
211
+ step={step}
212
+ aria-label={label ? `${label} maximum input` : 'Slider maximum input'}
213
+ fullWidth
214
+ onBlur={(event) => {
215
+ commitEndInput(event.target.value);
216
+ }}
217
+ onKeyDown={(event) => {
218
+ if (event.key === 'Enter') {
219
+ commitEndInput(event.currentTarget.value);
220
+ }
221
+ }}
222
+ />
223
+ )}
224
+ </div>
225
+ )}
127
226
  {helperText && (
128
227
  <span id={helperId} className={styles.helperText}>
129
228
  {helperText}
@@ -13,6 +13,7 @@ export interface SliderProps extends Omit<
13
13
  helperText?: string;
14
14
  fullWidth?: boolean;
15
15
  showValue?: boolean;
16
+ showInputs?: boolean;
16
17
  value?: SliderValue;
17
18
  defaultValue?: SliderValue;
18
19
  onValueChange?: (value: SliderValue) => void;
@@ -0,0 +1,5 @@
1
+ .root {
2
+ font-family: var(--ds-font-family-base);
3
+ font-size: var(--ds-font-size-sm);
4
+ line-height: var(--ds-line-height-base);
5
+ }
@@ -0,0 +1,113 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Tree } from './Tree';
3
+
4
+ const meta = {
5
+ title: 'Components/Tree',
6
+ component: Tree,
7
+ tags: ['autodocs'],
8
+ } satisfies Meta<typeof Tree>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Simple: Story = {
14
+ args: {
15
+ data: {
16
+ name: 'John Doe',
17
+ age: 30,
18
+ city: 'New York',
19
+ },
20
+ },
21
+ };
22
+
23
+ export const Nested: Story = {
24
+ args: {
25
+ data: {
26
+ user: {
27
+ id: 1,
28
+ profile: {
29
+ firstName: 'Jane',
30
+ lastName: 'Smith',
31
+ hobbies: ['reading', 'coding', 'hiking'],
32
+ },
33
+ },
34
+ status: 'active',
35
+ lastLogin: null,
36
+ },
37
+ },
38
+ };
39
+
40
+ export const ArrayOnly: Story = {
41
+ args: {
42
+ data: [
43
+ { id: 1, text: 'Item 1' },
44
+ { id: 2, text: 'Item 2', children: [1, 2, 3] },
45
+ ],
46
+ },
47
+ };
48
+
49
+ export const Primitive: Story = {
50
+ args: {
51
+ data: 'Hello World',
52
+ },
53
+ };
54
+
55
+ export const NullValue: Story = {
56
+ args: {
57
+ data: null,
58
+ },
59
+ };
60
+
61
+ export const Collapsed: Story = {
62
+ args: {
63
+ data: {
64
+ nested: {
65
+ deeply: {
66
+ data: 'hidden',
67
+ },
68
+ },
69
+ array: [1, 2, 3],
70
+ },
71
+ defaultExpanded: false,
72
+ },
73
+ };
74
+
75
+ export const ExpandedByDefault: Story = {
76
+ args: {
77
+ data: {
78
+ nested: {
79
+ deeply: {
80
+ data: 'visible',
81
+ },
82
+ },
83
+ },
84
+ defaultExpanded: true,
85
+ },
86
+ };
87
+
88
+ export const CustomIcons: Story = {
89
+ args: {
90
+ data: {
91
+ folder1: {
92
+ file1: 'content',
93
+ },
94
+ folder2: {
95
+ file2: 'content',
96
+ },
97
+ },
98
+ expandIcon: '+',
99
+ collapseIcon: '-',
100
+ },
101
+ };
102
+
103
+ export const EmptyStructures: Story = {
104
+ args: {
105
+ data: {
106
+ emptyObj: {},
107
+ emptyArray: [],
108
+ nullValue: null,
109
+ undefinedValue: undefined,
110
+ notEmpty: [1],
111
+ },
112
+ },
113
+ };
@@ -0,0 +1,27 @@
1
+ import clsx from 'clsx';
2
+ import type { TreeProps } from './Tree.types';
3
+ import styles from './Tree.module.css';
4
+ import { TreeItem } from './TreeItem';
5
+
6
+ export const Tree = ({
7
+ data,
8
+ className,
9
+ defaultExpanded,
10
+ expandIcon,
11
+ collapseIcon,
12
+ ...props
13
+ }: TreeProps) => {
14
+ return (
15
+ <div className={clsx(styles.root, className)} {...props}>
16
+ <TreeItem
17
+ data={data}
18
+ defaultExpanded={defaultExpanded}
19
+ expandIcon={expandIcon}
20
+ collapseIcon={collapseIcon}
21
+ isRoot
22
+ />
23
+ </div>
24
+ );
25
+ };
26
+
27
+ Tree.Item = TreeItem;
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+
3
+ export interface TreeProps extends HTMLAttributes<HTMLDivElement> {
4
+ /** The data to be displayed in the tree */
5
+ data: unknown;
6
+ /** Additional class name */
7
+ className?: string;
8
+ /** Whether the tree items should be expanded by default */
9
+ defaultExpanded?: boolean;
10
+ /** Custom icon for expanded state */
11
+ expandIcon?: ReactNode;
12
+ /** Custom icon for collapsed state */
13
+ collapseIcon?: ReactNode;
14
+ }
15
+
16
+ export * from './TreeItem.types';
@@ -0,0 +1,63 @@
1
+ .list {
2
+ list-style: none;
3
+ padding-left: var(--ds-space-4);
4
+ margin: 0;
5
+ }
6
+
7
+ .item {
8
+ margin: 0;
9
+ }
10
+
11
+ .itemHeader {
12
+ display: flex;
13
+ align-items: center;
14
+ min-height: var(--ds-space-6);
15
+ }
16
+
17
+ .toggleButton {
18
+ background: none;
19
+ border: none;
20
+ padding: 0;
21
+ margin: 0;
22
+ cursor: pointer;
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ color: var(--ds-text-2);
27
+ width: var(--ds-space-5);
28
+ height: var(--ds-space-5);
29
+ flex-shrink: 0;
30
+ transition: transform var(--ds-transition-fast);
31
+ }
32
+
33
+ .toggleButton:hover {
34
+ color: var(--ds-text-1);
35
+ }
36
+
37
+ .key {
38
+ font-weight: var(--ds-font-weight-medium);
39
+ color: var(--ds-text-2);
40
+ }
41
+
42
+ .value {
43
+ color: var(--ds-text-1);
44
+ margin-left: var(--ds-space-1);
45
+ }
46
+
47
+ .empty {
48
+ font-style: italic;
49
+ color: var(--ds-text-disabled);
50
+ margin-left: var(--ds-space-1);
51
+ }
52
+
53
+ .collapsibleContent {
54
+ display: none;
55
+ }
56
+
57
+ .expanded {
58
+ display: block;
59
+ }
60
+
61
+ .rootList {
62
+ padding-left: 0;
63
+ }