@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,314 @@
1
+ import * as React from "react";
2
+ import { useSortable } from "@dnd-kit/sortable";
3
+ import {
4
+ ContextMenu,
5
+ ContextMenuContent,
6
+ ContextMenuItem,
7
+ ContextMenuSeparator,
8
+ ContextMenuTrigger,
9
+ } from "@/components/ui/context-menu";
10
+ import type { BoardTaskAction, SortableBoardCardProps } from "./types";
11
+ import { BoardCardContent } from "./board-card-content";
12
+
13
+ function toText(value: unknown): string {
14
+ if (value == null) return "";
15
+ if (typeof value === "string") return value;
16
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
17
+ return "";
18
+ }
19
+
20
+ export function SortableBoardCard<T>({
21
+ item,
22
+ itemId,
23
+ index,
24
+ sectionId,
25
+ cardConfig,
26
+ isCompleted,
27
+ isDragDisabled,
28
+ onTitleEdit,
29
+ onComplete,
30
+ onTaskAction,
31
+ onFieldChange,
32
+ onItemOpen,
33
+ assignees,
34
+ }: SortableBoardCardProps<T>) {
35
+ const [isEditing, setIsEditing] = React.useState(false);
36
+ const [editTitle, setEditTitle] = React.useState("");
37
+ const inputRef = React.useRef<HTMLInputElement>(null);
38
+ const justDragged = React.useRef(false);
39
+ const clickTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
40
+ const title = toText((item as Record<string, unknown>)[cardConfig.titleField]);
41
+
42
+ const {
43
+ attributes,
44
+ listeners,
45
+ setNodeRef,
46
+ transform,
47
+ transition,
48
+ isDragging,
49
+ } = useSortable({
50
+ id: itemId,
51
+ disabled: isDragDisabled,
52
+ data: { type: "card", sectionId, index },
53
+ });
54
+
55
+ // Track when drag ends to prevent click from firing
56
+ React.useEffect(() => {
57
+ if (!isDragging && justDragged.current === false) {
58
+ // Not dragging and wasn't dragging
59
+ } else if (isDragging) {
60
+ justDragged.current = true;
61
+ } else if (!isDragging && justDragged.current) {
62
+ // Drag just ended, keep flag true briefly
63
+ const timeout = setTimeout(() => {
64
+ justDragged.current = false;
65
+ }, 100);
66
+ return () => clearTimeout(timeout);
67
+ }
68
+ }, [isDragging]);
69
+
70
+ const style: React.CSSProperties = {
71
+ transform: transform
72
+ ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
73
+ : undefined,
74
+ transition,
75
+ opacity: isDragging ? 0.5 : 1,
76
+ };
77
+ const draggableProps = !isDragDisabled ? { ...attributes, ...listeners } : {};
78
+
79
+ const handleDoubleClick = () => {
80
+ // Cancel any pending single-click action
81
+ if (clickTimeoutRef.current) {
82
+ clearTimeout(clickTimeoutRef.current);
83
+ clickTimeoutRef.current = null;
84
+ }
85
+ if (!onTitleEdit) return;
86
+ setEditTitle(title);
87
+ setIsEditing(true);
88
+ setTimeout(() => inputRef.current?.focus(), 0);
89
+ };
90
+
91
+ const handleBlur = () => {
92
+ setIsEditing(false);
93
+ if (editTitle.trim() && editTitle !== title) {
94
+ onTitleEdit?.(editTitle.trim());
95
+ }
96
+ };
97
+
98
+ const handleKeyDown = (e: React.KeyboardEvent) => {
99
+ if (e.key === "Enter") {
100
+ handleBlur();
101
+ } else if (e.key === "Escape") {
102
+ setIsEditing(false);
103
+ setEditTitle(title);
104
+ }
105
+ };
106
+
107
+ // Handle click to open item details
108
+ // Use a delay to allow double-click to cancel single click (for title editing)
109
+ const handleClick = (e: React.MouseEvent) => {
110
+ // Don't open if we just finished dragging
111
+ if (justDragged.current) {
112
+ return;
113
+ }
114
+
115
+ // Don't open if clicking interactive elements
116
+ const target = e.target as HTMLElement;
117
+ if (
118
+ target.closest("[data-no-item-open]") ||
119
+ target.closest("input") ||
120
+ target.closest("button") ||
121
+ target.closest('[role="checkbox"]') ||
122
+ target.closest('[data-slot="select"]') ||
123
+ target.closest('[data-slot="popover"]') ||
124
+ target.closest('[role="combobox"]')
125
+ ) {
126
+ return;
127
+ }
128
+
129
+ // Don't open if currently editing
130
+ if (isEditing) {
131
+ return;
132
+ }
133
+
134
+ // If title editing is enabled, delay the click to allow double-click to cancel it
135
+ if (onTitleEdit) {
136
+ // Clear any existing timeout
137
+ if (clickTimeoutRef.current) {
138
+ clearTimeout(clickTimeoutRef.current);
139
+ }
140
+ // Delay the item open to allow double-click to cancel
141
+ clickTimeoutRef.current = setTimeout(() => {
142
+ clickTimeoutRef.current = null;
143
+ onItemOpen?.("click");
144
+ }, 200); // 200ms is a typical double-click threshold
145
+ } else {
146
+ // No title editing, open immediately
147
+ onItemOpen?.("click");
148
+ }
149
+ };
150
+
151
+ // Handle keyboard to open item details
152
+ const handleCardKeyDown = (e: React.KeyboardEvent) => {
153
+ // Only handle Enter when not editing and not on interactive elements
154
+ if (e.key === "Enter" && !isEditing) {
155
+ const target = e.target as HTMLElement;
156
+ if (
157
+ target.closest("input") ||
158
+ target.closest("button") ||
159
+ target.closest('[role="checkbox"]')
160
+ ) {
161
+ return;
162
+ }
163
+ e.preventDefault();
164
+ onItemOpen?.("keyboard");
165
+ }
166
+ };
167
+
168
+ const taskLink = React.useMemo(() => {
169
+ if (typeof window === "undefined") return null;
170
+ const url = new URL(window.location.href);
171
+ url.hash = `task-${itemId}`;
172
+ return url.toString();
173
+ }, [itemId]);
174
+
175
+ const handleTaskAction = (action: BoardTaskAction) => {
176
+ onTaskAction?.(action);
177
+
178
+ if (action === "markComplete") {
179
+ onComplete?.(!isCompleted);
180
+ return;
181
+ }
182
+
183
+ if (action === "copyTaskLink") {
184
+ if (taskLink && typeof navigator !== "undefined" && navigator.clipboard) {
185
+ void navigator.clipboard.writeText(taskLink);
186
+ }
187
+ return;
188
+ }
189
+
190
+ if (action === "openTaskDetails") {
191
+ // Use callback if provided, otherwise fall back to hash navigation
192
+ if (onItemOpen) {
193
+ onItemOpen("context-menu");
194
+ } else if (taskLink && typeof window !== "undefined") {
195
+ window.location.hash = `task-${itemId}`;
196
+ }
197
+ return;
198
+ }
199
+
200
+ if (action === "openInNewTab") {
201
+ if (taskLink && typeof window !== "undefined") {
202
+ window.open(taskLink, "_blank", "noopener,noreferrer");
203
+ }
204
+ }
205
+ };
206
+
207
+ return (
208
+ <ContextMenu>
209
+ <ContextMenuTrigger asChild>
210
+ <div
211
+ ref={setNodeRef}
212
+ style={style}
213
+ className={`board-card ${isCompleted ? "completed" : ""} ${isDragging ? "dragging" : ""} ${!isDragDisabled ? "draggable" : "drag-disabled"}`}
214
+ data-testid={`board-card-${itemId}`}
215
+ {...draggableProps}
216
+ onClick={handleClick}
217
+ onDoubleClick={handleDoubleClick}
218
+ onKeyDown={handleCardKeyDown}
219
+ tabIndex={0}
220
+ role="button"
221
+ >
222
+ {/* Card Content */}
223
+ {/* Completion Checkbox */}
224
+ {onComplete && (
225
+ <div className="card-checkbox">
226
+ <input
227
+ type="checkbox"
228
+ checked={isCompleted}
229
+ onChange={(e) => onComplete(e.target.checked)}
230
+ onPointerDown={(e) => e.stopPropagation()}
231
+ onClick={(e) => e.stopPropagation()}
232
+ data-testid={`card-checkbox-${itemId}`}
233
+ aria-label="Mark task complete"
234
+ />
235
+ </div>
236
+ )}
237
+
238
+ <div className="card-content">
239
+ {isEditing ? (
240
+ <input
241
+ ref={inputRef}
242
+ type="text"
243
+ className="card-title-input"
244
+ value={editTitle}
245
+ onChange={(e) => setEditTitle(e.target.value)}
246
+ onBlur={handleBlur}
247
+ onKeyDown={handleKeyDown}
248
+ onPointerDown={(e) => e.stopPropagation()}
249
+ onClick={(e) => e.stopPropagation()}
250
+ data-testid={`card-title-input-${itemId}`}
251
+ />
252
+ ) : (
253
+ <BoardCardContent
254
+ item={item}
255
+ cardConfig={cardConfig}
256
+ onFieldChange={onFieldChange}
257
+ interactive
258
+ assignees={assignees}
259
+ />
260
+ )}
261
+ </div>
262
+ </div>
263
+ </ContextMenuTrigger>
264
+ <ContextMenuContent
265
+ data-testid={`board-card-context-menu-${itemId}`}
266
+ >
267
+ <ContextMenuItem
268
+ data-testid={`board-card-duplicate-${itemId}`}
269
+ onSelect={() => handleTaskAction("duplicate")}
270
+ >
271
+ Duplicate Task
272
+ </ContextMenuItem>
273
+ <ContextMenuItem
274
+ data-testid={`board-card-mark-complete-${itemId}`}
275
+ onSelect={() => handleTaskAction("markComplete")}
276
+ >
277
+ Mark Complete
278
+ </ContextMenuItem>
279
+ <ContextMenuItem
280
+ data-testid={`board-card-add-subtask-${itemId}`}
281
+ onSelect={() => handleTaskAction("addSubtask")}
282
+ >
283
+ Add Subtask
284
+ </ContextMenuItem>
285
+ <ContextMenuItem
286
+ data-testid={`board-card-open-details-${itemId}`}
287
+ onSelect={() => handleTaskAction("openTaskDetails")}
288
+ >
289
+ Open Task Details
290
+ </ContextMenuItem>
291
+ <ContextMenuItem
292
+ data-testid={`board-card-open-new-tab-${itemId}`}
293
+ onSelect={() => handleTaskAction("openInNewTab")}
294
+ >
295
+ Open in new Tab
296
+ </ContextMenuItem>
297
+ <ContextMenuItem
298
+ data-testid={`board-card-copy-link-${itemId}`}
299
+ onSelect={() => handleTaskAction("copyTaskLink")}
300
+ >
301
+ Copy Task Link
302
+ </ContextMenuItem>
303
+ <ContextMenuSeparator />
304
+ <ContextMenuItem
305
+ data-testid={`board-card-delete-${itemId}`}
306
+ variant="destructive"
307
+ onSelect={() => handleTaskAction("delete")}
308
+ >
309
+ Delete Task
310
+ </ContextMenuItem>
311
+ </ContextMenuContent>
312
+ </ContextMenu>
313
+ );
314
+ }
@@ -0,0 +1,319 @@
1
+ import * as React from "react";
2
+ import {
3
+ SortableContext,
4
+ useSortable,
5
+ verticalListSortingStrategy,
6
+ } from "@dnd-kit/sortable";
7
+ import { MoreHorizontal, Plus } from "lucide-react";
8
+ import type { SortableBoardSectionProps } from "./types";
9
+ import { SortableBoardCard } from "./sortable-card";
10
+
11
+ export function SortableBoardSection<T>({
12
+ sortableId,
13
+ section,
14
+ getItemId,
15
+ cardConfig,
16
+ onItemFieldChange,
17
+ onToggle,
18
+ onRename,
19
+ onAddItem,
20
+ onDelete,
21
+ onItemTitleEdit,
22
+ onItemComplete,
23
+ onItemAction,
24
+ onItemOpen,
25
+ isDragDisabled,
26
+ isItemCompleted,
27
+ collapseDuringDrag = false,
28
+ dropIndicatorPosition = null,
29
+ cardDropIndicatorIndex = null,
30
+ assignees,
31
+ }: SortableBoardSectionProps<T>) {
32
+ const [isEditing, setIsEditing] = React.useState(false);
33
+ const [editName, setEditName] = React.useState(section.name);
34
+ const [showContextMenu, setShowContextMenu] = React.useState(false);
35
+ const [contextMenuPos, setContextMenuPos] = React.useState({ x: 0, y: 0 });
36
+ const inputRef = React.useRef<HTMLInputElement>(null);
37
+
38
+ const {
39
+ attributes,
40
+ listeners,
41
+ setNodeRef,
42
+ transform,
43
+ transition,
44
+ isDragging,
45
+ } = useSortable({
46
+ id: sortableId,
47
+ data: { type: "section", sectionId: section.id },
48
+ });
49
+
50
+ const style: React.CSSProperties = {
51
+ transform: transform
52
+ ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
53
+ : undefined,
54
+ transition,
55
+ opacity: isDragging ? 0.75 : 1,
56
+ };
57
+
58
+ const isContentCollapsed =
59
+ Boolean(section.collapsed) || isDragging || collapseDuringDrag;
60
+ const cardIds = section.items.map(getItemId);
61
+
62
+ const startRename = () => {
63
+ setIsEditing(true);
64
+ setEditName(section.name);
65
+ setTimeout(() => inputRef.current?.focus(), 0);
66
+ };
67
+
68
+ const handleDoubleClick = () => {
69
+ startRename();
70
+ };
71
+
72
+ const handleBlur = () => {
73
+ setIsEditing(false);
74
+ if (editName.trim() && editName !== section.name) {
75
+ onRename(editName.trim());
76
+ }
77
+ };
78
+
79
+ const handleKeyDown = (e: React.KeyboardEvent) => {
80
+ if (e.key === "Enter") {
81
+ handleBlur();
82
+ } else if (e.key === "Escape") {
83
+ setIsEditing(false);
84
+ setEditName(section.name);
85
+ }
86
+ };
87
+
88
+ const handleContextMenu = (e: React.MouseEvent) => {
89
+ e.preventDefault();
90
+ setContextMenuPos({ x: e.clientX, y: e.clientY });
91
+ setShowContextMenu(true);
92
+ };
93
+
94
+ const handleMenuButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ const rect = e.currentTarget.getBoundingClientRect();
98
+ setContextMenuPos({ x: rect.left, y: rect.bottom + 6 });
99
+ setShowContextMenu((prev) => !prev);
100
+ };
101
+
102
+ const handleRenameFromMenu = () => {
103
+ setShowContextMenu(false);
104
+ startRename();
105
+ };
106
+
107
+ const handleToggleFromMenu = () => {
108
+ setShowContextMenu(false);
109
+ onToggle();
110
+ };
111
+
112
+ const handleDeleteFromMenu = () => {
113
+ setShowContextMenu(false);
114
+ onDelete();
115
+ };
116
+
117
+ React.useEffect(() => {
118
+ const handleClick = () => setShowContextMenu(false);
119
+ if (showContextMenu) {
120
+ document.addEventListener("click", handleClick);
121
+ return () => document.removeEventListener("click", handleClick);
122
+ }
123
+ }, [showContextMenu]);
124
+
125
+ return (
126
+ <div
127
+ ref={setNodeRef}
128
+ style={style}
129
+ className={`board-section ${isDragging ? "dragging" : ""}`}
130
+ data-testid={`board-section-${section.id}`}
131
+ >
132
+ {dropIndicatorPosition === "before" && (
133
+ <div className="section-drop-indicator section-drop-indicator-before" />
134
+ )}
135
+
136
+ <div
137
+ className="board-section-header"
138
+ onContextMenu={handleContextMenu}
139
+ >
140
+ <button
141
+ className="section-drag-handle"
142
+ {...attributes}
143
+ {...listeners}
144
+ data-testid={`section-drag-handle-${section.id}`}
145
+ title="Drag to reorder section"
146
+ >
147
+ <span className="drag-icon">&#x2847;</span>
148
+ </button>
149
+
150
+ <button
151
+ className="section-collapse-toggle"
152
+ onClick={onToggle}
153
+ data-testid={`section-toggle-${section.id}`}
154
+ aria-expanded={!isContentCollapsed}
155
+ >
156
+ <span className={`chevron ${isContentCollapsed ? "collapsed" : ""}`}>
157
+ &#x25BC;
158
+ </span>
159
+ </button>
160
+
161
+ {isEditing ? (
162
+ <input
163
+ ref={inputRef}
164
+ type="text"
165
+ className="section-name-input"
166
+ value={editName}
167
+ onChange={(e) => setEditName(e.target.value)}
168
+ onBlur={handleBlur}
169
+ onKeyDown={handleKeyDown}
170
+ data-testid={`section-name-input-${section.id}`}
171
+ />
172
+ ) : (
173
+ <span
174
+ className="section-name"
175
+ onDoubleClick={handleDoubleClick}
176
+ data-testid={`section-name-${section.id}`}
177
+ >
178
+ {section.name}
179
+ </span>
180
+ )}
181
+
182
+ <span className="section-count">({section.items.length})</span>
183
+ <div className="section-header-actions">
184
+ <button
185
+ className="section-add-task-header-btn"
186
+ onClick={onAddItem}
187
+ data-testid={`section-add-task-header-${section.id}`}
188
+ aria-label="Add task"
189
+ title="Add task"
190
+ >
191
+ <Plus className="section-header-action-icon" size={16} />
192
+ </button>
193
+ <button
194
+ className="section-menu-btn"
195
+ onClick={handleMenuButtonClick}
196
+ data-testid={`section-menu-trigger-${section.id}`}
197
+ aria-haspopup="menu"
198
+ aria-expanded={showContextMenu}
199
+ aria-label="Section options"
200
+ title="Section options"
201
+ >
202
+ <MoreHorizontal className="section-header-action-icon" size={20} />
203
+ </button>
204
+ </div>
205
+
206
+ </div>
207
+
208
+ {showContextMenu && (
209
+ <div
210
+ className="context-menu"
211
+ style={{ left: contextMenuPos.x, top: contextMenuPos.y }}
212
+ data-testid={`section-context-menu-${section.id}`}
213
+ >
214
+ <button onClick={handleRenameFromMenu} data-testid={`section-rename-${section.id}`}>
215
+ Rename section
216
+ </button>
217
+ <button onClick={handleToggleFromMenu} data-testid={`section-toggle-menu-${section.id}`}>
218
+ {section.collapsed ? "Expand section" : "Collapse section"}
219
+ </button>
220
+ <button
221
+ className="destructive"
222
+ onClick={handleDeleteFromMenu}
223
+ data-testid={`section-delete-${section.id}`}
224
+ >
225
+ Delete section
226
+ </button>
227
+ </div>
228
+ )}
229
+
230
+ {!isContentCollapsed && (
231
+ <div className="board-section-content">
232
+ <SortableContext items={cardIds} strategy={verticalListSortingStrategy}>
233
+ {section.items.length === 0 ? (
234
+ <div className="board-section-empty" data-testid={`section-empty-${section.id}`}>
235
+ <button
236
+ className="board-add-task-button board-add-task-button-empty"
237
+ onClick={onAddItem}
238
+ data-testid={`section-add-task-${section.id}`}
239
+ >
240
+ + Add Task
241
+ </button>
242
+ </div>
243
+ ) : (
244
+ <>
245
+ {section.items.map((item, index) => (
246
+ <React.Fragment key={getItemId(item)}>
247
+ {cardDropIndicatorIndex === index && (
248
+ <div className="card-drop-indicator" data-testid="card-drop-indicator" />
249
+ )}
250
+ <SortableBoardCard
251
+ item={item}
252
+ itemId={getItemId(item)}
253
+ index={index}
254
+ sectionId={section.id}
255
+ cardConfig={cardConfig}
256
+ isCompleted={isItemCompleted?.(item) ?? false}
257
+ isDragDisabled={isDragDisabled}
258
+ onTitleEdit={
259
+ onItemTitleEdit
260
+ ? (title) => onItemTitleEdit(getItemId(item), title)
261
+ : undefined
262
+ }
263
+ onComplete={
264
+ onItemComplete
265
+ ? (completed) => onItemComplete(getItemId(item), completed)
266
+ : undefined
267
+ }
268
+ onFieldChange={
269
+ onItemFieldChange
270
+ ? (field, value) => onItemFieldChange(getItemId(item), field, value)
271
+ : undefined
272
+ }
273
+ onTaskAction={
274
+ onItemAction
275
+ ? (action) => onItemAction(getItemId(item), action)
276
+ : undefined
277
+ }
278
+ onItemOpen={
279
+ onItemOpen
280
+ ? (source) => {
281
+ console.log("[SortableBoardSection] onItemOpen wrapper called, source:", source);
282
+ onItemOpen({
283
+ item,
284
+ itemId: getItemId(item),
285
+ sectionId: section.id,
286
+ source,
287
+ });
288
+ }
289
+ : undefined
290
+ }
291
+ assignees={assignees}
292
+ />
293
+ </React.Fragment>
294
+ ))}
295
+ {cardDropIndicatorIndex === section.items.length && (
296
+ <div className="card-drop-indicator" data-testid="card-drop-indicator" />
297
+ )}
298
+ <button
299
+ className="board-add-task-button"
300
+ onClick={onAddItem}
301
+ data-testid={`section-add-task-${section.id}`}
302
+ >
303
+ + Add Task
304
+ </button>
305
+ </>
306
+ )}
307
+ {section.items.length === 0 && cardDropIndicatorIndex === section.items.length && (
308
+ <div className="card-drop-indicator" data-testid="card-drop-indicator" />
309
+ )}
310
+ </SortableContext>
311
+ </div>
312
+ )}
313
+
314
+ {dropIndicatorPosition === "after" && (
315
+ <div className="section-drop-indicator section-drop-indicator-after" />
316
+ )}
317
+ </div>
318
+ );
319
+ }