@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.
- package/CHANGELOG.md +18 -0
- package/dist/index.d.ts +55 -3
- package/dist/index.es.js +5697 -6536
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +36 -40
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +2 -1
- package/package.json +14 -18
- package/src/lib/components/atoms/badge/Badge.tsx +4 -3
- package/src/lib/components/atoms/badge/badge.classes.ts +3 -0
- package/src/lib/components/atoms/badge/badge.enums.ts +1 -0
- package/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx +1 -1
- package/src/lib/components/atoms/label-text/LabelText.tsx +1 -1
- package/src/lib/components/atoms/tooltip/Tooltip.tsx +2 -2
- package/src/lib/components/molecules/date-picker/DatePicker.tsx +412 -0
- package/src/lib/components/molecules/date-picker/date-picker.classes.ts +42 -0
- package/src/lib/components/molecules/date-picker/date-picker.constants.ts +23 -0
- package/src/lib/components/molecules/date-picker/date-picker.enums.ts +8 -0
- package/src/lib/components/molecules/date-picker/date-picker.helpers.ts +67 -0
- package/src/lib/components/molecules/date-picker/index.ts +5 -0
- package/src/lib/components/molecules/index.ts +1 -0
- package/src/lib/styles/components.css +89 -0
- package/src/lib/styles/variables/dark-variables.css +15 -0
- package/src/lib/styles/variables/light-variables.css +15 -0
- package/src/lib/styles/variables/names-variables.css +15 -0
- package/src/storybook/stories/design-tokens/{colors.stories.mdx → colors.mdx} +1 -1
- package/src/storybook/stories/design-tokens/{icons.stories.mdx → icons.mdx} +1 -1
- package/src/storybook/stories/design-tokens/{typeset.stories.mdx → typeset.mdx} +1 -1
- package/src/storybook/stories/design-tokens/{variable-spacing.stories.mdx → variable-spacing.mdx} +1 -1
- 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.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"repository": "https://github.com/iotaledger/
|
|
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.
|
|
38
|
-
"@storybook/addon-
|
|
39
|
-
"@storybook/addon-
|
|
40
|
-
"@storybook/addon-
|
|
41
|
-
"@storybook/
|
|
42
|
-
"@storybook/
|
|
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": "^
|
|
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": "^
|
|
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": "^
|
|
61
|
-
"vite-plugin-dts": "^4.
|
|
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
|
};
|
|
@@ -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:
|
|
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;
|