@borisj74/bv-ds 0.1.3 → 0.1.4
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/dist/index.cjs +162 -60
- package/dist/index.d.cts +82 -25
- package/dist/index.d.ts +82 -25
- package/dist/index.js +162 -60
- package/package.json +1 -1
- package/src/components/CalendarCell/CalendarCell.tsx +63 -10
- package/src/components/CalendarCell/index.ts +5 -1
- package/src/components/CalendarCellDayWeekView/CalendarCellDayWeekView.tsx +28 -2
- package/src/components/CalendarCellDayWeekView/index.ts +5 -1
- package/src/components/CalendarColumnHeader/CalendarColumnHeader.tsx +21 -5
- package/src/components/CalendarColumnHeader/index.ts +2 -0
- package/src/components/CalendarEvent/CalendarEvent.tsx +69 -14
- package/src/components/CalendarEvent/index.ts +6 -1
- package/src/components/CalendarHeader/CalendarHeader.tsx +5 -1
- package/src/components/CalendarRowLabel/CalendarRowLabel.tsx +5 -3
- package/src/components/CalendarTimemarker/CalendarTimemarker.tsx +8 -2
- package/src/components/CalendarTimemarker/index.ts +1 -0
- package/src/components/CalendarViewDropdown/CalendarViewDropdown.tsx +18 -2
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import clsx from "clsx";
|
|
3
|
+
import { CalendarEvent, type CalendarEventColor } from "../CalendarEvent";
|
|
4
|
+
|
|
5
|
+
export type CalendarCellBreakpoint = "desktop" | "mobile";
|
|
6
|
+
|
|
7
|
+
export interface CalendarCellEvent {
|
|
8
|
+
title: string;
|
|
9
|
+
time?: string;
|
|
10
|
+
color?: CalendarEventColor;
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
export interface CalendarCellProps {
|
|
5
14
|
/** Day-of-month number. */
|
|
6
15
|
date: string | number;
|
|
16
|
+
/**
|
|
17
|
+
* Declarative events for this day — rendered as filled `CalendarEvent`
|
|
18
|
+
* chips (collapsed to dots on mobile). Use `children` instead for full
|
|
19
|
+
* control over each chip.
|
|
20
|
+
*/
|
|
21
|
+
events?: CalendarCellEvent[];
|
|
7
22
|
/** Event chips for this day — typically `CalendarEvent` instances. */
|
|
8
23
|
children?: ReactNode;
|
|
9
24
|
/** "+N more" overflow count shown beneath the events. */
|
|
10
25
|
moreCount?: number;
|
|
26
|
+
/** Today — date number in a brand-solid circle. */
|
|
27
|
+
isToday?: boolean;
|
|
28
|
+
/** Selected day — date number in a secondary-fill circle. */
|
|
29
|
+
isSelected?: boolean;
|
|
11
30
|
/** Other-month / disabled styling. */
|
|
12
|
-
|
|
13
|
-
/**
|
|
31
|
+
isDisabled?: boolean;
|
|
32
|
+
/** Legacy alias for `isToday`. */
|
|
14
33
|
current?: boolean;
|
|
34
|
+
/** Legacy alias for `isDisabled`. */
|
|
35
|
+
muted?: boolean;
|
|
36
|
+
/** Desktop renders full chips; mobile collapses chips to dots. */
|
|
37
|
+
breakpoint?: CalendarCellBreakpoint;
|
|
15
38
|
/** Renders a hover "+" add button bottom-right. */
|
|
16
39
|
onAdd?: () => void;
|
|
17
40
|
className?: string;
|
|
@@ -34,33 +57,63 @@ function PlusIcon() {
|
|
|
34
57
|
/** A single day cell in the month grid. Composes CalendarEvent chips. */
|
|
35
58
|
export function CalendarCell({
|
|
36
59
|
date,
|
|
60
|
+
events,
|
|
37
61
|
children,
|
|
38
62
|
moreCount,
|
|
39
|
-
|
|
63
|
+
isToday,
|
|
64
|
+
isSelected = false,
|
|
65
|
+
isDisabled,
|
|
40
66
|
current = false,
|
|
67
|
+
muted = false,
|
|
68
|
+
breakpoint = "desktop",
|
|
41
69
|
onAdd,
|
|
42
70
|
className,
|
|
43
71
|
}: CalendarCellProps) {
|
|
72
|
+
const today = isToday ?? current;
|
|
73
|
+
const disabled = isDisabled ?? muted;
|
|
74
|
+
|
|
44
75
|
return (
|
|
45
76
|
<div
|
|
46
77
|
className={clsx(
|
|
47
78
|
"group relative flex min-h-[120px] flex-col gap-xs bg-bg-primary p-md font-body",
|
|
48
|
-
|
|
79
|
+
disabled && "bg-bg-secondary",
|
|
49
80
|
className,
|
|
50
81
|
)}
|
|
51
82
|
>
|
|
52
83
|
<span
|
|
53
84
|
className={clsx(
|
|
54
|
-
"text-xs font-semibold",
|
|
55
|
-
|
|
56
|
-
? "
|
|
57
|
-
:
|
|
58
|
-
? "text-text-
|
|
59
|
-
:
|
|
85
|
+
"flex size-6 items-center justify-center text-xs font-semibold",
|
|
86
|
+
today
|
|
87
|
+
? "rounded-full bg-bg-brand-solid text-white"
|
|
88
|
+
: isSelected
|
|
89
|
+
? "rounded-full bg-bg-secondary text-text-secondary"
|
|
90
|
+
: disabled
|
|
91
|
+
? "text-text-quaternary"
|
|
92
|
+
: "text-text-secondary",
|
|
60
93
|
)}
|
|
61
94
|
>
|
|
62
95
|
{date}
|
|
63
96
|
</span>
|
|
97
|
+
{events && events.length > 0 ? (
|
|
98
|
+
<div
|
|
99
|
+
className={clsx(
|
|
100
|
+
breakpoint === "mobile"
|
|
101
|
+
? "flex flex-row flex-wrap gap-xxs"
|
|
102
|
+
: "flex flex-col gap-xs",
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{events.map((ev, i) => (
|
|
106
|
+
<CalendarEvent
|
|
107
|
+
key={i}
|
|
108
|
+
title={ev.title}
|
|
109
|
+
time={ev.time}
|
|
110
|
+
color={ev.color}
|
|
111
|
+
filled
|
|
112
|
+
breakpoint={breakpoint}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
) : null}
|
|
64
117
|
{children ? <div className="flex flex-col gap-xs">{children}</div> : null}
|
|
65
118
|
{moreCount && moreCount > 0 ? (
|
|
66
119
|
<span className="text-xs font-medium text-text-tertiary">
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import clsx from "clsx";
|
|
3
3
|
|
|
4
|
+
export type CalendarTimeslotType =
|
|
5
|
+
| "empty"
|
|
6
|
+
| "30min"
|
|
7
|
+
| "60min"
|
|
8
|
+
| "90min"
|
|
9
|
+
| "120min";
|
|
10
|
+
export type CalendarTimeslotState = "default" | "hover";
|
|
11
|
+
|
|
4
12
|
export interface CalendarCellDayWeekViewProps {
|
|
13
|
+
/**
|
|
14
|
+
* Slot duration — drives the row height (empty/30min → 48px, 60min → 96px,
|
|
15
|
+
* 90min → 144px, 120min → 192px). Omit to keep the auto min-height.
|
|
16
|
+
*/
|
|
17
|
+
type?: CalendarTimeslotType;
|
|
18
|
+
/** `hover` tints the slot (also rendered via CSS :hover). */
|
|
19
|
+
state?: CalendarTimeslotState;
|
|
5
20
|
/** Event block(s) occupying the slot — typically CalendarEventDayWeekView. */
|
|
6
21
|
children?: ReactNode;
|
|
7
22
|
/** Renders a hover "+" add button bottom-right. */
|
|
@@ -11,6 +26,14 @@ export interface CalendarCellDayWeekViewProps {
|
|
|
11
26
|
className?: string;
|
|
12
27
|
}
|
|
13
28
|
|
|
29
|
+
const heightClasses: Record<CalendarTimeslotType, string> = {
|
|
30
|
+
empty: "h-[48px]",
|
|
31
|
+
"30min": "h-[48px]",
|
|
32
|
+
"60min": "h-[96px]",
|
|
33
|
+
"90min": "h-[144px]",
|
|
34
|
+
"120min": "h-[192px]",
|
|
35
|
+
};
|
|
36
|
+
|
|
14
37
|
function PlusIcon() {
|
|
15
38
|
return (
|
|
16
39
|
<svg viewBox="0 0 20 20" fill="none" className="size-4" aria-hidden>
|
|
@@ -27,6 +50,8 @@ function PlusIcon() {
|
|
|
27
50
|
|
|
28
51
|
/** A time-slot cell in day/week view. Holds CalendarEventDayWeekView blocks. */
|
|
29
52
|
export function CalendarCellDayWeekView({
|
|
53
|
+
type,
|
|
54
|
+
state = "default",
|
|
30
55
|
children,
|
|
31
56
|
onAdd,
|
|
32
57
|
muted = false,
|
|
@@ -35,8 +60,9 @@ export function CalendarCellDayWeekView({
|
|
|
35
60
|
return (
|
|
36
61
|
<div
|
|
37
62
|
className={clsx(
|
|
38
|
-
"group relative
|
|
39
|
-
|
|
63
|
+
"group relative border-b border-border-secondary p-xs font-body transition-colors hover:bg-bg-secondary",
|
|
64
|
+
type ? heightClasses[type] : "min-h-[64px]",
|
|
65
|
+
muted ? "bg-bg-secondary" : state === "hover" ? "bg-bg-secondary" : "bg-bg-primary",
|
|
40
66
|
className,
|
|
41
67
|
)}
|
|
42
68
|
>
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { CalendarCellDayWeekView } from "./CalendarCellDayWeekView";
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
CalendarCellDayWeekViewProps,
|
|
4
|
+
CalendarTimeslotType,
|
|
5
|
+
CalendarTimeslotState,
|
|
6
|
+
} from "./CalendarCellDayWeekView";
|
|
@@ -1,30 +1,45 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
|
|
3
3
|
export type CalendarColumnHeaderOrientation = "horizontal" | "vertical";
|
|
4
|
+
export type CalendarColumnHeaderType = "default" | "selected" | "today";
|
|
5
|
+
export type CalendarBreakpoint = "desktop" | "mobile";
|
|
4
6
|
|
|
5
7
|
export interface CalendarColumnHeaderProps {
|
|
6
8
|
/** Weekday label (e.g. "Mon"). */
|
|
7
9
|
weekday: string;
|
|
8
10
|
/** Date number. */
|
|
9
11
|
date: string | number;
|
|
10
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Cell state. `selected` → brand-solid circle behind the date; `today` →
|
|
14
|
+
* brand text + brand underline. Takes precedence over `current` when set.
|
|
15
|
+
*/
|
|
16
|
+
type?: CalendarColumnHeaderType;
|
|
17
|
+
/** Legacy boolean — equivalent to `type="selected"`. Kept for back-compat. */
|
|
11
18
|
current?: boolean;
|
|
12
19
|
orientation?: CalendarColumnHeaderOrientation;
|
|
20
|
+
/** Desktop fixes the column to 160px; mobile lets it size to content. */
|
|
21
|
+
breakpoint?: CalendarBreakpoint;
|
|
13
22
|
className?: string;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
export function CalendarColumnHeader({
|
|
17
26
|
weekday,
|
|
18
27
|
date,
|
|
28
|
+
type,
|
|
19
29
|
current = false,
|
|
20
30
|
orientation = "vertical",
|
|
31
|
+
breakpoint = "desktop",
|
|
21
32
|
className,
|
|
22
33
|
}: CalendarColumnHeaderProps) {
|
|
34
|
+
// `type` wins; otherwise the legacy `current` flag maps to "selected".
|
|
35
|
+
const state: CalendarColumnHeaderType = type ?? (current ? "selected" : "default");
|
|
36
|
+
|
|
23
37
|
return (
|
|
24
38
|
<div
|
|
25
39
|
className={clsx(
|
|
26
40
|
"flex items-center justify-center gap-md py-md font-body",
|
|
27
41
|
orientation === "vertical" && "flex-col gap-xs",
|
|
42
|
+
breakpoint === "desktop" && "w-[160px]",
|
|
28
43
|
className,
|
|
29
44
|
)}
|
|
30
45
|
>
|
|
@@ -32,11 +47,12 @@ export function CalendarColumnHeader({
|
|
|
32
47
|
<span
|
|
33
48
|
className={clsx(
|
|
34
49
|
"flex items-center justify-center text-xs font-semibold",
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
state === "selected" && "size-6 rounded-full bg-bg-brand-solid text-white",
|
|
51
|
+
state === "today" &&
|
|
52
|
+
"border-b-2 border-border-brand pb-xxs text-text-brand-secondary",
|
|
53
|
+
state === "default" && "text-text-secondary",
|
|
38
54
|
)}
|
|
39
|
-
aria-current={
|
|
55
|
+
aria-current={state !== "default" ? "date" : undefined}
|
|
40
56
|
>
|
|
41
57
|
{date}
|
|
42
58
|
</span>
|
|
@@ -11,6 +11,9 @@ export type CalendarEventColor =
|
|
|
11
11
|
| "orange"
|
|
12
12
|
| "amber";
|
|
13
13
|
|
|
14
|
+
export type CalendarEventBreakpoint = "desktop" | "mobile";
|
|
15
|
+
export type CalendarEventState = "default" | "hover";
|
|
16
|
+
|
|
14
17
|
export interface CalendarEventProps {
|
|
15
18
|
/** Event title. */
|
|
16
19
|
title: string;
|
|
@@ -19,10 +22,14 @@ export interface CalendarEventProps {
|
|
|
19
22
|
color?: CalendarEventColor;
|
|
20
23
|
/** Filled colour-fill style vs the subtle white style. */
|
|
21
24
|
filled?: boolean;
|
|
25
|
+
/** Desktop renders the full chip; mobile collapses to an 8px dot. */
|
|
26
|
+
breakpoint?: CalendarEventBreakpoint;
|
|
27
|
+
/** `hover` darkens the fill one shade (filled chips only). */
|
|
28
|
+
state?: CalendarEventState;
|
|
22
29
|
className?: string;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
//
|
|
32
|
+
// Status dot (utility-<c>-500) — also the mobile collapsed form.
|
|
26
33
|
const dotClasses: Record<CalendarEventColor, string> = {
|
|
27
34
|
neutral: "bg-utility-neutral-500",
|
|
28
35
|
brand: "bg-utility-brand-500",
|
|
@@ -35,16 +42,43 @@ const dotClasses: Record<CalendarEventColor, string> = {
|
|
|
35
42
|
amber: "bg-utility-amber-500",
|
|
36
43
|
};
|
|
37
44
|
|
|
45
|
+
// Filled chip — Figma-confirmed: bg-50 / border-200 / title text-700 (node 7991:54084).
|
|
38
46
|
const filledClasses: Record<CalendarEventColor, string> = {
|
|
39
|
-
neutral: "bg-utility-neutral-
|
|
40
|
-
brand: "bg-utility-brand-
|
|
41
|
-
emerald: "bg-utility-emerald-
|
|
42
|
-
blue: "bg-utility-blue-
|
|
43
|
-
indigo: "bg-utility-indigo-
|
|
44
|
-
purple: "bg-utility-purple-
|
|
45
|
-
pink: "bg-utility-pink-
|
|
46
|
-
orange: "bg-utility-orange-
|
|
47
|
-
amber: "bg-utility-amber-
|
|
47
|
+
neutral: "bg-utility-neutral-50 border-utility-neutral-200 text-utility-neutral-700",
|
|
48
|
+
brand: "bg-utility-brand-50 border-utility-brand-200 text-utility-brand-700",
|
|
49
|
+
emerald: "bg-utility-emerald-50 border-utility-emerald-200 text-utility-emerald-700",
|
|
50
|
+
blue: "bg-utility-blue-50 border-utility-blue-200 text-utility-blue-700",
|
|
51
|
+
indigo: "bg-utility-indigo-50 border-utility-indigo-200 text-utility-indigo-700",
|
|
52
|
+
purple: "bg-utility-purple-50 border-utility-purple-200 text-utility-purple-700",
|
|
53
|
+
pink: "bg-utility-pink-50 border-utility-pink-200 text-utility-pink-700",
|
|
54
|
+
orange: "bg-utility-orange-50 border-utility-orange-200 text-utility-orange-700",
|
|
55
|
+
amber: "bg-utility-amber-50 border-utility-amber-200 text-utility-amber-700",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Hover fill — one shade darker (bg-100).
|
|
59
|
+
const filledHoverClasses: Record<CalendarEventColor, string> = {
|
|
60
|
+
neutral: "bg-utility-neutral-100",
|
|
61
|
+
brand: "bg-utility-brand-100",
|
|
62
|
+
emerald: "bg-utility-emerald-100",
|
|
63
|
+
blue: "bg-utility-blue-100",
|
|
64
|
+
indigo: "bg-utility-indigo-100",
|
|
65
|
+
purple: "bg-utility-purple-100",
|
|
66
|
+
pink: "bg-utility-pink-100",
|
|
67
|
+
orange: "bg-utility-orange-100",
|
|
68
|
+
amber: "bg-utility-amber-100",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Time-label colour for filled chips (utility-<c>-600).
|
|
72
|
+
const timeClasses: Record<CalendarEventColor, string> = {
|
|
73
|
+
neutral: "text-utility-neutral-600",
|
|
74
|
+
brand: "text-utility-brand-600",
|
|
75
|
+
emerald: "text-utility-emerald-600",
|
|
76
|
+
blue: "text-utility-blue-600",
|
|
77
|
+
indigo: "text-utility-indigo-600",
|
|
78
|
+
purple: "text-utility-purple-600",
|
|
79
|
+
pink: "text-utility-pink-600",
|
|
80
|
+
orange: "text-utility-orange-600",
|
|
81
|
+
amber: "text-utility-amber-600",
|
|
48
82
|
};
|
|
49
83
|
|
|
50
84
|
export function CalendarEvent({
|
|
@@ -52,22 +86,43 @@ export function CalendarEvent({
|
|
|
52
86
|
time,
|
|
53
87
|
color = "neutral",
|
|
54
88
|
filled = false,
|
|
89
|
+
breakpoint = "desktop",
|
|
90
|
+
state = "default",
|
|
55
91
|
className,
|
|
56
92
|
}: CalendarEventProps) {
|
|
93
|
+
// Mobile view collapses the chip down to a single 8px status dot.
|
|
94
|
+
if (breakpoint === "mobile") {
|
|
95
|
+
return (
|
|
96
|
+
<span
|
|
97
|
+
className={clsx("inline-block size-2 rounded-full", dotClasses[color], className)}
|
|
98
|
+
role="img"
|
|
99
|
+
aria-label={time ? `${title}, ${time}` : title}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
57
104
|
return (
|
|
58
105
|
<div
|
|
59
106
|
className={clsx(
|
|
60
|
-
"flex w-full items-center gap-md rounded-sm px-md py-xs font-body text-xs",
|
|
107
|
+
"flex w-full items-center gap-md rounded-sm border px-md py-xs font-body text-xs",
|
|
61
108
|
filled
|
|
62
|
-
? filledClasses[color]
|
|
63
|
-
:
|
|
109
|
+
? clsx(filledClasses[color], state === "hover" && filledHoverClasses[color])
|
|
110
|
+
: clsx(
|
|
111
|
+
"border-border-secondary bg-bg-primary text-text-secondary",
|
|
112
|
+
state === "hover" && "bg-bg-primary-hover",
|
|
113
|
+
),
|
|
64
114
|
className,
|
|
65
115
|
)}
|
|
66
116
|
>
|
|
67
117
|
<span className={clsx("size-2 shrink-0 rounded-full", dotClasses[color])} aria-hidden />
|
|
68
118
|
<span className="truncate font-semibold">{title}</span>
|
|
69
119
|
{time ? (
|
|
70
|
-
<span
|
|
120
|
+
<span
|
|
121
|
+
className={clsx(
|
|
122
|
+
"ml-auto shrink-0 font-normal",
|
|
123
|
+
filled ? timeClasses[color] : "text-text-tertiary",
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
71
126
|
{time}
|
|
72
127
|
</span>
|
|
73
128
|
) : null}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export { CalendarEvent } from "./CalendarEvent";
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
CalendarEventProps,
|
|
4
|
+
CalendarEventColor,
|
|
5
|
+
CalendarEventBreakpoint,
|
|
6
|
+
CalendarEventState,
|
|
7
|
+
} from "./CalendarEvent";
|
|
@@ -6,6 +6,8 @@ export interface CalendarHeaderProps {
|
|
|
6
6
|
title: string;
|
|
7
7
|
/** Date range subtitle, e.g. "Jan 1, 2027 – Jan 31, 2027". */
|
|
8
8
|
range?: string;
|
|
9
|
+
/** Alias for `range` — Batch-36 prop name. `range` wins if both set. */
|
|
10
|
+
supportingText?: string;
|
|
9
11
|
/** Badge beside the title (e.g. a PillBadge "Week 1"). */
|
|
10
12
|
badge?: ReactNode;
|
|
11
13
|
/** Leading date chip — typically a CalendarDateIcon. */
|
|
@@ -19,11 +21,13 @@ export interface CalendarHeaderProps {
|
|
|
19
21
|
export function CalendarHeader({
|
|
20
22
|
title,
|
|
21
23
|
range,
|
|
24
|
+
supportingText,
|
|
22
25
|
badge,
|
|
23
26
|
dateIcon,
|
|
24
27
|
actions,
|
|
25
28
|
className,
|
|
26
29
|
}: CalendarHeaderProps) {
|
|
30
|
+
const subtitle = range ?? supportingText;
|
|
27
31
|
return (
|
|
28
32
|
<div
|
|
29
33
|
className={clsx(
|
|
@@ -38,7 +42,7 @@ export function CalendarHeader({
|
|
|
38
42
|
<h2 className="text-lg font-bold text-text-primary">{title}</h2>
|
|
39
43
|
{badge}
|
|
40
44
|
</div>
|
|
41
|
-
{
|
|
45
|
+
{subtitle ? <p className="text-sm text-text-tertiary">{subtitle}</p> : null}
|
|
42
46
|
</div>
|
|
43
47
|
</div>
|
|
44
48
|
{actions ? <div className="flex items-center gap-md">{actions}</div> : null}
|
|
@@ -2,12 +2,14 @@ import clsx from "clsx";
|
|
|
2
2
|
|
|
3
3
|
export interface CalendarRowLabelProps {
|
|
4
4
|
/** Time label for the row gutter (e.g. "9 AM"). */
|
|
5
|
-
label
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Alias for `label` — Batch-36 prop name. One of the two is required. */
|
|
7
|
+
time?: string;
|
|
6
8
|
className?: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** Left-gutter time label aligned to the top of a day/week-view row. */
|
|
10
|
-
export function CalendarRowLabel({ label, className }: CalendarRowLabelProps) {
|
|
12
|
+
export function CalendarRowLabel({ label, time, className }: CalendarRowLabelProps) {
|
|
11
13
|
return (
|
|
12
14
|
<div
|
|
13
15
|
className={clsx(
|
|
@@ -15,7 +17,7 @@ export function CalendarRowLabel({ label, className }: CalendarRowLabelProps) {
|
|
|
15
17
|
className,
|
|
16
18
|
)}
|
|
17
19
|
>
|
|
18
|
-
{label}
|
|
20
|
+
{time ?? label}
|
|
19
21
|
</div>
|
|
20
22
|
);
|
|
21
23
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
|
|
3
3
|
export type CalendarTimemarkerAlign = "left" | "center";
|
|
4
|
+
export type CalendarTimemarkerBreakpoint = "desktop" | "mobile";
|
|
4
5
|
|
|
5
6
|
export interface CalendarTimemarkerProps {
|
|
6
7
|
/** Time label (e.g. "2:20 PM"). */
|
|
7
8
|
time: string;
|
|
8
9
|
/** Label position: leading, or centered between two line halves. */
|
|
9
10
|
align?: CalendarTimemarkerAlign;
|
|
11
|
+
/** Desktop shows the time label; mobile collapses to dot + line only. */
|
|
12
|
+
breakpoint?: CalendarTimemarkerBreakpoint;
|
|
10
13
|
className?: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -23,20 +26,23 @@ const Label = ({ time }: { time: string }) => (
|
|
|
23
26
|
export function CalendarTimemarker({
|
|
24
27
|
time,
|
|
25
28
|
align = "left",
|
|
29
|
+
breakpoint = "desktop",
|
|
26
30
|
className,
|
|
27
31
|
}: CalendarTimemarkerProps) {
|
|
32
|
+
const showLabel = breakpoint === "desktop";
|
|
33
|
+
|
|
28
34
|
return (
|
|
29
35
|
<div className={clsx("flex w-full items-center gap-xs font-body", className)}>
|
|
30
36
|
{align === "center" ? (
|
|
31
37
|
<>
|
|
32
38
|
<Dot />
|
|
33
39
|
<Line />
|
|
34
|
-
<Label time={time} />
|
|
40
|
+
{showLabel ? <Label time={time} /> : null}
|
|
35
41
|
<Line />
|
|
36
42
|
</>
|
|
37
43
|
) : (
|
|
38
44
|
<>
|
|
39
|
-
<Label time={time} />
|
|
45
|
+
{showLabel ? <Label time={time} /> : null}
|
|
40
46
|
<Dot />
|
|
41
47
|
<Line />
|
|
42
48
|
</>
|
|
@@ -12,6 +12,12 @@ export interface CalendarViewOption {
|
|
|
12
12
|
export interface CalendarViewDropdownProps {
|
|
13
13
|
value: CalendarView;
|
|
14
14
|
onChange?: (value: CalendarView) => void;
|
|
15
|
+
/** Alias for `onChange` — Batch-36 prop name. Both fire on selection. */
|
|
16
|
+
onSelect?: (value: CalendarView) => void;
|
|
17
|
+
/** Controlled open state. Omit to let the component manage its own. */
|
|
18
|
+
open?: boolean;
|
|
19
|
+
/** Fires whenever the menu wants to open/close (controlled or not). */
|
|
20
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
21
|
options?: CalendarViewOption[];
|
|
16
22
|
className?: string;
|
|
17
23
|
}
|
|
@@ -39,10 +45,19 @@ function Chevron() {
|
|
|
39
45
|
export function CalendarViewDropdown({
|
|
40
46
|
value,
|
|
41
47
|
onChange,
|
|
48
|
+
onSelect,
|
|
49
|
+
open: openProp,
|
|
50
|
+
onOpenChange,
|
|
42
51
|
options = DEFAULT_OPTIONS,
|
|
43
52
|
className,
|
|
44
53
|
}: CalendarViewDropdownProps) {
|
|
45
|
-
const [
|
|
54
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
55
|
+
const isControlled = openProp !== undefined;
|
|
56
|
+
const open = isControlled ? openProp : internalOpen;
|
|
57
|
+
const setOpen = (next: boolean) => {
|
|
58
|
+
if (!isControlled) setInternalOpen(next);
|
|
59
|
+
onOpenChange?.(next);
|
|
60
|
+
};
|
|
46
61
|
const selected = options.find((o) => o.value === value) ?? options[0];
|
|
47
62
|
|
|
48
63
|
return (
|
|
@@ -51,7 +66,7 @@ export function CalendarViewDropdown({
|
|
|
51
66
|
type="button"
|
|
52
67
|
aria-haspopup="listbox"
|
|
53
68
|
aria-expanded={open}
|
|
54
|
-
onClick={() => setOpen(
|
|
69
|
+
onClick={() => setOpen(!open)}
|
|
55
70
|
className="inline-flex items-center gap-md rounded-md border border-border-primary bg-bg-primary px-lg py-md text-sm font-semibold text-text-secondary shadow-skeuomorphic transition-colors hover:bg-bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary"
|
|
56
71
|
>
|
|
57
72
|
{selected.label}
|
|
@@ -72,6 +87,7 @@ export function CalendarViewDropdown({
|
|
|
72
87
|
type="button"
|
|
73
88
|
onClick={() => {
|
|
74
89
|
onChange?.(opt.value);
|
|
90
|
+
onSelect?.(opt.value);
|
|
75
91
|
setOpen(false);
|
|
76
92
|
}}
|
|
77
93
|
className="flex w-full items-center gap-md rounded-sm px-md py-xs text-sm font-medium text-text-secondary transition-colors hover:bg-bg-primary-hover"
|