tharaday 0.1.1

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 (247) hide show
  1. package/.gitignore +30 -0
  2. package/.prettierrc.json +10 -0
  3. package/.storybook/main.ts +8 -0
  4. package/.storybook/preview.ts +50 -0
  5. package/.versionrc.json +6 -0
  6. package/README.md +73 -0
  7. package/dist/components/Accordion/Accordion.d.ts +2 -0
  8. package/dist/components/Accordion/Accordion.stories.d.ts +14 -0
  9. package/dist/components/Accordion/Accordion.types.d.ts +18 -0
  10. package/dist/components/Avatar/Avatar.d.ts +2 -0
  11. package/dist/components/Avatar/Avatar.stories.d.ts +14 -0
  12. package/dist/components/Avatar/Avatar.types.d.ts +10 -0
  13. package/dist/components/Badge/Badge.d.ts +2 -0
  14. package/dist/components/Badge/Badge.stories.d.ts +33 -0
  15. package/dist/components/Badge/Badge.types.d.ts +9 -0
  16. package/dist/components/Box/Box.d.ts +2 -0
  17. package/dist/components/Box/Box.stories.d.ts +38 -0
  18. package/dist/components/Box/Box.types.d.ts +49 -0
  19. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +3 -0
  20. package/dist/components/Breadcrumbs/Breadcrumbs.stories.d.ts +13 -0
  21. package/dist/components/Breadcrumbs/Breadcrumbs.types.d.ts +11 -0
  22. package/dist/components/Button/Button.d.ts +2 -0
  23. package/dist/components/Button/Button.stories.d.ts +22 -0
  24. package/dist/components/Button/Button.types.d.ts +12 -0
  25. package/dist/components/Card/Card.d.ts +5 -0
  26. package/dist/components/Card/Card.stories.d.ts +27 -0
  27. package/dist/components/Card/Card.types.d.ts +15 -0
  28. package/dist/components/Checkbox/Checkbox.d.ts +2 -0
  29. package/dist/components/Checkbox/Checkbox.stories.d.ts +17 -0
  30. package/dist/components/Checkbox/Checkbox.types.d.ts +6 -0
  31. package/dist/components/Dropdown/Dropdown.d.ts +2 -0
  32. package/dist/components/Dropdown/Dropdown.stories.d.ts +12 -0
  33. package/dist/components/Dropdown/Dropdown.types.d.ts +24 -0
  34. package/dist/components/Header/Header.d.ts +2 -0
  35. package/dist/components/Header/Header.stories.d.ts +18 -0
  36. package/dist/components/Header/Header.types.d.ts +14 -0
  37. package/dist/components/Input/Input.d.ts +2 -0
  38. package/dist/components/Input/Input.stories.d.ts +29 -0
  39. package/dist/components/Input/Input.types.d.ts +8 -0
  40. package/dist/components/Loader/Loader.d.ts +2 -0
  41. package/dist/components/Loader/Loader.stories.d.ts +25 -0
  42. package/dist/components/Loader/Loader.types.d.ts +8 -0
  43. package/dist/components/Modal/Modal.d.ts +2 -0
  44. package/dist/components/Modal/Modal.stories.d.ts +22 -0
  45. package/dist/components/Modal/Modal.types.d.ts +12 -0
  46. package/dist/components/NavBar/NavBar.d.ts +6 -0
  47. package/dist/components/NavBar/NavBar.stories.d.ts +8 -0
  48. package/dist/components/NavBar/NavBar.types.d.ts +34 -0
  49. package/dist/components/Notification/Notification.d.ts +2 -0
  50. package/dist/components/Notification/Notification.stories.d.ts +26 -0
  51. package/dist/components/Notification/Notification.types.d.ts +8 -0
  52. package/dist/components/Pagination/Pagination.d.ts +2 -0
  53. package/dist/components/Pagination/Pagination.stories.d.ts +21 -0
  54. package/dist/components/Pagination/Pagination.types.d.ts +34 -0
  55. package/dist/components/ProgressBar/ProgressBar.d.ts +2 -0
  56. package/dist/components/ProgressBar/ProgressBar.stories.d.ts +32 -0
  57. package/dist/components/ProgressBar/ProgressBar.types.d.ts +12 -0
  58. package/dist/components/RadioButton/RadioButton.d.ts +2 -0
  59. package/dist/components/RadioButton/RadioButton.stories.d.ts +30 -0
  60. package/dist/components/RadioButton/RadioButton.types.d.ts +8 -0
  61. package/dist/components/Select/Select.d.ts +2 -0
  62. package/dist/components/Select/Select.stories.d.ts +29 -0
  63. package/dist/components/Select/Select.types.d.ts +15 -0
  64. package/dist/components/Skeleton/Skeleton.d.ts +2 -0
  65. package/dist/components/Skeleton/Skeleton.stories.d.ts +15 -0
  66. package/dist/components/Skeleton/Skeleton.types.d.ts +8 -0
  67. package/dist/components/Stepper/Step.d.ts +2 -0
  68. package/dist/components/Stepper/Step.types.d.ts +17 -0
  69. package/dist/components/Stepper/Stepper.d.ts +2 -0
  70. package/dist/components/Stepper/Stepper.stories.d.ts +15 -0
  71. package/dist/components/Stepper/Stepper.types.d.ts +13 -0
  72. package/dist/components/Stepper/stepper.utils.d.ts +3 -0
  73. package/dist/components/Switch/Switch.d.ts +2 -0
  74. package/dist/components/Switch/Switch.stories.d.ts +16 -0
  75. package/dist/components/Switch/Switch.types.d.ts +5 -0
  76. package/dist/components/Table/Table.d.ts +8 -0
  77. package/dist/components/Table/Table.stories.d.ts +27 -0
  78. package/dist/components/Table/Table.types.d.ts +17 -0
  79. package/dist/components/Tabs/Tabs.d.ts +2 -0
  80. package/dist/components/Tabs/Tabs.stories.d.ts +14 -0
  81. package/dist/components/Tabs/Tabs.types.d.ts +16 -0
  82. package/dist/components/Text/Text.d.ts +2 -0
  83. package/dist/components/Text/Text.stories.d.ts +35 -0
  84. package/dist/components/Text/Text.types.d.ts +21 -0
  85. package/dist/components/Textarea/Textarea.d.ts +2 -0
  86. package/dist/components/Textarea/Textarea.stories.d.ts +29 -0
  87. package/dist/components/Textarea/Textarea.types.d.ts +9 -0
  88. package/dist/components/Tooltip/Tooltip.d.ts +2 -0
  89. package/dist/components/Tooltip/Tooltip.stories.d.ts +10 -0
  90. package/dist/components/Tooltip/Tooltip.types.d.ts +12 -0
  91. package/dist/ds.css +1 -0
  92. package/dist/ds.js +1930 -0
  93. package/dist/ds.umd.cjs +1 -0
  94. package/dist/index.d.ts +63 -0
  95. package/dist/layouts/AppLayout/AppLayout.d.ts +8 -0
  96. package/dist/layouts/AppLayout/AppLayout.stories.d.ts +19 -0
  97. package/dist/layouts/AppLayout/AppLayout.types.d.ts +50 -0
  98. package/dist/layouts/AuthLayout/AuthLayout.d.ts +2 -0
  99. package/dist/layouts/AuthLayout/AuthLayout.stories.d.ts +12 -0
  100. package/dist/layouts/AuthLayout/AuthLayout.types.d.ts +7 -0
  101. package/dist/layouts/DashboardLayout/DashboardLayout.d.ts +2 -0
  102. package/dist/layouts/DashboardLayout/DashboardLayout.stories.d.ts +17 -0
  103. package/dist/layouts/DashboardLayout/DashboardLayout.types.d.ts +15 -0
  104. package/dist/layouts/SettingsLayout/SettingsLayout.d.ts +2 -0
  105. package/dist/layouts/SettingsLayout/SettingsLayout.stories.d.ts +17 -0
  106. package/dist/layouts/SettingsLayout/SettingsLayout.types.d.ts +14 -0
  107. package/eslint.config.js +45 -0
  108. package/package.json +100 -0
  109. package/src/components/Accordion/Accordion.module.css +158 -0
  110. package/src/components/Accordion/Accordion.stories.tsx +133 -0
  111. package/src/components/Accordion/Accordion.tsx +68 -0
  112. package/src/components/Accordion/Accordion.types.ts +21 -0
  113. package/src/components/Avatar/Avatar.module.css +58 -0
  114. package/src/components/Avatar/Avatar.stories.tsx +41 -0
  115. package/src/components/Avatar/Avatar.tsx +64 -0
  116. package/src/components/Avatar/Avatar.types.ts +12 -0
  117. package/src/components/Badge/Badge.module.css +98 -0
  118. package/src/components/Badge/Badge.stories.tsx +108 -0
  119. package/src/components/Badge/Badge.tsx +22 -0
  120. package/src/components/Badge/Badge.types.ts +11 -0
  121. package/src/components/Box/Box.module.css +638 -0
  122. package/src/components/Box/Box.stories.tsx +109 -0
  123. package/src/components/Box/Box.tsx +95 -0
  124. package/src/components/Box/Box.types.ts +65 -0
  125. package/src/components/Breadcrumbs/Breadcrumbs.module.css +41 -0
  126. package/src/components/Breadcrumbs/Breadcrumbs.stories.tsx +40 -0
  127. package/src/components/Breadcrumbs/Breadcrumbs.tsx +54 -0
  128. package/src/components/Breadcrumbs/Breadcrumbs.types.ts +13 -0
  129. package/src/components/Button/Button.module.css +247 -0
  130. package/src/components/Button/Button.stories.tsx +93 -0
  131. package/src/components/Button/Button.tsx +31 -0
  132. package/src/components/Button/Button.types.ts +14 -0
  133. package/src/components/Card/Card.module.css +112 -0
  134. package/src/components/Card/Card.stories.tsx +86 -0
  135. package/src/components/Card/Card.tsx +70 -0
  136. package/src/components/Card/Card.types.ts +20 -0
  137. package/src/components/Checkbox/Checkbox.module.css +88 -0
  138. package/src/components/Checkbox/Checkbox.stories.tsx +65 -0
  139. package/src/components/Checkbox/Checkbox.tsx +57 -0
  140. package/src/components/Checkbox/Checkbox.types.ts +7 -0
  141. package/src/components/Dropdown/Dropdown.module.css +140 -0
  142. package/src/components/Dropdown/Dropdown.stories.tsx +86 -0
  143. package/src/components/Dropdown/Dropdown.tsx +251 -0
  144. package/src/components/Dropdown/Dropdown.types.ts +27 -0
  145. package/src/components/Header/Header.module.css +38 -0
  146. package/src/components/Header/Header.stories.tsx +53 -0
  147. package/src/components/Header/Header.tsx +49 -0
  148. package/src/components/Header/Header.types.ts +15 -0
  149. package/src/components/Input/Input.module.css +87 -0
  150. package/src/components/Input/Input.stories.tsx +101 -0
  151. package/src/components/Input/Input.tsx +41 -0
  152. package/src/components/Input/Input.types.ts +10 -0
  153. package/src/components/Loader/Loader.module.css +49 -0
  154. package/src/components/Loader/Loader.stories.tsx +75 -0
  155. package/src/components/Loader/Loader.tsx +15 -0
  156. package/src/components/Loader/Loader.types.ts +9 -0
  157. package/src/components/Modal/Modal.module.css +88 -0
  158. package/src/components/Modal/Modal.stories.tsx +94 -0
  159. package/src/components/Modal/Modal.tsx +115 -0
  160. package/src/components/Modal/Modal.types.ts +13 -0
  161. package/src/components/NavBar/NavBar.module.css +77 -0
  162. package/src/components/NavBar/NavBar.stories.tsx +55 -0
  163. package/src/components/NavBar/NavBar.tsx +50 -0
  164. package/src/components/NavBar/NavBar.types.ts +36 -0
  165. package/src/components/Notification/Notification.module.css +72 -0
  166. package/src/components/Notification/Notification.stories.tsx +81 -0
  167. package/src/components/Notification/Notification.tsx +34 -0
  168. package/src/components/Notification/Notification.types.ts +10 -0
  169. package/src/components/Pagination/Pagination.module.css +31 -0
  170. package/src/components/Pagination/Pagination.stories.tsx +128 -0
  171. package/src/components/Pagination/Pagination.tsx +245 -0
  172. package/src/components/Pagination/Pagination.types.ts +37 -0
  173. package/src/components/ProgressBar/ProgressBar.module.css +67 -0
  174. package/src/components/ProgressBar/ProgressBar.stories.tsx +91 -0
  175. package/src/components/ProgressBar/ProgressBar.tsx +49 -0
  176. package/src/components/ProgressBar/ProgressBar.types.ts +13 -0
  177. package/src/components/RadioButton/RadioButton.module.css +162 -0
  178. package/src/components/RadioButton/RadioButton.stories.tsx +114 -0
  179. package/src/components/RadioButton/RadioButton.tsx +43 -0
  180. package/src/components/RadioButton/RadioButton.types.ts +10 -0
  181. package/src/components/Select/Select.module.css +88 -0
  182. package/src/components/Select/Select.stories.tsx +99 -0
  183. package/src/components/Select/Select.tsx +51 -0
  184. package/src/components/Select/Select.types.ts +18 -0
  185. package/src/components/Skeleton/Skeleton.module.css +71 -0
  186. package/src/components/Skeleton/Skeleton.stories.tsx +54 -0
  187. package/src/components/Skeleton/Skeleton.tsx +32 -0
  188. package/src/components/Skeleton/Skeleton.types.ts +10 -0
  189. package/src/components/Stepper/Step.module.css +162 -0
  190. package/src/components/Stepper/Step.tsx +62 -0
  191. package/src/components/Stepper/Step.types.ts +19 -0
  192. package/src/components/Stepper/Stepper.module.css +39 -0
  193. package/src/components/Stepper/Stepper.stories.tsx +88 -0
  194. package/src/components/Stepper/Stepper.tsx +47 -0
  195. package/src/components/Stepper/Stepper.types.ts +16 -0
  196. package/src/components/Stepper/stepper.utils.ts +41 -0
  197. package/src/components/Switch/Switch.module.css +74 -0
  198. package/src/components/Switch/Switch.stories.tsx +56 -0
  199. package/src/components/Switch/Switch.tsx +36 -0
  200. package/src/components/Switch/Switch.types.ts +6 -0
  201. package/src/components/Table/Table.module.css +78 -0
  202. package/src/components/Table/Table.stories.tsx +124 -0
  203. package/src/components/Table/Table.tsx +75 -0
  204. package/src/components/Table/Table.types.ts +29 -0
  205. package/src/components/Tabs/Tabs.module.css +74 -0
  206. package/src/components/Tabs/Tabs.stories.tsx +48 -0
  207. package/src/components/Tabs/Tabs.tsx +113 -0
  208. package/src/components/Tabs/Tabs.types.ts +18 -0
  209. package/src/components/Text/Text.module.css +126 -0
  210. package/src/components/Text/Text.stories.tsx +128 -0
  211. package/src/components/Text/Text.tsx +50 -0
  212. package/src/components/Text/Text.types.ts +43 -0
  213. package/src/components/Textarea/Textarea.module.css +82 -0
  214. package/src/components/Textarea/Textarea.stories.tsx +99 -0
  215. package/src/components/Textarea/Textarea.tsx +43 -0
  216. package/src/components/Textarea/Textarea.types.ts +11 -0
  217. package/src/components/Tooltip/Tooltip.module.css +125 -0
  218. package/src/components/Tooltip/Tooltip.stories.tsx +68 -0
  219. package/src/components/Tooltip/Tooltip.tsx +87 -0
  220. package/src/components/Tooltip/Tooltip.types.ts +14 -0
  221. package/src/index.ts +132 -0
  222. package/src/layouts/AppLayout/AppLayout.module.css +17 -0
  223. package/src/layouts/AppLayout/AppLayout.stories.tsx +124 -0
  224. package/src/layouts/AppLayout/AppLayout.tsx +46 -0
  225. package/src/layouts/AppLayout/AppLayout.types.ts +50 -0
  226. package/src/layouts/AuthLayout/AuthLayout.module.css +49 -0
  227. package/src/layouts/AuthLayout/AuthLayout.stories.tsx +109 -0
  228. package/src/layouts/AuthLayout/AuthLayout.tsx +27 -0
  229. package/src/layouts/AuthLayout/AuthLayout.types.tsx +8 -0
  230. package/src/layouts/DashboardLayout/DashboardLayout.module.css +37 -0
  231. package/src/layouts/DashboardLayout/DashboardLayout.stories.tsx +144 -0
  232. package/src/layouts/DashboardLayout/DashboardLayout.tsx +41 -0
  233. package/src/layouts/DashboardLayout/DashboardLayout.types.tsx +14 -0
  234. package/src/layouts/SettingsLayout/SettingsLayout.module.css +39 -0
  235. package/src/layouts/SettingsLayout/SettingsLayout.stories.tsx +103 -0
  236. package/src/layouts/SettingsLayout/SettingsLayout.tsx +37 -0
  237. package/src/layouts/SettingsLayout/SettingsLayout.types.tsx +13 -0
  238. package/src/styles/ds.css +12 -0
  239. package/src/styles/semantic.css +56 -0
  240. package/src/styles/themes/dark.css +58 -0
  241. package/src/styles/themes/light.css +58 -0
  242. package/src/styles/themes/retro.css +58 -0
  243. package/src/styles/tokens.css +138 -0
  244. package/tsconfig.app.json +28 -0
  245. package/tsconfig.json +7 -0
  246. package/tsconfig.node.json +26 -0
  247. package/vite.config.ts +33 -0
