@borisj74/bv-ds 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borisj74/bv-ds",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "bv-ds — React component library synced from Figma (Untitled UI v8.0), built on Tailwind CSS",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,8 +39,10 @@
39
39
  "@tiptap/extension-text-align": ">=2.0.0",
40
40
  "@tiptap/react": ">=2.0.0",
41
41
  "@tiptap/starter-kit": ">=2.0.0",
42
+ "date-fns": ">=2.0.0",
42
43
  "react": ">=18.0.0",
43
- "react-dom": ">=18.0.0"
44
+ "react-dom": ">=18.0.0",
45
+ "react-big-calendar": ">=1.8.0"
44
46
  },
45
47
  "devDependencies": {
46
48
  "@borisj74/bv-ds-icons": "^0.1.0",
@@ -54,11 +56,14 @@
54
56
  "@tiptap/react": "^3.27.1",
55
57
  "@tiptap/starter-kit": "^3.27.1",
56
58
  "@types/react": "^18.2.0",
59
+ "@types/react-big-calendar": "^1.16.3",
57
60
  "@types/react-dom": "^18.2.0",
58
61
  "autoprefixer": "^10.4.0",
59
62
  "clsx": "^2.1.0",
63
+ "date-fns": "^4.4.0",
60
64
  "postcss": "^8.4.0",
61
65
  "react": "^18.2.0",
66
+ "react-big-calendar": "^1.20.0",
62
67
  "react-dom": "^18.2.0",
63
68
  "recharts": "^2.15.4",
64
69
  "storybook": "^8.0.0",
@@ -0,0 +1,249 @@
1
+ import { useMemo, useState, useCallback } from "react";
2
+ import {
3
+ Calendar as BigCalendar,
4
+ dateFnsLocalizer,
5
+ type SlotInfo,
6
+ type View,
7
+ type NavigateAction,
8
+ } from "react-big-calendar";
9
+ import { format, parse, startOfWeek, getDay, isSameDay } from "date-fns";
10
+ import { enUS } from "date-fns/locale";
11
+ import clsx from "clsx";
12
+ import { ArrowLeft, ArrowRight, Plus } from "@borisj74/bv-ds-icons";
13
+ import { IconBox } from "../../internal/iconBox";
14
+ import { CalendarHeader } from "../CalendarHeader";
15
+ import { CalendarColumnHeader } from "../CalendarColumnHeader";
16
+ import { CalendarEvent, type CalendarEventColor } from "../CalendarEvent";
17
+ import { CalendarViewDropdown, type CalendarView } from "../CalendarViewDropdown";
18
+
19
+ const localizer = dateFnsLocalizer({
20
+ format,
21
+ parse,
22
+ // Monday-first week, matching the Figma column-header order (Mon…Sun).
23
+ startOfWeek: (date: Date) => startOfWeek(date, { weekStartsOn: 1 }),
24
+ getDay,
25
+ locales: { "en-US": enUS },
26
+ });
27
+
28
+ /**
29
+ * Event shape consumed by the `Calendar` composite. Named `CalendarEventData`
30
+ * (not `CalendarEvent`) to avoid colliding with the `CalendarEvent` chip
31
+ * component already exported from the primitives.
32
+ */
33
+ export interface CalendarEventData {
34
+ id: string | number;
35
+ title: string;
36
+ start: Date;
37
+ end: Date;
38
+ color?: CalendarEventColor;
39
+ allDay?: boolean;
40
+ }
41
+
42
+ export interface CalendarProps {
43
+ events: CalendarEventData[];
44
+ defaultView?: CalendarView;
45
+ defaultDate?: Date;
46
+ onNavigate?: (date: Date) => void;
47
+ onView?: (view: CalendarView) => void;
48
+ onSelectEvent?: (event: CalendarEventData) => void;
49
+ onSelectSlot?: (slotInfo: SlotInfo) => void;
50
+ /** Wires the header "Add event" button. */
51
+ onAddEvent?: () => void;
52
+ className?: string;
53
+ }
54
+
55
+ // Structural layout for react-big-calendar's month grid. We deliberately do NOT
56
+ // import rbc's stylesheet — every visible token comes from our classes here +
57
+ // the component overrides below.
58
+ const rbcStructural = clsx(
59
+ "h-full font-body",
60
+ "[&_.rbc-month-view]:flex [&_.rbc-month-view]:flex-1 [&_.rbc-month-view]:flex-col [&_.rbc-month-view]:overflow-hidden",
61
+ "[&_.rbc-month-header]:flex [&_.rbc-month-header]:flex-row",
62
+ "[&_.rbc-header]:min-w-0 [&_.rbc-header]:flex-1 [&_.rbc-header]:overflow-hidden",
63
+ "[&_.rbc-month-row]:relative [&_.rbc-month-row]:flex [&_.rbc-month-row]:min-h-[120px] [&_.rbc-month-row]:flex-1 [&_.rbc-month-row]:flex-col [&_.rbc-month-row]:overflow-hidden",
64
+ "[&_.rbc-row-bg]:absolute [&_.rbc-row-bg]:inset-0 [&_.rbc-row-bg]:flex [&_.rbc-row-bg]:flex-row",
65
+ "[&_.rbc-day-bg]:min-w-0 [&_.rbc-day-bg]:flex-1 [&_.rbc-day-bg]:border-b [&_.rbc-day-bg]:border-r [&_.rbc-day-bg]:border-border-secondary [&_.rbc-day-bg]:bg-bg-primary",
66
+ "[&_.rbc-off-range-bg]:!bg-bg-secondary-alt",
67
+ "[&_.rbc-row-content]:relative [&_.rbc-row-content]:z-[1] [&_.rbc-row-content]:flex [&_.rbc-row-content]:flex-1 [&_.rbc-row-content]:flex-col",
68
+ "[&_.rbc-row]:flex [&_.rbc-row]:flex-row",
69
+ "[&_.rbc-date-cell]:min-w-0 [&_.rbc-date-cell]:flex-1",
70
+ "[&_.rbc-off-range]:opacity-50",
71
+ "[&_.rbc-row-segment]:min-w-0 [&_.rbc-row-segment]:px-md [&_.rbc-row-segment]:pb-xxs",
72
+ "[&_.rbc-event]:block [&_.rbc-event]:w-full [&_.rbc-event]:outline-none",
73
+ "[&_.rbc-show-more]:block [&_.rbc-show-more]:px-md [&_.rbc-show-more]:text-xs [&_.rbc-show-more]:font-semibold [&_.rbc-show-more]:text-utility-neutral-500",
74
+ );
75
+
76
+ function NavArrow({ dir }: { dir: "left" | "right" }) {
77
+ return (
78
+ <IconBox size={20}>{dir === "left" ? <ArrowLeft /> : <ArrowRight />}</IconBox>
79
+ );
80
+ }
81
+
82
+ export function Calendar({
83
+ events,
84
+ defaultView = "month",
85
+ defaultDate,
86
+ onNavigate,
87
+ onView,
88
+ onSelectEvent,
89
+ onSelectSlot,
90
+ onAddEvent,
91
+ className,
92
+ }: CalendarProps) {
93
+ const [date, setDate] = useState<Date>(defaultDate ?? new Date(2027, 0, 1));
94
+ const [view, setView] = useState<View>(defaultView);
95
+ const [selected, setSelected] = useState<Date | null>(null);
96
+
97
+ const handleNavigate = useCallback(
98
+ (next: Date) => {
99
+ setDate(next);
100
+ onNavigate?.(next);
101
+ },
102
+ [onNavigate],
103
+ );
104
+
105
+ const handleView = useCallback(
106
+ (next: View) => {
107
+ setView(next);
108
+ onView?.(next as CalendarView);
109
+ },
110
+ [onView],
111
+ );
112
+
113
+ const handleSelectSlot = useCallback(
114
+ (slot: SlotInfo) => {
115
+ setSelected(slot.start);
116
+ onSelectSlot?.(slot);
117
+ },
118
+ [onSelectSlot],
119
+ );
120
+
121
+ const components = useMemo(
122
+ () => ({
123
+ // Header bar → CalendarHeader primitive + nav/view/add controls.
124
+ toolbar: (tb: {
125
+ label: string;
126
+ onNavigate: (action: NavigateAction) => void;
127
+ onView: (view: View) => void;
128
+ view: View;
129
+ }) => (
130
+ <div className="border-b border-border-secondary px-3xl py-2xl">
131
+ <CalendarHeader
132
+ title={tb.label}
133
+ actions={
134
+ <div className="flex items-center gap-lg">
135
+ <div className="flex items-center overflow-hidden rounded-md border border-border-primary shadow-xs">
136
+ <button
137
+ type="button"
138
+ aria-label="Previous"
139
+ onClick={() => tb.onNavigate("PREV")}
140
+ className="flex items-center justify-center bg-bg-primary px-[10px] py-md text-fg-quaternary transition-colors hover:bg-bg-primary-hover"
141
+ >
142
+ <NavArrow dir="left" />
143
+ </button>
144
+ <button
145
+ type="button"
146
+ onClick={() => tb.onNavigate("TODAY")}
147
+ className="border-x border-border-primary bg-bg-primary px-[14px] py-md text-sm font-semibold text-text-secondary transition-colors hover:bg-bg-primary-hover"
148
+ >
149
+ Today
150
+ </button>
151
+ <button
152
+ type="button"
153
+ aria-label="Next"
154
+ onClick={() => tb.onNavigate("NEXT")}
155
+ className="flex items-center justify-center bg-bg-primary px-[10px] py-md text-fg-quaternary transition-colors hover:bg-bg-primary-hover"
156
+ >
157
+ <NavArrow dir="right" />
158
+ </button>
159
+ </div>
160
+ <CalendarViewDropdown
161
+ value={tb.view as CalendarView}
162
+ onChange={(v) => tb.onView(v)}
163
+ />
164
+ {onAddEvent && (
165
+ <button
166
+ type="button"
167
+ onClick={onAddEvent}
168
+ className="flex items-center gap-xs rounded-md border-2 border-white/[0.12] bg-bg-brand-solid px-lg py-md text-sm font-semibold text-text-white shadow-skeuomorphic"
169
+ >
170
+ <span className="opacity-60"><IconBox size={20}><Plus /></IconBox></span>
171
+ Add event
172
+ </button>
173
+ )}
174
+ </div>
175
+ }
176
+ />
177
+ </div>
178
+ ),
179
+ month: {
180
+ // Weekday column header → CalendarColumnHeader primitive.
181
+ header: ({ label }: { label: string }) => (
182
+ <div className="flex h-full items-center justify-center border-b border-r border-border-secondary bg-bg-primary p-md">
183
+ <CalendarColumnHeader weekday={label} date="" />
184
+ </div>
185
+ ),
186
+ // Date-number circle (Figma 7991:81840): default / today / selected.
187
+ dateHeader: ({ date: cellDate, label }: { date: Date; label: string }) => {
188
+ const today = isSameDay(cellDate, new Date());
189
+ const isSel = selected ? isSameDay(cellDate, selected) : false;
190
+ return (
191
+ <div className="flex p-md">
192
+ <span
193
+ className={clsx(
194
+ "flex size-6 items-center justify-center rounded-full text-xs font-semibold",
195
+ today
196
+ ? "bg-bg-brand-solid text-text-white"
197
+ : isSel
198
+ ? "bg-bg-secondary text-text-secondary"
199
+ : "text-text-secondary",
200
+ )}
201
+ >
202
+ {label}
203
+ </span>
204
+ </div>
205
+ );
206
+ },
207
+ // Event chip → CalendarEvent primitive (desktop, filled).
208
+ event: ({ event }: { event: CalendarEventData }) => (
209
+ <CalendarEvent
210
+ title={event.title}
211
+ time={event.allDay ? undefined : format(event.start, "h:mm a")}
212
+ color={event.color ?? "brand"}
213
+ filled
214
+ />
215
+ ),
216
+ },
217
+ }),
218
+ [onAddEvent, selected],
219
+ );
220
+
221
+ return (
222
+ <div
223
+ className={clsx(
224
+ "flex w-full flex-col overflow-hidden rounded-xl border border-border-secondary bg-bg-primary shadow-xs",
225
+ className,
226
+ )}
227
+ >
228
+ <BigCalendar
229
+ localizer={localizer}
230
+ events={events}
231
+ date={date}
232
+ view={view}
233
+ onNavigate={handleNavigate}
234
+ onView={handleView}
235
+ views={["month", "week", "day"]}
236
+ selectable
237
+ popup
238
+ startAccessor="start"
239
+ endAccessor="end"
240
+ onSelectEvent={(e) => onSelectEvent?.(e as CalendarEventData)}
241
+ onSelectSlot={handleSelectSlot}
242
+ messages={{ showMore: (total: number) => `${total} more…` }}
243
+ components={components as never}
244
+ className={rbcStructural}
245
+ style={{ height: 800 }}
246
+ />
247
+ </div>
248
+ );
249
+ }
@@ -0,0 +1,2 @@
1
+ export { Calendar } from "./Calendar";
2
+ export type { CalendarProps, CalendarEventData } from "./Calendar";
@@ -73,6 +73,19 @@ const proseClass = clsx(
73
73
  * surfaces link controls when the caret sits inside a link.
74
74
  *
75
75
  * Pass `content` as initial HTML and read changes via `onUpdate(html)`.
76
+ *
77
+ * Behavioral constraints (confirmed in Batch 38):
78
+ *
79
+ * 1. **`content` is initial-only.** It seeds the document on mount; changing it
80
+ * afterwards does NOT resync into the editor (TipTap owns its state once
81
+ * mounted). For controlled behavior, lift state via `onUpdate(html)` instead
82
+ * of feeding `content` back in.
83
+ *
84
+ * 2. **The link tooltip is an inline affordance, not a floating BubbleMenu.**
85
+ * It renders inside the editor area when the caret is in a link — it is not
86
+ * positioned over the selection and pulls in no `@tiptap/extension-bubble-menu`
87
+ * dependency. Swap to a real BubbleMenu if positioned-over-selection UX is
88
+ * needed.
76
89
  */
77
90
  export function TextEditor({
78
91
  placeholder,
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./components/ButtonDestructive";
18
18
  export * from "./components/ButtonGroup";
19
19
  export * from "./components/ButtonGroupSegment";
20
20
  export * from "./components/ButtonUtility";
21
+ export * from "./components/Calendar";
21
22
  export * from "./components/CalendarCell";
22
23
  export * from "./components/CalendarCellDayWeekView";
23
24
  export * from "./components/CalendarColumnHeader";