@tangle-network/ui 11.28.2 → 11.30.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.
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
3
+ import * as React from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/primitives/time-select.tsx
6
+ const TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;
7
+ /**
8
+ * A time of day as a Select on the control scale: the day in `step`-minute
9
+ * slots, labelled in the viewer's locale (9:30 AM, 21:30). A value between
10
+ * slots stays listed so the current time is never lost.
11
+ */
12
+ const TimeSelect = React.forwardRef(({ value, onChange, step = 15, placeholder = "Pick a time", size = "md", disabled, locale, id, name, className, ...aria }, ref) => {
13
+ const format = React.useMemo(() => new Intl.DateTimeFormat(locale, {
14
+ hour: "numeric",
15
+ minute: "2-digit",
16
+ timeZone: "UTC"
17
+ }), [locale]);
18
+ const options = React.useMemo(() => {
19
+ const values = [];
20
+ for (let minutes = 0; minutes < 1440; minutes += Math.max(1, step)) values.push(`${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`);
21
+ if (value && TIME_PATTERN.test(value) && !values.includes(value)) values.push(value);
22
+ return values.sort();
23
+ }, [step, value]);
24
+ const label = (time) => {
25
+ const [hours, minutes] = time.split(":").map(Number);
26
+ return format.format(Date.UTC(1970, 0, 1, hours, minutes));
27
+ };
28
+ return /* @__PURE__ */ jsxs(Select, {
29
+ value: value && TIME_PATTERN.test(value) ? value : void 0,
30
+ onValueChange: onChange,
31
+ disabled,
32
+ name,
33
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
34
+ ref,
35
+ id,
36
+ size,
37
+ className,
38
+ ...aria,
39
+ children: /* @__PURE__ */ jsx(SelectValue, { placeholder })
40
+ }), /* @__PURE__ */ jsx(SelectContent, {
41
+ className: "max-h-72",
42
+ children: options.map((time) => /* @__PURE__ */ jsx(SelectItem, {
43
+ value: time,
44
+ children: label(time)
45
+ }, time))
46
+ })]
47
+ });
48
+ });
49
+ TimeSelect.displayName = "TimeSelect";
50
+ //#endregion
51
+ export { TimeSelect };
@@ -4,7 +4,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/primitives/toolbar.tsx
5
5
  const Toolbar = React.forwardRef(({ className, search, filters, actions, ...props }, ref) => /* @__PURE__ */ jsxs("div", {
6
6
  ref,
7
- className: cn("mb-4 flex flex-col gap-3 lg:flex-row lg:items-center", className),
7
+ className: cn("mb-4 flex flex-col gap-3 lg:flex-row lg:items-center [--field-surface:hsl(var(--card))]", className),
8
8
  ...props,
9
9
  children: [
10
10
  search && /* @__PURE__ */ jsx("div", {
@@ -11,6 +11,9 @@ import { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger }
11
11
  import { Slider } from "./primitives/slider.js";
12
12
  import { Input, InputProps, Textarea, TextareaProps } from "./primitives/input.js";
13
13
  import { HelpText, HelpTextProps } from "./primitives/help-text.js";
14
+ import { Calendar, CalendarDate, CalendarProps, formatCalendarDate, localToday, parseCalendarDate } from "./primitives/calendar.js";
15
+ import { DatePicker, DatePickerProps } from "./primitives/date-picker.js";
16
+ import { TimeSelect, TimeSelectProps } from "./primitives/time-select.js";
14
17
  import { Badge, BadgeProps, badgeVariants } from "./primitives/badge.js";
15
18
  import { Avatar, AvatarFallback, AvatarImage } from "./primitives/avatar.js";
16
19
  import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./primitives/dropdown-menu.js";
@@ -21,6 +24,7 @@ import { SegmentedControl, SegmentedControlOption, SegmentedControlProps } from
21
24
  import { Progress } from "./primitives/progress.js";
22
25
  import { Switch } from "./primitives/switch.js";
23
26
  import { Checkbox } from "./primitives/checkbox.js";
27
+ import { RadioGroup, RadioGroupItem } from "./primitives/radio-group.js";
24
28
  import { Separator } from "./primitives/separator.js";
25
29
  import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger } from "./primitives/sheet.js";
26
30
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/tooltip.js";
@@ -48,4 +52,4 @@ import { Chip, ChipProps } from "./primitives/chip.js";
48
52
  import { Metric, MetricProps, MetricStrip, MetricStripProps } from "./primitives/metric-strip.js";
49
53
  import { FilterField, FilterFieldProps, Toolbar, ToolbarProps } from "./primitives/toolbar.js";
50
54
  import { Logo, LogoProps, TangleKnot } from "@tangle-network/brand";
51
- export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, CATEGORY_TONES, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, badgeVariants, buttonVariants, initialsOf, toneFor, useTheme, useToast };
55
+ export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, CATEGORY_TONES, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, badgeVariants, buttonVariants, formatCalendarDate, initialsOf, localToday, parseCalendarDate, toneFor, useTheme, useToast };
@@ -10,16 +10,20 @@ import { Slider } from "./primitives/slider.js";
10
10
  import { HelpText } from "./primitives/help-text.js";
11
11
  import { Label } from "./primitives/label.js";
12
12
  import { Input, Textarea } from "./primitives/input.js";
13
+ import { Calendar, formatCalendarDate, localToday, parseCalendarDate } from "./primitives/calendar.js";
14
+ import { DatePicker } from "./primitives/date-picker.js";
15
+ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "./primitives/select.js";
16
+ import { TimeSelect } from "./primitives/time-select.js";
13
17
  import { Badge, badgeVariants } from "./primitives/badge.js";
14
18
  import { Avatar, AvatarFallback, AvatarImage } from "./primitives/avatar.js";
15
19
  import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./primitives/dropdown-menu.js";
16
- import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "./primitives/select.js";
17
20
  import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "./primitives/table.js";
18
21
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/tabs.js";
19
22
  import { SegmentedControl } from "./primitives/segmented-control.js";
20
23
  import { Progress } from "./primitives/progress.js";
21
24
  import { Switch } from "./primitives/switch.js";
22
25
  import { Checkbox } from "./primitives/checkbox.js";
26
+ import { RadioGroup, RadioGroupItem } from "./primitives/radio-group.js";
23
27
  import { Separator } from "./primitives/separator.js";
24
28
  import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger } from "./primitives/sheet.js";
25
29
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/tooltip.js";
@@ -48,4 +52,4 @@ import { IconTile, initialsOf } from "./primitives/icon-tile.js";
48
52
  import { Metric, MetricStrip } from "./primitives/metric-strip.js";
49
53
  import { FilterField, Toolbar } from "./primitives/toolbar.js";
50
54
  import { Logo, TangleKnot } from "@tangle-network/brand";
51
- export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, FilterField, Heading, HelpText, IconTile, InlineCode, Input, Label, Logo, Metric, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, badgeVariants, buttonVariants, initialsOf, toneFor, useTheme, useToast };
55
+ export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, FilterField, Heading, HelpText, IconTile, InlineCode, Input, Label, Logo, Metric, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, TimeSelect, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, badgeVariants, buttonVariants, formatCalendarDate, initialsOf, localToday, parseCalendarDate, toneFor, useTheme, useToast };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.28.2",
3
+ "version": "11.30.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -126,6 +126,7 @@
126
126
  "@radix-ui/react-label": "^2.1.15",
