@iota/apps-ui-kit 0.9.5 → 0.10.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 (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.ts +55 -3
  3. package/dist/index.es.js +5697 -6536
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +36 -40
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/style.css +2 -1
  8. package/package.json +14 -18
  9. package/src/lib/components/atoms/badge/Badge.tsx +4 -3
  10. package/src/lib/components/atoms/badge/badge.classes.ts +3 -0
  11. package/src/lib/components/atoms/badge/badge.enums.ts +1 -0
  12. package/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx +1 -1
  13. package/src/lib/components/atoms/label-text/LabelText.tsx +1 -1
  14. package/src/lib/components/atoms/tooltip/Tooltip.tsx +2 -2
  15. package/src/lib/components/molecules/date-picker/DatePicker.tsx +412 -0
  16. package/src/lib/components/molecules/date-picker/date-picker.classes.ts +42 -0
  17. package/src/lib/components/molecules/date-picker/date-picker.constants.ts +23 -0
  18. package/src/lib/components/molecules/date-picker/date-picker.enums.ts +8 -0
  19. package/src/lib/components/molecules/date-picker/date-picker.helpers.ts +67 -0
  20. package/src/lib/components/molecules/date-picker/index.ts +5 -0
  21. package/src/lib/components/molecules/index.ts +1 -0
  22. package/src/lib/styles/components.css +89 -0
  23. package/src/lib/styles/variables/dark-variables.css +15 -0
  24. package/src/lib/styles/variables/light-variables.css +15 -0
  25. package/src/lib/styles/variables/names-variables.css +15 -0
  26. package/src/storybook/stories/design-tokens/{colors.stories.mdx → colors.mdx} +1 -1
  27. package/src/storybook/stories/design-tokens/{icons.stories.mdx → icons.mdx} +1 -1
  28. package/src/storybook/stories/design-tokens/{typeset.stories.mdx → typeset.mdx} +1 -1
  29. package/src/storybook/stories/design-tokens/{variable-spacing.stories.mdx → variable-spacing.mdx} +1 -1
  30. package/src/storybook/stories/molecules/DatePicker.stories.tsx +132 -0
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@iota/apps-ui-kit",
3
- "version": "0.9.5",
3
+ "version": "0.10.1",
4
4
  "sideEffects": false,
5
- "repository": "https://github.com/iotaledger/iota",
5
+ "repository": "https://github.com/iotaledger/ts-packages",
6
6
  "license": "Apache-2.0",
7
7
  "files": [
8
8
  "CHANGELOG.md",
@@ -12,9 +12,9 @@
12
12
  ],
13
13
  "exports": {
14
14
  ".": {
15
+ "types": "./dist/index.d.ts",
15
16
  "import": "./dist/index.es.js",
16
- "require": "./dist/index.umd.js",
17
- "types": "./dist/index.d.ts"
17
+ "require": "./dist/index.umd.js"
18
18
  },
19
19
  "./styles": "./dist/style.css"
20
20
  },
@@ -34,32 +34,28 @@
34
34
  "react-dom": "^18 || ^19"
35
35
  },
