@zvk/ui 0.1.1 → 0.1.3
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 +4 -3
- package/dist/components/calendar/calendar.d.ts +16 -0
- package/dist/components/calendar/calendar.js +178 -0
- package/dist/components/calendar/index.d.ts +2 -0
- package/dist/components/calendar/index.js +2 -0
- package/dist/components/carousel/carousel.d.ts +51 -0
- package/dist/components/carousel/carousel.js +210 -0
- package/dist/components/carousel/index.d.ts +2 -0
- package/dist/components/carousel/index.js +2 -0
- package/dist/components/date-picker/date-picker.d.ts +16 -0
- package/dist/components/date-picker/date-picker.js +50 -0
- package/dist/components/date-picker/index.d.ts +2 -0
- package/dist/components/date-picker/index.js +2 -0
- package/dist/components/hover-card/hover-card.d.ts +31 -0
- package/dist/components/hover-card/hover-card.js +268 -0
- package/dist/components/hover-card/index.d.ts +2 -0
- package/dist/components/hover-card/index.js +2 -0
- package/dist/components/index.d.ts +10 -2
- package/dist/components/index.js +5 -1
- package/dist/components/popover/popover.d.ts +2 -1
- package/dist/components/popover/popover.js +4 -3
- package/dist/components/toast/index.d.ts +2 -2
- package/dist/components/toast/index.js +2 -1
- package/dist/components/toast/toast.d.ts +26 -0
- package/dist/components/toast/toast.js +101 -1
- package/dist/styles.css +335 -3
- package/package.json +17 -1
package/README.md
CHANGED
|
@@ -20,9 +20,10 @@ import "@zvk/ui/styles.css";
|
|
|
20
20
|
|
|
21
21
|
## Release Status
|
|
22
22
|
|
|
23
|
-
GitHub Actions
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
GitHub Actions runs `npm run preflight` before release publishing. Release Please opens
|
|
24
|
+
reviewable version-bump PRs from conventional commits; merging a release PR creates the
|
|
25
|
+
GitHub release and publishes `@zvk/ui` to npm through trusted publishing in the protected
|
|
26
|
+
`npm-publish` environment.
|
|
26
27
|
|
|
27
28
|
## First Slice
|
|
28
29
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface CalendarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
3
|
+
selected?: Date | null;
|
|
4
|
+
defaultSelected?: Date | null;
|
|
5
|
+
onSelect?: (date: Date | null) => void;
|
|
6
|
+
month?: Date;
|
|
7
|
+
defaultMonth?: Date;
|
|
8
|
+
onMonthChange?: (month: Date) => void;
|
|
9
|
+
minDate?: Date;
|
|
10
|
+
maxDate?: Date;
|
|
11
|
+
disabledDate?: (date: Date) => boolean;
|
|
12
|
+
weekStartsOn?: 0 | 1;
|
|
13
|
+
locale?: string;
|
|
14
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
15
|
+
}
|
|
16
|
+
export declare function Calendar({ className, defaultMonth, defaultSelected, disabledDate, locale, maxDate, minDate, month, onKeyDown, onMonthChange, onSelect, ref, selected, weekStartsOn, ...props }: CalendarProps): React.JSX.Element;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.js";
|
|
5
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
6
|
+
function startOfDay(date) {
|
|
7
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
8
|
+
}
|
|
9
|
+
function startOfMonth(date) {
|
|
10
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
11
|
+
}
|
|
12
|
+
function addDays(date, amount) {
|
|
13
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
|
|
14
|
+
}
|
|
15
|
+
function addMonths(date, amount) {
|
|
16
|
+
return new Date(date.getFullYear(), date.getMonth() + amount, 1);
|
|
17
|
+
}
|
|
18
|
+
function isSameDay(left, right) {
|
|
19
|
+
if (!left || !right) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
|
|
23
|
+
}
|
|
24
|
+
function isBeforeDay(left, right) {
|
|
25
|
+
return startOfDay(left).getTime() < startOfDay(right).getTime();
|
|
26
|
+
}
|
|
27
|
+
function isAfterDay(left, right) {
|
|
28
|
+
return startOfDay(left).getTime() > startOfDay(right).getTime();
|
|
29
|
+
}
|
|
30
|
+
function getInitialMonth(month, selected) {
|
|
31
|
+
if (month) {
|
|
32
|
+
return startOfMonth(month);
|
|
33
|
+
}
|
|
34
|
+
if (selected) {
|
|
35
|
+
return startOfMonth(selected);
|
|
36
|
+
}
|
|
37
|
+
return startOfMonth(new Date());
|
|
38
|
+
}
|
|
39
|
+
function getCalendarStart(month, weekStartsOn) {
|
|
40
|
+
const firstDay = startOfMonth(month);
|
|
41
|
+
const offset = (firstDay.getDay() - weekStartsOn + 7) % 7;
|
|
42
|
+
return addDays(firstDay, -offset);
|
|
43
|
+
}
|
|
44
|
+
function getCalendarDays(month, weekStartsOn) {
|
|
45
|
+
const start = getCalendarStart(month, weekStartsOn);
|
|
46
|
+
return Array.from({ length: 42 }, (_, index) => addDays(start, index));
|
|
47
|
+
}
|
|
48
|
+
function isDisabled(date, props) {
|
|
49
|
+
if (props.minDate && isBeforeDay(date, props.minDate)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (props.maxDate && isAfterDay(date, props.maxDate)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return props.disabledDate ? props.disabledDate(startOfDay(date)) : false;
|
|
56
|
+
}
|
|
57
|
+
function getMonthLabel(date, locale) {
|
|
58
|
+
return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(date);
|
|
59
|
+
}
|
|
60
|
+
function getDayLabel(date, locale) {
|
|
61
|
+
return new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }).format(date);
|
|
62
|
+
}
|
|
63
|
+
function getWeekdayLabels(weekStartsOn, locale) {
|
|
64
|
+
const sunday = new Date(2026, 5, 7);
|
|
65
|
+
return Array.from({ length: 7 }, (_, index) => {
|
|
66
|
+
const dayIndex = (index + weekStartsOn) % 7;
|
|
67
|
+
return new Intl.DateTimeFormat(locale, { weekday: "short" }).format(addDays(sunday, dayIndex));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function getNearestEnabledDate(date, visibleMonth, props) {
|
|
71
|
+
if (!isDisabled(date, props)) {
|
|
72
|
+
return date;
|
|
73
|
+
}
|
|
74
|
+
const days = getCalendarDays(visibleMonth, 0);
|
|
75
|
+
return days.find((day) => !isDisabled(day, props)) ?? date;
|
|
76
|
+
}
|
|
77
|
+
export function Calendar({ className, defaultMonth, defaultSelected = null, disabledDate, locale, maxDate, minDate, month, onKeyDown, onMonthChange, onSelect, ref, selected, weekStartsOn = 0, ...props }) {
|
|
78
|
+
const isSelectedControlled = selected !== undefined;
|
|
79
|
+
const selectedDate = isSelectedControlled ? selected : defaultSelected;
|
|
80
|
+
const [uncontrolledSelected, setUncontrolledSelected] = React.useState(defaultSelected);
|
|
81
|
+
const currentSelected = isSelectedControlled ? selected : uncontrolledSelected;
|
|
82
|
+
const [uncontrolledMonth, setUncontrolledMonth] = React.useState(() => getInitialMonth(defaultMonth, selectedDate));
|
|
83
|
+
const visibleMonth = startOfMonth(month ?? uncontrolledMonth);
|
|
84
|
+
const [focusedDate, setFocusedDate] = React.useState(() => startOfDay(currentSelected ?? visibleMonth));
|
|
85
|
+
const headingId = React.useId();
|
|
86
|
+
const dayButtonRefs = React.useRef(new Map());
|
|
87
|
+
const disabledProps = React.useMemo(() => ({ disabledDate, maxDate, minDate }), [disabledDate, maxDate, minDate]);
|
|
88
|
+
const days = React.useMemo(() => getCalendarDays(visibleMonth, weekStartsOn), [visibleMonth, weekStartsOn]);
|
|
89
|
+
const weekdays = React.useMemo(() => getWeekdayLabels(weekStartsOn, locale), [locale, weekStartsOn]);
|
|
90
|
+
const monthLabel = getMonthLabel(visibleMonth, locale);
|
|
91
|
+
const today = startOfDay(new Date());
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (currentSelected) {
|
|
94
|
+
setFocusedDate(startOfDay(currentSelected));
|
|
95
|
+
}
|
|
96
|
+
}, [currentSelected]);
|
|
97
|
+
const setVisibleMonth = (nextMonth) => {
|
|
98
|
+
const normalizedMonth = startOfMonth(nextMonth);
|
|
99
|
+
if (month === undefined) {
|
|
100
|
+
setUncontrolledMonth(normalizedMonth);
|
|
101
|
+
}
|
|
102
|
+
onMonthChange?.(normalizedMonth);
|
|
103
|
+
};
|
|
104
|
+
const focusDay = React.useCallback((date) => {
|
|
105
|
+
const key = startOfDay(date).getTime();
|
|
106
|
+
dayButtonRefs.current.get(key)?.focus();
|
|
107
|
+
}, []);
|
|
108
|
+
const moveFocus = (nextDate) => {
|
|
109
|
+
const normalizedDate = getNearestEnabledDate(startOfDay(nextDate), startOfMonth(nextDate), disabledProps);
|
|
110
|
+
setFocusedDate(normalizedDate);
|
|
111
|
+
if (normalizedDate.getMonth() !== visibleMonth.getMonth() || normalizedDate.getFullYear() !== visibleMonth.getFullYear()) {
|
|
112
|
+
setVisibleMonth(startOfMonth(normalizedDate));
|
|
113
|
+
}
|
|
114
|
+
window.requestAnimationFrame(() => focusDay(normalizedDate));
|
|
115
|
+
};
|
|
116
|
+
const selectDate = (date) => {
|
|
117
|
+
const normalizedDate = startOfDay(date);
|
|
118
|
+
const nextSelected = isSameDay(currentSelected, normalizedDate) ? null : normalizedDate;
|
|
119
|
+
if (!isSelectedControlled) {
|
|
120
|
+
setUncontrolledSelected(nextSelected);
|
|
121
|
+
}
|
|
122
|
+
onSelect?.(nextSelected);
|
|
123
|
+
};
|
|
124
|
+
const handleKeyDown = (event) => {
|
|
125
|
+
onKeyDown?.(event);
|
|
126
|
+
if (event.defaultPrevented) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const keyOffsets = {
|
|
130
|
+
ArrowDown: 7,
|
|
131
|
+
ArrowLeft: -1,
|
|
132
|
+
ArrowRight: 1,
|
|
133
|
+
ArrowUp: -7,
|
|
134
|
+
PageDown: dayMs * 31,
|
|
135
|
+
PageUp: -dayMs * 31
|
|
136
|
+
};
|
|
137
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
138
|
+
const targetDate = getNearestEnabledDate(focusedDate, visibleMonth, disabledProps);
|
|
139
|
+
if (!isDisabled(targetDate, disabledProps)) {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
selectDate(targetDate);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (event.key === "Home" || event.key === "End") {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
const weekDay = (focusedDate.getDay() - weekStartsOn + 7) % 7;
|
|
148
|
+
moveFocus(addDays(focusedDate, event.key === "Home" ? -weekDay : 6 - weekDay));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const offset = keyOffsets[event.key];
|
|
152
|
+
if (offset === undefined) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
if (event.key === "PageDown" || event.key === "PageUp") {
|
|
157
|
+
moveFocus(addMonths(focusedDate, offset > 0 ? 1 : -1));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
moveFocus(addDays(focusedDate, offset));
|
|
161
|
+
};
|
|
162
|
+
return (_jsxs("div", { ...props, ref: ref, className: cn("liano-calendar", className), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "liano-calendar__header", children: [_jsx("button", { "aria-label": "Previous month", className: "liano-calendar__nav", type: "button", onClick: () => setVisibleMonth(addMonths(visibleMonth, -1)), children: _jsx("span", { "aria-hidden": "true", children: "\u2039" }) }), _jsx("h2", { className: "liano-calendar__heading", id: headingId, children: monthLabel }), _jsx("button", { "aria-label": "Next month", className: "liano-calendar__nav", type: "button", onClick: () => setVisibleMonth(addMonths(visibleMonth, 1)), children: _jsx("span", { "aria-hidden": "true", children: "\u203A" }) })] }), _jsxs("div", { "aria-labelledby": headingId, className: "liano-calendar__grid", role: "grid", children: [_jsx("div", { className: "liano-calendar__weekdays", role: "row", children: weekdays.map((weekday) => (_jsx("span", { className: "liano-calendar__weekday", role: "columnheader", children: weekday }, weekday))) }), Array.from({ length: 6 }, (_, rowIndex) => (_jsx("div", { className: "liano-calendar__week", role: "row", children: days.slice(rowIndex * 7, rowIndex * 7 + 7).map((date) => {
|
|
163
|
+
const normalizedDate = startOfDay(date);
|
|
164
|
+
const dateKey = normalizedDate.getTime();
|
|
165
|
+
const outsideMonth = date.getMonth() !== visibleMonth.getMonth();
|
|
166
|
+
const disabled = isDisabled(date, disabledProps);
|
|
167
|
+
const selectedDay = isSameDay(currentSelected, date);
|
|
168
|
+
const focusedDay = isSameDay(focusedDate, date);
|
|
169
|
+
return (_jsx("span", { className: "liano-calendar__cell", role: "gridcell", children: _jsx("button", { "aria-label": getDayLabel(date, locale), "aria-pressed": selectedDay, className: "liano-calendar__day", "data-disabled": disabled ? "true" : undefined, "data-outside-month": outsideMonth ? "true" : undefined, "data-selected": selectedDay ? "true" : undefined, "data-today": isSameDay(today, date) ? "true" : undefined, disabled: disabled, ref: (node) => {
|
|
170
|
+
if (node) {
|
|
171
|
+
dayButtonRefs.current.set(dateKey, node);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
dayButtonRefs.current.delete(dateKey);
|
|
175
|
+
}
|
|
176
|
+
}, tabIndex: focusedDay ? 0 : -1, type: "button", onClick: () => selectDate(date), onFocus: () => setFocusedDate(normalizedDate), children: date.getDate() }) }, dateKey));
|
|
177
|
+
}) }, rowIndex)))] })] }));
|
|
178
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type CarouselOrientation = "horizontal" | "vertical";
|
|
3
|
+
export interface CarouselApi {
|
|
4
|
+
scrollPrev: () => void;
|
|
5
|
+
scrollNext: () => void;
|
|
6
|
+
scrollTo: (index: number) => void;
|
|
7
|
+
selectedIndex: () => number;
|
|
8
|
+
slideCount: () => number;
|
|
9
|
+
canScrollPrev: () => boolean;
|
|
10
|
+
canScrollNext: () => boolean;
|
|
11
|
+
subscribe: (listener: () => void) => () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
orientation?: CarouselOrientation;
|
|
15
|
+
defaultIndex?: number;
|
|
16
|
+
index?: number;
|
|
17
|
+
onIndexChange?: (index: number) => void;
|
|
18
|
+
onApiChange?: (api: CarouselApi | null) => void;
|
|
19
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
20
|
+
}
|
|
21
|
+
export interface CarouselViewportProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
22
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
23
|
+
}
|
|
24
|
+
export interface CarouselTrackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
25
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
26
|
+
}
|
|
27
|
+
export interface CarouselSlideProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
28
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
29
|
+
}
|
|
30
|
+
export interface CarouselButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
31
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
32
|
+
}
|
|
33
|
+
export interface CarouselPageProps extends React.OutputHTMLAttributes<HTMLOutputElement> {
|
|
34
|
+
ref?: React.Ref<HTMLOutputElement>;
|
|
35
|
+
}
|
|
36
|
+
declare function CarouselRoot({ children, className, defaultIndex, index, onApiChange, onIndexChange, onKeyDown, orientation, ref, tabIndex, ...props }: CarouselProps): React.JSX.Element;
|
|
37
|
+
declare function CarouselViewport({ className, ref, ...props }: CarouselViewportProps): React.JSX.Element;
|
|
38
|
+
declare function CarouselTrack({ className, ref, ...props }: CarouselTrackProps): React.JSX.Element;
|
|
39
|
+
declare function CarouselSlide({ className, ref, ...props }: CarouselSlideProps): React.JSX.Element;
|
|
40
|
+
declare function CarouselPrevious({ className, disabled, onClick, ref, type, ...props }: CarouselButtonProps): React.JSX.Element;
|
|
41
|
+
declare function CarouselNext({ className, disabled, onClick, ref, type, ...props }: CarouselButtonProps): React.JSX.Element;
|
|
42
|
+
declare function CarouselPage({ children, className, ref, ...props }: CarouselPageProps): React.JSX.Element;
|
|
43
|
+
export declare const Carousel: typeof CarouselRoot & {
|
|
44
|
+
Next: typeof CarouselNext;
|
|
45
|
+
Page: typeof CarouselPage;
|
|
46
|
+
Previous: typeof CarouselPrevious;
|
|
47
|
+
Slide: typeof CarouselSlide;
|
|
48
|
+
Track: typeof CarouselTrack;
|
|
49
|
+
Viewport: typeof CarouselViewport;
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { composeEventHandlers } from "../../utils/compose-event-handlers.js";
|
|
5
|
+
import { useControllableState } from "../../hooks/use-controllable-state.js";
|
|
6
|
+
import { cn } from "../../utils/cn.js";
|
|
7
|
+
const CarouselContext = React.createContext(null);
|
|
8
|
+
function useCarouselContext(calledBy) {
|
|
9
|
+
const context = React.useContext(CarouselContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error(`"${calledBy}" must be used within <Carousel />`);
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
function normalizeIndex(value, fallback = 0) {
|
|
16
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
return Math.max(0, Math.floor(value));
|
|
20
|
+
}
|
|
21
|
+
function clampIndex(value, slideCount) {
|
|
22
|
+
if (slideCount <= 0) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
return Math.max(0, Math.min(normalizeIndex(value), slideCount - 1));
|
|
26
|
+
}
|
|
27
|
+
function setRef(ref, value) {
|
|
28
|
+
if (typeof ref === "function") {
|
|
29
|
+
ref(value);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (ref) {
|
|
33
|
+
ref.current = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function CarouselRoot({ children, className, defaultIndex = 0, index, onApiChange, onIndexChange, onKeyDown, orientation = "horizontal", ref, tabIndex, ...props }) {
|
|
37
|
+
const slideRegistry = React.useRef(new Map());
|
|
38
|
+
const listeners = React.useRef(new Set());
|
|
39
|
+
const [slideIds, setSlideIds] = React.useState([]);
|
|
40
|
+
const [currentIndex, setCurrentIndex] = useControllableState({
|
|
41
|
+
...(index !== undefined ? { value: normalizeIndex(index) } : {}),
|
|
42
|
+
defaultValue: normalizeIndex(defaultIndex),
|
|
43
|
+
...(onIndexChange ? { onChange: onIndexChange } : {})
|
|
44
|
+
});
|
|
45
|
+
const slideCount = slideIds.length;
|
|
46
|
+
const selectedIndex = clampIndex(currentIndex, slideCount);
|
|
47
|
+
const canScrollPrev = selectedIndex > 0;
|
|
48
|
+
const canScrollNext = selectedIndex < slideCount - 1;
|
|
49
|
+
const stateRef = React.useRef({ selectedIndex, slideCount });
|
|
50
|
+
React.useLayoutEffect(() => {
|
|
51
|
+
stateRef.current = { selectedIndex, slideCount };
|
|
52
|
+
}, [selectedIndex, slideCount]);
|
|
53
|
+
const scrollTo = React.useCallback((nextIndex) => {
|
|
54
|
+
setCurrentIndex(clampIndex(nextIndex, slideIds.length));
|
|
55
|
+
}, [setCurrentIndex, slideIds.length]);
|
|
56
|
+
const scrollPrev = React.useCallback(() => {
|
|
57
|
+
scrollTo(stateRef.current.selectedIndex - 1);
|
|
58
|
+
}, [scrollTo]);
|
|
59
|
+
const scrollNext = React.useCallback(() => {
|
|
60
|
+
scrollTo(stateRef.current.selectedIndex + 1);
|
|
61
|
+
}, [scrollTo]);
|
|
62
|
+
const registerSlide = React.useCallback((id, node) => {
|
|
63
|
+
slideRegistry.current.set(id, node);
|
|
64
|
+
setSlideIds((current) => (current.includes(id) ? current : [...current, id]));
|
|
65
|
+
}, []);
|
|
66
|
+
const unregisterSlide = React.useCallback((id) => {
|
|
67
|
+
slideRegistry.current.delete(id);
|
|
68
|
+
setSlideIds((current) => current.filter((slideId) => slideId !== id));
|
|
69
|
+
}, []);
|
|
70
|
+
const getSlideIndex = React.useCallback((id) => slideIds.indexOf(id), [slideIds]);
|
|
71
|
+
React.useLayoutEffect(() => {
|
|
72
|
+
const nextIndex = clampIndex(currentIndex, slideCount);
|
|
73
|
+
if (nextIndex !== currentIndex) {
|
|
74
|
+
setCurrentIndex(nextIndex);
|
|
75
|
+
}
|
|
76
|
+
}, [currentIndex, setCurrentIndex, slideCount]);
|
|
77
|
+
React.useLayoutEffect(() => {
|
|
78
|
+
const selectedSlideId = slideIds[selectedIndex];
|
|
79
|
+
const selectedSlide = selectedSlideId ? slideRegistry.current.get(selectedSlideId) : null;
|
|
80
|
+
if (typeof selectedSlide?.scrollIntoView === "function") {
|
|
81
|
+
selectedSlide.scrollIntoView({
|
|
82
|
+
behavior: "auto",
|
|
83
|
+
block: orientation === "vertical" ? "start" : "nearest",
|
|
84
|
+
inline: orientation === "horizontal" ? "start" : "nearest"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}, [orientation, selectedIndex, slideIds]);
|
|
88
|
+
React.useLayoutEffect(() => {
|
|
89
|
+
listeners.current.forEach((listener) => listener());
|
|
90
|
+
}, [selectedIndex, slideCount]);
|
|
91
|
+
const api = React.useMemo(() => ({
|
|
92
|
+
canScrollNext: () => stateRef.current.selectedIndex < stateRef.current.slideCount - 1,
|
|
93
|
+
canScrollPrev: () => stateRef.current.selectedIndex > 0,
|
|
94
|
+
scrollNext,
|
|
95
|
+
scrollPrev,
|
|
96
|
+
scrollTo,
|
|
97
|
+
selectedIndex: () => stateRef.current.selectedIndex,
|
|
98
|
+
slideCount: () => stateRef.current.slideCount,
|
|
99
|
+
subscribe(listener) {
|
|
100
|
+
listeners.current.add(listener);
|
|
101
|
+
return () => {
|
|
102
|
+
listeners.current.delete(listener);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}), [scrollNext, scrollPrev, scrollTo]);
|
|
106
|
+
React.useLayoutEffect(() => {
|
|
107
|
+
onApiChange?.(api);
|
|
108
|
+
return () => {
|
|
109
|
+
onApiChange?.(null);
|
|
110
|
+
};
|
|
111
|
+
}, [api, onApiChange]);
|
|
112
|
+
const handleKeyDown = React.useCallback((event) => {
|
|
113
|
+
const previousKey = orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
|
|
114
|
+
const nextKey = orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
|
|
115
|
+
if (event.key === previousKey) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
scrollPrev();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (event.key === nextKey) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
scrollNext();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (event.key === "Home") {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
scrollTo(0);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (event.key === "End") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
scrollTo(slideCount - 1);
|
|
133
|
+
}
|
|
134
|
+
}, [orientation, scrollNext, scrollPrev, scrollTo, slideCount]);
|
|
135
|
+
const context = React.useMemo(() => ({
|
|
136
|
+
canScrollNext,
|
|
137
|
+
canScrollPrev,
|
|
138
|
+
getSlideIndex,
|
|
139
|
+
orientation,
|
|
140
|
+
registerSlide,
|
|
141
|
+
scrollNext,
|
|
142
|
+
scrollPrev,
|
|
143
|
+
scrollTo,
|
|
144
|
+
selectedIndex,
|
|
145
|
+
slideCount,
|
|
146
|
+
unregisterSlide
|
|
147
|
+
}), [
|
|
148
|
+
canScrollNext,
|
|
149
|
+
canScrollPrev,
|
|
150
|
+
getSlideIndex,
|
|
151
|
+
orientation,
|
|
152
|
+
registerSlide,
|
|
153
|
+
scrollNext,
|
|
154
|
+
scrollPrev,
|
|
155
|
+
scrollTo,
|
|
156
|
+
selectedIndex,
|
|
157
|
+
slideCount,
|
|
158
|
+
unregisterSlide
|
|
159
|
+
]);
|
|
160
|
+
return (_jsx(CarouselContext.Provider, { value: context, children: _jsx("div", { ...props, ref: ref, "aria-roledescription": "carousel", className: cn("liano-carousel", className), "data-orientation": orientation, onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown), role: props.role ?? "region", tabIndex: tabIndex ?? 0, children: children }) }));
|
|
161
|
+
}
|
|
162
|
+
function CarouselViewport({ className, ref, ...props }) {
|
|
163
|
+
const context = useCarouselContext("Carousel.Viewport");
|
|
164
|
+
return (_jsx("div", { ...props, ref: ref, className: cn("liano-carousel__viewport", className), "data-orientation": context.orientation }));
|
|
165
|
+
}
|
|
166
|
+
function CarouselTrack({ className, ref, ...props }) {
|
|
167
|
+
const context = useCarouselContext("Carousel.Track");
|
|
168
|
+
return (_jsx("div", { ...props, ref: ref, className: cn("liano-carousel__track", className), "data-orientation": context.orientation }));
|
|
169
|
+
}
|
|
170
|
+
function CarouselSlide({ className, ref, ...props }) {
|
|
171
|
+
const context = useCarouselContext("Carousel.Slide");
|
|
172
|
+
const id = React.useId();
|
|
173
|
+
const slideRef = React.useRef(null);
|
|
174
|
+
const slideIndex = context.getSlideIndex(id);
|
|
175
|
+
const isSelected = slideIndex === context.selectedIndex;
|
|
176
|
+
const safeIndex = slideIndex === -1 ? 0 : slideIndex;
|
|
177
|
+
React.useLayoutEffect(() => {
|
|
178
|
+
context.registerSlide(id, slideRef.current);
|
|
179
|
+
return () => {
|
|
180
|
+
context.unregisterSlide(id);
|
|
181
|
+
};
|
|
182
|
+
}, [context.registerSlide, context.unregisterSlide, id]);
|
|
183
|
+
return (_jsx("div", { ...props, ref: (node) => {
|
|
184
|
+
slideRef.current = node;
|
|
185
|
+
setRef(ref, node);
|
|
186
|
+
}, "aria-label": props["aria-label"] ?? `${safeIndex + 1} of ${context.slideCount}`, "aria-roledescription": "slide", className: cn("liano-carousel__slide", className), "data-orientation": context.orientation, "data-selected": isSelected ? "true" : undefined, role: props.role ?? "group" }));
|
|
187
|
+
}
|
|
188
|
+
function CarouselPrevious({ className, disabled, onClick, ref, type = "button", ...props }) {
|
|
189
|
+
const context = useCarouselContext("Carousel.Previous");
|
|
190
|
+
const isDisabled = disabled === true || !context.canScrollPrev;
|
|
191
|
+
return (_jsx("button", { ...props, ref: ref, "aria-label": props["aria-label"] ?? "Previous slide", className: cn("liano-carousel__button liano-carousel__button--previous", className), "data-disabled": isDisabled ? "true" : undefined, disabled: isDisabled, onClick: composeEventHandlers(onClick, () => context.scrollPrev()), type: type }));
|
|
192
|
+
}
|
|
193
|
+
function CarouselNext({ className, disabled, onClick, ref, type = "button", ...props }) {
|
|
194
|
+
const context = useCarouselContext("Carousel.Next");
|
|
195
|
+
const isDisabled = disabled === true || !context.canScrollNext;
|
|
196
|
+
return (_jsx("button", { ...props, ref: ref, "aria-label": props["aria-label"] ?? "Next slide", className: cn("liano-carousel__button liano-carousel__button--next", className), "data-disabled": isDisabled ? "true" : undefined, disabled: isDisabled, onClick: composeEventHandlers(onClick, () => context.scrollNext()), type: type }));
|
|
197
|
+
}
|
|
198
|
+
function CarouselPage({ children, className, ref, ...props }) {
|
|
199
|
+
const context = useCarouselContext("Carousel.Page");
|
|
200
|
+
const label = context.slideCount === 0 ? "0 / 0" : `${context.selectedIndex + 1} / ${context.slideCount}`;
|
|
201
|
+
return (_jsx("output", { ...props, ref: ref, "aria-live": props["aria-live"] ?? "polite", className: cn("liano-carousel__page", className), children: children ?? label }));
|
|
202
|
+
}
|
|
203
|
+
export const Carousel = Object.assign(CarouselRoot, {
|
|
204
|
+
Next: CarouselNext,
|
|
205
|
+
Page: CarouselPage,
|
|
206
|
+
Previous: CarouselPrevious,
|
|
207
|
+
Slide: CarouselSlide,
|
|
208
|
+
Track: CarouselTrack,
|
|
209
|
+
Viewport: CarouselViewport
|
|
210
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface DatePickerProps {
|
|
3
|
+
label?: React.ReactNode;
|
|
4
|
+
description?: React.ReactNode;
|
|
5
|
+
error?: React.ReactNode;
|
|
6
|
+
value?: Date | null;
|
|
7
|
+
defaultValue?: Date | null;
|
|
8
|
+
onValueChange?: (date: Date | null) => void;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
formatDate?: (date: Date) => string;
|
|
11
|
+
minDate?: Date;
|
|
12
|
+
maxDate?: Date;
|
|
13
|
+
disabledDate?: (date: Date) => boolean;
|
|
14
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
15
|
+
}
|
|
16
|
+
export declare function DatePicker({ defaultValue, description, disabledDate, error, formatDate, label, maxDate, minDate, onValueChange, placeholder, ref, value }: DatePickerProps): React.JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Calendar } from "../calendar/calendar.js";
|
|
5
|
+
import { Field } from "../field/field.js";
|
|
6
|
+
import { Popover } from "../popover/popover.js";
|
|
7
|
+
import { cn } from "../../utils/cn.js";
|
|
8
|
+
function hasRenderableNode(value) {
|
|
9
|
+
return value !== undefined && value !== null && value !== false;
|
|
10
|
+
}
|
|
11
|
+
function joinIds(...ids) {
|
|
12
|
+
const value = ids.filter(Boolean).join(" ");
|
|
13
|
+
return value.length > 0 ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
function defaultFormatDate(date) {
|
|
16
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
17
|
+
day: "numeric",
|
|
18
|
+
month: "short",
|
|
19
|
+
year: "numeric"
|
|
20
|
+
}).format(date);
|
|
21
|
+
}
|
|
22
|
+
export function DatePicker({ defaultValue = null, description, disabledDate, error, formatDate = defaultFormatDate, label, maxDate, minDate, onValueChange, placeholder = "Select date", ref, value }) {
|
|
23
|
+
const generatedId = React.useId();
|
|
24
|
+
const triggerId = `${generatedId}-trigger`;
|
|
25
|
+
const descriptionId = hasRenderableNode(description) ? `${generatedId}-description` : undefined;
|
|
26
|
+
const errorId = hasRenderableNode(error) ? `${generatedId}-error` : undefined;
|
|
27
|
+
const hasLabel = hasRenderableNode(label);
|
|
28
|
+
const hasError = hasRenderableNode(error);
|
|
29
|
+
const isControlled = value !== undefined;
|
|
30
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
|
|
31
|
+
const [open, setOpen] = React.useState(false);
|
|
32
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
33
|
+
const describedBy = joinIds(descriptionId, errorId);
|
|
34
|
+
const calendarMonth = currentValue ?? minDate ?? undefined;
|
|
35
|
+
const fieldRefProps = ref !== undefined ? { ref } : {};
|
|
36
|
+
const calendarProps = {
|
|
37
|
+
...(calendarMonth !== undefined ? { defaultMonth: calendarMonth } : {}),
|
|
38
|
+
...(disabledDate !== undefined ? { disabledDate } : {}),
|
|
39
|
+
...(maxDate !== undefined ? { maxDate } : {}),
|
|
40
|
+
...(minDate !== undefined ? { minDate } : {})
|
|
41
|
+
};
|
|
42
|
+
const handleSelect = (date) => {
|
|
43
|
+
if (!isControlled) {
|
|
44
|
+
setUncontrolledValue(date);
|
|
45
|
+
}
|
|
46
|
+
onValueChange?.(date);
|
|
47
|
+
setOpen(false);
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs(Field, { className: "liano-date-picker", invalid: hasError, ...fieldRefProps, children: [hasLabel ? _jsx(Field.Label, { htmlFor: triggerId, children: label }) : null, _jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-start", children: [_jsx(Popover.Trigger, { "aria-describedby": describedBy, "aria-invalid": hasError ? true : undefined, "aria-label": hasLabel ? undefined : "Selected date", className: cn("liano-date-picker__trigger", !currentValue && "liano-date-picker__trigger--placeholder"), id: triggerId, children: _jsx("span", { className: "liano-date-picker__value", children: currentValue ? formatDate(currentValue) : placeholder }) }), _jsx(Popover.Content, { className: "liano-date-picker__content", matchTriggerWidth: false, sideOffset: 4, children: _jsx(Calendar, { ...calendarProps, selected: currentValue, onSelect: handleSelect }) })] }), hasRenderableNode(description) ? _jsx(Field.Description, { id: descriptionId, children: description }) : null, hasError ? _jsx(Field.Error, { id: errorId, children: error }) : null] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { FloatingPlacement } from "../../internal/floating/index.js";
|
|
3
|
+
import type { PortalProps } from "../../internal/portal/index.js";
|
|
4
|
+
export interface HoverCardProps {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
open?: boolean;
|
|
7
|
+
defaultOpen?: boolean;
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
openDelay?: number;
|
|
10
|
+
closeDelay?: number;
|
|
11
|
+
placement?: FloatingPlacement;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface HoverCardTriggerProps {
|
|
15
|
+
children: React.ReactElement;
|
|
16
|
+
}
|
|
17
|
+
export interface HoverCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
sideOffset?: number;
|
|
19
|
+
collisionPadding?: number;
|
|
20
|
+
forceMount?: boolean;
|
|
21
|
+
container?: PortalProps["container"];
|
|
22
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
23
|
+
}
|
|
24
|
+
declare function HoverCardRoot({ children, closeDelay, defaultOpen, disabled, onOpenChange, open: openProp, openDelay, placement }: HoverCardProps): React.JSX.Element;
|
|
25
|
+
declare function HoverCardTrigger({ children }: HoverCardTriggerProps): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
|
26
|
+
declare function HoverCardContent({ children, className, collisionPadding, container, forceMount, id, onBlur, onFocus, onMouseEnter, onMouseLeave, onPointerEnter, onPointerLeave, ref, sideOffset, style, ...props }: HoverCardContentProps): React.JSX.Element | null;
|
|
27
|
+
export declare const HoverCard: typeof HoverCardRoot & {
|
|
28
|
+
Trigger: typeof HoverCardTrigger;
|
|
29
|
+
Content: typeof HoverCardContent;
|
|
30
|
+
};
|
|
31
|
+
export {};
|