@tangle-network/ui 11.28.1 → 11.29.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 +12 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -2
- package/dist/lib/control-presentation.d.ts +10 -0
- package/dist/lib/control-presentation.js +1 -1
- package/dist/primitives/button.d.ts +1 -1
- package/dist/primitives/button.js +3 -2
- package/dist/primitives/calendar.d.ts +38 -0
- package/dist/primitives/calendar.js +186 -0
- package/dist/primitives/date-picker.d.ts +32 -0
- package/dist/primitives/date-picker.js +90 -0
- package/dist/primitives/input.js +1 -1
- package/dist/primitives/time-select.d.ts +27 -0
- package/dist/primitives/time-select.js +51 -0
- package/dist/primitives/toolbar.js +1 -1
- package/dist/primitives.d.ts +4 -1
- package/dist/primitives.js +5 -2
- package/package.json +2 -2
- package/src/lib/control-presentation.ts +3 -1
- package/src/primitives/button.tsx +9 -3
- package/src/primitives/calendar.tsx +242 -0
- package/src/primitives/control-presentation.test.tsx +9 -2
- package/src/primitives/date-picker.stories.tsx +55 -0
- package/src/primitives/date-picker.test.tsx +146 -0
- package/src/primitives/date-picker.tsx +126 -0
- package/src/primitives/index.ts +7 -0
- package/src/primitives/input.tsx +1 -1
- package/src/primitives/time-select.tsx +66 -0
- package/src/primitives/toolbar.tsx +3 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CalendarDays } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { fieldPresentation, fieldSizes, fieldTouchText, type ControlSize } from "../lib/control-presentation";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
import { Calendar, parseCalendarDate, type CalendarDate, type CalendarProps } from "./calendar";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
10
|
+
|
|
11
|
+
export interface DatePickerProps
|
|
12
|
+
extends Pick<CalendarProps, "min" | "max" | "isDateDisabled" | "today" | "weekStartsOn" | "locale" | "defaultMonth"> {
|
|
13
|
+
/** The chosen date, `YYYY-MM-DD`, or empty for none. */
|
|
14
|
+
value?: CalendarDate | null;
|
|
15
|
+
onChange?: (value: CalendarDate | "") => void;
|
|
16
|
+
/** Shown when no date is chosen. */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
size?: ControlSize;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Offer a Clear action while a date is chosen. Default true. */
|
|
21
|
+
clearable?: boolean;
|
|
22
|
+
/** How the chosen date reads on the trigger. */
|
|
23
|
+
formatOptions?: Intl.DateTimeFormatOptions;
|
|
24
|
+
id?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
"aria-label"?: string;
|
|
28
|
+
"aria-labelledby"?: string;
|
|
29
|
+
"aria-describedby"?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_FORMAT: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", year: "numeric" };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A date field: a trigger on the control scale that opens a Calendar. It reads
|
|
36
|
+
* like a Select (same height, well, border, focus ring), shows the date in the
|
|
37
|
+
* viewer's locale instead of a `mm/dd/yyyy` mask, and reports `YYYY-MM-DD`.
|
|
38
|
+
* With `name` it also submits that value in a form.
|
|
39
|
+
*/
|
|
40
|
+
const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
placeholder = "Pick a date",
|
|
46
|
+
size = "md",
|
|
47
|
+
disabled,
|
|
48
|
+
clearable = true,
|
|
49
|
+
formatOptions = DEFAULT_FORMAT,
|
|
50
|
+
id,
|
|
51
|
+
name,
|
|
52
|
+
className,
|
|
53
|
+
locale,
|
|
54
|
+
"aria-label": ariaLabel,
|
|
55
|
+
"aria-labelledby": ariaLabelledBy,
|
|
56
|
+
"aria-describedby": ariaDescribedBy,
|
|
57
|
+
...calendarProps
|
|
58
|
+
},
|
|
59
|
+
ref,
|
|
60
|
+
) => {
|
|
61
|
+
const [open, setOpen] = React.useState(false);
|
|
62
|
+
const selected = parseCalendarDate(value);
|
|
63
|
+
const label = selected === null
|
|
64
|
+
? null
|
|
65
|
+
: new Intl.DateTimeFormat(locale, { ...formatOptions, timeZone: "UTC" }).format(selected);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
69
|
+
<PopoverTrigger asChild>
|
|
70
|
+
<button
|
|
71
|
+
ref={ref}
|
|
72
|
+
id={id}
|
|
73
|
+
type="button"
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
aria-label={ariaLabel}
|
|
76
|
+
aria-labelledby={ariaLabelledBy}
|
|
77
|
+
aria-describedby={ariaDescribedBy}
|
|
78
|
+
aria-haspopup="dialog"
|
|
79
|
+
data-placeholder={selected === null ? "" : undefined}
|
|
80
|
+
className={cn(
|
|
81
|
+
"inline-flex w-full items-center justify-between gap-2 whitespace-nowrap border py-1 text-left shadow-sm disabled:cursor-not-allowed disabled:opacity-50",
|
|
82
|
+
fieldPresentation,
|
|
83
|
+
fieldTouchText,
|
|
84
|
+
fieldSizes[size],
|
|
85
|
+
"data-[placeholder]:text-[var(--text-dim)] data-[state=open]:border-primary",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<span className="min-w-0 truncate">{label ?? placeholder}</span>
|
|
90
|
+
<CalendarDays aria-hidden className="size-4 shrink-0 opacity-60" />
|
|
91
|
+
</button>
|
|
92
|
+
</PopoverTrigger>
|
|
93
|
+
{name && <input type="hidden" name={name} value={selected === null ? "" : (value as string)} />}
|
|
94
|
+
<PopoverContent align="start" className="w-auto p-3">
|
|
95
|
+
<Calendar
|
|
96
|
+
{...calendarProps}
|
|
97
|
+
locale={locale}
|
|
98
|
+
value={value}
|
|
99
|
+
onChange={(next) => {
|
|
100
|
+
onChange?.(next);
|
|
101
|
+
setOpen(false);
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
{clearable && selected !== null && (
|
|
105
|
+
<div className="mt-2 flex justify-end border-t border-border pt-2">
|
|
106
|
+
<Button
|
|
107
|
+
type="button"
|
|
108
|
+
variant="ghost"
|
|
109
|
+
size="sm"
|
|
110
|
+
onClick={() => {
|
|
111
|
+
onChange?.("");
|
|
112
|
+
setOpen(false);
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Clear
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</PopoverContent>
|
|
120
|
+
</Popover>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
DatePicker.displayName = "DatePicker";
|
|
125
|
+
|
|
126
|
+
export { DatePicker };
|
package/src/primitives/index.ts
CHANGED
|
@@ -68,6 +68,13 @@ export type { InputProps, TextareaProps } from "./input";
|
|
|
68
68
|
export { HelpText } from "./help-text";
|
|
69
69
|
export type { HelpTextProps } from "./help-text";
|
|
70
70
|
|
|
71
|
+
export { Calendar, formatCalendarDate, localToday, parseCalendarDate } from "./calendar";
|
|
72
|
+
export type { CalendarDate, CalendarProps } from "./calendar";
|
|
73
|
+
export { DatePicker } from "./date-picker";
|
|
74
|
+
export type { DatePickerProps } from "./date-picker";
|
|
75
|
+
export { TimeSelect } from "./time-select";
|
|
76
|
+
export type { TimeSelectProps } from "./time-select";
|
|
77
|
+
|
|
71
78
|
export { Badge, badgeVariants } from "./badge";
|
|
72
79
|
export type { BadgeProps } from "./badge";
|
|
73
80
|
|
package/src/primitives/input.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
13
13
|
// and a Button at the same size share a height, text size and corner.
|
|
14
14
|
const inputVariants = cva(
|
|
15
15
|
cn(
|
|
16
|
-
"flex w-full border py-1 placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium
|
|
16
|
+
"flex w-full border py-1 placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium",
|
|
17
17
|
fieldPresentation,
|
|
18
18
|
fieldTouchText,
|
|
19
19
|
),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import type { ControlSize } from "../lib/control-presentation";
|
|
5
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
6
|
+
|
|
7
|
+
const TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
8
|
+
|
|
9
|
+
export interface TimeSelectProps {
|
|
10
|
+
/** `HH:MM`, 24-hour, the value a native time input reports. */
|
|
11
|
+
value?: string;
|
|
12
|
+
onChange?: (value: string) => void;
|
|
13
|
+
/** Minutes between choices. Default 15. */
|
|
14
|
+
step?: number;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
size?: ControlSize;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
locale?: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
className?: string;
|
|
22
|
+
"aria-label"?: string;
|
|
23
|
+
"aria-labelledby"?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A time of day as a Select on the control scale: the day in `step`-minute
|
|
28
|
+
* slots, labelled in the viewer's locale (9:30 AM, 21:30). A value between
|
|
29
|
+
* slots stays listed so the current time is never lost.
|
|
30
|
+
*/
|
|
31
|
+
const TimeSelect = React.forwardRef<HTMLButtonElement, TimeSelectProps>(
|
|
32
|
+
({ value, onChange, step = 15, placeholder = "Pick a time", size = "md", disabled, locale, id, name, className, ...aria }, ref) => {
|
|
33
|
+
const format = React.useMemo(
|
|
34
|
+
() => new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone: "UTC" }),
|
|
35
|
+
[locale],
|
|
36
|
+
);
|
|
37
|
+
const options = React.useMemo(() => {
|
|
38
|
+
const values: string[] = [];
|
|
39
|
+
for (let minutes = 0; minutes < 24 * 60; minutes += Math.max(1, step)) {
|
|
40
|
+
values.push(`${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`);
|
|
41
|
+
}
|
|
42
|
+
if (value && TIME_PATTERN.test(value) && !values.includes(value)) values.push(value);
|
|
43
|
+
return values.sort();
|
|
44
|
+
}, [step, value]);
|
|
45
|
+
const label = (time: string) => {
|
|
46
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
47
|
+
return format.format(Date.UTC(1970, 0, 1, hours, minutes));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Select value={value && TIME_PATTERN.test(value) ? value : undefined} onValueChange={onChange} disabled={disabled} name={name}>
|
|
52
|
+
<SelectTrigger ref={ref} id={id} size={size} className={className} {...aria}>
|
|
53
|
+
<SelectValue placeholder={placeholder} />
|
|
54
|
+
</SelectTrigger>
|
|
55
|
+
<SelectContent className="max-h-72">
|
|
56
|
+
{options.map((time) => (
|
|
57
|
+
<SelectItem key={time} value={time}>{label(time)}</SelectItem>
|
|
58
|
+
))}
|
|
59
|
+
</SelectContent>
|
|
60
|
+
</Select>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
TimeSelect.displayName = "TimeSelect";
|
|
65
|
+
|
|
66
|
+
export { TimeSelect };
|
|
@@ -47,7 +47,9 @@ const Toolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
|
|
|
47
47
|
<div
|
|
48
48
|
ref={ref}
|
|
49
49
|
className={cn(
|
|
50
|
-
|
|
50
|
+
// Filters sit on the page canvas, where the recessed field well barely
|
|
51
|
+
// differs from the page. Raise every field inside onto the card surface.
|
|
52
|
+
"mb-4 flex flex-col gap-3 lg:flex-row lg:items-center [--field-surface:hsl(var(--card))]",
|
|
51
53
|
className,
|
|
52
54
|
)}
|
|
53
55
|
{...props}
|