36
36
  "devDependencies": {
37
- "@chromatic-com/storybook": "^1.6.0",
38
- "@storybook/addon-essentials": "^7.1.0",
39
- "@storybook/addon-interactions": "^7.1.0",
40
- "@storybook/addon-links": "^7.1.0",
41
- "@storybook/addon-themes": "^7.6.20",
42
- "@storybook/blocks": "^7.1.0",
43
- "@storybook/react": "^7.1.0",
44
- "@storybook/react-vite": "^7.1.0",
37
+ "@chromatic-com/storybook": "^5.1.2",
38
+ "@storybook/addon-a11y": "^10.3.6",
39
+ "@storybook/addon-docs": "^10.3.6",
40
+ "@storybook/addon-themes": "^10.3.6",
41
+ "@storybook/react": "^10.3.6",
42
+ "@storybook/react-vite": "^10.3.6",
45
43
  "@types/lodash.merge": "^4.6.9",
46
44
  "@types/node": "^24.0.0",
47
45
  "@types/react": "^18.3.3",
48
46
  "@types/react-dom": "^18.3.0",
49
- "@vitejs/plugin-react-swc": "^3.3.2",
47
+ "@vitejs/plugin-react-swc": "^4.3.0",
50
48
  "autoprefixer": "^10.4.19",
51
49
  "cross-env": "^7.0.3",
52
50
  "memfs": "^4.11.1",
53
51
  "postcss": "^8.4.31",
54
52
  "react": "^18.3.1",
55
53
  "react-dom": "^18.3.1",
56
- "storybook": "^7.1.0",
57
- "storybook-dark-mode": "^4.0.2",
54
+ "storybook": "^10.3.6",
58
55
  "tailwindcss": "^3.3.3",
59
56
  "typescript": "^5.5.3",
60
- "vite": "^5.4.12",
61
- "vite-plugin-dts": "^4.3.0",
62
- "vite-tsconfig-paths": "^4.2.0"
57
+ "vite": "^8.0.1",
58
+ "vite-plugin-dts": "^4.5.4"
63
59
  },