@@ -0,0 +1,86 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { Dropdown } from './Dropdown.tsx';
4
+
5
+ const meta: Meta<typeof Dropdown> = {
6
+ title: 'Components/Dropdown',
7
+ component: Dropdown,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ onChange: { action: 'changed' },
11
+ },
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof Dropdown>;
16
+
17
+ const options = [
18
+ { value: 'apple', label: 'Apple' },
19
+ { value: 'banana', label: 'Banana' },
20
+ { value: 'cherry', label: 'Cherry', description: 'Sweet red fruit' },
21
+ { value: 'dragonfruit', label: 'Dragonfruit', disabled: true },
22
+ { value: 'elderberry', label: 'Elderberry' },
23
+ ];
24
+
25
+ export const Default: Story = {
26
+ args: {
27
+ options,
28
+ placeholder: 'Select a fruit',
29
+ },
30
+ };
31
+
32
+ export const WithLabel: Story = {
33
+ args: {
34
+ options,
35
+ label: 'Fruits',
36
+ helperText: 'Choose your favorite fruit',
37
+ },
38
+ };
39
+
40
+ export const WithValue: Story = {
41
+ args: {
42
+ options,
43
+ label: 'Prefilled',
44
+ defaultValue: 'cherry',
45
+ },
46
+ };
47
+
48
+ export const Error: Story = {
49
+ args: {
50
+ options,
51
+ label: 'Fruit selection',
52
+ error: true,
53
+ helperText: 'This field is required',
54
+ },
55
+ };
56
+
57
+ export const Disabled: Story = {
58
+ args: {
59
+ options,
60
+ label: 'Disabled dropdown',
61
+ disabled: true,
62
+ },
63
+ };
64
+
65
+ export const Sizes: Story = {
66
+ render: () => (
67
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
68
+ <Dropdown options={options} size="sm" placeholder="Small" />
69
+ <Dropdown options={options} size="md" placeholder="Medium" />
70
+ <Dropdown options={options} size="lg" placeholder="Large" />
71
+ </div>
72
+ ),
73
+ };
74
+
75
+ export const FullWidth: Story = {
76
+ args: {
77
+ options,
78
+ label: 'Full Width',
79
+ fullWidth: true,
80
+ },
81
+ render: (args) => (
82
+ <div style={{ width: '400px' }}>
83
+ <Dropdown {...args} />
84
+ </div>
85
+ ),
86
+ };
@@ -0,0 +1,251 @@
1
+ import clsx from 'clsx';
2
+ import { useId, useState, useRef, useEffect } from 'react';
3
+
4
+ import styles from './Dropdown.module.css';
5
+ import type { DropdownProps, DropdownOption } from './Dropdown.types.ts';
6
+
7
+ export const Dropdown = ({
8
+ options,
9
+ value,
10
+ defaultValue,
11
+ onChange,
12
+ placeholder = 'Select an option',
13
+ label,
14
+ helperText,
15
+ error,
16
+ disabled,
17
+ size = 'md',
18
+ className,
19
+ id,
20
+ fullWidth = false,
21
+ }: DropdownProps) => {
22
+ const baseId = useId();
23
+ const componentId = id ?? `ds-dropdown-${baseId}`;
24
+ const helperId = helperText ? `${componentId}-help` : undefined;
25
+
26
+ const [isOpen, setIsOpen] = useState(false);
27
+ const [internalValue, setInternalValue] = useState(defaultValue);
28
+ const [focusedIndex, setFocusedIndex] = useState(-1);
29
+ const containerRef = useRef<HTMLDivElement>(null);
30
+ const triggerRef = useRef<HTMLButtonElement>(null);
31
+
32
+ const currentValue = value !== undefined ? value : internalValue;
33
+ const selectedOption = options.find((opt) => opt.value === currentValue);
34
+ const activeOptionId = focusedIndex >= 0 ? `${componentId}-opt-${focusedIndex}` : undefined;
35
+
36
+ const toggleDropdown = () => {
37
+ if (!disabled) {
38
+ const nextIsOpen = !isOpen;
39
+ setIsOpen(nextIsOpen);
40
+
41
+ if (nextIsOpen) {
42
+ if (focusedIndex === -1) {
43
+ const selectedIdx = options.findIndex((opt) => opt.value === currentValue);
44
+ setFocusedIndex(selectedIdx >= 0 ? selectedIdx : 0);
45
+ }
46
+ } else {
47
+ setFocusedIndex(-1);
48
+ }
49
+ }
50
+ };
51
+
52
+ const handleOptionClick = (option: DropdownOption, index: number) => {
53
+ if (option.disabled) {
54
+ return;
55
+ }
56
+
57
+ if (value === undefined) {
58
+ setInternalValue(option.value);
59
+ }
60
+
61
+ onChange?.(option.value);
62
+ setIsOpen(false);
63
+ setFocusedIndex(index);
64
+ triggerRef.current?.focus();
65
+ };
66
+
67
+ const handleKeyDown = (event: React.KeyboardEvent) => {
68
+ if (disabled) {
69
+ return;
70
+ }
71
+
72
+ switch (event.key) {
73
+ case 'ArrowDown':
74
+ event.preventDefault();
75
+ if (!isOpen) {
76
+ setIsOpen(true);
77
+ setFocusedIndex(options.findIndex((opt) => opt.value === currentValue) || 0);
78
+ } else {
79
+ setFocusedIndex((prev) => (prev < options.length - 1 ? prev + 1 : prev));
80
+ }
81
+ break;
82
+ case 'ArrowUp':
83
+ event.preventDefault();
84
+ if (!isOpen) {
85
+ setIsOpen(true);
86
+ setFocusedIndex(
87
+ options.findIndex((opt) => opt.value === currentValue) || options.length - 1
88
+ );
89
+ } else {
90
+ setFocusedIndex((prev) => (prev > 0 ? prev - 1 : prev));
91
+ }
92
+ break;
93
+ case 'Enter':
94
+ case ' ':
95
+ event.preventDefault();
96
+ if (!isOpen) {
97
+ setIsOpen(true);
98
+ setFocusedIndex(options.findIndex((opt) => opt.value === currentValue) || 0);
99
+ } else if (focusedIndex >= 0) {
100
+ handleOptionClick(options[focusedIndex], focusedIndex);
101
+ }
102
+ break;
103
+ case 'Escape':
104
+ if (isOpen) {
105
+ event.preventDefault();
106
+ setIsOpen(false);
107
+ setFocusedIndex(-1);
108
+ triggerRef.current?.focus();
109
+ }
110
+ break;
111
+ case 'Tab':
112
+ if (isOpen) {
113
+ setIsOpen(false);
114
+ setFocusedIndex(-1);
115
+ }
116
+ break;
117
+ case 'Home':
118
+ if (isOpen) {
119
+ event.preventDefault();
120
+ setFocusedIndex(0);
121
+ }
122
+ break;
123
+ case 'End':
124
+ if (isOpen) {
125
+ event.preventDefault();
126
+ setFocusedIndex(options.length - 1);
127
+ }
128
+ break;
129
+ default:
130
+ break;
131
+ }
132
+ };
133
+
134
+ useEffect(() => {
135
+ if (isOpen && focusedIndex >= 0) {
136
+ const element = document.getElementById(`${componentId}-opt-${focusedIndex}`);
137
+ element?.scrollIntoView({ block: 'nearest' });
138
+ }
139
+ }, [focusedIndex, isOpen, componentId]);
140
+
141
+ useEffect(() => {
142
+ const handleClickOutside = (event: MouseEvent) => {
143
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
144
+ setIsOpen(false);
145
+ setFocusedIndex(-1);
146
+ }
147
+ };
148
+
149
+ document.addEventListener('mousedown', handleClickOutside);
150
+ return () => {
151
+ document.removeEventListener('mousedown', handleClickOutside);
152
+ };
153
+ }, []);
154
+
155
+ return (
156
+ <div
157
+ className={clsx(
158
+ styles.wrapper,
159
+ styles[size],
160
+ fullWidth && styles.fullWidth,
161
+ error && styles.error,
162
+ className
163
+ )}
164
+ ref={containerRef}
165
+ >
166
+ {label && (
167
+ <label htmlFor={componentId} className={styles.label}>
168
+ {label}
169
+ </label>
170
+ )}
171
+ <div className={styles.container}>
172
+ <button
173
+ id={componentId}
174
+ ref={triggerRef}
175
+ type="button"
176
+ className={clsx(styles.trigger, isOpen && styles.isOpen)}
177
+ onClick={toggleDropdown}
178
+ onKeyDown={handleKeyDown}
179
+ disabled={disabled}
180
+ aria-haspopup="listbox"
181
+ aria-expanded={isOpen}
182
+ aria-describedby={helperId}
183
+ aria-invalid={error || undefined}
184
+ aria-controls={isOpen ? `${componentId}-menu` : undefined}
185
+ aria-activedescendant={isOpen ? activeOptionId : undefined}
186
+ >
187
+ <span className={styles.selectedLabel}>
188
+ {selectedOption ? selectedOption.label : placeholder}
189
+ </span>
190
+ <span className={styles.icon} aria-hidden="true">
191
+ <svg
192
+ width="12"
193
+ height="12"
194
+ viewBox="0 0 12 12"
195
+ fill="none"
196
+ xmlns="http://www.w3.org/2000/svg"
197
+ >
198
+ <path
199
+ d="M2.5 4.5L6 8L9.5 4.5"
200
+ stroke="currentColor"
201
+ strokeWidth="1.5"
202
+ strokeLinecap="round"
203
+ strokeLinejoin="round"
204
+ />
205
+ </svg>
206
+ </span>
207
+ </button>
208
+
209
+ {isOpen && (
210
+ <ul
211
+ id={`${componentId}-menu`}
212
+ className={styles.menu}
213
+ role="listbox"
214
+ aria-labelledby={label ? undefined : componentId}
215
+ >
216
+ {options.map((option, index) => {
217
+ const isSelected = option.value === currentValue;
218
+ const isFocused = index === focusedIndex;
219
+ return (
220
+ <li
221
+ key={option.value}
222
+ id={`${componentId}-opt-${index}`}
223
+ role="option"
224
+ aria-selected={isSelected}
225
+ className={clsx(
226
+ styles.option,
227
+ isSelected && styles.optionSelected,
228
+ isFocused && styles.optionFocused,
229
+ option.disabled && styles.optionDisabled
230
+ )}
231
+ onClick={() => handleOptionClick(option, index)}
232
+ onMouseEnter={() => !option.disabled && setFocusedIndex(index)}
233
+ >
234
+ <span className={styles.optionLabel}>{option.label}</span>
235
+ {option.description && (
236
+ <span className={styles.optionDescription}>{option.description}</span>
237
+ )}
238
+ </li>
239
+ );
240
+ })}
241
+ </ul>
242
+ )}
243
+ </div>
244
+ {helperText && (
245
+ <span id={helperId} className={clsx(styles.helperText, error && styles.errorText)}>
246
+ {helperText}
247
+ </span>
248
+ )}
249
+ </div>
250
+ );
251
+ };
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type DropdownSize = 'sm' | 'md' | 'lg';
4
+
5
+ export interface DropdownOption {
6
+ value: string | number;
7
+ label: string;
8
+ disabled?: boolean;
9
+ icon?: ReactNode;
10
+ description?: string;
11
+ }
12
+
13
+ export interface DropdownProps {
14
+ options: DropdownOption[];
15
+ value?: string | number;
16
+ defaultValue?: string | number;
17
+ onChange?: (value: string | number) => void;
18
+ placeholder?: string;
19
+ label?: string;
20
+ helperText?: string;
21
+ error?: boolean;
22
+ disabled?: boolean;
23
+ size?: DropdownSize;
24
+ className?: string;
25
+ id?: string;
26
+ fullWidth?: boolean;
27
+ }
@@ -0,0 +1,38 @@
1
+ .root {
2
+ background-color: var(--ds-surface-0);
3
+ border-bottom: 1px solid var(--ds-border-1);
4
+ padding: var(--ds-space-3) 0;
5
+ width: 100%;
6
+ }
7
+
8
+ .container {
9
+ max-width: 75rem;
10
+ margin: 0 auto;
11
+ padding: 0 var(--ds-space-4);
12
+ display: flex;
13
+ justify-content: space-between;
14
+ align-items: center;
15
+ }
16
+
17
+ .sideContainer {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: var(--ds-space-2);
21
+ }
22
+
23
+ svg {
24
+ display: inline-block;
25
+ vertical-align: top;
26
+ }
27
+
28
+ .title {
29
+ display: inline-block;
30
+ vertical-align: top;
31
+ font-weight: var(--ds-font-weight-bold);
32
+ font-size: var(--ds-font-size-lg);
33
+ line-height: var(--ds-line-height-tight);
34
+ }
35
+
36
+ .welcome {
37
+ font-size: var(--ds-font-size-sm);
38
+ }
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { fn } from 'storybook/test';
3
+
4
+ import { Header } from './Header.tsx';
5
+
6
+ const meta = {
7
+ title: 'Components/Header',
8
+ component: Header,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ },
13
+ args: {
14
+ onLogin: fn(),
15
+ onLogout: fn(),
16
+ onCreateAccount: fn(),
17
+ },
18
+ } satisfies Meta<typeof Header>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ const logo = (
24
+ <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
25
+ <g fill="none" fillRule="evenodd">
26
+ <path
27
+ d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
28
+ fill="#FFF"
29
+ />
30
+ <path d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" fill="#555AB9" />
31
+ <path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
32
+ </g>
33
+ </svg>
34
+ );
35
+
36
+ const title = 'Company';
37
+
38
+ export const LoggedIn: Story = {
39
+ args: {
40
+ logo,
41
+ title,
42
+ user: {
43
+ name: 'Jane Doe',
44
+ },
45
+ },
46
+ };
47
+
48
+ export const LoggedOut: Story = {
49
+ args: {
50
+ logo,
51
+ title,
52
+ },
53
+ };
@@ -0,0 +1,49 @@
1
+ import './Header.module.css';
2
+ import styles from './Header.module.css';
3
+ import type { HeaderProps } from './Header.types.ts';
4
+ import { Button } from '../Button/Button.tsx';
5
+ import type { ButtonProps } from '../Button/Button.types.ts';
6
+
7
+ export const Header = ({
8
+ id,
9
+ logo,
10
+ title,
11
+ user,
12
+ onLogin,
13
+ onLogout,
14
+ onCreateAccount,
15
+ }: HeaderProps) => {
16
+ const buttonProps = { size: 'sm', intent: 'info' } as ButtonProps;
17
+
18
+ return (
19
+ <header id={id} className={styles.root}>
20
+ <div className={styles.container}>
21
+ <div className={styles.sideContainer}>
22
+ {logo}
23
+ <h1 className={styles.title}>{title}</h1>
24
+ </div>
25
+ <div className={styles.sideContainer}>
26
+ {user ? (
27
+ <>
28
+ <span className={styles.welcome}>
29
+ Welcome, <b>{user.name}</b>!
30
+ </span>
31
+ <Button {...buttonProps} onClick={onLogout} children="Log out" />
32
+ </>
33
+ ) : (
34
+ <>
35
+ <Button {...buttonProps} onClick={onLogin} children="Log in" />
36
+ <Button
37
+ size="sm"
38
+ intent="info"
39
+ variant="solid"
40
+ onClick={onCreateAccount}
41
+ children="Sign up"
42
+ />
43
+ </>
44
+ )}
45
+ </div>
46
+ </div>
47
+ </header>
48
+ );
49
+ };
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ type User = {
4
+ name: string;
5
+ };
6
+
7
+ export interface HeaderProps {
8
+ id?: string;
9
+ logo?: ReactNode;
10
+ title?: string;
11
+ user?: User;
12
+ onLogin?: () => void;
13
+ onLogout?: () => void;
14
+ onCreateAccount?: () => void;
15
+ }
@@ -0,0 +1,87 @@
1
+ .wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--ds-space-1);
5
+ width: 100%;
6
+ }
7
+
8
+ .label {
9
+ font-family: var(--ds-font-family-base);
10
+ font-size: var(--ds-font-size-sm);
11
+ font-weight: var(--ds-font-weight-medium);
12
+ color: var(--ds-text-1);
13
+ }
14
+
15
+ .inputRoot {
16
+ box-sizing: border-box;
17
+ width: 100%;
18
+ font-family: var(--ds-font-family-base);
19
+ border: 1px solid var(--ds-border-1);
20
+ border-radius: var(--ds-radius-md);
21
+ background-color: var(--ds-surface-0);
22
+ color: var(--ds-text-1);
23
+ transition:
24
+ border-color var(--ds-transition-fast),
25
+ box-shadow var(--ds-transition-fast);
26
+ }
27
+
28
+ .inputRoot:focus {
29
+ outline: none;
30
+ border-color: var(--ds-ring);
31
+ box-shadow:
32
+ 0 0 0 2px var(--ds-ring-offset),
33
+ 0 0 0 4px var(--ds-ring);
34
+ }
35
+
36
+ .inputRoot::placeholder {
37
+ color: var(--ds-text-2);
38
+ }
39
+
40
+ .inputRoot:disabled {
41
+ background-color: var(--ds-surface-1);
42
+ cursor: not-allowed;
43
+ opacity: 0.7;
44
+ }
45
+
46
+ /* Sizes */
47
+ .sm {
48
+ height: var(--ds-space-8);
49
+ padding: 0 var(--ds-space-3);
50
+ font-size: var(--ds-font-size-xs);
51
+ }
52
+
53
+ .md {
54
+ height: var(
55
+ --ds-space-10,
56
+ 2.5rem
57
+ ); /* tokens.css doesn't have space-10, but Button uses 2.5 * space-4 */
58
+ height: calc(var(--ds-space-4) * 2.5);
59
+ padding: 0 var(--ds-space-4);
60
+ font-size: var(--ds-font-size-sm);
61
+ }
62
+
63
+ .lg {
64
+ height: var(--ds-space-12);
65
+ padding: 0 var(--ds-space-5, 1.25rem);
66
+ font-size: var(--ds-font-size-base);
67
+ }
68
+
69
+ /* Error state */
70
+ .error {
71
+ border-color: var(--ds-danger);
72
+ }
73
+
74
+ .error:focus {
75
+ border-color: var(--ds-danger);
76
+ box-shadow: 0 0 0 0.125rem var(--ds-danger-subtle);
77
+ }
78
+
79
+ .helperText {
80
+ font-family: var(--ds-font-family-base);
81
+ font-size: var(--ds-font-size-xs);
82
+ color: var(--ds-text-2);
83
+ }
84
+
85
+ .helperText.errorText {
86
+ color: var(--ds-danger);
87
+ }
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+
3
+ import { Input } from './Input.tsx';
4
+ import { Box } from '../Box/Box.tsx';
5
+
6
+ const meta = {
7
+ title: 'Components/Input',
8
+ component: Input,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ argTypes: {
14
+ size: {
15
+ control: 'select',
16
+ options: ['sm', 'md', 'lg'],
17
+ },
18
+ error: {
19
+ control: 'boolean',
20
+ },
21
+ disabled: {
22
+ control: 'boolean',
23
+ },
24
+ },
25
+ } satisfies Meta<typeof Input>;
26
+
27
+ export default meta;
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ export const Default: Story = {
31
+ args: {
32
+ label: 'Email',
33
+ placeholder: 'Enter your email',
34
+ helperText: 'We will never share your email.',
35
+ },
36
+ };
37
+
38
+ export const WithError: Story = {
39
+ args: {
40
+ label: 'Password',
41
+ placeholder: 'Enter password',
42
+ error: true,
43
+ helperText: 'Password is too short.',
44
+ defaultValue: '123',
45
+ },
46
+ };
47
+
48
+ export const Disabled: Story = {
49
+ args: {
50
+ label: 'Username',
51
+ placeholder: 'Enter username',
52
+ disabled: true,
53
+ defaultValue: 'janedoe',
54
+ },
55
+ };
56
+
57
+ export const Small: Story = {
58
+ args: {
59
+ size: 'sm',
60
+ label: 'Small Input',
61
+ placeholder: 'Small input',
62
+ },
63
+ };
64
+
65
+ export const Large: Story = {
66
+ args: {
67
+ size: 'lg',
68
+ label: 'Large Input',
69
+ placeholder: 'Large input',
70
+ },
71
+ };
72
+
73
+ export const All: Story = {
74
+ name: 'Gallery',
75
+ render: () => (
76
+ <Box display="flex" flexDirection="column" gap={8} width="400px">
77
+ {(['sm', 'md', 'lg'] as const).map((size) => (
78
+ <Box key={size} display="flex" flexDirection="column" gap={4}>
79
+ <Box as="h3" m={0} style={{ textTransform: 'capitalize' }}>
80
+ Size: {size}
81
+ </Box>
82
+ <Input size={size} label="Default" placeholder="Default placeholder" />
83
+ <Input
84
+ size={size}
85
+ label="With Error"
86
+ placeholder="Error placeholder"
87
+ error
88
+ helperText="This field is required"
89
+ />
90
+ <Input
91
+ size={size}
92
+ label="Disabled"
93
+ placeholder="Disabled placeholder"
94
+ disabled
95
+ defaultValue="Some value"
96
+ />
97
+ </Box>
98
+ ))}
99
+ </Box>
100
+ ),
101
+ };