@mostrom/app-shell 0.1.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.
Files changed (142) hide show
  1. package/.claude/ralph-loop.local.md +9 -0
  2. package/README.md +172 -0
  3. package/bin/init.js +269 -0
  4. package/bun.lock +401 -0
  5. package/components.json +28 -0
  6. package/package.json +74 -0
  7. package/scripts/publish-npm.sh +202 -0
  8. package/src/AppShell.tsx +847 -0
  9. package/src/components/PageHeader.tsx +160 -0
  10. package/src/components/data-table/README.md +447 -0
  11. package/src/components/data-table/data-table-preferences.tsx +184 -0
  12. package/src/components/data-table/data-table-toolbar.tsx +118 -0
  13. package/src/components/data-table/data-table.tsx +37 -0
  14. package/src/components/data-table/index.ts +32 -0
  15. package/src/components/global-header/AllServicesButton.tsx +127 -0
  16. package/src/components/global-header/CategoriesButton.tsx +120 -0
  17. package/src/components/global-header/GlobalHeader.tsx +59 -0
  18. package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
  19. package/src/components/global-header/HeaderUtilities.tsx +243 -0
  20. package/src/components/global-header/ServicesMenu.tsx +246 -0
  21. package/src/components/layout/AppBreadcrumb.tsx +70 -0
  22. package/src/components/layout/AppFlashbar.tsx +95 -0
  23. package/src/components/layout/AppLayout.tsx +271 -0
  24. package/src/components/layout/AppNavigation.tsx +313 -0
  25. package/src/components/layout/AppSidebar.tsx +229 -0
  26. package/src/components/patterns/index.ts +14 -0
  27. package/src/components/patterns/p-alert-5.tsx +19 -0
  28. package/src/components/patterns/p-autocomplete-5.tsx +89 -0
  29. package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
  30. package/src/components/patterns/p-button-42.tsx +37 -0
  31. package/src/components/patterns/p-button-51.tsx +14 -0
  32. package/src/components/patterns/p-button-6.tsx +5 -0
  33. package/src/components/patterns/p-calendar-1.tsx +18 -0
  34. package/src/components/patterns/p-card-1.tsx +33 -0
  35. package/src/components/patterns/p-card-2.tsx +26 -0
  36. package/src/components/patterns/p-card-5.tsx +31 -0
  37. package/src/components/patterns/p-collapsible-7.tsx +121 -0
  38. package/src/components/patterns/p-command-6.tsx +113 -0
  39. package/src/components/patterns/p-dialog-1.tsx +56 -0
  40. package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
  41. package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
  42. package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
  43. package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
  44. package/src/components/patterns/p-empty-2.tsx +34 -0
  45. package/src/components/patterns/p-file-upload-1.tsx +72 -0
  46. package/src/components/patterns/p-filters-1.tsx +666 -0
  47. package/src/components/patterns/p-frame-2.tsx +26 -0
  48. package/src/components/patterns/p-tabs-2.tsx +129 -0
  49. package/src/components/reui/alert.tsx +92 -0
  50. package/src/components/reui/autocomplete.tsx +343 -0
  51. package/src/components/reui/badge.tsx +87 -0
  52. package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
  53. package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
  54. package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
  55. package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
  56. package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
  57. package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
  58. package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
  59. package/src/components/reui/data-grid/data-grid.tsx +209 -0
  60. package/src/components/reui/date-selector.tsx +1330 -0
  61. package/src/components/reui/filters.tsx +1869 -0
  62. package/src/components/reui/frame.tsx +134 -0
  63. package/src/components/reui/index.ts +17 -0
  64. package/src/components/reui/timeline.tsx +219 -0
  65. package/src/components/search/Autocomplete.tsx +183 -0
  66. package/src/components/search/AutocompleteClient.tsx +293 -0
  67. package/src/components/search/GlobalSearch.tsx +187 -0
  68. package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
  69. package/src/components/section-drawer/index.ts +19 -0
  70. package/src/components/section-drawer/section-drawer.css +665 -0
  71. package/src/components/section-drawer/section-drawer.tsx +467 -0
  72. package/src/components/sectioned-list-board/README.md +78 -0
  73. package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
  74. package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
  75. package/src/components/sectioned-list-board/index.ts +19 -0
  76. package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
  77. package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
  78. package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
  79. package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
  80. package/src/components/sectioned-list-board/types.ts +216 -0
  81. package/src/components/sectioned-list-table/README.md +80 -0
  82. package/src/components/sectioned-list-table/index.ts +14 -0
  83. package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
  84. package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
  85. package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
  86. package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
  87. package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
  88. package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
  89. package/src/components/sectioned-list-table/types.ts +120 -0
  90. package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
  91. package/src/components/ui/actions-dropdown.tsx +109 -0
  92. package/src/components/ui/assignee-selector.tsx +209 -0
  93. package/src/components/ui/avatar.tsx +107 -0
  94. package/src/components/ui/breadcrumb.tsx +109 -0
  95. package/src/components/ui/button-group.tsx +83 -0
  96. package/src/components/ui/button.tsx +64 -0
  97. package/src/components/ui/calendar.tsx +220 -0
  98. package/src/components/ui/card.tsx +92 -0
  99. package/src/components/ui/chart.tsx +376 -0
  100. package/src/components/ui/checkbox.tsx +30 -0
  101. package/src/components/ui/collapsible.tsx +33 -0
  102. package/src/components/ui/command.tsx +182 -0
  103. package/src/components/ui/context-menu.tsx +250 -0
  104. package/src/components/ui/create-button-group.tsx +128 -0
  105. package/src/components/ui/dialog.tsx +156 -0
  106. package/src/components/ui/drawer.tsx +133 -0
  107. package/src/components/ui/dropdown-menu.tsx +255 -0
  108. package/src/components/ui/empty.tsx +104 -0
  109. package/src/components/ui/field.tsx +248 -0
  110. package/src/components/ui/form.tsx +165 -0
  111. package/src/components/ui/index.ts +37 -0
  112. package/src/components/ui/input-group.tsx +168 -0
  113. package/src/components/ui/input.tsx +21 -0
  114. package/src/components/ui/kbd.tsx +28 -0
  115. package/src/components/ui/label.tsx +22 -0
  116. package/src/components/ui/navigation-menu.tsx +168 -0
  117. package/src/components/ui/page-header.tsx +80 -0
  118. package/src/components/ui/popover.tsx +87 -0
  119. package/src/components/ui/scroll-area.tsx +56 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +26 -0
  122. package/src/components/ui/sheet.tsx +141 -0
  123. package/src/components/ui/sidebar.tsx +726 -0
  124. package/src/components/ui/skeleton.tsx +13 -0
  125. package/src/components/ui/sonner.tsx +38 -0
  126. package/src/components/ui/switch.tsx +33 -0
  127. package/src/components/ui/tabs.tsx +91 -0
  128. package/src/components/ui/textarea.tsx +18 -0
  129. package/src/components/ui/toggle-group.tsx +83 -0
  130. package/src/components/ui/toggle.tsx +45 -0
  131. package/src/components/ui/tooltip.tsx +57 -0
  132. package/src/hooks/use-copy-to-clipboard.ts +37 -0
  133. package/src/hooks/use-file-upload.ts +415 -0
  134. package/src/hooks/use-mobile.ts +19 -0
  135. package/src/index.ts +95 -0
  136. package/src/lib/utils.ts +6 -0
  137. package/src/styles.css +1859 -0
  138. package/src/urls.ts +83 -0
  139. package/src/vite.d.ts +22 -0
  140. package/src/vite.js +241 -0
  141. package/tsconfig.base.json +18 -0
  142. package/tsconfig.json +24 -0