127
127
  "@radix-ui/react-popover": "^1.1.23",
128
128
  "@radix-ui/react-progress": "^1.1.16",
129
+ "@radix-ui/react-radio-group": "^1.4.8",
129
130
  "@radix-ui/react-select": "^2.3.7",
130
131
  "@radix-ui/react-separator": "^1.1.15",
131
132
  "@radix-ui/react-slider": "^1.4.7",
@@ -149,7 +150,7 @@
149
150
  "peerDependencies": {
150
151
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
151
152
  "@nanostores/react": ">=0.8.4 <3.0.0",
152
- "@tangle-network/brand": "^1.18.0",
153
+ "@tangle-network/brand": "^1.19.2",
153
154
  "@tiptap/core": ">=3.20.4 <4.0.0",
154
155
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
155
156
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -64,10 +64,12 @@ export const controlSizes = {
64
64
  // resolves the recessed well inside every canonical/named light/dark scope.
65
65
  // An autofill inset shadow coexists with the Tailwind focus ring; never hide
66
66
  // autofill with a seconds-long background transition or clear the focus halo.
67
+ // --field-surface lets a context raise its fields: Toolbar sets it to the card
68
+ // surface so filters on the page canvas read as controls, not as holes in it.
67
69
  export const fieldPresentation = cn(
68
70
  focusField,
69
71
  controlMotion,
70
- "min-w-0 bg-[var(--bg-input)] text-foreground",
72
+ "min-w-0 bg-[var(--field-surface,var(--bg-input))] text-foreground",
71
73
  "aria-invalid:border-[var(--surface-danger-border)] aria-invalid:hover:border-[var(--surface-danger-border)] aria-invalid:focus:border-[var(--focus-border-danger)] aria-invalid:focus:ring-[var(--focus-halo-danger)]",
72
74
  "autofill:shadow-[inset_0_0_0_1000px_var(--bg-input)] autofill:[-webkit-text-fill-color:var(--text-primary)] autofill:caret-[var(--text-primary)]",
73
75
  );
@@ -46,9 +46,15 @@ const buttonVariants = cva(
46
46
  },
47
47
  );
48
48
 
49
+ // `bare` keeps the shared focus ring, disabled and loading behavior and drops
50
+ // every surface, size and type class. It is for pressable rows, tiles and pills
51
+ // that draw their own layout; anything that reads as a button uses a styled
52
+ // variant and a size.
53
+ const bareButton = `${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50`;
54
+
49
55
  export interface ButtonProps
50
56
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
51
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "sandbox" | null;
57
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "sandbox" | "bare" | null;
52
58
  /** sm 32px, default/md 36px (`--control-height`), lg 44px; icon sizes are squares on the same heights. */
53
59
  size?: "default" | "md" | "sm" | "lg" | "xl" | "icon" | "icon-sm" | "icon-lg" | "compact" | "touch" | null;
54
60
  asChild?: boolean;
@@ -109,7 +115,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
109
115
  // Keep slotted content intact: its action name must survive loading too.
110
116
  return (
111
117
  <Slot
112
- className={cn(buttonVariants({ variant, size, className }))}
118
+ className={variant === "bare" ? cn(bareButton, className) : cn(buttonVariants({ variant, size, className }))}
113
119
  ref={ref}
114
120
  {...props}
115
121
  {...(isDisabled ? disabledHandlers(props) : {})}
@@ -134,7 +140,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
134
140
 
135
141
  return (
136
142
  <button
137
- className={cn(buttonVariants({ variant, size, className }))}
143
+ className={variant === "bare" ? cn(bareButton, className) : cn(buttonVariants({ variant, size, className }))}
138
144
  ref={ref}
139
145
  {...props}
140
146
  disabled={disabled || loading}
@@ -0,0 +1,242 @@
1
+ "use client";
2
+
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+ import * as React from "react";
5
+ import { focusRing } from "../lib/focus";
6
+ import { cn } from "../lib/utils";
7
+ import { Button } from "./button";
8
+
9
+ /**
10
+ * Calendar dates are plain `YYYY-MM-DD` strings, the same value a native date
11
+ * input reports. All arithmetic runs in UTC so a date never shifts with the
12
+ * viewer's time zone or a daylight-saving boundary.
13
+ */
14
+ export type CalendarDate = string;
15
+
16
+ const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
17
+ const DAY_MS = 86_400_000;
18
+
19
+ export function parseCalendarDate(value: string | null | undefined): number | null {
20
+ const match = value ? DATE_PATTERN.exec(value) : null;
21
+ if (!match) return null;
22
+ const time = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
23
+ const date = new Date(time);
24
+ // Reject impossible dates such as 2026-02-31 rather than rolling them over.
25
+ return date.getUTCMonth() === Number(match[2]) - 1 ? time : null;
26
+ }
27
+
28
+ export function formatCalendarDate(time: number): CalendarDate {
29
+ const date = new Date(time);
30
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
31
+ const day = String(date.getUTCDate()).padStart(2, "0");
32
+ return `${date.getUTCFullYear()}-${month}-${day}`;
33
+ }
34
+
35
+ /** Today in the viewer's local calendar, as a `YYYY-MM-DD` string. */
36
+ export function localToday(now = new Date()): CalendarDate {
37
+ return formatCalendarDate(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
38
+ }
39
+
40
+ function firstOfMonth(time: number): number {
41
+ const date = new Date(time);
42
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
43
+ }
44
+
45
+ function addMonths(time: number, delta: number): number {
46
+ const date = new Date(time);
47
+ const target = Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + delta, 1);
48
+ const lastDay = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + delta + 1, 0)).getUTCDate();
49
+ const day = Math.min(date.getUTCDate(), lastDay);
50
+ return target + (day - 1) * DAY_MS;
51
+ }
52
+
53
+ export interface CalendarProps {
54
+ /** The selected date, `YYYY-MM-DD`. */
55
+ value?: CalendarDate | null;
56
+ onChange?: (value: CalendarDate) => void;
57
+ /** The month shown first (any date inside it). Defaults to the value, else today. */
58
+ defaultMonth?: CalendarDate;
59
+ /** Earliest and latest selectable dates, inclusive. */
60
+ min?: CalendarDate;
61
+ max?: CalendarDate;
62
+ isDateDisabled?: (value: CalendarDate) => boolean;
63
+ /** The date marked as today. Pass it to use a workspace time zone instead of the viewer's. */
64
+ today?: CalendarDate;
65
+ /** 0 starts weeks on Sunday, 1 on Monday. */
66
+ weekStartsOn?: 0 | 1;
67
+ locale?: string;
68
+ className?: string;
69
+ "aria-label"?: string;
70
+ }
71
+
72
+ /**
73
+ * A month grid that picks one date. Arrow keys move by day and week, Page Up
74
+ * and Page Down by month, Home and End to the week's ends; Enter or Space picks
75
+ * the focused date. Six weeks always render so the grid never changes height.
76
+ */
77
+ const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
78
+ (
79
+ {
80
+ value,
81
+ onChange,
82
+ defaultMonth,
83
+ min,
84
+ max,
85
+ isDateDisabled,
86
+ today: todayProp,
87
+ weekStartsOn = 0,
88
+ locale,
89
+ className,
90
+ "aria-label": ariaLabel,
91
+ },
92
+ ref,
93
+ ) => {
94
+ const today = parseCalendarDate(todayProp) ?? (parseCalendarDate(localToday()) as number);
95
+ const selected = parseCalendarDate(value);
96
+ const minTime = parseCalendarDate(min);
97
+ const maxTime = parseCalendarDate(max);
98
+ const [focused, setFocused] = React.useState<number>(
99
+ () => selected ?? parseCalendarDate(defaultMonth) ?? today,
100
+ );
101
+ const viewMonth = firstOfMonth(focused);
102
+ const gridRef = React.useRef<HTMLDivElement>(null);
103
+ const moveFocusRef = React.useRef(false);
104
+
105
+ React.useEffect(() => {
106
+ if (selected !== null) setFocused(selected);
107
+ }, [selected]);
108
+
109
+ React.useEffect(() => {
110
+ if (!moveFocusRef.current) return;
111
+ moveFocusRef.current = false;
112
+ gridRef.current?.querySelector<HTMLButtonElement>('[data-focused="true"]')?.focus();
113
+ }, [focused]);
114
+
115
+ const disabled = (time: number) =>
116
+ (minTime !== null && time < minTime) ||
117
+ (maxTime !== null && time > maxTime) ||
118
+ Boolean(isDateDisabled?.(formatCalendarDate(time)));
119
+
120
+ const leading = (new Date(viewMonth).getUTCDay() - weekStartsOn + 7) % 7;
121
+ const start = viewMonth - leading * DAY_MS;
122
+ const days = Array.from({ length: 42 }, (_, index) => start + index * DAY_MS);
123
+ const weeks = Array.from({ length: 6 }, (_, row) => days.slice(row * 7, row * 7 + 7));
124
+
125
+ const monthLabel = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(viewMonth);
126
+ const dayLabel = new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
127
+ const weekdayShort = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" });
128
+ const weekdayLong = new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC" });
129
+ const weekdays = days.slice(0, 7);
130
+
131
+ const move = (next: number) => {
132
+ moveFocusRef.current = true;
133
+ setFocused(next);
134
+ };
135
+
136
+ const onKeyDown = (event: React.KeyboardEvent) => {
137
+ const weekday = (new Date(focused).getUTCDay() - weekStartsOn + 7) % 7;
138
+ const steps: Record<string, () => number> = {
139
+ ArrowLeft: () => focused - DAY_MS,
140
+ ArrowRight: () => focused + DAY_MS,
141
+ ArrowUp: () => focused - 7 * DAY_MS,
142
+ ArrowDown: () => focused + 7 * DAY_MS,
143
+ PageUp: () => addMonths(focused, event.shiftKey ? -12 : -1),
144
+ PageDown: () => addMonths(focused, event.shiftKey ? 12 : 1),
145
+ Home: () => focused - weekday * DAY_MS,
146
+ End: () => focused + (6 - weekday) * DAY_MS,
147
+ };
148
+ const step = steps[event.key];
149
+ if (!step) return;
150
+ event.preventDefault();
151
+ move(step());
152
+ };
153
+
154
+ const pick = (time: number) => {
155
+ if (disabled(time)) return;
156
+ setFocused(time);
157
+ onChange?.(formatCalendarDate(time));
158
+ };
159
+
160
+ return (
161
+ <div ref={ref} className={cn("w-fit select-none", className)}>
162
+ <div className="mb-2 flex items-center justify-between gap-2">
163
+ <Button
164
+ type="button"
165
+ variant="ghost"
166
+ size="icon-sm"
167
+ aria-label="Previous month"
168
+ onClick={() => setFocused(addMonths(focused, -1))}
169
+ >
170
+ <ChevronLeft />
171
+ </Button>
172
+ <div aria-live="polite" className="font-medium text-[length:var(--control-text-md,0.875rem)] text-foreground">
173
+ {monthLabel}
174
+ </div>
175
+ <Button
176
+ type="button"
177
+ variant="ghost"
178
+ size="icon-sm"
179
+ aria-label="Next month"
180
+ onClick={() => setFocused(addMonths(focused, 1))}
181
+ >
182
+ <ChevronRight />
183
+ </Button>
184
+ </div>
185
+ <div ref={gridRef} role="grid" aria-label={ariaLabel ?? monthLabel} onKeyDown={onKeyDown}>
186
+ <div role="row" className="grid grid-cols-7">
187
+ {weekdays.map((time) => (
188
+ <div
189
+ key={time}
190
+ role="columnheader"
191
+ aria-label={weekdayLong.format(time)}
192
+ className="flex h-8 items-center justify-center text-[length:var(--font-size-help,0.75rem)] text-muted-foreground"
193
+ >
194
+ {weekdayShort.format(time).slice(0, 2)}
195
+ </div>
196
+ ))}
197
+ </div>
198
+ {weeks.map((week) => (
199
+ <div key={week[0]} role="row" className="grid grid-cols-7">
200
+ {week.map((time) => {
201
+ const outside = firstOfMonth(time) !== viewMonth;
202
+ const isSelected = selected === time;
203
+ const isToday = today === time;
204
+ const isDisabled = disabled(time);
205
+ return (
206
+ <div key={time} role="gridcell" aria-selected={isSelected}>
207
+ <button
208
+ type="button"
209
+ tabIndex={time === focused ? 0 : -1}
210
+ data-focused={time === focused}
211
+ data-outside={outside || undefined}
212
+ data-today={isToday || undefined}
213
+ aria-label={dayLabel.format(time)}
214
+ aria-current={isToday ? "date" : undefined}
215
+ aria-disabled={isDisabled || undefined}
216
+ onClick={() => pick(time)}
217
+ onFocus={() => { if (time !== focused) setFocused(time); }}
218
+ className={cn(
219
+ "flex size-[var(--control-height,2.25rem)] items-center justify-center rounded-lg text-[length:var(--control-text-md,0.875rem)] tabular-nums transition-colors",
220
+ focusRing,
221
+ "hover:bg-muted",
222
+ outside && "text-muted-foreground/60",
223
+ isToday && !isSelected && "font-semibold text-[var(--accent-text)]",
224
+ isSelected && "bg-primary font-medium text-primary-foreground hover:bg-primary/90",
225
+ isDisabled && "cursor-not-allowed opacity-40 hover:bg-transparent",
226
+ )}
227
+ >
228
+ {new Date(time).getUTCDate()}
229
+ </button>
230
+ </div>
231
+ );
232
+ })}
233
+ </div>
234
+ ))}
235
+ </div>
236
+ </div>
237
+ );
238
+ },
239
+ );
240
+ Calendar.displayName = "Calendar";
241
+
242
+ export { Calendar };
@@ -12,7 +12,7 @@ afterEach(cleanup);
12
12
 
