@scalewing/react 1.8.0 → 1.10.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/CHANGELOG.md +153 -0
- package/README.md +93 -2
- package/dist/anchored-position.d.ts +28 -0
- package/dist/anchored-position.js +17 -0
- package/dist/calendar-button-labels.d.ts +19 -0
- package/dist/calendar-button-labels.js +10 -0
- package/dist/calendar-keys.d.ts +19 -0
- package/dist/calendar-keys.js +39 -0
- package/dist/calendar-labels.d.ts +19 -0
- package/dist/calendar-labels.js +41 -0
- package/dist/calendar-month.d.ts +38 -0
- package/dist/calendar-month.js +116 -0
- package/dist/calendar-years.d.ts +14 -0
- package/dist/calendar-years.js +20 -0
- package/dist/components/Accordion.d.ts +6 -1
- package/dist/components/Accordion.js +12 -3
- package/dist/components/ActionBar.d.ts +30 -0
- package/dist/components/ActionBar.js +13 -0
- package/dist/components/Box.d.ts +7 -3
- package/dist/components/Box.js +2 -2
- package/dist/components/CalendarButton.d.ts +43 -0
- package/dist/components/CalendarButton.js +47 -0
- package/dist/components/DateField.d.ts +15 -1
- package/dist/components/DateField.js +47 -18
- package/dist/components/Field.d.ts +5 -1
- package/dist/components/Field.js +16 -4
- package/dist/components/FieldAdornment.d.ts +20 -0
- package/dist/components/FieldAdornment.js +34 -0
- package/dist/components/Grid.d.ts +4 -1
- package/dist/components/Grid.js +2 -2
- package/dist/components/Progress.d.ts +3 -1
- package/dist/components/Progress.js +2 -2
- package/dist/components/date-field/CalendarDialog.d.ts +26 -0
- package/dist/components/date-field/CalendarDialog.js +93 -0
- package/dist/components/date-field/CalendarGrid.d.ts +23 -0
- package/dist/components/date-field/CalendarGrid.js +36 -0
- package/dist/components/date-field/CalendarHeader.d.ts +18 -0
- package/dist/components/date-field/CalendarHeader.js +33 -0
- package/dist/components/date-field/DateFieldGlyph.d.ts +9 -0
- package/dist/components/date-field/DateFieldGlyph.js +15 -0
- package/dist/components/date-field/use-anchored-popover.d.ts +8 -0
- package/dist/components/date-field/use-anchored-popover.js +61 -0
- package/dist/components/date-field/use-calendar-popup.d.ts +36 -0
- package/dist/components/date-field/use-calendar-popup.js +43 -0
- package/dist/components/date-field/use-date-entry.d.ts +20 -0
- package/dist/components/date-field/use-date-entry.js +61 -0
- package/dist/components/date-field/use-lang-locale.d.ts +7 -0
- package/dist/components/date-field/use-lang-locale.js +17 -0
- package/dist/css/css-accordion.js +55 -8
- package/dist/css/css-action-bar.d.ts +4 -0
- package/dist/css/css-action-bar.js +99 -0
- package/dist/css/css-action-menu.js +2 -1
- package/dist/css/css-box-border.d.ts +5 -0
- package/dist/css/css-box-border.js +24 -0
- package/dist/css/css-calendar-button.d.ts +2 -0
- package/dist/css/css-calendar-button.js +33 -0
- package/dist/css/css-chrome.js +2 -1
- package/dist/css/css-components.js +13 -6
- package/dist/css/css-data.js +2 -1
- package/dist/css/css-date-field.js +241 -11
- package/dist/css/css-denomination-grid.js +2 -1
- package/dist/css/css-document.d.ts +2 -0
- package/dist/css/css-document.js +2 -1
- package/dist/css/css-field.js +61 -1
- package/dist/css/css-select.js +2 -1
- package/dist/css/css-tooltip.js +2 -1
- package/dist/css/stacking.d.ts +10 -0
- package/dist/css/stacking.js +49 -0
- package/dist/css/stylesheet.js +6 -0
- package/dist/date-entry-validity.d.ts +10 -0
- package/dist/date-entry-validity.js +16 -0
- package/dist/date-entry.d.ts +32 -0
- package/dist/date-entry.js +106 -0
- package/dist/date-field-labels.d.ts +38 -0
- package/dist/date-field-labels.js +22 -0
- package/dist/date-field-locale.d.ts +8 -0
- package/dist/date-field-locale.js +27 -0
- package/dist/date-formatters.d.ts +13 -0
- package/dist/date-formatters.js +28 -0
- package/dist/date-only.d.ts +23 -0
- package/dist/date-only.js +63 -9
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/styles.css +410 -21
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { daysInMonth, formatDateOnly, isValidDateParts, parseDateOnly, } from './date-only.js';
|
|
2
|
+
/** Fails closed on a first weekday other than Sunday or Monday. */
|
|
3
|
+
export function assertWeekStart(weekStartsOn) {
|
|
4
|
+
if (weekStartsOn !== 0 && weekStartsOn !== 1)
|
|
5
|
+
throw new RangeError('weekStartsOn must be 0 or 1');
|
|
6
|
+
}
|
|
7
|
+
/*
|
|
8
|
+
* Day arithmetic runs on a proleptic Gregorian day count, so it never passes
|
|
9
|
+
* through a Date, a time zone, or a daylight-saving jump.
|
|
10
|
+
* Algorithm: Howard Hinnant, "chrono-compatible low-level date algorithms".
|
|
11
|
+
*/
|
|
12
|
+
function toDayNumber({ year, month, day }) {
|
|
13
|
+
const y = month <= 2 ? year - 1 : year;
|
|
14
|
+
const era = Math.floor(y / 400);
|
|
15
|
+
const yearOfEra = y - era * 400;
|
|
16
|
+
const monthIndex = (month + 9) % 12;
|
|
17
|
+
const dayOfYear = Math.floor((153 * monthIndex + 2) / 5) + day - 1;
|
|
18
|
+
const dayOfEra = yearOfEra * 365 +
|
|
19
|
+
Math.floor(yearOfEra / 4) -
|
|
20
|
+
Math.floor(yearOfEra / 100) +
|
|
21
|
+
dayOfYear;
|
|
22
|
+
return era * 146097 + dayOfEra - 719468;
|
|
23
|
+
}
|
|
24
|
+
function fromDayNumber(dayNumber) {
|
|
25
|
+
const z = dayNumber + 719468;
|
|
26
|
+
const era = Math.floor(z / 146097);
|
|
27
|
+
const dayOfEra = z - era * 146097;
|
|
28
|
+
const yearOfEra = Math.floor((dayOfEra -
|
|
29
|
+
Math.floor(dayOfEra / 1460) +
|
|
30
|
+
Math.floor(dayOfEra / 36524) -
|
|
31
|
+
Math.floor(dayOfEra / 146096)) /
|
|
32
|
+
365);
|
|
33
|
+
const dayOfYear = dayOfEra -
|
|
34
|
+
(365 * yearOfEra + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100));
|
|
35
|
+
const monthIndex = Math.floor((5 * dayOfYear + 2) / 153);
|
|
36
|
+
const day = dayOfYear - Math.floor((153 * monthIndex + 2) / 5) + 1;
|
|
37
|
+
const month = monthIndex < 10 ? monthIndex + 3 : monthIndex - 9;
|
|
38
|
+
const year = yearOfEra + era * 400 + (month <= 2 ? 1 : 0);
|
|
39
|
+
return { year, month, day };
|
|
40
|
+
}
|
|
41
|
+
function requireDate(value) {
|
|
42
|
+
const parts = parseDateOnly(value);
|
|
43
|
+
if (!parts)
|
|
44
|
+
throw new RangeError('expected a valid YYYY-MM-DD date');
|
|
45
|
+
return parts;
|
|
46
|
+
}
|
|
47
|
+
/** 0 is Sunday. */
|
|
48
|
+
export function weekdayOf(value) {
|
|
49
|
+
// Day 0 of the count is 1970-01-01, a Thursday.
|
|
50
|
+
return (((toDayNumber(requireDate(value)) + 4) % 7) + 7) % 7;
|
|
51
|
+
}
|
|
52
|
+
export function addDays(value, delta) {
|
|
53
|
+
return formatDateOnly(fromDayNumber(toDayNumber(requireDate(value)) + delta));
|
|
54
|
+
}
|
|
55
|
+
/** Same day of the month `delta` months away, capped at that month's length. */
|
|
56
|
+
export function addMonths(value, delta) {
|
|
57
|
+
const parts = requireDate(value);
|
|
58
|
+
const month = shiftMonth(parts, delta);
|
|
59
|
+
return formatDateOnly({
|
|
60
|
+
...month,
|
|
61
|
+
day: Math.min(parts.day, daysInMonth(month.year, month.month)),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function monthOf(value) {
|
|
65
|
+
const parts = requireDate(value);
|
|
66
|
+
return { year: parts.year, month: parts.month };
|
|
67
|
+
}
|
|
68
|
+
export function shiftMonth(month, delta) {
|
|
69
|
+
const index = month.year * 12 + (month.month - 1) + delta;
|
|
70
|
+
return { year: Math.floor(index / 12), month: (index % 12) + 1 };
|
|
71
|
+
}
|
|
72
|
+
export function firstOfMonth(month) {
|
|
73
|
+
return formatDateOnly({ ...month, day: 1 });
|
|
74
|
+
}
|
|
75
|
+
export function lastOfMonth(month) {
|
|
76
|
+
return formatDateOnly({
|
|
77
|
+
...month,
|
|
78
|
+
day: daysInMonth(month.year, month.month),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Whether any day of the month falls inside `[min, max]`. */
|
|
82
|
+
export function monthInRange(month, min, max) {
|
|
83
|
+
return ((min === undefined || lastOfMonth(month) >= min) &&
|
|
84
|
+
(max === undefined || firstOfMonth(month) <= max));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The months of `year` with any day inside `[min, max]`, 1–12, for a month
|
|
88
|
+
* selector that only offers months a person can open.
|
|
89
|
+
*/
|
|
90
|
+
export function monthsInRange(year, min, max) {
|
|
91
|
+
return Array.from({ length: 12 }, (_, index) => index + 1).filter((month) => monthInRange({ year, month }, min, max));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Six rows of seven cells covering the month, padded with the neighboring
|
|
95
|
+
* months so the grid never changes height. Padding before 0001-01-01 or
|
|
96
|
+
* after 9999-12-31 is `null`: no four-digit date exists there.
|
|
97
|
+
*/
|
|
98
|
+
export function monthGrid(month, weekStartsOn) {
|
|
99
|
+
const first = firstOfMonth(month);
|
|
100
|
+
const leading = (weekdayOf(first) - weekStartsOn + 7) % 7;
|
|
101
|
+
const start = toDayNumber(requireDate(first)) - leading;
|
|
102
|
+
return Array.from({ length: 6 }, (_, row) => Array.from({ length: 7 }, (_, column) => {
|
|
103
|
+
const parts = fromDayNumber(start + row * 7 + column);
|
|
104
|
+
if (!isValidDateParts(parts))
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
value: formatDateOnly(parts),
|
|
108
|
+
day: parts.day,
|
|
109
|
+
inMonth: parts.year === month.year && parts.month === month.month,
|
|
110
|
+
};
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/** Weekday indexes in display order for the given week start. */
|
|
114
|
+
export function weekdayOrder(weekStartsOn) {
|
|
115
|
+
return Array.from({ length: 7 }, (_, index) => (index + weekStartsOn) % 7);
|
|
116
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** How far the year list reaches past today and the shown month by default. */
|
|
2
|
+
export declare const YEARS_BACK = 120;
|
|
3
|
+
export declare const YEARS_AHEAD = 20;
|
|
4
|
+
export type YearRange = {
|
|
5
|
+
first: number;
|
|
6
|
+
last: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The years a calendar's year selector offers: 120 years before and 20
|
|
10
|
+
* after both today and the shown year, cut to `min` and `max`. The shown
|
|
11
|
+
* year is always inside, so the selector can display it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function calendarYearRange(shownYear: number, todayYear: number, min?: string, max?: string): YearRange;
|
|
14
|
+
export declare function yearsIn({ first, last }: YearRange): number[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parseDateOnly } from './date-only.js';
|
|
2
|
+
/** How far the year list reaches past today and the shown month by default. */
|
|
3
|
+
export const YEARS_BACK = 120;
|
|
4
|
+
export const YEARS_AHEAD = 20;
|
|
5
|
+
/**
|
|
6
|
+
* The years a calendar's year selector offers: 120 years before and 20
|
|
7
|
+
* after both today and the shown year, cut to `min` and `max`. The shown
|
|
8
|
+
* year is always inside, so the selector can display it.
|
|
9
|
+
*/
|
|
10
|
+
export function calendarYearRange(shownYear, todayYear, min, max) {
|
|
11
|
+
const minYear = min === undefined ? 1 : (parseDateOnly(min)?.year ?? 1);
|
|
12
|
+
const maxYear = max === undefined ? 9999 : (parseDateOnly(max)?.year ?? 9999);
|
|
13
|
+
return {
|
|
14
|
+
first: Math.max(minYear, 1, Math.min(shownYear, todayYear) - YEARS_BACK),
|
|
15
|
+
last: Math.min(maxYear, 9999, Math.max(shownYear, todayYear) + YEARS_AHEAD),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function yearsIn({ first, last }) {
|
|
19
|
+
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
20
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type DetailsHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
export type AccordionSize = 'sm' | 'md';
|
|
2
3
|
export type AccordionProps = Omit<DetailsHTMLAttributes<HTMLDetailsElement>, 'onToggle' | 'open' | 'title' | 'children'> & {
|
|
3
4
|
open: boolean;
|
|
4
5
|
onOpenChange: (open: boolean) => void;
|
|
5
6
|
title: string;
|
|
7
|
+
/** One muted line under the title, such as a summary of the content. */
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
/** `sm` is a quieter disclosure nested inside other content. */
|
|
10
|
+
size?: AccordionSize;
|
|
6
11
|
children: ReactNode;
|
|
7
12
|
};
|
|
8
|
-
export declare function Accordion({ children, className, onOpenChange, open, title, ...rest }: AccordionProps): import("react").JSX.Element;
|
|
13
|
+
export declare function Accordion({ children, className, onOpenChange, open, size, subtitle, title, ...rest }: AccordionProps): import("react").JSX.Element;
|
|
@@ -4,11 +4,20 @@ import { cx } from '../class-names.js';
|
|
|
4
4
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
5
5
|
import { Stack } from './Stack.js';
|
|
6
6
|
import { Text } from './Text.js';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const spaceBySize = { sm: 3, md: 4 };
|
|
8
|
+
/* The sm header is an sm control: a step of block padding over its generated
|
|
9
|
+
min-height instead of the full inset, so it sits lower than a section. */
|
|
10
|
+
const summarySpacingBySize = {
|
|
11
|
+
sm: { paddingX: 3, paddingY: 1 },
|
|
12
|
+
md: { padding: 4 },
|
|
13
|
+
};
|
|
14
|
+
const titleVariantBySize = { sm: 'label', md: 'title' };
|
|
15
|
+
export function Accordion({ children, className, onOpenChange, open, size = 'md', subtitle, title, ...rest }) {
|
|
16
|
+
const space = spaceBySize[size];
|
|
17
|
+
return (_jsxs("details", { ...rest, className: cx('sw-accordion', size === 'sm' && 'sw-accordion-sm', className), open: open, onToggle: (event) => {
|
|
9
18
|
const next = event.currentTarget.open;
|
|
10
19
|
if (next !== open) {
|
|
11
20
|
onOpenChange(next);
|
|
12
21
|
}
|
|
13
|
-
}, children: [
|
|
22
|
+
}, children: [_jsxs("summary", { className: cx('sw-accordion-summary', ...spacingClassNames(summarySpacingBySize[size])), children: [_jsxs("span", { className: "sw-accordion-heading", children: [_jsx(Text, { as: "span", variant: titleVariantBySize[size], children: title }), ' ', subtitle ? (_jsx(Text, { as: "span", color: "muted", variant: "caption", children: subtitle })) : null] }), _jsx("span", { "aria-hidden": "true", className: "sw-accordion-marker" })] }), _jsx(Stack, { gap: space, padding: space, paddingTop: 0, children: children })] }));
|
|
14
23
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../css/breakpoints.js';
|
|
3
|
+
export type ActionBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
4
|
+
/** The actions, usually two or three `Button`s. */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* One short line about the work, such as when it was last saved. It is a
|
|
8
|
+
* polite live region, so a new status is announced without moving focus.
|
|
9
|
+
*/
|
|
10
|
+
status?: string;
|
|
11
|
+
/** Sticky only below this breakpoint; from it up the bar sits in page flow. */
|
|
12
|
+
stickyBelow?: Breakpoint;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The actions for a long page on a glass bar that sticks to the bottom of
|
|
16
|
+
* the viewport while the content it follows scrolls by. Place it last in
|
|
17
|
+
* that content: it stays stuck while its parent is on screen and then rests
|
|
18
|
+
* in its own place at the end.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ActionBar: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
21
|
+
/** The actions, usually two or three `Button`s. */
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* One short line about the work, such as when it was last saved. It is a
|
|
25
|
+
* polite live region, so a new status is announced without moving focus.
|
|
26
|
+
*/
|
|
27
|
+
status?: string;
|
|
28
|
+
/** Sticky only below this breakpoint; from it up the bar sits in page flow. */
|
|
29
|
+
stickyBelow?: Breakpoint;
|
|
30
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { actionBarStickyClass } from '../css/css-action-bar.js';
|
|
5
|
+
/**
|
|
6
|
+
* The actions for a long page on a glass bar that sticks to the bottom of
|
|
7
|
+
* the viewport while the content it follows scrolls by. Place it last in
|
|
8
|
+
* that content: it stays stuck while its parent is on screen and then rests
|
|
9
|
+
* in its own place at the end.
|
|
10
|
+
*/
|
|
11
|
+
export const ActionBar = forwardRef(function ActionBar({ children, className, status, stickyBelow, ...rest }, ref) {
|
|
12
|
+
return (_jsxs("div", { ref: ref, className: cx('sw-action-bar', actionBarStickyClass(stickyBelow), className), ...rest, children: [_jsx("p", { className: "sw-action-bar-status", role: "status", children: status }), _jsx("div", { className: "sw-action-bar-actions", children: children })] }));
|
|
13
|
+
});
|
package/dist/components/Box.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RadiusStep, type SemanticColorKey } from '@scalewing/tokens';
|
|
2
2
|
import { type AnchorHTMLAttributes, type HTMLAttributes } from 'react';
|
|
3
|
+
import { type BoxBorderValue } from '../css/css-box-border.js';
|
|
3
4
|
import { type ColumnSpanProps } from '../column-span-classes.js';
|
|
4
5
|
import { type SpacingProps } from '../spacing-classes.js';
|
|
5
6
|
import { type VisibilityProps } from '../visibility-classes.js';
|
|
@@ -8,11 +9,14 @@ export type BoxProps = SpacingProps & VisibilityProps & ColumnSpanProps & HTMLAt
|
|
|
8
9
|
as?: BoxElement;
|
|
9
10
|
background?: Extract<SemanticColorKey, 'background' | 'surface'>;
|
|
10
11
|
radius?: RadiusStep;
|
|
11
|
-
border
|
|
12
|
+
/** A hairline token border: `true` is solid, `'dashed'` marks a space to fill in by hand. */
|
|
13
|
+
border?: BoxBorder;
|
|
12
14
|
};
|
|
13
|
-
export
|
|
15
|
+
export type BoxBorder = BoxBorderValue;
|
|
16
|
+
export declare const Box: import("react").ForwardRefExoticComponent<SpacingProps & VisibilityProps & ColumnSpanProps & HTMLAttributes<HTMLElement> & Pick<AnchorHTMLAttributes<HTMLAnchorElement>, "rel" | "download" | "href" | "target"> & {
|
|
14
17
|
as?: BoxElement;
|
|
15
18
|
background?: Extract<SemanticColorKey, "background" | "surface">;
|
|
16
19
|
radius?: RadiusStep;
|
|
17
|
-
border
|
|
20
|
+
/** A hairline token border: `true` is solid, `'dashed'` marks a space to fill in by hand. */
|
|
21
|
+
border?: BoxBorder;
|
|
18
22
|
} & import("react").RefAttributes<HTMLElement>>;
|
package/dist/components/Box.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, } from 'react';
|
|
3
3
|
import { cx } from '../class-names.js';
|
|
4
|
+
import { boxBorderClass } from '../css/css-box-border.js';
|
|
4
5
|
import { columnSpanClassNames, } from '../column-span-classes.js';
|
|
5
6
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
6
7
|
import { visibilityClassNames, } from '../visibility-classes.js';
|
|
@@ -14,11 +15,10 @@ export const Box = forwardRef(function Box({ as = 'div', background, border, cla
|
|
|
14
15
|
paddingTop,
|
|
15
16
|
paddingX,
|
|
16
17
|
paddingY,
|
|
17
|
-
}), ...visibilityClassNames({ hideBelow, hideFrom }), ...columnSpanClassNames({ columnSpan }), className), style: {
|
|
18
|
+
}), ...visibilityClassNames({ hideBelow, hideFrom }), ...columnSpanClassNames({ columnSpan }), boxBorderClass(border), className), style: {
|
|
18
19
|
backgroundColor: background
|
|
19
20
|
? `var(--sw-color-${background})`
|
|
20
21
|
: undefined,
|
|
21
|
-
border: border ? '1px solid var(--sw-color-border)' : undefined,
|
|
22
22
|
borderRadius: radius ? `var(--sw-radius-${radius})` : undefined,
|
|
23
23
|
...style,
|
|
24
24
|
}, ...rest }));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CalendarButtonLabels } from '../calendar-button-labels.js';
|
|
2
|
+
import { type WeekStart } from '../calendar-month.js';
|
|
3
|
+
import { type ButtonSize, type ButtonVariant } from './Button.js';
|
|
4
|
+
export type { CalendarButtonLabels };
|
|
5
|
+
export type CalendarButtonProps = {
|
|
6
|
+
/**
|
|
7
|
+
* What pressing it does, such as "Choose survey day". The button's name
|
|
8
|
+
* is this label, `labels.nameSeparator`, then `value` spoken in full; the
|
|
9
|
+
* calendar is named by the label.
|
|
10
|
+
*/
|
|
11
|
+
label: string;
|
|
12
|
+
/**
|
|
13
|
+
* Calendar date `YYYY-MM-DD`. The button always holds a date. A value
|
|
14
|
+
* outside `min`/`max` stays the value, as on DateField; the calendar then
|
|
15
|
+
* opens on the nearest allowed day.
|
|
16
|
+
*/
|
|
17
|
+
value: string;
|
|
18
|
+
/** Receives the chosen `YYYY-MM-DD`, never converted through UTC. */
|
|
19
|
+
onChange: (value: string) => void;
|
|
20
|
+
min?: string;
|
|
21
|
+
max?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** The `<button>` element's id. */
|
|
24
|
+
id?: string;
|
|
25
|
+
/**
|
|
26
|
+
* BCP 47 tag for the spoken date and the calendar's month and weekday
|
|
27
|
+
* names. Defaults to the nearest `lang` attribute, then en-US.
|
|
28
|
+
*/
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** First column of the calendar: 0 Sunday (default) or 1 Monday. */
|
|
31
|
+
weekStartsOn?: WeekStart;
|
|
32
|
+
/** The calendar's words and the name separator; English by default. */
|
|
33
|
+
labels?: Partial<CalendarButtonLabels>;
|
|
34
|
+
/** A Button size; a coarse pointer always gets a 44 px target. */
|
|
35
|
+
size?: ButtonSize;
|
|
36
|
+
variant?: ButtonVariant;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* An icon-only button that opens DateField's calendar dialog to pick a
|
|
40
|
+
* date for something the page already shows, such as a day heading. It has
|
|
41
|
+
* no text entry and no empty value. The ref reaches the `<button>`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const CalendarButton: import("react").ForwardRefExoticComponent<CalendarButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useId } from 'react';
|
|
4
|
+
import { resolveCalendarButtonLabels, } from '../calendar-button-labels.js';
|
|
5
|
+
import { calendarTriggerName } from '../calendar-labels.js';
|
|
6
|
+
import { assertWeekStart } from '../calendar-month.js';
|
|
7
|
+
import { assertDateBounds, assertDateOnly } from '../date-only.js';
|
|
8
|
+
import { Button } from './Button.js';
|
|
9
|
+
import { CalendarDialog } from './date-field/CalendarDialog.js';
|
|
10
|
+
import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
|
|
11
|
+
import { useCalendarPopup } from './date-field/use-calendar-popup.js';
|
|
12
|
+
import { useLangLocale } from './date-field/use-lang-locale.js';
|
|
13
|
+
function assertLabel(label) {
|
|
14
|
+
if (typeof label !== 'string' || label.trim() === '')
|
|
15
|
+
throw new RangeError('label must be non-empty text');
|
|
16
|
+
}
|
|
17
|
+
/** Sets a forwarded ref, callback or object, to the same node. */
|
|
18
|
+
function setForwardedRef(ref, node) {
|
|
19
|
+
if (typeof ref === 'function')
|
|
20
|
+
ref(node);
|
|
21
|
+
else if (ref)
|
|
22
|
+
ref.current = node;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An icon-only button that opens DateField's calendar dialog to pick a
|
|
26
|
+
* date for something the page already shows, such as a day heading. It has
|
|
27
|
+
* no text entry and no empty value. The ref reaches the `<button>`.
|
|
28
|
+
*/
|
|
29
|
+
export const CalendarButton = forwardRef(function CalendarButton({ label, value, onChange, min, max, disabled = false, id, locale, weekStartsOn = 0, labels, size = 'md', variant = 'ghost', }, ref) {
|
|
30
|
+
assertLabel(label);
|
|
31
|
+
assertDateOnly('value', value);
|
|
32
|
+
assertDateBounds(value, min, max);
|
|
33
|
+
assertWeekStart(weekStartsOn);
|
|
34
|
+
const words = resolveCalendarButtonLabels(labels);
|
|
35
|
+
const { buttonRef, shown, toggle, trigger, dialog } = useCalendarPopup({
|
|
36
|
+
value,
|
|
37
|
+
disabled,
|
|
38
|
+
onChange,
|
|
39
|
+
});
|
|
40
|
+
const resolvedLocale = useLangLocale(locale, buttonRef);
|
|
41
|
+
const labelId = useId();
|
|
42
|
+
const setButton = useCallback((node) => {
|
|
43
|
+
buttonRef.current = node;
|
|
44
|
+
setForwardedRef(ref, node);
|
|
45
|
+
}, [buttonRef, ref]);
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { ...trigger, className: "sw-calendar-button", disabled: disabled, id: id, onPress: toggle, ref: setButton, size: size, variant: variant, children: [_jsx("span", { className: "sw-sr-only", children: calendarTriggerName(label, value, resolvedLocale, words.nameSeparator) }), _jsx(DateFieldGlyph, { name: "calendar" })] }), shown ? (_jsxs(_Fragment, { children: [_jsx("span", { hidden: true, id: labelId, children: label }), _jsx(CalendarDialog, { ...dialog, anchorRef: buttonRef, buttonRef: buttonRef, labelId: labelId, labels: words.calendar, locale: resolvedLocale, max: max, min: min, required: true, value: value, weekStartsOn: weekStartsOn })] })) : null] }));
|
|
47
|
+
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { type WeekStart } from '../calendar-month.js';
|
|
2
|
+
import { type DateFieldLabels } from '../date-field-labels.js';
|
|
3
|
+
export type { DateFieldLabels, WeekStart };
|
|
1
4
|
export type DateFieldProps = {
|
|
2
5
|
label: string;
|
|
6
|
+
/** Calendar date `YYYY-MM-DD`, or '' for no date. */
|
|
3
7
|
value: string;
|
|
8
|
+
/** Receives `YYYY-MM-DD` or '', never converted through UTC. */
|
|
4
9
|
onChange: (value: string) => void;
|
|
5
10
|
min?: string;
|
|
6
11
|
max?: string;
|
|
@@ -8,5 +13,14 @@ export type DateFieldProps = {
|
|
|
8
13
|
required?: boolean;
|
|
9
14
|
description?: string;
|
|
10
15
|
error?: string;
|
|
16
|
+
/**
|
|
17
|
+
* BCP 47 tag for month and weekday names, spoken dates, and the typed
|
|
18
|
+
* field order. Defaults to the nearest `lang` attribute, then en-US.
|
|
19
|
+
*/
|
|
20
|
+
locale?: string;
|
|
21
|
+
/** First column of the calendar: 0 Sunday (default) or 1 Monday. */
|
|
22
|
+
weekStartsOn?: WeekStart;
|
|
23
|
+
/** The control's own words; English by default. */
|
|
24
|
+
labels?: Partial<DateFieldLabels>;
|
|
11
25
|
};
|
|
12
|
-
export declare function DateField({ label, value, onChange, min, max, disabled, required, description, error, }: DateFieldProps): import("react").JSX.Element;
|
|
26
|
+
export declare function DateField({ label, value, onChange, min, max, disabled, required, description, error, locale, weekStartsOn, labels, }: DateFieldProps): import("react").JSX.Element;
|
|
@@ -1,30 +1,59 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
import { useEffect, useId, useMemo, useRef } from 'react';
|
|
4
|
+
import { assertWeekStart } from '../calendar-month.js';
|
|
5
|
+
import { resolveDateFieldLabels, } from '../date-field-labels.js';
|
|
6
|
+
import { dateEntryPattern, dateEntryPlaceholder } from '../date-entry.js';
|
|
7
|
+
import { dateEntryValidity } from '../date-entry-validity.js';
|
|
8
|
+
import { assertDateBounds, isOutsideDateRange } from '../date-only.js';
|
|
9
|
+
import { CalendarDialog } from './date-field/CalendarDialog.js';
|
|
10
|
+
import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
|
|
11
|
+
import { useCalendarPopup } from './date-field/use-calendar-popup.js';
|
|
12
|
+
import { useDateEntry } from './date-field/use-date-entry.js';
|
|
13
|
+
import { useLangLocale } from './date-field/use-lang-locale.js';
|
|
14
|
+
export function DateField({ label, value, onChange, min, max, disabled = false, required = false, description, error, locale, weekStartsOn = 0, labels, }) {
|
|
15
|
+
assertDateBounds(value, min, max);
|
|
16
|
+
assertWeekStart(weekStartsOn);
|
|
17
|
+
const words = resolveDateFieldLabels(labels);
|
|
18
|
+
const rootRef = useRef(null);
|
|
19
|
+
const controlRef = useRef(null);
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
const resolvedLocale = useLangLocale(locale, rootRef);
|
|
22
|
+
const pattern = useMemo(() => dateEntryPattern(resolvedLocale), [resolvedLocale]);
|
|
23
|
+
const entry = useDateEntry(value, pattern, (next) => {
|
|
24
|
+
if (!disabled)
|
|
25
|
+
onChange(next);
|
|
26
|
+
});
|
|
27
|
+
const validity = dateEntryValidity(entry.parsed, words, min, max);
|
|
28
|
+
// Blocks form submission as the native date input's validity did.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
inputRef.current?.setCustomValidity(validity);
|
|
31
|
+
}, [validity]);
|
|
32
|
+
const { buttonRef, shown, toggle, trigger, dialog } = useCalendarPopup({
|
|
33
|
+
value,
|
|
34
|
+
disabled,
|
|
35
|
+
onChange,
|
|
36
|
+
beforeSelect: entry.reset,
|
|
37
|
+
});
|
|
13
38
|
const inputId = useId();
|
|
39
|
+
const labelId = useId();
|
|
14
40
|
const descriptionId = useId();
|
|
41
|
+
const entryErrorId = useId();
|
|
15
42
|
const errorId = useId();
|
|
16
43
|
const describedBy = [
|
|
17
44
|
description ? descriptionId : null,
|
|
45
|
+
entry.invalid ? entryErrorId : null,
|
|
18
46
|
error ? errorId : null,
|
|
19
47
|
]
|
|
20
48
|
.filter((id) => id !== null)
|
|
21
49
|
.join(' ');
|
|
22
|
-
const invalid = Boolean(error) || isOutsideDateRange(value, min, max);
|
|
23
|
-
return (_jsxs("div", { className: "sw-date-field", children: [_jsx("label", { className: "sw-date-field-label", htmlFor: inputId, children: label }), _jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": invalid, className: "sw-date-field-input", disabled: disabled, id: inputId,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
const invalid = Boolean(error) || entry.invalid || isOutsideDateRange(value, min, max);
|
|
51
|
+
return (_jsxs("div", { className: "sw-date-field", ref: rootRef, children: [_jsx("label", { className: "sw-date-field-label", htmlFor: inputId, id: labelId, children: label }), _jsxs("div", { className: "sw-date-field-control", ref: controlRef, children: [_jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": invalid, autoComplete: "off", className: "sw-date-field-input", disabled: disabled, id: inputId, onBlur: entry.check, onChange: (event) => entry.change(event.currentTarget.value), onKeyDown: (event) => {
|
|
52
|
+
if (event.key === 'Enter')
|
|
53
|
+
entry.check();
|
|
54
|
+
}, placeholder: dateEntryPlaceholder(pattern, {
|
|
55
|
+
day: words.dayPlaceholder,
|
|
56
|
+
month: words.monthPlaceholder,
|
|
57
|
+
year: words.yearPlaceholder,
|
|
58
|
+
}), ref: inputRef, required: required, spellCheck: false, type: "text", value: entry.text }), _jsxs("button", { ...trigger, "aria-describedby": labelId, className: "sw-date-field-button", disabled: disabled, onClick: toggle, ref: buttonRef, type: "button", children: [_jsx("span", { className: "sw-sr-only", children: words.chooseDate }), _jsx(DateFieldGlyph, { name: "calendar" })] })] }), description ? (_jsx("span", { className: "sw-date-field-description", id: descriptionId, children: description })) : null, entry.invalid ? (_jsx("span", { className: "sw-date-field-error", id: entryErrorId, children: words.invalidEntry })) : null, error ? (_jsx("span", { className: "sw-date-field-error", id: errorId, children: error })) : null, shown ? (_jsx(CalendarDialog, { ...dialog, buttonRef: buttonRef, anchorRef: controlRef, labelId: labelId, labels: words, locale: resolvedLocale, max: max, min: min, required: required, value: value, weekStartsOn: weekStartsOn })) : null] }));
|
|
30
59
|
}
|
|
@@ -10,5 +10,9 @@ export type FieldProps = {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
error?: string;
|
|
12
12
|
required?: boolean;
|
|
13
|
+
/** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/** Short text inside the control frame after the value, such as a unit. Not part of the value. */
|
|
16
|
+
suffix?: string;
|
|
13
17
|
};
|
|
14
|
-
export declare function Field({ children, gap, label, labelVisuallyHidden, size, description, error, required, }: FieldProps): import("react").JSX.Element;
|
|
18
|
+
export declare function Field({ children, gap, label, labelVisuallyHidden, size, description, error, required, prefix, suffix, }: FieldProps): import("react").JSX.Element;
|
package/dist/components/Field.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cloneElement, isValidElement, useId } from 'react';
|
|
4
|
+
import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
|
|
4
5
|
import { Stack } from './Stack.js';
|
|
5
6
|
import { Text } from './Text.js';
|
|
6
|
-
export function Field({ children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, required = false, }) {
|
|
7
|
+
export function Field({ children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, required = false, prefix, suffix, }) {
|
|
7
8
|
const controlId = useId();
|
|
8
9
|
const messageId = useId();
|
|
10
|
+
const labelId = useId();
|
|
11
|
+
const prefixId = useId();
|
|
12
|
+
const suffixId = useId();
|
|
9
13
|
const message = error || description;
|
|
10
|
-
const
|
|
14
|
+
const adorned = Boolean(prefix || suffix);
|
|
15
|
+
const needsControl = Boolean(message || required || adorned);
|
|
11
16
|
const className = [
|
|
12
17
|
'sw-field',
|
|
13
18
|
size === 'xs' && 'sw-field-xs',
|
|
@@ -20,11 +25,17 @@ export function Field({ children, gap = 1, label, labelVisuallyHidden = false, s
|
|
|
20
25
|
typeof children.type !== 'string' ||
|
|
21
26
|
!['input', 'select', 'textarea'].includes(children.type)) {
|
|
22
27
|
if (needsControl)
|
|
23
|
-
throw new TypeError('Field validation
|
|
28
|
+
throw new TypeError('Field validation and adornments require one native input, select, or textarea child');
|
|
24
29
|
return (_jsxs(Stack, { as: "label", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [labelText, children] }));
|
|
25
30
|
}
|
|
31
|
+
if (adorned && children.type !== 'input')
|
|
32
|
+
throw new TypeError('Field prefix and suffix require a native input child');
|
|
33
|
+
const adornment = adorned
|
|
34
|
+
? adornedControlAria(children.props, labelId, prefix ? prefixId : undefined, suffix ? suffixId : undefined)
|
|
35
|
+
: { labelledBy: children.props['aria-labelledby'] };
|
|
26
36
|
const describedBy = [
|
|
27
37
|
children.props['aria-describedby'],
|
|
38
|
+
adornment.describedBy,
|
|
28
39
|
message ? messageId : null,
|
|
29
40
|
]
|
|
30
41
|
.filter(Boolean)
|
|
@@ -33,7 +44,8 @@ export function Field({ children, gap = 1, label, labelVisuallyHidden = false, s
|
|
|
33
44
|
id: children.props.id || controlId,
|
|
34
45
|
'aria-describedby': describedBy || undefined,
|
|
35
46
|
'aria-invalid': error ? true : children.props['aria-invalid'],
|
|
47
|
+
'aria-labelledby': adornment.labelledBy,
|
|
36
48
|
required: required || children.props.required,
|
|
37
49
|
});
|
|
38
|
-
return (_jsxs(Stack, { as: "div", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [_jsx("label", { htmlFor: children.props.id || controlId, children: labelText }), control, message ? (_jsx("span", { className: error ? 'sw-field-error' : 'sw-field-description', id: messageId, role: error ? 'alert' : undefined, children: message })) : null] }));
|
|
50
|
+
return (_jsxs(Stack, { as: "div", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [_jsx("label", { htmlFor: children.props.id || controlId, id: labelId, children: labelText }), adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), message ? (_jsx("span", { className: error ? 'sw-field-error' : 'sw-field-description', id: messageId, role: error ? 'alert' : undefined, children: message })) : null] }));
|
|
39
51
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type FieldAdornmentProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
prefix?: string;
|
|
5
|
+
prefixId: string;
|
|
6
|
+
suffix?: string;
|
|
7
|
+
suffixId: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function FieldAdornment({ children, prefix, prefixId, suffix, suffixId, }: FieldAdornmentProps): import("react").JSX.Element;
|
|
10
|
+
export type AdornedControlNaming = {
|
|
11
|
+
'aria-label'?: string;
|
|
12
|
+
'aria-labelledby'?: string;
|
|
13
|
+
};
|
|
14
|
+
export type AdornedControlAria = {
|
|
15
|
+
/** The input's `aria-labelledby`; undefined keeps the input's own name. */
|
|
16
|
+
labelledBy?: string;
|
|
17
|
+
/** Adornment ids to add to the input's `aria-describedby`, if any. */
|
|
18
|
+
describedBy?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function adornedControlAria(own: AdornedControlNaming, labelId: string, prefixId: string | undefined, suffixId: string | undefined): AdornedControlAria;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* A press on the frame's own chrome (the prefix, the suffix or the padding)
|
|
4
|
+
* focuses the input, as the text cursor over the frame promises. A press on
|
|
5
|
+
* the input itself keeps the browser's own caret placement and selection.
|
|
6
|
+
*/
|
|
7
|
+
function focusAdornedInput(event) {
|
|
8
|
+
const input = event.currentTarget.querySelector('input');
|
|
9
|
+
if (!input || input.disabled || event.target === input)
|
|
10
|
+
return;
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
input.focus();
|
|
13
|
+
}
|
|
14
|
+
/*
|
|
15
|
+
* Draws the control frame around a native input with short text before or
|
|
16
|
+
* after it. The text is hidden from the reading order so it is not read twice:
|
|
17
|
+
* the input names or describes itself with this text by id (see
|
|
18
|
+
* `adornedControlAria`), which still reads a hidden node it references directly.
|
|
19
|
+
*/
|
|
20
|
+
export function FieldAdornment({ children, prefix, prefixId, suffix, suffixId, }) {
|
|
21
|
+
return (_jsxs("span", { className: "sw-field-adorned", onMouseDown: focusAdornedInput, children: [prefix ? (_jsx("span", { "aria-hidden": "true", className: "sw-field-prefix", id: prefixId, children: prefix })) : null, children, suffix ? (_jsx("span", { "aria-hidden": "true", className: "sw-field-suffix", id: suffixId, children: suffix })) : null] }));
|
|
22
|
+
}
|
|
23
|
+
/*
|
|
24
|
+
* An adorned input is named by its label plus the adornment ("Drop amount $").
|
|
25
|
+
* When the input already names itself with `aria-label` or `aria-labelledby`,
|
|
26
|
+
* that name stands and the adornment describes the input instead, so the unit
|
|
27
|
+
* is still read.
|
|
28
|
+
*/
|
|
29
|
+
export function adornedControlAria(own, labelId, prefixId, suffixId) {
|
|
30
|
+
const adornment = [prefixId, suffixId].filter(Boolean).join(' ');
|
|
31
|
+
if (own['aria-label'] || own['aria-labelledby'])
|
|
32
|
+
return { labelledBy: own['aria-labelledby'], describedBy: adornment };
|
|
33
|
+
return { labelledBy: [labelId, adornment].join(' ') };
|
|
34
|
+
}
|
|
@@ -2,10 +2,13 @@ import { type SpacingStep } from '@scalewing/tokens';
|
|
|
2
2
|
import { type Breakpoint } from '../css/breakpoints.js';
|
|
3
3
|
import { type GridColumns } from '../css/css-grid.js';
|
|
4
4
|
import { type BoxProps } from './Box.js';
|
|
5
|
+
import { type Align } from './Stack.js';
|
|
5
6
|
export type { GridColumns };
|
|
6
7
|
export type GridProps = BoxProps & {
|
|
8
|
+
/** Where each child sits in the height of its row; unset, children stretch. */
|
|
9
|
+
align?: Align;
|
|
7
10
|
columns?: GridColumns;
|
|
8
11
|
columnsBelow?: Partial<Record<Breakpoint, GridColumns>>;
|
|
9
12
|
gap?: SpacingStep;
|
|
10
13
|
};
|
|
11
|
-
export declare function Grid({ className, columns, columnsBelow, gap, ...rest }: GridProps): import("react").JSX.Element;
|
|
14
|
+
export declare function Grid({ align, className, columns, columnsBelow, gap, ...rest }: GridProps): import("react").JSX.Element;
|