@@ -0,0 +1,340 @@
1
+ import * as React from "react";
2
+ import * as Popover from "@radix-ui/react-popover";
3
+ import Badge from "@cloudscape-design/components/badge";
4
+ import Button from "@cloudscape-design/components/button";
5
+ import SpaceBetween from "@cloudscape-design/components/space-between";
6
+ import { format, isToday, isTomorrow, isValid, parseISO } from "date-fns";
7
+ import {
8
+ DateSelector,
9
+ type DateSelectorValue,
10
+ } from "@platform/app-shell/components/reui";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "@/components/ui/select";
18
+ import type { BoardCardContentProps, BoardBadgeColor } from "./types";
19
+ import { AssigneeSelector } from "@/components/ui/assignee-selector";
20
+
21
+ function getItemFieldValue<T>(item: T, field: Extract<keyof T, string>): unknown {
22
+ return (item as Record<string, unknown>)[field];
23
+ }
24
+
25
+ function toDisplayText(value: unknown): string {
26
+ if (value == null) return "";
27
+ if (typeof value === "string") return value;
28
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
29
+ return "";
30
+ }
31
+
32
+ function getBadgeColor(
33
+ value: string,
34
+ colorMap?: Record<string, BoardBadgeColor>,
35
+ fallback: BoardBadgeColor = "blue"
36
+ ): BoardBadgeColor {
37
+ return colorMap?.[value] ?? fallback;
38
+ }
39
+
40
+ function getInitials(name: string): string {
41
+ const parts = name
42
+ .trim()
43
+ .split(/\s+/)
44
+ .filter(Boolean);
45
+
46
+ if (parts.length === 0) return "?";
47
+
48
+ return parts
49
+ .slice(0, 2)
50
+ .map((part) => part[0]?.toUpperCase() ?? "")
51
+ .join("");
52
+ }
53
+
54
+ function formatDateLabel(value: string): string | null {
55
+ if (!value) return null;
56
+ const parsed = parseISO(value);
57
+ if (!isValid(parsed)) return null;
58
+ if (isToday(parsed)) return "Today";
59
+ if (isTomorrow(parsed)) return "Tomorrow";
60
+ return format(parsed, "MMM d");
61
+ }
62
+
63
+ function CalendarIcon() {
64
+ return (
65
+ <svg
66
+ width="12"
67
+ height="12"
68
+ viewBox="0 0 24 24"
69
+ fill="none"
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ aria-hidden="true"
72
+ >
73
+ <rect x="3.5" y="5.5" width="17" height="15" rx="2.5" stroke="currentColor" />
74
+ <path d="M7 3V8" stroke="currentColor" strokeLinecap="round" />
75
+ <path d="M17 3V8" stroke="currentColor" strokeLinecap="round" />
76
+ <path d="M3.5 9.5H20.5" stroke="currentColor" />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ function createSingleDayValue(date?: Date): DateSelectorValue {
82
+ return {
83
+ period: "day",
84
+ operator: "is",
85
+ startDate: date,
86
+ endDate: undefined,
87
+ };
88
+ }
89
+
90
+ export function BoardCardContent<T>({
91
+ item,
92
+ cardConfig,
93
+ titleContent,
94
+ onFieldChange,
95
+ interactive = true,
96
+ assignees,
97
+ }: BoardCardContentProps<T>) {
98
+ const [isDateSelectorOpen, setIsDateSelectorOpen] = React.useState(false);
99
+ const [isAssigneeSelectorOpen, setIsAssigneeSelectorOpen] = React.useState(false);
100
+
101
+ const rawTitle = getItemFieldValue(item, cardConfig.titleField);
102
+ const title = toDisplayText(rawTitle);
103
+
104
+ const badgeValue = cardConfig.badgeField
105
+ ? toDisplayText(getItemFieldValue(item, cardConfig.badgeField.field))
106
+ : "";
107
+ const isPriorityField = cardConfig.badgeField?.field === "priority";
108
+ const priorityOptions =
109
+ cardConfig.badgeField && isPriorityField && cardConfig.badgeField.colorMap
110
+ ? Object.keys(cardConfig.badgeField.colorMap)
111
+ : [];
112
+ const badgeSelectOptions =
113
+ badgeValue && !priorityOptions.includes(badgeValue)
114
+ ? [...priorityOptions, badgeValue]
115
+ : priorityOptions;
116
+ const canEditPriority = Boolean(
117
+ interactive &&
118
+ onFieldChange &&
119
+ cardConfig.badgeField &&
120
+ isPriorityField &&
121
+ badgeSelectOptions.length > 0
122
+ );
123
+ const badgeColor = cardConfig.badgeField
124
+ ? getBadgeColor(
125
+ badgeValue,
126
+ cardConfig.badgeField.colorMap,
127
+ cardConfig.badgeField.defaultColor ?? "blue"
128
+ )
129
+ : "blue";
130
+
131
+ const avatarValue = cardConfig.avatarField
132
+ ? toDisplayText(getItemFieldValue(item, cardConfig.avatarField.field))
133
+ : "";
134
+
135
+ const dateValue = cardConfig.dateField
136
+ ? toDisplayText(getItemFieldValue(item, cardConfig.dateField.field))
137
+ : "";
138
+ const dueDateLabel = formatDateLabel(dateValue);
139
+
140
+ const selectedDate = React.useMemo(() => {
141
+ if (!dateValue) return undefined;
142
+ const parsed = parseISO(dateValue);
143
+ return isValid(parsed) ? parsed : undefined;
144
+ }, [dateValue]);
145
+
146
+ const [internalDateValue, setInternalDateValue] = React.useState<DateSelectorValue>(
147
+ createSingleDayValue(selectedDate)
148
+ );
149
+
150
+ React.useEffect(() => {
151
+ if (isDateSelectorOpen) {
152
+ setInternalDateValue(createSingleDayValue(selectedDate));
153
+ }
154
+ }, [isDateSelectorOpen, selectedDate]);
155
+
156
+ const canEditDate = Boolean(
157
+ interactive &&
158
+ cardConfig.dateField?.editable &&
159
+ cardConfig.dateField &&
160
+ onFieldChange
161
+ );
162
+
163
+ const canEditAssignee = Boolean(
164
+ interactive &&
165
+ cardConfig.avatarField &&
166
+ cardConfig.avatarField.editable !== false &&
167
+ onFieldChange &&
168
+ assignees &&
169
+ assignees.length > 0
170
+ );
171
+
172
+ // Find current assignee by name
173
+ const currentAssignee = assignees?.find(
174
+ (a) => a.name === avatarValue
175
+ );
176
+
177
+ const handleAssigneeSelect = (assignee: { id: string; name: string; email: string; avatar?: string } | null) => {
178
+ if (!cardConfig.avatarField || !onFieldChange) return;
179
+ onFieldChange(cardConfig.avatarField.field, assignee?.name ?? "");
180
+ };
181
+
182
+ const handleOpenDatePicker = (e: React.MouseEvent<HTMLButtonElement>) => {
183
+ e.stopPropagation();
184
+ if (!canEditDate) return;
185
+ setIsDateSelectorOpen(true);
186
+ };
187
+
188
+ const handleApplyDate = () => {
189
+ if (!cardConfig.dateField || !onFieldChange) return;
190
+ const value = internalDateValue.startDate
191
+ ? format(internalDateValue.startDate, "yyyy-MM-dd")
192
+ : "";
193
+ onFieldChange(cardConfig.dateField.field, value);
194
+ setIsDateSelectorOpen(false);
195
+ };
196
+
197
+ const handleClearDate = () => {
198
+ if (!cardConfig.dateField || !onFieldChange) return;
199
+ setInternalDateValue(createSingleDayValue(undefined));
200
+ onFieldChange(cardConfig.dateField.field, "");
201
+ setIsDateSelectorOpen(false);
202
+ };
203
+
204
+ const handleCancelDate = () => {
205
+ setIsDateSelectorOpen(false);
206
+ };
207
+
208
+ const handlePriorityChange = (value: string) => {
209
+ if (!cardConfig.badgeField || !onFieldChange) return;
210
+ onFieldChange(cardConfig.badgeField.field, value);
211
+ };
212
+
213
+ return (
214
+ <div className="board-card-content-inner">
215
+ <div className="board-card-title">{titleContent ?? title}</div>
216
+
217
+ {cardConfig.badgeField && badgeValue ? (
218
+ <div className="board-card-badge">
219
+ {canEditPriority ? (
220
+ <div
221
+ onPointerDown={(e) => e.stopPropagation()}
222
+ onClick={(e) => e.stopPropagation()}
223
+ onDoubleClick={(e) => e.stopPropagation()}
224
+ >
225
+ <Select value={badgeValue} onValueChange={handlePriorityChange}>
226
+ <SelectTrigger aria-label="Priority" data-testid="board-card-priority-select">
227
+ <SelectValue />
228
+ </SelectTrigger>
229
+ <SelectContent>
230
+ {badgeSelectOptions.map((option) => (
231
+ <SelectItem key={option} value={option}>
232
+ {option}
233
+ </SelectItem>
234
+ ))}
235
+ </SelectContent>
236
+ </Select>
237
+ </div>
238
+ ) : (
239
+ <Badge color={badgeColor}>{badgeValue}</Badge>
240
+ )}
241
+ </div>
242
+ ) : null}
243
+
244
+ {(cardConfig.avatarField || cardConfig.dateField) && (
245
+ <div className="board-card-footer">
246
+ {cardConfig.avatarField ? (
247
+ canEditAssignee ? (
248
+ <AssigneeSelector
249
+ open={isAssigneeSelectorOpen}
250
+ onOpenChange={setIsAssigneeSelectorOpen}
251
+ assignees={assignees ?? []}
252
+ selectedId={currentAssignee?.id}
253
+ onSelect={handleAssigneeSelect}
254
+ mode="popover"
255
+ align="start"
256
+ >
257
+ <button
258
+ type="button"
259
+ className="board-card-avatar board-card-avatar-editable"
260
+ title={avatarValue ? `${avatarValue} (click to change)` : "Assign someone"}
261
+ onClick={(e) => {
262
+ e.stopPropagation();
263
+ setIsAssigneeSelectorOpen(true);
264
+ }}
265
+ onDoubleClick={(e) => e.stopPropagation()}
266
+ >
267
+ {getInitials(avatarValue)}
268
+ </button>
269
+ </AssigneeSelector>
270
+ ) : (
271
+ <span className="board-card-avatar" title={avatarValue || "Unassigned"}>
272
+ {getInitials(avatarValue)}
273
+ </span>
274
+ )
275
+ ) : null}
276
+
277
+ {cardConfig.dateField ? (
278
+ canEditDate ? (
279
+ <Popover.Root open={isDateSelectorOpen} onOpenChange={setIsDateSelectorOpen}>
280
+ <Popover.Trigger asChild>
281
+ <button
282
+ type="button"
283
+ className="board-card-date-control"
284
+ onClick={handleOpenDatePicker}
285
+ onDoubleClick={(e) => e.stopPropagation()}
286
+ title={dateValue ? "Edit date" : "Add date"}
287
+ aria-label={dateValue ? "Edit date" : "Add date"}
288
+ >
289
+ <span className="board-card-date-icon-circle">
290
+ <CalendarIcon />
291
+ </span>
292
+ {dueDateLabel ? <span className="board-card-date-label">{dueDateLabel}</span> : null}
293
+ </button>
294
+ </Popover.Trigger>
295
+ <Popover.Portal>
296
+ <Popover.Content
297
+ className="board-card-date-popover"
298
+ side="bottom"
299
+ align="start"
300
+ sideOffset={8}
301
+ onClick={(e) => e.stopPropagation()}
302
+ onDoubleClick={(e) => e.stopPropagation()}
303
+ >
304
+ <DateSelector
305
+ value={internalDateValue}
306
+ onChange={setInternalDateValue}
307
+ allowRange={false}
308
+ periodTypes={["day"]}
309
+ presetMode="is"
310
+ showInput={false}
311
+ />
312
+ <div className="board-card-date-actions">
313
+ <SpaceBetween direction="horizontal" size="xs">
314
+ <Button
315
+ variant="normal"
316
+ onClick={handleClearDate}
317
+ disabled={!dateValue && !internalDateValue}
318
+ >
319
+ Clear
320
+ </Button>
321
+ <Button variant="normal" onClick={handleCancelDate}>
322
+ Cancel
323
+ </Button>
324
+ <Button variant="primary" onClick={handleApplyDate}>
325
+ Apply
326
+ </Button>
327
+ </SpaceBetween>
328
+ </div>
329
+ </Popover.Content>
330
+ </Popover.Portal>
331
+ </Popover.Root>
332
+ ) : dueDateLabel ? (
333
+ <span className="board-card-date-label">{dueDateLabel}</span>
334
+ ) : null
335
+ ) : null}
336
+ </div>
337
+ )}
338
+ </div>
339
+ );
340
+ }
@@ -0,0 +1,249 @@
1
+ import * as React from "react";
2
+ import {
3
+ endOfMonth,
4
+ endOfYear,
5
+ startOfMonth,
6
+ startOfYear,
7
+ subDays,
8
+ subMonths,
9
+ subYears,
10
+ addDays,
11
+ addMonths,
12
+ format,
13
+ } from "date-fns";
14
+ import type { DateRange } from "react-day-picker";
15
+ import { Calendar as CalendarIcon, ChevronDown } from "lucide-react";
16
+
17
+ import { Button } from "@/components/ui/button";
18
+ import { Calendar } from "@/components/ui/calendar";
19
+ import { Card, CardContent, CardFooter } from "@/components/ui/card";
20
+ import {
21
+ Popover,
22
+ PopoverContent,
23
+ PopoverTrigger,
24
+ } from "@/components/ui/popover";
25
+ import type { DateRangeState } from "./types";
26
+
27
+ interface DateRangeFilterProps {
28
+ dateRange: DateRangeState;
29
+ onDateRangeChange: (dateRange: DateRangeState) => void;
30
+ label?: string;
31
+ }
32
+
33
+ export function DateRangeFilter({
34
+ dateRange,
35
+ onDateRangeChange,
36
+ label = "Date",
37
+ }: DateRangeFilterProps) {
38
+ const today = new Date();
39
+ const [month, setMonth] = React.useState(dateRange.from ?? today);
40
+ const [open, setOpen] = React.useState(false);
41
+
42
+ const yesterday = {
43
+ from: subDays(today, 1),
44
+ to: subDays(today, 1),
45
+ };
46
+
47
+ const tomorrow = {
48
+ from: today,
49
+ to: addDays(today, 1),
50
+ };
51
+
52
+ const last7Days = {
53
+ from: subDays(today, 6),
54
+ to: today,
55
+ };
56
+
57
+ const next7Days = {
58
+ from: addDays(today, 1),
59
+ to: addDays(today, 7),
60
+ };
61
+
62
+ const last30Days = {
63
+ from: subDays(today, 29),
64
+ to: today,
65
+ };
66
+
67
+ const monthToDate = {
68
+ from: startOfMonth(today),
69
+ to: today,
70
+ };
71
+
72
+ const lastMonth = {
73
+ from: startOfMonth(subMonths(today, 1)),
74
+ to: endOfMonth(subMonths(today, 1)),
75
+ };
76
+
77
+ const nextMonth = {
78
+ from: startOfMonth(addMonths(today, 1)),
79
+ to: endOfMonth(addMonths(today, 1)),
80
+ };
81
+
82
+ const yearToDate = {
83
+ from: startOfYear(today),
84
+ to: today,
85
+ };
86
+
87
+ const lastYear = {
88
+ from: startOfYear(subYears(today, 1)),
89
+ to: endOfYear(subYears(today, 1)),
90
+ };
91
+
92
+ const handleSelect = (newDate: DateRange | undefined) => {
93
+ if (newDate) {
94
+ onDateRangeChange({ from: newDate.from, to: newDate.to });
95
+ }
96
+ };
97
+
98
+ const handlePresetClick = (preset: { from: Date; to: Date }) => {
99
+ onDateRangeChange(preset);
100
+ setMonth(preset.to);
101
+ };
102
+
103
+ const handleClear = () => {
104
+ onDateRangeChange({ from: undefined, to: undefined });
105
+ setOpen(false);
106
+ };
107
+
108
+ const formatDateRange = () => {
109
+ if (!dateRange.from) return label;
110
+ if (!dateRange.to || dateRange.from.getTime() === dateRange.to.getTime()) {
111
+ return format(dateRange.from, "MMM d, yyyy");
112
+ }
113
+ if (
114
+ dateRange.from.getFullYear() === dateRange.to.getFullYear() &&
115
+ dateRange.from.getMonth() === dateRange.to.getMonth()
116
+ ) {
117
+ return `${format(dateRange.from, "MMM d")} - ${format(dateRange.to, "d, yyyy")}`;
118
+ }
119
+ if (dateRange.from.getFullYear() === dateRange.to.getFullYear()) {
120
+ return `${format(dateRange.from, "MMM d")} - ${format(dateRange.to, "MMM d, yyyy")}`;
121
+ }
122
+ return `${format(dateRange.from, "MMM d, yyyy")} - ${format(dateRange.to, "MMM d, yyyy")}`;
123
+ };
124
+
125
+ const hasValue = dateRange.from !== undefined;
126
+
127
+ return (
128
+ <Popover open={open} onOpenChange={setOpen}>
129
+ <PopoverTrigger asChild>
130
+ <button
131
+ className="board-dropdown-trigger"
132
+ data-testid="board-date-range-select"
133
+ >
134
+ <CalendarIcon size={16} />
135
+ <span>{formatDateRange()}</span>
136
+ <ChevronDown size={14} />
137
+ </button>
138
+ </PopoverTrigger>
139
+ <PopoverContent className="w-auto p-0" align="start">
140
+ <Card className="max-w-xs border-0 py-4 shadow-none">
141
+ <CardContent className="px-4">
142
+ <Calendar
143
+ mode="range"
144
+ selected={{ from: dateRange.from, to: dateRange.to }}
145
+ onSelect={handleSelect}
146
+ month={month}
147
+ onMonthChange={setMonth}
148
+ className="w-full bg-transparent p-0"
149
+ />
150
+ </CardContent>
151
+ <CardFooter className="flex flex-wrap gap-2 border-t px-4 !pt-4">
152
+ <Button
153
+ variant="outline"
154
+ size="sm"
155
+ onClick={() => {
156
+ onDateRangeChange({ from: today, to: today });
157
+ setMonth(today);
158
+ }}
159
+ >
160
+ Today
161
+ </Button>
162
+ <Button
163
+ variant="outline"
164
+ size="sm"
165
+ onClick={() => handlePresetClick(yesterday)}
166
+ >
167
+ Yesterday
168
+ </Button>
169
+ <Button
170
+ variant="outline"
171
+ size="sm"
172
+ onClick={() => handlePresetClick(tomorrow)}
173
+ >
174
+ Tomorrow
175
+ </Button>
176
+ <Button
177
+ variant="outline"
178
+ size="sm"
179
+ onClick={() => handlePresetClick(last7Days)}
180
+ >
181
+ Last 7 days
182
+ </Button>
183
+ <Button
184
+ variant="outline"
185
+ size="sm"
186
+ onClick={() => handlePresetClick(next7Days)}
187
+ >
188
+ Next 7 days
189
+ </Button>
190
+ <Button
191
+ variant="outline"
192
+ size="sm"
193
+ onClick={() => handlePresetClick(last30Days)}
194
+ >
195
+ Last 30 days
196
+ </Button>
197
+ <Button
198
+ variant="outline"
199
+ size="sm"
200
+ onClick={() => handlePresetClick(monthToDate)}
201
+ >
202
+ Month to date
203
+ </Button>
204
+ <Button
205
+ variant="outline"
206
+ size="sm"
207
+ onClick={() => handlePresetClick(lastMonth)}
208
+ >
209
+ Last month
210
+ </Button>
211
+ <Button
212
+ variant="outline"
213
+ size="sm"
214
+ onClick={() => handlePresetClick(nextMonth)}
215
+ >
216
+ Next month
217
+ </Button>
218
+ <Button
219
+ variant="outline"
220
+ size="sm"
221
+ onClick={() => handlePresetClick(yearToDate)}
222
+ >
223
+ Year to date
224
+ </Button>
225
+ <Button
226
+ variant="outline"
227
+ size="sm"
228
+ onClick={() => handlePresetClick(lastYear)}
229
+ >
230
+ Last year
231
+ </Button>
232
+ </CardFooter>
233
+ {hasValue && (
234
+ <div className="border-t px-4 py-3">
235
+ <Button
236
+ variant="ghost"
237
+ size="sm"
238
+ className="w-full"
239
+ onClick={handleClear}
240
+ >
241
+ Clear
242
+ </Button>
243
+ </div>
244
+ )}
245
+ </Card>
246
+ </PopoverContent>
247
+ </Popover>
248
+ );
249
+ }
@@ -0,0 +1,19 @@
1
+ export { SectionedListBoard } from "./sectioned-list-board";
2
+ export { DateRangeFilter } from "./date-range-filter";
3
+ // Re-export AssigneeSelector from shared ui location
4
+ export { AssigneeSelector } from "@/components/ui/assignee-selector";
5
+ export type {
6
+ Assignee,
7
+ BoardSection,
8
+ BoardCardConfig,
9
+ BoardBadgeColor,
10
+ BoardTaskAction,
11
+ FilterOption,
12
+ SortOption,
13
+ SortState,
14
+ FilterState,
15
+ DateRangeState,
16
+ SectionedListBoardProps,
17
+ ItemOpenEvent,
18
+ DrawerProps,
19
+ } from "./types";