13
13
  describe("shared control presentation", () => {
14
14
  it("uses the field well, not the border/track alias, with scoped foreground and time-only motion", () => {
15
- expect(fieldPresentation).toContain("bg-[var(--bg-input)]");
15
+ expect(fieldPresentation).toContain("bg-[var(--field-surface,var(--bg-input))]");
16
16
  expect(fieldPresentation.split(" ")).not.toContain("bg-input");
17
17
  expect(fieldPresentation.split(" ")).not.toContain("bg-card");
18
18
  expect(fieldPresentation).toContain("border-border");
@@ -100,7 +100,7 @@ describe("shared control presentation", () => {
100
100
  expect(input.current).toHaveAttribute("aria-invalid", "true");
101
101
  expect(input.current!.className).toContain("autofill:shadow-");
102
102
  expect(input.current!.className.split(" ")).toContain("bg-card");
103
- expect(input.current!.className).not.toContain("bg-[var(--bg-input)]");
103
+ expect(input.current!.className).not.toContain("bg-[var(--field-surface,var(--bg-input))]");
104
104
  expect(textarea.current).toBeDisabled();
105
105
  expect(trigger.current).toBe(screen.getByLabelText("Choice"));
106
106
  expect(trigger.current).toBeDisabled();
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Button } from "./button";
4
+ import { Calendar } from "./calendar";
5
+ import { DatePicker } from "./date-picker";
6
+ import { Input } from "./input";
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
8
+ import { TimeSelect } from "./time-select";
9
+ import { FilterField, Toolbar } from "./toolbar";
10
+
11
+ function FilterBar() {
12
+ const [date, setDate] = React.useState("");
13
+ const [format, setFormat] = React.useState("all");
14
+ const [time, setTime] = React.useState("09:30");
15
+ return (
16
+ <main className="space-y-8 bg-background p-6 text-foreground" data-date-picker-story>
17
+ <section>
18
+ <h2 className="mb-3 font-semibold text-section">On the page canvas, in a Toolbar</h2>
19
+ <Toolbar
20
+ search={<Input aria-label="Filter assets" placeholder="Filter assets…" />}
21
+ filters={<>
22
+ <FilterField label="Format" htmlFor="format">
23
+ <Select value={format} onValueChange={setFormat}>
24
+ <SelectTrigger id="format" className="w-40"><SelectValue /></SelectTrigger>
25
+ <SelectContent>
26
+ <SelectItem value="all">All formats</SelectItem>
27
+ <SelectItem value="email">Email</SelectItem>
28
+ <SelectItem value="story">Story</SelectItem>
29
+ </SelectContent>
30
+ </Select>
31
+ </FilterField>
32
+ <FilterField label="Schedule for" htmlFor="schedule">
33
+ <DatePicker id="schedule" className="w-40" placeholder="Any date" value={date} onChange={setDate} />
34
+ </FilterField>
35
+ <FilterField label="At" htmlFor="at">
36
+ <TimeSelect id="at" className="w-32" value={time} onChange={setTime} />
37
+ </FilterField>
38
+ </>}
39
+ actions={<Button variant="outline">Approve all (4)</Button>}
40
+ />
41
+ </section>
42
+ <section className="max-w-sm space-y-2 rounded-xl border border-border bg-card p-4">
43
+ <h2 className="font-semibold text-section">In a card, in the field well</h2>
44
+ <DatePicker aria-label="Due" value="2026-10-08" />
45
+ </section>
46
+ <section className="w-fit rounded-xl border border-border bg-card p-3">
47
+ <Calendar value="2026-10-08" today="2026-10-08" weekStartsOn={1} />
48
+ </section>
49
+ </main>
50
+ );
51
+ }
52
+
53
+ const meta = { title: "Primitives/Date Picker", component: FilterBar, parameters: { layout: "fullscreen" } } satisfies Meta<typeof FilterBar>;
54
+ export default meta;
55
+ export const InAToolbar: StoryObj<typeof meta> = {};