64
60
  "scripts": {
65
61
  "prettier:check": "prettier -c --ignore-unknown --ignore-path=../../.prettierignore --ignore-path=.prettierignore .",
@@ -2,8 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import cx from 'classnames';
5
- import { BadgeSize } from './badge.enums';
6
- import type { BadgeType } from './badge.enums';
5
+ import { BadgeSize, BadgeType } from './badge.enums';
7
6
  import { BACKGROUND_COLORS, BORDER_COLORS, TEXT_COLORS } from './badge.classes';
8
7
 
9
8
  interface BadgeProps {
@@ -25,6 +24,7 @@ export function Badge({ type, label, size = BadgeSize.Medium }: BadgeProps): Rea
25
24
  const backgroundClasses = BACKGROUND_COLORS[type];
26
25
  const textClasses = TEXT_COLORS[type];
27
26
  const isSmall = size === BadgeSize.Small;
27
+ const isOutlined = type === BadgeType.Outlined;
28
28
  const labelClasses = label ? 'px-xs py-xxs' : 'h-1.5 w-1.5';
29
29
  const textSizeClass = isSmall ? 'text-label-sm' : 'text-label-md';
30
30
 
@@ -33,8 +33,9 @@ export function Badge({ type, label, size = BadgeSize.Medium }: BadgeProps): Rea
33
33
  className={cx(
34
34
  'inline-flex items-center space-x-2 rounded-full disabled:opacity-30',
35
35
  { border: !isSmall },
36
- backgroundClasses,
37
36
  { [BORDER_COLORS[type]]: !isSmall },
37
+ { 'badge-ring-outlined': isSmall && isOutlined },
38
+ backgroundClasses,
38
39
  labelClasses,
39
40
  )}
40
41
  >
@@ -9,6 +9,7 @@ export const BACKGROUND_COLORS = {
9
9
  [BadgeType.PrimarySoft]: 'badge-bg-color-primary-soft',
10
10
  [BadgeType.Success]: 'bg-success-surface',
11
11
  [BadgeType.Warning]: 'bg-warning-surface',
12
+ [BadgeType.Outlined]: 'bg-transparent',
12
13
  [BadgeType.Error]: 'bg-error-surface',
13
14
  };
14
15
 
@@ -18,6 +19,7 @@ export const TEXT_COLORS: Record<BadgeType, string> = {
18
19
  [BadgeType.PrimarySoft]: 'badge-text-color-primary-soft',
19
20
  [BadgeType.Success]: 'text-on-success',
20
21
  [BadgeType.Warning]: 'text-on-warning',
22
+ [BadgeType.Outlined]: 'badge-text-color-neutral',
21
23
  [BadgeType.Error]: 'text-on-error',
22
24
  };
23
25
 
@@ -27,5 +29,6 @@ export const BORDER_COLORS: Record<BadgeType, string> = {
27
29
  [BadgeType.PrimarySoft]: 'badge-border-color-soft',
28
30
  [BadgeType.Success]: 'border-success-surface',
29
31
  [BadgeType.Warning]: 'border-warning-surface',
32
+ [BadgeType.Outlined]: 'badge-border-color-outlined',
30
33
  [BadgeType.Error]: 'border-error-surface',
31
34
  };
@@ -7,6 +7,7 @@ export enum BadgeType {
7
7
  PrimarySoft = 'primary-soft',
8
8
  Success = 'success',
9
9
  Warning = 'warning',
10
+ Outlined = 'outlined',
10
11
  Error = 'error',
11
12
  }
12
13
 
@@ -25,7 +25,7 @@ interface KeyValueProps {
25
25
  /**
26
26
  * The tooltip text.
27
27
  */
28
- tooltipText?: string;
28
+ tooltipText?: ReactNode;
29
29
  /**
30
30
  * The supporting label of the KeyValue (optional).
31
31
  */
@@ -28,7 +28,7 @@ interface LabelTextProps {
28
28
  /**
29
29
  * The text of the LabelText.
30
30
  */
31
- text: string;
31
+ text: React.ReactNode;
32
32
  /**
33
33
  * The tooltip position.
34
34
  */
@@ -2,13 +2,13 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { useRef, useState, useLayoutEffect } from 'react';
5
- import type { PropsWithChildren } from 'react';
5
+ import type { PropsWithChildren, ReactNode } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import cx from 'classnames';
8
8
  import { TooltipPosition } from './tooltip.enums';
9
9
 
10
10
  interface TooltipProps {
11
- text: string;
11
+ text: ReactNode;
12
12
  position?: TooltipPosition;
13
13
  maxWidth?: string;
14
14
  offset?: number;
@@ -0,0 +1,412 @@
1
+ // Copyright (c) 2026 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import cx from 'classnames';
6
+ import {
7
+ ArrowLeft,
8
+ ArrowRight,
9
+ Calendar,
10
+ DoubleArrowLeft,
11
+ DoubleArrowRight,
12
+ } from '@iota/apps-ui-icons';
13
+ import { ButtonUnstyled } from '@/components/atoms/button';
14
+ import { InputWrapper, LabelHtmlTag } from '../input/InputWrapper';
15
+ import {
16
+ DATE_PICKER_CALENDAR_CLASSES,
17
+ DATE_PICKER_DAY_BASE_CLASSES,
18
+ DATE_PICKER_DAY_DISABLED_CLASSES,
19
+ DATE_PICKER_DAY_HOVER_CLASSES,
20
+ DATE_PICKER_DAY_OUTSIDE_MONTH_CLASSES,
21
+ DATE_PICKER_DAY_SELECTED_CLASSES,
22
+ DATE_PICKER_DAY_TODAY_CLASSES,
23
+ DATE_PICKER_HEADER_BUTTON_CLASSES,
24
+ DATE_PICKER_NAV_BUTTON_CLASSES,
25
+ DATE_PICKER_TRIGGER_CLASSES,
26
+ DATE_PICKER_WEEKDAY_CLASSES,
27
+ DATE_PICKER_YEAR_CELL_BASE_CLASSES,
28
+ DATE_PICKER_YEAR_CELL_CURRENT_CLASSES,
29
+ DATE_PICKER_YEAR_CELL_DISABLED_CLASSES,
30
+ DATE_PICKER_YEAR_CELL_HOVER_CLASSES,
31
+ DATE_PICKER_YEAR_CELL_SELECTED_CLASSES,
32
+ } from './date-picker.classes';
33
+ import { DatePickerFormat } from './date-picker.enums';
34
+ import { DECADE_SIZE, YEAR_GRID_SIZE, MONTHS, WEEKDAYS } from './date-picker.constants';
35
+ import {
36
+ startOfDay,
37
+ decadeStart,
38
+ isDateDisabled,
39
+ buildCalendarGrid,
40
+ isSameDay,
41
+ isYearDisabled,
42
+ formatDate,
43
+ } from './date-picker.helpers';
44
+
45
+ type CalendarView = 'days' | 'years';
46
+
47
+ export interface DatePickerProps {
48
+ /**
49
+ * The currently selected date value.
50
+ */
51
+ value?: Date;
52
+ /**
53
+ * Callback fired when the user selects a date.
54
+ */
55
+ onChange?: (date: Date) => void;
56
+ /**
57
+ * The earliest selectable date (inclusive).
58
+ */
59
+ minDate?: Date;
60
+ /**
61
+ * The latest selectable date (inclusive).
62
+ */
63
+ maxDate?: Date;
64
+ /**
65
+ * Controls how the selected date is displayed in the trigger field.
66
+ */
67
+ dateFormat?: DatePickerFormat;
68
+ /**
69
+ * The field label shown above the trigger.
70
+ */
71
+ label?: string;
72
+ /**
73
+ * Caption text shown below the trigger.
74
+ */
75
+ caption?: string;
76
+ /**
77
+ * Error message; overrides caption when set.
78
+ */
79
+ errorMessage?: string;
80
+ /**
81
+ * Whether the field is disabled.
82
+ */
83
+ disabled?: boolean;
84
+ /**
85
+ * Placeholder text shown when no date is selected.
86
+ */
87
+ placeholder?: string;
88
+ }
89
+
90
+ export function DatePicker({
91
+ value,
92
+ onChange,
93
+ minDate,
94
+ maxDate,
95
+ dateFormat = DatePickerFormat.DayMonthYear,
96
+ label,
97
+ caption,
98
+ errorMessage,
99
+ disabled,
100
+ placeholder = 'Select a date',
101
+ }: DatePickerProps): React.JSX.Element {
102
+ const today = startOfDay(new Date());
103
+
104
+ const [isOpen, setIsOpen] = useState(false);
105
+ const [calendarView, setCalendarView] = useState<CalendarView>('days');
106
+ const [viewYear, setViewYear] = useState(() => value?.getFullYear() ?? today.getFullYear());
107
+ const [viewMonth, setViewMonth] = useState(() => value?.getMonth() ?? today.getMonth());
108
+
109
+ const wrapperRef = useRef<HTMLDivElement>(null);
110
+
111
+ useEffect(() => {
112
+ if (value) {
113
+ setViewYear(value.getFullYear());
114
+ setViewMonth(value.getMonth());
115
+ }
116
+ }, [value]);
117
+
118
+ useEffect(() => {
119
+ if (!isOpen) setCalendarView('days');
120
+ }, [isOpen]);
121
+
122
+ useEffect(() => {
123
+ if (!isOpen) return;
124
+ function handleClickOutside(e: MouseEvent) {
125
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
126
+ setIsOpen(false);
127
+ }
128
+ }
129
+ document.addEventListener('mousedown', handleClickOutside);
130
+ return () => document.removeEventListener('mousedown', handleClickOutside);
131
+ }, [isOpen]);
132
+
133
+ useEffect(() => {
134
+ if (!isOpen) return;
135
+ function handleKeyDown(e: KeyboardEvent) {
136
+ if (e.key === 'Escape') setIsOpen(false);
137
+ }
138
+ document.addEventListener('keydown', handleKeyDown);
139
+ return () => document.removeEventListener('keydown', handleKeyDown);
140
+ }, [isOpen]);
141
+
142
+ function toggleOpen() {
143
+ if (!disabled) setIsOpen((prev) => !prev);
144
+ }
145
+
146
+ function navigatePrevMonth() {
147
+ if (viewMonth === 0) {
148
+ setViewMonth(11);
149
+ setViewYear((year) => year - 1);
150
+ } else {
151
+ setViewMonth((month) => month - 1);
152
+ }
153
+ }
154
+
155
+ function navigateNextMonth() {
156
+ if (viewMonth === 11) {
157
+ setViewMonth(0);
158
+ setViewYear((year) => year + 1);
159
+ } else {
160
+ setViewMonth((month) => month + 1);
161
+ }
162
+ }
163
+
164
+ function goBackTenYears() {
165
+ setViewYear((year) => year - DECADE_SIZE);
166
+ }
167
+
168
+ function goForwardTenYears() {
169
+ setViewYear((year) => year + DECADE_SIZE);
170
+ }
171
+
172
+ function goBackOneDecade() {
173
+ setViewYear((year) => decadeStart(year) - DECADE_SIZE);
174
+ }
175
+
176
+ function goToNextDecade() {
177
+ setViewYear((year) => decadeStart(year) + DECADE_SIZE);
178
+ }
179
+
180
+ function handleYearSelect(year: number) {
181
+ setViewYear(year);
182
+ setCalendarView('days');
183
+ }
184
+
185
+ function handleDayClick(date: Date) {
186
+ if (isDateDisabled(date, minDate, maxDate)) return;
187
+ onChange?.(date);
188
+ setIsOpen(false);
189
+ }
190
+
191
+ const calendarGrid = buildCalendarGrid(viewYear, viewMonth);
192
+
193
+ const decadeStartYear = decadeStart(viewYear);
194
+ const yearsInDecade = Array.from({ length: YEAR_GRID_SIZE }, (_, i) => decadeStartYear + i);
195
+
196
+ const canNavigateBackDecade =
197
+ !minDate || new Date(viewYear - DECADE_SIZE, viewMonth + 1, 0) >= startOfDay(minDate);
198
+ const canNavigateToPreviousMonth =
199
+ !minDate || new Date(viewYear, viewMonth, 0) >= startOfDay(minDate);
200
+ const canNavigateNextMonth =
201
+ !maxDate || new Date(viewYear, viewMonth + 1, 1) <= startOfDay(maxDate);
202
+ const canNavigateNextDecade =
203
+ !maxDate || new Date(viewYear + DECADE_SIZE, viewMonth, 1) <= startOfDay(maxDate);
204
+
205
+ const isPreviousDecadeAccessible =
206
+ !minDate || new Date(decadeStartYear - 1, 11, 31) >= startOfDay(minDate);
207
+ const isNextDecadeAccessible =
208
+ !maxDate || new Date(decadeStartYear + YEAR_GRID_SIZE, 0, 1) <= startOfDay(maxDate);
209
+
210
+ return (
211
+ <InputWrapper
212
+ label={label}
213
+ caption={caption}
214
+ disabled={disabled}
215
+ errorMessage={errorMessage}
216
+ labelHtmlTag={LabelHtmlTag.Div}
217
+ >
218
+ <div className="relative w-full" ref={wrapperRef}>
219
+ <ButtonUnstyled
220
+ onClick={toggleOpen}
221
+ disabled={disabled}
222
+ aria-expanded={isOpen}
223
+ aria-haspopup="dialog"
224
+ aria-label={value ? formatDate(value, dateFormat) : placeholder}
225
+ className={cx(DATE_PICKER_TRIGGER_CLASSES, {
226
+ 'date-picker-trigger-border-focus-color': isOpen,
227
+ })}
228
+ >
229
+ <span
230
+ className={cx('block w-full text-start text-body-lg', {
231
+ 'date-picker-trigger-placeholder-color': !value,
232
+ 'date-picker-trigger-text-color': !!value,
233
+ })}
234
+ >
235
+ {value ? formatDate(value, dateFormat) : placeholder}
236
+ </span>
237
+ <span className="input-icon-color flex-shrink-0">
238
+ <Calendar />
239
+ </span>
240
+ </ButtonUnstyled>
241
+
242
+ {isOpen && (
243
+ <div
244
+ role="dialog"
245
+ aria-label="Date picker calendar"
246
+ className={cx(DATE_PICKER_CALENDAR_CLASSES, 'w-[280px]')}
247
+ >
248
+ {calendarView === 'days' ? (
249
+ <>
250
+ <div className="mb-1 flex items-center justify-between">
251
+ <ButtonUnstyled
252
+ onClick={goBackTenYears}
253
+ disabled={!canNavigateBackDecade}
254
+ aria-label="Previous 10 years"
255
+ className={DATE_PICKER_NAV_BUTTON_CLASSES}
256
+ >
257
+ <DoubleArrowLeft />
258
+ </ButtonUnstyled>
259
+
260
+ <div className="flex flex-1 items-center justify-between px-1">
261
+ <ButtonUnstyled
262
+ onClick={navigatePrevMonth}
263
+ disabled={!canNavigateToPreviousMonth}
264
+ aria-label="Previous month"
265
+ className={DATE_PICKER_NAV_BUTTON_CLASSES}
266
+ >
267
+ <ArrowLeft />
268
+ </ButtonUnstyled>
269
+
270
+ <ButtonUnstyled
271
+ onClick={() => setCalendarView('years')}
272
+ aria-label="Select year"
273
+ className={DATE_PICKER_HEADER_BUTTON_CLASSES}
274
+ >
275
+ {MONTHS[viewMonth]} {viewYear}
276
+ </ButtonUnstyled>
277
+
278
+ <ButtonUnstyled
279
+ onClick={navigateNextMonth}
280
+ disabled={!canNavigateNextMonth}
281
+ aria-label="Next month"
282
+ className={DATE_PICKER_NAV_BUTTON_CLASSES}
283
+ >
284
+ <ArrowRight />
285
+ </ButtonUnstyled>
286
+ </div>
287
+
288
+ <ButtonUnstyled
289
+ onClick={goForwardTenYears}
290
+ disabled={!canNavigateNextDecade}
291
+ aria-label="Next 10 years"
292
+ className={DATE_PICKER_NAV_BUTTON_CLASSES}
293
+ >
294
+ <DoubleArrowRight />
295
+ </ButtonUnstyled>
296
+ </div>
297
+
298
+ <div className="mb-1 grid grid-cols-7">
299
+ {WEEKDAYS.map((day) => (
300
+ <div key={day} className={DATE_PICKER_WEEKDAY_CLASSES}>
301
+ {day}
302
+ </div>
303
+ ))}
304
+ </div>
305
+
306
+ <div className="grid grid-cols-7">
307
+ {calendarGrid.map((date, index) => {
308
+ const isCurrentMonth = date.getMonth() === viewMonth;
309
+ const isSelected = !!value && isSameDay(date, value);
310
+ const isToday = isSameDay(date, today);
311
+ const isDisabled = isDateDisabled(date, minDate, maxDate);
312
+ const isOutsideMonth = !isCurrentMonth;
313
+
314
+ return (
315
+ <div
316
+ key={index}
317
+ className="flex items-center justify-center p-0.5"
318
+ >
319
+ <ButtonUnstyled
320
+ onClick={() => handleDayClick(date)}
321
+ disabled={isDisabled || isOutsideMonth}
322
+ aria-label={formatDate(
323
+ date,
324
+ DatePickerFormat.DayMonthYear,
325
+ )}
326
+ aria-selected={isSelected}
327
+ aria-disabled={isDisabled || isOutsideMonth}
328
+ className={cx(DATE_PICKER_DAY_BASE_CLASSES, {
329
+ [DATE_PICKER_DAY_SELECTED_CLASSES]:
330
+ isSelected,
331
+ [DATE_PICKER_DAY_TODAY_CLASSES]:
332
+ isToday && !isSelected,
333
+ [DATE_PICKER_DAY_DISABLED_CLASSES]:
334
+ isDisabled && !isOutsideMonth,
335
+ [DATE_PICKER_DAY_OUTSIDE_MONTH_CLASSES]:
336
+ isOutsideMonth,
337
+ [DATE_PICKER_DAY_HOVER_CLASSES]:
338
+ !isSelected &&
339
+ !isDisabled &&
340
+ !isOutsideMonth,
341
+ })}
342
+ >
343
+ {date.getDate()}
344
+ </ButtonUnstyled>
345
+ </div>
346
+ );
347
+ })}
348
+ </div>
349
+ </>
350
+ ) : (
351
+ <>
352
+ <div className="mb-2 flex items-center justify-between">
353
+ <ButtonUnstyled
354
+ onClick={goBackOneDecade}
355
+ disabled={!isPreviousDecadeAccessible}
356
+ aria-label="Previous decade"
357
+ className={DATE_PICKER_NAV_BUTTON_CLASSES}
358
+ >
359
+ <DoubleArrowLeft />
360
+ </ButtonUnstyled>
361
+
362
+ <span className="date-picker-header-text-color text-label-lg font-semibold">
363
+ {decadeStartYear} – {decadeStartYear + YEAR_GRID_SIZE - 1}
364
+ </span>
365
+
366
+ <ButtonUnstyled
367
+ onClick={goToNextDecade}
368
+ disabled={!isNextDecadeAccessible}
369
+ aria-label="Next decade"
370
+ className={DATE_PICKER_NAV_BUTTON_CLASSES}
371
+ >
372
+ <DoubleArrowRight />
373
+ </ButtonUnstyled>
374
+ </div>
375
+
376
+ <div className="grid grid-cols-3 gap-1">
377
+ {yearsInDecade.map((year) => {
378
+ const isSelected = value?.getFullYear() === year;
379
+ const isCurrent = today.getFullYear() === year;
380
+ const isDisabled = isYearDisabled(year, minDate, maxDate);
381
+
382
+ return (
383
+ <ButtonUnstyled
384
+ key={year}
385
+ onClick={() => handleYearSelect(year)}
386
+ disabled={isDisabled}
387
+ aria-label={String(year)}
388
+ aria-selected={isSelected}
389
+ className={cx(DATE_PICKER_YEAR_CELL_BASE_CLASSES, {
390
+ [DATE_PICKER_YEAR_CELL_SELECTED_CLASSES]:
391
+ isSelected,
392
+ [DATE_PICKER_YEAR_CELL_CURRENT_CLASSES]:
393
+ isCurrent && !isSelected,
394
+ [DATE_PICKER_YEAR_CELL_DISABLED_CLASSES]:
395
+ isDisabled,
396
+ [DATE_PICKER_YEAR_CELL_HOVER_CLASSES]:
397
+ !isSelected && !isDisabled,
398
+ })}
399
+ >
400
+ {year}
401
+ </ButtonUnstyled>
402
+ );
403
+ })}
404
+ </div>
405
+ </>
406
+ )}
407
+ </div>
408
+ )}
409
+ </div>
410
+ </InputWrapper>
411
+ );
412
+ }
@@ -0,0 +1,42 @@
1
+ // Copyright (c) 2026 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const DATE_PICKER_TRIGGER_CLASSES =
5
+ 'date-picker-trigger-border-color focus-visible:enabled:date-picker-trigger-border-focus-color hover:enabled:date-picker-trigger-border-hover-color group-[.errored]:date-picker-trigger-border-error-color flex w-full flex-row items-center gap-x-3 rounded-lg border px-md py-sm disabled:cursor-not-allowed [&_svg]:h-5 [&_svg]:w-5';
6
+
7
+ export const DATE_PICKER_CALENDAR_CLASSES =
8
+ 'date-picker-calendar-bg date-picker-calendar-border-color absolute z-50 mt-1 rounded-lg border p-3 shadow-lg';
9
+
10
+ export const DATE_PICKER_DAY_BASE_CLASSES =
11
+ 'date-picker-day-text-color flex h-8 w-8 items-center justify-center rounded-full text-body-md transition-colors hover:delay-75';
12
+
13
+ export const DATE_PICKER_DAY_HOVER_CLASSES =
14
+ 'enabled:hover:date-picker-day-bg-hover enabled:hover:date-picker-day-text-color-hover cursor-pointer';
15
+
16
+ export const DATE_PICKER_DAY_SELECTED_CLASSES =
17
+ 'date-picker-day-bg-selected date-picker-day-text-selected cursor-pointer';
18
+
19
+ export const DATE_PICKER_DAY_TODAY_CLASSES = 'date-picker-day-today-ring font-semibold';
20
+
21
+ export const DATE_PICKER_DAY_DISABLED_CLASSES =
22
+ 'date-picker-day-text-disabled cursor-not-allowed opacity-40';
23
+
24
+ export const DATE_PICKER_DAY_OUTSIDE_MONTH_CLASSES =
25
+ 'date-picker-day-text-outside-month cursor-default opacity-30';
26
+
27
+ export const DATE_PICKER_WEEKDAY_CLASSES =
28
+ 'date-picker-weekday-text-color flex h-8 w-8 items-center justify-center text-label-sm font-medium';
29
+
30
+ export const DATE_PICKER_NAV_BUTTON_CLASSES =
31
+ 'date-picker-nav-icon-color enabled:hover:date-picker-nav-bg-hover flex h-8 w-8 items-center justify-center rounded-full transition-colors hover:delay-75 disabled:cursor-not-allowed disabled:opacity-40 [&_svg]:h-4 [&_svg]:w-4';
32
+
33
+ export const DATE_PICKER_HEADER_BUTTON_CLASSES =
34
+ 'date-picker-header-text-color enabled:hover:date-picker-nav-bg-hover rounded-md px-1 py-0.5 text-label-lg font-semibold transition-colors hover:delay-75 shrink-0';
35
+
36
+ export const DATE_PICKER_YEAR_CELL_BASE_CLASSES =
37
+ 'date-picker-day-text-color flex w-full items-center justify-center rounded-lg py-2 text-body-md transition-colors hover:delay-75';
38
+
39
+ export const DATE_PICKER_YEAR_CELL_HOVER_CLASSES = DATE_PICKER_DAY_HOVER_CLASSES;
40
+ export const DATE_PICKER_YEAR_CELL_SELECTED_CLASSES = DATE_PICKER_DAY_SELECTED_CLASSES;
41
+ export const DATE_PICKER_YEAR_CELL_CURRENT_CLASSES = DATE_PICKER_DAY_TODAY_CLASSES;
42
+ export const DATE_PICKER_YEAR_CELL_DISABLED_CLASSES = DATE_PICKER_DAY_DISABLED_CLASSES;
@@ -0,0 +1,23 @@
1
+ // Copyright (c) 2026 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
5
+
6
+ export const MONTHS = [
7
+ 'January',
8
+ 'February',
9
+ 'March',
10
+ 'April',
11
+ 'May',
12
+ 'June',
13
+ 'July',
14
+ 'August',
15
+ 'September',
16
+ 'October',
17
+ 'November',
18
+ 'December',
19
+ ];
20
+
21
+ export const DECADE_SIZE = 10;
22
+
23
+ export const YEAR_GRID_SIZE = 12;
@@ -0,0 +1,8 @@
1
+ // Copyright (c) 2026 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export enum DatePickerFormat {
5
+ DayMonthYear = 'DD/MM/YYYY',
6
+ MonthDayYear = 'MM/DD/YYYY',
7
+ YearMonthDay = 'YYYY/MM/DD',
8
+ }