@harismawan/stamp-ui 0.1.1 → 0.2.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.
- package/README.md +3 -2
- package/dist/components/Checkbox.d.ts +6 -0
- package/dist/components/Checkbox.js +12 -4
- package/dist/components/Combobox.d.ts +30 -0
- package/dist/components/Combobox.js +352 -0
- package/dist/components/Command.d.ts +20 -0
- package/dist/components/Command.js +273 -0
- package/dist/components/DataTable.d.ts +37 -0
- package/dist/components/DataTable.js +229 -0
- package/dist/components/DatePicker.d.ts +15 -0
- package/dist/components/DatePicker.js +199 -0
- package/dist/components/DateRangePicker.d.ts +19 -0
- package/dist/components/DateRangePicker.js +255 -0
- package/dist/components/FileUpload.d.ts +33 -0
- package/dist/components/FileUpload.js +269 -0
- package/dist/components/TagInput.d.ts +28 -0
- package/dist/components/TagInput.js +112 -0
- package/dist/components/TreeView.d.ts +19 -0
- package/dist/components/TreeView.js +282 -0
- package/dist/components/internal/calendar.d.ts +52 -0
- package/dist/components/internal/calendar.js +301 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { useFloating, autoUpdate, offset, flip, shift, useClick, useDismiss, useRole, useInteractions, FloatingPortal, FloatingFocusManager, } from '@floating-ui/react';
|
|
5
|
+
import { Calendar, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
|
6
|
+
import { startOfMonth, addMonths, isSameDay, isWithin, monthLabel, MonthGrid, } from './internal/calendar';
|
|
7
|
+
const Trigger = styled.button `
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: ${(p) => p.theme.space[2]};
|
|
11
|
+
font-family: ${(p) => p.theme.font.body};
|
|
12
|
+
font-weight: 800;
|
|
13
|
+
font-size: 14px;
|
|
14
|
+
color: ${(p) => p.theme.colors.text};
|
|
15
|
+
background: ${(p) => p.theme.colors.surface};
|
|
16
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
17
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
18
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
19
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
transition: transform 80ms ${(p) => p.theme.easing.out},
|
|
22
|
+
box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
23
|
+
|
|
24
|
+
&:hover:not(:disabled) {
|
|
25
|
+
transform: translate(2px, 2px);
|
|
26
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
27
|
+
}
|
|
28
|
+
&:active:not(:disabled) {
|
|
29
|
+
transform: translate(4px, 4px);
|
|
30
|
+
box-shadow: ${(p) => p.theme.shadow.none};
|
|
31
|
+
}
|
|
32
|
+
&:disabled {
|
|
33
|
+
opacity: 0.55;
|
|
34
|
+
cursor: not-allowed;
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
const TriggerLabel = styled.span `
|
|
38
|
+
color: ${(p) => (p.$placeholder ? p.theme.colors.textSubtle : p.theme.colors.text)};
|
|
39
|
+
`;
|
|
40
|
+
const TriggerWrap = styled.span `
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
`;
|
|
44
|
+
const ClearButton = styled.button `
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
border: none;
|
|
49
|
+
background: transparent;
|
|
50
|
+
color: ${(p) => p.theme.colors.textSubtle};
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
padding: 0;
|
|
53
|
+
margin-left: ${(p) => p.theme.space[1]};
|
|
54
|
+
border-radius: ${(p) => p.theme.radii.xs};
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
color: ${(p) => p.theme.colors.text};
|
|
58
|
+
}
|
|
59
|
+
&:focus-visible {
|
|
60
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
61
|
+
outline-offset: 2px;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
const Panel = styled.div `
|
|
65
|
+
background: ${(p) => p.theme.colors.surface};
|
|
66
|
+
color: ${(p) => p.theme.colors.text};
|
|
67
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
68
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
69
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
70
|
+
padding: ${(p) => p.theme.space[4]};
|
|
71
|
+
font-family: ${(p) => p.theme.font.body};
|
|
72
|
+
min-width: 280px;
|
|
73
|
+
z-index: 1000;
|
|
74
|
+
outline: none;
|
|
75
|
+
`;
|
|
76
|
+
const GridWrap = styled.div `
|
|
77
|
+
display: contents;
|
|
78
|
+
`;
|
|
79
|
+
const Header = styled.div `
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
margin-bottom: ${(p) => p.theme.space[3]};
|
|
84
|
+
`;
|
|
85
|
+
const MonthTitle = styled.div `
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
font-weight: 800;
|
|
88
|
+
color: ${(p) => p.theme.colors.text};
|
|
89
|
+
`;
|
|
90
|
+
const NavButton = styled.button `
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
width: 32px;
|
|
95
|
+
height: 32px;
|
|
96
|
+
color: ${(p) => p.theme.colors.text};
|
|
97
|
+
background: ${(p) => p.theme.colors.surface};
|
|
98
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
99
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition: transform 80ms ${(p) => p.theme.easing.out},
|
|
102
|
+
box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
103
|
+
|
|
104
|
+
&:hover:not(:disabled) {
|
|
105
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
106
|
+
}
|
|
107
|
+
&:active:not(:disabled) {
|
|
108
|
+
transform: translate(2px, 2px);
|
|
109
|
+
box-shadow: ${(p) => p.theme.shadow.none};
|
|
110
|
+
}
|
|
111
|
+
&:focus-visible {
|
|
112
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
113
|
+
outline-offset: 2px;
|
|
114
|
+
}
|
|
115
|
+
&:disabled {
|
|
116
|
+
opacity: 0.55;
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
export function DatePicker(props) {
|
|
121
|
+
const { value, defaultValue, onChange, min, max, placeholder = 'Select date', disabled = false, clearable = false, format, weekStartsOn = 0, id, } = props;
|
|
122
|
+
const isControlled = value !== undefined;
|
|
123
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
|
|
124
|
+
const selected = isControlled ? (value ?? null) : internalValue;
|
|
125
|
+
const [open, setOpen] = React.useState(false);
|
|
126
|
+
const [viewedMonth, setViewedMonth] = React.useState(() => startOfMonth(selected ?? new Date()));
|
|
127
|
+
const [focusedDay, setFocusedDay] = React.useState(null);
|
|
128
|
+
// Resolves to the roving day cell (tabIndex 0) so focus lands on a day,
|
|
129
|
+
// not the first nav button, when the popover opens.
|
|
130
|
+
const initialFocusRef = React.useRef(null);
|
|
131
|
+
const generatedId = React.useId();
|
|
132
|
+
const baseId = id ?? generatedId;
|
|
133
|
+
const labelId = `${baseId}-label`;
|
|
134
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
135
|
+
open,
|
|
136
|
+
onOpenChange: setOpen,
|
|
137
|
+
placement: 'bottom-start',
|
|
138
|
+
whileElementsMounted: autoUpdate,
|
|
139
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
140
|
+
});
|
|
141
|
+
const click = useClick(context, { enabled: !disabled });
|
|
142
|
+
const dismiss = useDismiss(context);
|
|
143
|
+
const role = useRole(context, { role: 'dialog' });
|
|
144
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
145
|
+
click,
|
|
146
|
+
dismiss,
|
|
147
|
+
role,
|
|
148
|
+
]);
|
|
149
|
+
// When the popover opens, sync the viewed month to the current selection (or
|
|
150
|
+
// today) and seed roving focus on that day so arrow keys work immediately.
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
if (open) {
|
|
153
|
+
const initial = selected ?? new Date();
|
|
154
|
+
setViewedMonth(startOfMonth(initial));
|
|
155
|
+
setFocusedDay(initial);
|
|
156
|
+
}
|
|
157
|
+
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
158
|
+
const commit = (date) => {
|
|
159
|
+
if (!isControlled)
|
|
160
|
+
setInternalValue(date);
|
|
161
|
+
onChange?.(date);
|
|
162
|
+
};
|
|
163
|
+
const handleSelect = (day) => {
|
|
164
|
+
commit(day);
|
|
165
|
+
setOpen(false);
|
|
166
|
+
};
|
|
167
|
+
const handleFocusDay = (day) => {
|
|
168
|
+
setFocusedDay(day);
|
|
169
|
+
// Keep the focused cell mounted: if it falls outside the viewed month,
|
|
170
|
+
// scroll the calendar to that month.
|
|
171
|
+
if (day.getMonth() !== viewedMonth.getMonth() ||
|
|
172
|
+
day.getFullYear() !== viewedMonth.getFullYear()) {
|
|
173
|
+
setViewedMonth(startOfMonth(day));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const handleClear = (e) => {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
if (selected)
|
|
179
|
+
commit(null);
|
|
180
|
+
};
|
|
181
|
+
const triggerText = selected
|
|
182
|
+
? format
|
|
183
|
+
? format(selected)
|
|
184
|
+
: selected.toLocaleDateString()
|
|
185
|
+
: placeholder;
|
|
186
|
+
return (_jsxs(_Fragment, { children: [_jsxs(TriggerWrap, { children: [_jsxs(Trigger, { type: "button", ref: refs.setReference, disabled: disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": selected ? `Choose date, selected ${triggerText}` : 'Choose date', ...getReferenceProps(), children: [_jsx(Calendar, { size: 16, "aria-hidden": "true" }), _jsx(TriggerLabel, { "$placeholder": !selected, children: triggerText })] }), clearable && selected && !disabled && (_jsx(ClearButton, { type: "button", "aria-label": "Clear date", onClick: handleClear, onKeyDown: (e) => {
|
|
187
|
+
// Keep Enter/Space from bubbling to the trigger and opening the
|
|
188
|
+
// popover; the button's own activation still fires onClick.
|
|
189
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
}
|
|
192
|
+
}, children: _jsx(X, { size: 16, "aria-hidden": "true" }) }))] }), open && (_jsx(FloatingPortal, { children: _jsx(FloatingFocusManager, { context: context, modal: false, initialFocus: initialFocusRef, children: _jsxs(Panel, { ref: refs.setFloating, style: floatingStyles, "aria-labelledby": labelId, ...getFloatingProps(), children: [_jsxs(Header, { children: [_jsx(NavButton, { type: "button", "aria-label": "Previous month", onClick: () => setViewedMonth((m) => addMonths(m, -1)), children: _jsx(ChevronLeft, { size: 18, "aria-hidden": "true" }) }), _jsx(MonthTitle, { id: labelId, children: monthLabel(viewedMonth) }), _jsx(NavButton, { type: "button", "aria-label": "Next month", onClick: () => setViewedMonth((m) => addMonths(m, 1)), children: _jsx(ChevronRight, { size: 18, "aria-hidden": "true" }) })] }), _jsx(GridWrap, { ref: (el) => {
|
|
193
|
+
// Point FloatingFocusManager's initialFocus at the roving day
|
|
194
|
+
// cell (the only day button with tabIndex 0) so opening lands
|
|
195
|
+
// focus on a day, enabling arrow navigation immediately.
|
|
196
|
+
initialFocusRef.current =
|
|
197
|
+
el?.querySelector('button[tabindex="0"]') ?? null;
|
|
198
|
+
}, children: _jsx(MonthGrid, { month: viewedMonth, weekStartsOn: weekStartsOn, isSelected: (day) => Boolean(selected) && isSameDay(day, selected), isDisabled: (day) => !isWithin(day, min, max), onSelect: handleSelect, focusedDay: focusedDay, onFocusDay: handleFocusDay, labelId: labelId }) })] }) }) }))] }));
|
|
199
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface DateRange {
|
|
3
|
+
start: Date | null;
|
|
4
|
+
end: Date | null;
|
|
5
|
+
}
|
|
6
|
+
export interface DateRangePickerProps {
|
|
7
|
+
value?: DateRange;
|
|
8
|
+
defaultValue?: DateRange;
|
|
9
|
+
onChange?: (range: DateRange) => void;
|
|
10
|
+
min?: Date;
|
|
11
|
+
max?: Date;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
clearable?: boolean;
|
|
15
|
+
weekStartsOn?: 0 | 1;
|
|
16
|
+
format?: (date: Date) => string;
|
|
17
|
+
id?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function DateRangePicker({ value, defaultValue, onChange, min, max, placeholder, disabled, clearable, weekStartsOn, format, id, }: DateRangePickerProps): React.ReactElement;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { Calendar, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
|
5
|
+
import { useFloating, autoUpdate, offset, flip, shift, useClick, useDismiss, useRole, useInteractions, FloatingPortal, FloatingFocusManager, } from '@floating-ui/react';
|
|
6
|
+
import { MonthGrid, addMonths, isAfter, isBefore, isSameDay, isWithin, monthLabel, startOfMonth, } from './internal/calendar';
|
|
7
|
+
const TriggerWrap = styled.span `
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
`;
|
|
11
|
+
const EMPTY_RANGE = { start: null, end: null };
|
|
12
|
+
/** Local-midnight Date for the last calendar day of `d`'s month. */
|
|
13
|
+
function lastDayOfMonth(d) {
|
|
14
|
+
return new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
|
15
|
+
}
|
|
16
|
+
const Trigger = styled.button `
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: ${(p) => p.theme.space[2]};
|
|
20
|
+
font-family: ${(p) => p.theme.font.body};
|
|
21
|
+
font-weight: 800;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
color: ${(p) => (p.$placeholder ? p.theme.colors.textSubtle : p.theme.colors.text)};
|
|
24
|
+
background: ${(p) => p.theme.colors.surface};
|
|
25
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
26
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
27
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
28
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
transition:
|
|
31
|
+
transform 80ms ${(p) => p.theme.easing.out},
|
|
32
|
+
box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
33
|
+
|
|
34
|
+
&:hover:not(:disabled) {
|
|
35
|
+
transform: translate(2px, 2px);
|
|
36
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
37
|
+
}
|
|
38
|
+
&:active:not(:disabled) {
|
|
39
|
+
transform: translate(4px, 4px);
|
|
40
|
+
box-shadow: ${(p) => p.theme.shadow.none};
|
|
41
|
+
}
|
|
42
|
+
&:focus-visible {
|
|
43
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
44
|
+
outline-offset: 2px;
|
|
45
|
+
}
|
|
46
|
+
&:disabled {
|
|
47
|
+
opacity: 0.55;
|
|
48
|
+
cursor: not-allowed;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
const TriggerLabel = styled.span `
|
|
52
|
+
flex: 1;
|
|
53
|
+
text-align: left;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
`;
|
|
56
|
+
const ClearButton = styled.button `
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
width: 20px;
|
|
61
|
+
height: 20px;
|
|
62
|
+
padding: 0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
flex: none;
|
|
65
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
66
|
+
background: transparent;
|
|
67
|
+
border: none;
|
|
68
|
+
border-radius: ${(p) => p.theme.radii.xs};
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
transition: color 80ms ${(p) => p.theme.easing.out};
|
|
71
|
+
|
|
72
|
+
&:hover {
|
|
73
|
+
color: ${(p) => p.theme.colors.text};
|
|
74
|
+
}
|
|
75
|
+
&:focus-visible {
|
|
76
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
77
|
+
outline-offset: 1px;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
const Panel = styled.div `
|
|
81
|
+
background: ${(p) => p.theme.colors.surface};
|
|
82
|
+
color: ${(p) => p.theme.colors.text};
|
|
83
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
84
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
85
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
86
|
+
padding: ${(p) => p.theme.space[4]};
|
|
87
|
+
font-family: ${(p) => p.theme.font.body};
|
|
88
|
+
z-index: 1000;
|
|
89
|
+
outline: none;
|
|
90
|
+
`;
|
|
91
|
+
const Header = styled.div `
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
margin-bottom: ${(p) => p.theme.space[3]};
|
|
96
|
+
`;
|
|
97
|
+
const MonthsRow = styled.div `
|
|
98
|
+
display: flex;
|
|
99
|
+
gap: ${(p) => p.theme.space[5]};
|
|
100
|
+
`;
|
|
101
|
+
const MonthColumn = styled.div `
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: ${(p) => p.theme.space[2]};
|
|
105
|
+
`;
|
|
106
|
+
const MonthTitle = styled.div `
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
font-weight: 800;
|
|
109
|
+
text-align: center;
|
|
110
|
+
color: ${(p) => p.theme.colors.text};
|
|
111
|
+
`;
|
|
112
|
+
const NavButton = styled.button `
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
width: 32px;
|
|
117
|
+
height: 32px;
|
|
118
|
+
padding: 0;
|
|
119
|
+
flex: none;
|
|
120
|
+
color: ${(p) => p.theme.colors.text};
|
|
121
|
+
background: ${(p) => p.theme.colors.surface};
|
|
122
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
123
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition:
|
|
126
|
+
transform 80ms ${(p) => p.theme.easing.out},
|
|
127
|
+
box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
128
|
+
|
|
129
|
+
&:hover:not(:disabled) {
|
|
130
|
+
transform: translate(1px, 1px);
|
|
131
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
132
|
+
}
|
|
133
|
+
&:focus-visible {
|
|
134
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
135
|
+
outline-offset: 2px;
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
export function DateRangePicker({ value, defaultValue, onChange, min, max, placeholder = 'Select range', disabled = false, clearable = false, weekStartsOn = 0, format, id, }) {
|
|
139
|
+
const isControlled = value !== undefined;
|
|
140
|
+
const [internal, setInternal] = React.useState(() => defaultValue ?? EMPTY_RANGE);
|
|
141
|
+
const range = isControlled ? value : internal;
|
|
142
|
+
const [open, setOpen] = React.useState(false);
|
|
143
|
+
// The left month shown in the popover; the right month is +1.
|
|
144
|
+
const [viewedMonth, setViewedMonth] = React.useState(() => startOfMonth(range.start ?? new Date()));
|
|
145
|
+
// The day that owns roving focus across both grids.
|
|
146
|
+
const [focusedDay, setFocusedDay] = React.useState(null);
|
|
147
|
+
// Resolves to the roving day cell (tabIndex 0) so focus lands on a day,
|
|
148
|
+
// not the first nav button, when the popover opens.
|
|
149
|
+
const initialFocusRef = React.useRef(null);
|
|
150
|
+
const generatedId = React.useId();
|
|
151
|
+
const baseId = id ?? generatedId;
|
|
152
|
+
const leftLabelId = `${baseId}-left`;
|
|
153
|
+
const rightLabelId = `${baseId}-right`;
|
|
154
|
+
const dialogLabelId = `${baseId}-dialog`;
|
|
155
|
+
// When opening, re-anchor the view on the current start (or today) and seed
|
|
156
|
+
// roving focus on that day so arrow keys work immediately.
|
|
157
|
+
const rangeStart = range.start;
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (open) {
|
|
160
|
+
const initial = rangeStart ?? new Date();
|
|
161
|
+
setViewedMonth(startOfMonth(initial));
|
|
162
|
+
setFocusedDay(initial);
|
|
163
|
+
}
|
|
164
|
+
// Re-anchor on the open transition; rangeStart is included so reopening
|
|
165
|
+
// anchors to the current start rather than a stale value.
|
|
166
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
|
+
}, [open, rangeStart]);
|
|
168
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
169
|
+
open,
|
|
170
|
+
onOpenChange: (next) => {
|
|
171
|
+
if (disabled)
|
|
172
|
+
return;
|
|
173
|
+
setOpen(next);
|
|
174
|
+
},
|
|
175
|
+
placement: 'bottom-start',
|
|
176
|
+
whileElementsMounted: autoUpdate,
|
|
177
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
178
|
+
});
|
|
179
|
+
const click = useClick(context, { enabled: !disabled });
|
|
180
|
+
const dismiss = useDismiss(context);
|
|
181
|
+
const role = useRole(context, { role: 'dialog' });
|
|
182
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
|
|
183
|
+
const commit = (next) => {
|
|
184
|
+
if (!isControlled)
|
|
185
|
+
setInternal(next);
|
|
186
|
+
onChange?.(next);
|
|
187
|
+
};
|
|
188
|
+
const fmt = format ?? ((d) => d.toLocaleDateString());
|
|
189
|
+
const triggerText = React.useMemo(() => {
|
|
190
|
+
if (range.start && range.end)
|
|
191
|
+
return `${fmt(range.start)} - ${fmt(range.end)}`;
|
|
192
|
+
if (range.start)
|
|
193
|
+
return fmt(range.start);
|
|
194
|
+
return placeholder;
|
|
195
|
+
}, [range.start, range.end, fmt, placeholder]);
|
|
196
|
+
const hasValue = range.start != null || range.end != null;
|
|
197
|
+
const handleSelect = (day) => {
|
|
198
|
+
const { start, end } = range;
|
|
199
|
+
// No start, or a complete range already exists -> begin a new range.
|
|
200
|
+
// Fire onChange so a controlled parent gains the start and can drive the
|
|
201
|
+
// rest of the flow; without this the controlled value never advances.
|
|
202
|
+
if (!start || (start && end)) {
|
|
203
|
+
commit({ start: day, end: null });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// start set, end unset. A click before the start restarts the range.
|
|
207
|
+
if (isBefore(day, start)) {
|
|
208
|
+
commit({ start: day, end: null });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// day on/after start completes the range, fires onChange, and closes.
|
|
212
|
+
commit({ start, end: day });
|
|
213
|
+
setOpen(false);
|
|
214
|
+
};
|
|
215
|
+
const handleFocusDay = (day) => {
|
|
216
|
+
setFocusedDay(day);
|
|
217
|
+
// Keep the focused cell mounted within the visible two-month window. If it
|
|
218
|
+
// falls before the left month, shift the view left to anchor on it; if it
|
|
219
|
+
// falls after the right month, shift right so the day stays in view.
|
|
220
|
+
const left = viewedMonth;
|
|
221
|
+
const right = addMonths(viewedMonth, 1);
|
|
222
|
+
if (isBefore(day, startOfMonth(left))) {
|
|
223
|
+
setViewedMonth(startOfMonth(day));
|
|
224
|
+
}
|
|
225
|
+
else if (isAfter(day, lastDayOfMonth(right))) {
|
|
226
|
+
// Anchor so the focused day's month becomes the right pane.
|
|
227
|
+
setViewedMonth(startOfMonth(addMonths(day, -1)));
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const handleClear = (e) => {
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
commit(EMPTY_RANGE);
|
|
233
|
+
};
|
|
234
|
+
const shiftMonths = (delta) => {
|
|
235
|
+
setViewedMonth((m) => startOfMonth(addMonths(m, delta)));
|
|
236
|
+
};
|
|
237
|
+
const isRangeStart = (day) => Boolean(range.start && isSameDay(day, range.start));
|
|
238
|
+
const isRangeEnd = (day) => Boolean(range.end && isSameDay(day, range.end));
|
|
239
|
+
const isInRange = (day) => Boolean(range.start && range.end && isAfter(day, range.start) && isBefore(day, range.end));
|
|
240
|
+
const isDisabled = (day) => !isWithin(day, min, max);
|
|
241
|
+
const rightMonth = addMonths(viewedMonth, 1);
|
|
242
|
+
return (_jsxs(_Fragment, { children: [_jsxs(TriggerWrap, { children: [_jsxs(Trigger, { type: "button", id: baseId, ref: refs.setReference, disabled: disabled, "$placeholder": !range.start, "aria-haspopup": "dialog", "aria-expanded": open, ...getReferenceProps(), children: [_jsx(Calendar, { size: 16, strokeWidth: 2.5, "aria-hidden": "true" }), _jsx(TriggerLabel, { children: triggerText })] }), clearable && hasValue && !disabled && (_jsx(ClearButton, { type: "button", "aria-label": "Clear range", onClick: handleClear, onKeyDown: (e) => {
|
|
243
|
+
// Keep Enter/Space from bubbling to the trigger and reopening the
|
|
244
|
+
// popover; the button's own activation still fires onClick.
|
|
245
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
}
|
|
248
|
+
}, children: _jsx(X, { size: 14, strokeWidth: 2.5, "aria-hidden": "true" }) }))] }), open && (_jsx(FloatingPortal, { children: _jsx(FloatingFocusManager, { context: context, modal: false, initialFocus: initialFocusRef, children: _jsxs(Panel, { ref: refs.setFloating, style: floatingStyles, "aria-labelledby": dialogLabelId, ...getFloatingProps(), children: [_jsx("span", { id: dialogLabelId, hidden: true, children: "Choose date range" }), _jsxs(Header, { children: [_jsx(NavButton, { type: "button", "aria-label": "Previous month", onClick: () => shiftMonths(-1), children: _jsx(ChevronLeft, { size: 18, strokeWidth: 2.5, "aria-hidden": "true" }) }), _jsx(NavButton, { type: "button", "aria-label": "Next month", onClick: () => shiftMonths(1), children: _jsx(ChevronRight, { size: 18, strokeWidth: 2.5, "aria-hidden": "true" }) })] }), _jsxs(MonthsRow, { ref: (el) => {
|
|
249
|
+
// Point FloatingFocusManager's initialFocus at the roving day
|
|
250
|
+
// cell (the first day button with tabIndex 0) so opening lands
|
|
251
|
+
// focus on a day, enabling arrow navigation immediately.
|
|
252
|
+
initialFocusRef.current =
|
|
253
|
+
el?.querySelector('button[tabindex="0"]') ?? null;
|
|
254
|
+
}, children: [_jsxs(MonthColumn, { children: [_jsx(MonthTitle, { id: leftLabelId, children: monthLabel(viewedMonth) }), _jsx(MonthGrid, { month: viewedMonth, weekStartsOn: weekStartsOn, labelId: leftLabelId, isRangeStart: isRangeStart, isRangeEnd: isRangeEnd, isInRange: isInRange, isDisabled: isDisabled, onSelect: handleSelect, focusedDay: focusedDay, onFocusDay: handleFocusDay })] }), _jsxs(MonthColumn, { children: [_jsx(MonthTitle, { id: rightLabelId, children: monthLabel(rightMonth) }), _jsx(MonthGrid, { month: rightMonth, weekStartsOn: weekStartsOn, labelId: rightLabelId, isRangeStart: isRangeStart, isRangeEnd: isRangeEnd, isInRange: isInRange, isDisabled: isDisabled, onSelect: handleSelect, focusedDay: focusedDay, onFocusDay: handleFocusDay })] })] })] }) }) }))] }));
|
|
255
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface FileUploadProps {
|
|
3
|
+
/** Controlled list of selected files. When provided, the component is controlled. */
|
|
4
|
+
value?: File[];
|
|
5
|
+
/** Initial files when uncontrolled. Defaults to `[]`. */
|
|
6
|
+
defaultValue?: File[];
|
|
7
|
+
/** Fired with the next file list whenever files are added or removed. */
|
|
8
|
+
onChange?: (files: File[]) => void;
|
|
9
|
+
/** Accept string, e.g. `"image/*,.pdf"`. Validated loosely against MIME/extension. */
|
|
10
|
+
accept?: string;
|
|
11
|
+
/** Allow more than one file. Defaults to `false` (a new file replaces the current one). */
|
|
12
|
+
multiple?: boolean;
|
|
13
|
+
/** Maximum size per file, in bytes. */
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
/** Maximum number of files that may be held at once. */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
/** Disable the whole control (blocks focus, click, drop and the input). */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Fired with the rejected files and a machine-readable reason. */
|
|
20
|
+
onReject?: (rejections: FileUploadRejection[]) => void;
|
|
21
|
+
/** Dropzone prompt. Defaults to "Drag files here or click to browse". */
|
|
22
|
+
label?: React.ReactNode;
|
|
23
|
+
/** Optional id; an internal one is generated with `useId` otherwise. */
|
|
24
|
+
id?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface FileUploadRejection {
|
|
27
|
+
file: File;
|
|
28
|
+
reason: FileUploadRejectReason;
|
|
29
|
+
}
|
|
30
|
+
export type FileUploadRejectReason = 'too-large' | 'too-many' | 'wrong-type';
|
|
31
|
+
/** Humanize a byte count into B / KB / MB. */
|
|
32
|
+
export declare function formatFileSize(bytes: number): string;
|
|
33
|
+
export declare const FileUpload: React.FC<FileUploadProps>;
|