@yogiswara/honcho-editor-ui 3.9.1 → 4.0.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,22 @@
1
+ import { type FC } from "react";
2
+ export interface AlbumSection {
3
+ id: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ interface Props {
8
+ sections: AlbumSection[];
9
+ activeSectionIndex: number;
10
+ onSectionChange: (index: number) => void;
11
+ onSectionsReorder: (reordered: AlbumSection[]) => void;
12
+ onAddSection: (name: string, description: string) => void;
13
+ onEditSection?: (index: number, name: string, description: string) => void;
14
+ onDeleteSection?: (sectionId: string) => void;
15
+ compact?: boolean;
16
+ selectedMode?: boolean;
17
+ onLoadMore?: () => void;
18
+ isLoadingMoreSections?: boolean;
19
+ hasMoreSections?: boolean;
20
+ }
21
+ declare const AlbumSectionTabs: FC<Props>;
22
+ export default AlbumSectionTabs;
@@ -0,0 +1,414 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState, useEffect } from "react";
3
+ import { Box, Typography, Menu, MenuItem, TextField, Stack, Skeleton } from "@mui/material";
4
+ import useHonchoTypography from '../../../themes/honchoTheme';
5
+ import useColorsSecond from '../../../themes/colours';
6
+ import useIsMobile from '../../../utils/isMobile';
7
+ import IcPlusButton from "../svg/IcPlusButton";
8
+ import { IcChevronArrowDarkColor } from "../svg/IcChevron";
9
+ import { IcPencilIconDark } from "../svg/IcPencilIcon";
10
+ import { IcTrashIcon } from "../svg/IcTrashIcon";
11
+ import { HBaseDialog, NegativeButton, PositiveButton } from "../../editor/SectionAlbum/HBaseDialog";
12
+ const DRAG_THRESHOLD = 4;
13
+ const AlbumSectionTabs = ({ sections, activeSectionIndex, onSectionChange, onSectionsReorder, onAddSection, onEditSection, onDeleteSection, compact = false, onLoadMore, isLoadingMoreSections = false, hasMoreSections = false, }) => {
14
+ const isMobile = useIsMobile();
15
+ const colors = useColorsSecond();
16
+ const typography = useHonchoTypography();
17
+ // ---------- drag state ----------
18
+ const [dragIndex, setDragIndex] = useState(null);
19
+ const [overIndex, setOverIndex] = useState(null);
20
+ const [isDragging, setIsDragging] = useState(false);
21
+ const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
22
+ const [openMenuIndex, setOpenMenuIndex] = useState(null);
23
+ const [menuAnchorEl, setMenuAnchorEl] = useState(null);
24
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
25
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
26
+ const [isCreateSectionDialogOpen, setIsCreateSectionDialogOpen] = useState(false);
27
+ const [selectedSectionIndex, setSelectedSectionIndex] = useState(null);
28
+ const [sectionName, setSectionName] = useState("");
29
+ const [sectionDescription, setSectionDescription] = useState("");
30
+ const pointerDownPos = useRef(null);
31
+ const pointerDownIndex = useRef(null);
32
+ const dragStarted = useRef(false);
33
+ const scrollRef = useRef(null);
34
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
35
+ const [canScrollRight, setCanScrollRight] = useState(false);
36
+ // Prevents firing onLoadMore multiple times for the same scroll position
37
+ const isLoadingMoreRef = useRef(false);
38
+ const updateScrollButtons = () => {
39
+ const el = scrollRef.current;
40
+ if (!el)
41
+ return;
42
+ setCanScrollLeft(el.scrollLeft > 0);
43
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
44
+ // Trigger load-more when within 80px of the right edge
45
+ const nearRightEdge = el.scrollLeft + el.clientWidth >= el.scrollWidth - 80;
46
+ if (nearRightEdge && hasMoreSections && !isLoadingMoreSections && !isLoadingMoreRef.current) {
47
+ isLoadingMoreRef.current = true;
48
+ onLoadMore?.();
49
+ }
50
+ if (!nearRightEdge) {
51
+ isLoadingMoreRef.current = false;
52
+ }
53
+ };
54
+ useEffect(() => {
55
+ updateScrollButtons();
56
+ const el = scrollRef.current;
57
+ if (!el)
58
+ return;
59
+ el.addEventListener("scroll", updateScrollButtons);
60
+ const ro = new ResizeObserver(updateScrollButtons);
61
+ ro.observe(el);
62
+ return () => {
63
+ el.removeEventListener("scroll", updateScrollButtons);
64
+ ro.disconnect();
65
+ };
66
+ }, [sections, hasMoreSections, isLoadingMoreSections]);
67
+ const scrollBy = (direction) => {
68
+ const el = scrollRef.current;
69
+ if (!el)
70
+ return;
71
+ el.scrollBy({ left: direction === "left" ? -160 : 160, behavior: "smooth" });
72
+ };
73
+ const activeSection = sections[activeSectionIndex]; // Get the current section
74
+ const handleEditSection = (index) => {
75
+ setOpenMenuIndex(null);
76
+ setMenuAnchorEl(null);
77
+ setSelectedSectionIndex(index);
78
+ setSectionName(sections[index].label);
79
+ setSectionDescription(sections[index].description || "");
80
+ setIsEditDialogOpen(true);
81
+ };
82
+ const handleDeleteSection = (index) => {
83
+ setOpenMenuIndex(null);
84
+ setMenuAnchorEl(null);
85
+ setSelectedSectionIndex(index);
86
+ setIsDeleteDialogOpen(true);
87
+ };
88
+ const handleCreateSection = () => {
89
+ setOpenMenuIndex(null);
90
+ setMenuAnchorEl(null);
91
+ setSectionName("");
92
+ setSectionDescription("");
93
+ setIsCreateSectionDialogOpen(true);
94
+ };
95
+ // ---------- pointer handlers ----------
96
+ const handlePointerDown = (e, index) => {
97
+ pointerDownPos.current = { x: e.clientX, y: e.clientY };
98
+ pointerDownIndex.current = index;
99
+ dragStarted.current = false;
100
+ e.currentTarget.setPointerCapture(e.pointerId);
101
+ };
102
+ const handlePointerMove = (e, index) => {
103
+ if (pointerDownPos.current === null)
104
+ return;
105
+ const dx = Math.abs(e.clientX - pointerDownPos.current.x);
106
+ const dy = Math.abs(e.clientY - pointerDownPos.current.y);
107
+ if (!dragStarted.current && (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD)) {
108
+ dragStarted.current = true;
109
+ setDragIndex(pointerDownIndex.current);
110
+ setIsDragging(true);
111
+ }
112
+ if (dragStarted.current) {
113
+ setCursorPos({ x: e.clientX, y: e.clientY });
114
+ const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
115
+ if (elementUnderPointer) {
116
+ const tabNode = elementUnderPointer.closest('[data-index]');
117
+ if (tabNode) {
118
+ const hoveredIndex = parseInt(tabNode.getAttribute('data-index') || '-1', 10);
119
+ if (hoveredIndex !== -1 && hoveredIndex !== overIndex) {
120
+ setOverIndex(hoveredIndex);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ };
126
+ const handlePointerUp = (e, index) => {
127
+ if (!dragStarted.current) {
128
+ onSectionChange(index);
129
+ }
130
+ else if (dragIndex !== null && overIndex !== null && dragIndex !== overIndex) {
131
+ const reordered = [...sections];
132
+ const [moved] = reordered.splice(dragIndex, 1);
133
+ reordered.splice(overIndex, 0, moved);
134
+ let newActive = activeSectionIndex;
135
+ if (activeSectionIndex === dragIndex) {
136
+ newActive = overIndex;
137
+ }
138
+ else if (dragIndex < activeSectionIndex && overIndex >= activeSectionIndex) {
139
+ newActive = activeSectionIndex - 1;
140
+ }
141
+ else if (dragIndex > activeSectionIndex && overIndex <= activeSectionIndex) {
142
+ newActive = activeSectionIndex + 1;
143
+ }
144
+ onSectionsReorder(reordered);
145
+ onSectionChange(newActive);
146
+ }
147
+ setDragIndex(null);
148
+ setOverIndex(null);
149
+ setIsDragging(false);
150
+ dragStarted.current = false;
151
+ pointerDownPos.current = null;
152
+ pointerDownIndex.current = null;
153
+ };
154
+ const handlePointerCancel = () => {
155
+ setDragIndex(null);
156
+ setOverIndex(null);
157
+ setIsDragging(false);
158
+ dragStarted.current = false;
159
+ pointerDownPos.current = null;
160
+ pointerDownIndex.current = null;
161
+ };
162
+ useEffect(() => {
163
+ if (!scrollRef.current)
164
+ return;
165
+ const timeout = setTimeout(() => {
166
+ const activeTabNode = scrollRef.current?.querySelector(`[data-index="${activeSectionIndex}"]`);
167
+ if (activeTabNode) {
168
+ activeTabNode.scrollIntoView({
169
+ behavior: "smooth",
170
+ block: "nearest",
171
+ inline: "center",
172
+ });
173
+ }
174
+ }, 50);
175
+ return () => clearTimeout(timeout);
176
+ }, [activeSectionIndex]);
177
+ // ---------- render ----------
178
+ return (_jsxs(Box, { sx: {
179
+ // Removed the border from here, moved to the tab row so it sits above the description
180
+ ...(compact ? {
181
+ width: "100%",
182
+ } : {
183
+ width: "100vw",
184
+ position: "relative",
185
+ left: "50%",
186
+ right: "50%",
187
+ marginLeft: "-50vw",
188
+ marginRight: "-50vw",
189
+ }),
190
+ }, children: [_jsxs(Box, { sx: { position: "relative" }, children: [!isMobile && canScrollLeft && (_jsx(Box, { onClick: () => scrollBy("left"), sx: {
191
+ position: "absolute",
192
+ left: 0,
193
+ top: 0,
194
+ bottom: "1px",
195
+ zIndex: 2,
196
+ display: "flex",
197
+ alignItems: "center",
198
+ px: "4px",
199
+ pl: "24px",
200
+ cursor: "pointer",
201
+ background: colors.surface,
202
+ }, children: _jsx(IcChevronArrowDarkColor, { sx: {
203
+ width: "20px",
204
+ height: "20px",
205
+ color: colors.onSurface,
206
+ transform: "rotate(90deg)",
207
+ } }) })), !isMobile && canScrollRight && (_jsx(Box, { onClick: () => scrollBy("right"), sx: {
208
+ position: "absolute",
209
+ right: 0,
210
+ top: 0,
211
+ bottom: "1px",
212
+ zIndex: 2,
213
+ display: "flex",
214
+ alignItems: "center",
215
+ px: "4px",
216
+ pr: "24px",
217
+ cursor: "pointer",
218
+ background: colors.surface,
219
+ }, children: _jsx(IcChevronArrowDarkColor, { sx: {
220
+ width: "20px",
221
+ height: "20px",
222
+ color: colors.onSurface,
223
+ transform: "rotate(270deg)",
224
+ } }) })), _jsxs(Box, { ref: scrollRef, sx: {
225
+ display: "flex",
226
+ flexDirection: "row",
227
+ alignItems: "flex-end", // Aligns active bars to bottom
228
+ px: isMobile ? "16px" : "24px",
229
+ gap: "20px",
230
+ overflowX: "auto",
231
+ overflowY: "visible",
232
+ "&::-webkit-scrollbar": { display: "none" },
233
+ scrollbarWidth: "none",
234
+ userSelect: "none",
235
+ borderBottom: "1px solid", // Bottom border lives here now
236
+ borderColor: colors.outlineVariant,
237
+ }, children: [sections.map((section, index) => {
238
+ const isActive = index === activeSectionIndex;
239
+ const isBeingDragged = index === dragIndex;
240
+ const isDropTarget = index === overIndex && dragIndex !== null && dragIndex !== overIndex;
241
+ return (_jsxs(Box, { "data-index": index, onPointerDown: (e) => handlePointerDown(e, index), onPointerMove: (e) => handlePointerMove(e, index), onPointerUp: (e) => handlePointerUp(e, index), onPointerCancel: handlePointerCancel, sx: {
242
+ position: "relative",
243
+ overflow: "visible",
244
+ flexShrink: 0,
245
+ display: "flex",
246
+ alignItems: "center",
247
+ cursor: isDragging ? "grabbing" : "pointer",
248
+ height: "44px", // FIXED HEIGHT to ensure perfect alignment
249
+ boxSizing: "border-box",
250
+ px: "0px",
251
+ // active indicator
252
+ "&::after": {
253
+ content: '""',
254
+ position: "absolute",
255
+ bottom: "0px",
256
+ left: 0,
257
+ right: 0,
258
+ height: "1.5px",
259
+ backgroundColor: isActive ? colors.onSurface : "transparent",
260
+ transition: "background-color 0.15s ease",
261
+ },
262
+ // drop-target indicator
263
+ "&::before": isDropTarget
264
+ ? {
265
+ content: '""',
266
+ position: "absolute",
267
+ // Dynamic placement based on drag direction
268
+ left: dragIndex !== null && dragIndex > index ? "-10px" : "auto",
269
+ right: dragIndex !== null && dragIndex < index ? "-10px" : "auto",
270
+ top: "20%",
271
+ height: "60%",
272
+ width: "2px",
273
+ borderRadius: "2px",
274
+ backgroundColor: colors.onSurface,
275
+ }
276
+ : {},
277
+ opacity: isBeingDragged ? 0.35 : 1,
278
+ transform: isBeingDragged ? "scale(0.97)" : "scale(1)",
279
+ transition: "opacity 0.1s ease, transform 0.1s ease",
280
+ touchAction: "none",
281
+ }, children: [_jsx(Typography, { sx: {
282
+ ...typography.bodyMedium,
283
+ color: colors.onSurface,
284
+ opacity: isActive ? 1 : 0.6,
285
+ transition: "opacity 0.15s ease",
286
+ whiteSpace: "nowrap",
287
+ lineHeight: 1,
288
+ }, children: section.label }), _jsx(Box, { component: "span", className: "chevron-toggle", onPointerDown: (e) => {
289
+ e.stopPropagation();
290
+ }, onClick: (e) => {
291
+ e.stopPropagation();
292
+ if (openMenuIndex === index) {
293
+ setOpenMenuIndex(null);
294
+ setMenuAnchorEl(null);
295
+ }
296
+ else {
297
+ setOpenMenuIndex(index);
298
+ setMenuAnchorEl(e.currentTarget);
299
+ }
300
+ }, sx: { display: "flex", alignItems: "center", ml: "4px" }, children: _jsx(IcChevronArrowDarkColor, { sx: {
301
+ width: "16px",
302
+ height: "16px",
303
+ color: colors.onSurface,
304
+ opacity: (isActive || isMobile) ? 1 : 0,
305
+ visibility: isBeingDragged ? "hidden" : "visible",
306
+ transform: openMenuIndex === index ? 'rotate(180deg)' : 'rotate(0deg)',
307
+ transition: 'transform 0.2s ease, opacity 0.15s ease'
308
+ } }) })] }, section.id));
309
+ }), isLoadingMoreSections && (_jsx(Box, { sx: {
310
+ flexShrink: 0,
311
+ display: "flex",
312
+ alignItems: "center",
313
+ height: "44px",
314
+ px: "4px",
315
+ opacity: 0.4,
316
+ }, children: _jsx(Skeleton, { variant: "rectangular", width: 80, height: 24 }) })), _jsxs(Box, { onClick: handleCreateSection, sx: {
317
+ flexShrink: 0,
318
+ display: "flex",
319
+ alignItems: "center",
320
+ cursor: "pointer",
321
+ height: "44px", // FIXED HEIGHT matches the section tabs
322
+ boxSizing: "border-box",
323
+ px: "0px",
324
+ gap: "4px",
325
+ opacity: 0.5,
326
+ "&:hover": {
327
+ opacity: 1,
328
+ },
329
+ transition: "opacity 0.15s ease",
330
+ }, children: [_jsx(IcPlusButton, { sx: { width: "20px", height: "20px" } }), _jsx(Typography, { sx: {
331
+ ...typography.labelMedium,
332
+ color: colors.onSurface,
333
+ whiteSpace: "nowrap",
334
+ }, children: "Add section" })] })] })] }), activeSection?.description && (_jsx(Box, { sx: {
335
+ width: "100%",
336
+ px: isMobile ? "18px" : "24px",
337
+ pt: "32px", // Space between tabs and text
338
+ pb: "0px", // Tiny space before filter menu
339
+ }, children: _jsx(Typography, { sx: {
340
+ ...typography.bodyMedium,
341
+ color: colors.onSurface, // Subtle grey color for description
342
+ textAlign: "center",
343
+ whiteSpace: "pre-wrap"
344
+ }, children: activeSection.description }) })), _jsxs(Menu, { anchorEl: menuAnchorEl, open: Boolean(menuAnchorEl) && openMenuIndex !== null, onClose: () => {
345
+ setMenuAnchorEl(null);
346
+ setOpenMenuIndex(null);
347
+ }, slotProps: {
348
+ paper: {
349
+ sx: {
350
+ backgroundColor: colors.surface,
351
+ border: "1px solid",
352
+ borderColor: colors.outlineVariant,
353
+ borderRadius: "8px",
354
+ boxShadow: "0px 4px 12px rgba(0,0,0,0.08)",
355
+ minWidth: "105px",
356
+ mt: "4px",
357
+ }
358
+ }
359
+ }, children: [_jsxs(MenuItem, { onClick: () => {
360
+ if (openMenuIndex !== null)
361
+ handleEditSection(openMenuIndex);
362
+ }, sx: { display: "flex", alignItems: "center", py: "10px", px: "16px" }, children: [_jsx(IcPencilIconDark, { sx: { width: "20px", height: "20px", color: colors.onSurface, mr: "8px" } }), _jsx(Typography, { sx: { ...typography.bodyMedium, color: colors.onSurface }, children: "Edit" })] }), _jsxs(MenuItem, { onClick: () => {
363
+ if (openMenuIndex !== null)
364
+ handleDeleteSection(openMenuIndex);
365
+ }, sx: { display: "flex", alignItems: "center", py: "10px", px: "16px" }, children: [_jsx(IcTrashIcon, { sx: { width: "20px", height: "20px", color: colors.error, mr: "8px" } }), _jsx(Typography, { sx: { ...typography.bodyMedium, color: colors.error }, children: "Delete" })] })] }), isDragging && dragIndex !== null && (_jsx(Box, { sx: {
366
+ position: "fixed",
367
+ top: cursorPos.y - 14,
368
+ left: cursorPos.x + 12,
369
+ pointerEvents: "none",
370
+ zIndex: 9999,
371
+ backgroundColor: colors.surface,
372
+ border: "1px solid",
373
+ borderColor: colors.outlineVariant,
374
+ borderRadius: "6px",
375
+ px: "10px",
376
+ py: "6px",
377
+ boxShadow: "0px 4px 12px rgba(0,0,0,0.12)",
378
+ display: "flex",
379
+ alignItems: "center",
380
+ gap: "6px",
381
+ }, children: _jsx(Typography, { sx: {
382
+ ...typography.bodyMedium,
383
+ color: colors.onSurface,
384
+ lineHeight: 1,
385
+ whiteSpace: "nowrap",
386
+ }, children: sections[dragIndex]?.label }) })), isCreateSectionDialogOpen && (_jsx(HBaseDialog, { title: "Add section", description:
387
+ // ADDED onKeyDown to stop propagation to the background gallery
388
+ _jsxs(Stack, { spacing: "12px", onKeyDown: (e) => e.stopPropagation(), children: [_jsxs(Box, { sx: { backgroundColor: colors.onSurfaceVariant2, py: "8px", px: "16px", borderRadius: "4px" }, children: [_jsx(Typography, { sx: { ...typography.bodySmall, color: colors.onSurfaceVariant1 }, children: "Section name" }), _jsx(TextField, { autoFocus: true, placeholder: "Enter section name", type: "text", fullWidth: true, variant: "standard", InputProps: { disableUnderline: true }, value: sectionName, onChange: (e) => setSectionName(e.target.value), sx: { "& .MuiInputBase-input": { padding: 0, marginTop: "4px", ...typography.bodyMedium } } })] }), _jsxs(Box, { sx: { backgroundColor: colors.onSurfaceVariant2, py: "8px", px: "16px", borderRadius: "4px" }, children: [_jsx(Typography, { sx: { ...typography.bodySmall, color: colors.onSurfaceVariant1 }, children: "Description (Optional)" }), _jsx(TextField, { placeholder: "Add a description...", type: "text", fullWidth: true, multiline: true, rows: 3, variant: "standard", InputProps: { disableUnderline: true }, value: sectionDescription, onChange: (e) => setSectionDescription(e.target.value), sx: { "& .MuiInputBase-input": { padding: 0, marginTop: "4px", ...typography.bodyMedium } } })] })] }), action: _jsxs(_Fragment, { children: [_jsx(PositiveButton, { text: "Cancel", onClick: () => setIsCreateSectionDialogOpen(false) }), _jsx(PositiveButton, { text: "Save", disabled: !sectionName.trim(), onClick: () => {
389
+ onAddSection(sectionName, sectionDescription);
390
+ setIsCreateSectionDialogOpen(false);
391
+ } })] }) })), isEditDialogOpen && selectedSectionIndex !== null && (_jsx(HBaseDialog, { title: "Edit section", description:
392
+ // ADDED onKeyDown to stop propagation to the background gallery
393
+ _jsxs(Stack, { spacing: "12px", onKeyDown: (e) => e.stopPropagation(), children: [_jsxs(Box, { sx: { backgroundColor: colors.onSurfaceVariant2, py: "8px", px: "16px", borderRadius: "4px" }, children: [_jsx(Typography, { sx: { ...typography.bodySmall, color: colors.onSurfaceVariant1 }, children: "Section name" }), _jsx(TextField, { autoFocus: true, placeholder: "Enter section name", type: "text", fullWidth: true, variant: "standard", InputProps: { disableUnderline: true }, value: sectionName, onChange: (e) => setSectionName(e.target.value), sx: { "& .MuiInputBase-input": { padding: 0, marginTop: "4px", ...typography.bodyMedium } } })] }), _jsxs(Box, { sx: { backgroundColor: colors.onSurfaceVariant2, py: "8px", px: "16px", borderRadius: "4px" }, children: [_jsx(Typography, { sx: { ...typography.bodySmall, color: colors.onSurfaceVariant1 }, children: "Description (Optional)" }), _jsx(TextField, { placeholder: "Add a description...", type: "text", fullWidth: true, multiline: true, rows: 3, variant: "standard", InputProps: { disableUnderline: true }, value: sectionDescription, onChange: (e) => setSectionDescription(e.target.value), sx: { "& .MuiInputBase-input": { padding: 0, marginTop: "4px", ...typography.bodyMedium } } })] })] }), action: _jsxs(_Fragment, { children: [_jsx(PositiveButton, { text: "Cancel", onClick: () => setIsEditDialogOpen(false) }), _jsx(PositiveButton, { text: "Save", disabled: !sectionName.trim(), onClick: () => {
394
+ if (onEditSection) {
395
+ onEditSection(selectedSectionIndex, sectionName, sectionDescription);
396
+ }
397
+ setIsEditDialogOpen(false);
398
+ } })] }) })), isDeleteDialogOpen && selectedSectionIndex !== null && (_jsx(HBaseDialog, { title: "Delete section?", description: "Permanently delete this section and its photos.", action: _jsxs(_Fragment, { children: [_jsx(PositiveButton, { text: "Cancel", onClick: () => setIsDeleteDialogOpen(false) }), _jsx(NegativeButton, { text: "Delete", onClick: () => {
399
+ // 1. Fire the actual delete function
400
+ if (onDeleteSection) {
401
+ onDeleteSection(sections[selectedSectionIndex].id);
402
+ }
403
+ // 2. Adjust the active tab if you are deleting the one you are currently viewing
404
+ const reorderedLength = sections.length - 1;
405
+ const newActive = activeSectionIndex >= reorderedLength
406
+ ? Math.max(0, reorderedLength - 1)
407
+ : activeSectionIndex === selectedSectionIndex
408
+ ? Math.max(0, selectedSectionIndex - 1)
409
+ : activeSectionIndex;
410
+ onSectionChange(newActive);
411
+ setIsDeleteDialogOpen(false);
412
+ } })] }) }))] }));
413
+ };
414
+ export default AlbumSectionTabs;
@@ -0,0 +1,14 @@
1
+ import { type FC } from "react";
2
+ import { AlbumSection } from "./AlbumSectionTabs";
3
+ interface Props {
4
+ sections: AlbumSection[];
5
+ activeSectionIndex: number;
6
+ onSectionChange: (index: number) => void;
7
+ selectedCounts?: Record<string, number>;
8
+ onLoadMore?: () => void;
9
+ isLoadingMoreSections?: boolean;
10
+ hasMoreSections?: boolean;
11
+ isEditorSelectionMode?: boolean;
12
+ }
13
+ declare const AlbumSectionTabsSelectMode: FC<Props>;
14
+ export default AlbumSectionTabsSelectMode;
@@ -0,0 +1,180 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState, useEffect } from "react";
3
+ import { Box, Typography, Skeleton } from "@mui/material"; // Added Skeleton
4
+ import useHonchoTypography from '../../../themes/honchoTheme';
5
+ import useColors from '../../../themes/colors';
6
+ import useIsMobile from '../../../utils/isMobile';
7
+ import { IcChevronArrowDarkColor, IcChevronArrowLightColor } from "../svg/IcChevron";
8
+ const AlbumSectionTabsSelectMode = ({ sections, activeSectionIndex, onSectionChange, selectedCounts = {}, onLoadMore, isLoadingMoreSections = false, hasMoreSections = false, isEditorSelectionMode = false, }) => {
9
+ const isMobile = useIsMobile();
10
+ const colors = useColors();
11
+ const typography = useHonchoTypography();
12
+ // --- Pagination / Scroll Logic ---
13
+ const scrollRef = useRef(null);
14
+ const [showLeftButton, setShowLeftButton] = useState(false);
15
+ const [showRightButton, setShowRightButton] = useState(false);
16
+ // NEW: Prevent firing multiple load requests at the same scroll position
17
+ const isLoadingMoreRef = useRef(false);
18
+ const checkScrollButtons = () => {
19
+ const el = scrollRef.current;
20
+ if (!el)
21
+ return;
22
+ const { scrollLeft, scrollWidth, clientWidth } = el;
23
+ setShowLeftButton(scrollLeft > 0);
24
+ setShowRightButton(Math.ceil(scrollLeft + clientWidth) < scrollWidth);
25
+ // NEW: Trigger load-more when within 80px of the right edge
26
+ const nearRightEdge = scrollLeft + clientWidth >= scrollWidth - 80;
27
+ if (nearRightEdge && hasMoreSections && !isLoadingMoreSections && !isLoadingMoreRef.current) {
28
+ isLoadingMoreRef.current = true;
29
+ if (onLoadMore)
30
+ onLoadMore();
31
+ }
32
+ if (!nearRightEdge) {
33
+ isLoadingMoreRef.current = false;
34
+ }
35
+ };
36
+ useEffect(() => {
37
+ checkScrollButtons();
38
+ window.addEventListener("resize", checkScrollButtons);
39
+ return () => window.removeEventListener("resize", checkScrollButtons);
40
+ }, [sections, hasMoreSections, isLoadingMoreSections]);
41
+ const handleScroll = (direction) => {
42
+ if (scrollRef.current) {
43
+ const scrollAmount = 200;
44
+ scrollRef.current.scrollBy({
45
+ left: direction === "left" ? -scrollAmount : scrollAmount,
46
+ behavior: "smooth",
47
+ });
48
+ }
49
+ };
50
+ const onScroll = () => {
51
+ checkScrollButtons();
52
+ };
53
+ // Auto-scroll to active tab when changed
54
+ useEffect(() => {
55
+ if (!scrollRef.current)
56
+ return;
57
+ const timeout = setTimeout(() => {
58
+ const activeTabNode = scrollRef.current?.querySelector(`[data-index="${activeSectionIndex}"]`);
59
+ if (activeTabNode) {
60
+ activeTabNode.scrollIntoView({
61
+ behavior: "smooth",
62
+ block: "nearest",
63
+ inline: "center",
64
+ });
65
+ }
66
+ }, 50);
67
+ return () => clearTimeout(timeout);
68
+ }, [activeSectionIndex]);
69
+ return (_jsxs(Box, { sx: {
70
+ borderBottom: "1px solid",
71
+ borderColor: isEditorSelectionMode ? colors.onSurfaceVariantDark : colors.outlineVariant,
72
+ width: "100vw",
73
+ position: "relative",
74
+ left: "50%",
75
+ right: "50%",
76
+ marginLeft: "-50vw",
77
+ marginRight: "-50vw",
78
+ backgroundColor: isEditorSelectionMode ? colors.onBackground : colors.surface,
79
+ display: "flex",
80
+ alignItems: "center",
81
+ }, children: [!isMobile && showLeftButton && (_jsx(Box, { sx: {
82
+ position: "absolute",
83
+ left: 0,
84
+ top: 0,
85
+ bottom: "1px",
86
+ display: "flex",
87
+ alignItems: "center",
88
+ background: isEditorSelectionMode ? colors.onBackground : colors.surface,
89
+ paddingRight: "24px",
90
+ paddingLeft: "16px",
91
+ zIndex: 2,
92
+ }, children: isEditorSelectionMode ?
93
+ _jsx(IcChevronArrowLightColor, { sx: {
94
+ width: "20px",
95
+ height: "20px",
96
+ color: colors.onSurface,
97
+ transform: "rotate(180deg)",
98
+ } }) :
99
+ _jsx(IcChevronArrowDarkColor, { sx: {
100
+ width: "20px",
101
+ height: "20px",
102
+ color: colors.onSurface,
103
+ transform: "rotate(90deg)",
104
+ } }) })), _jsxs(Box, { ref: scrollRef, onScroll: onScroll, sx: {
105
+ display: "flex",
106
+ flexDirection: "row",
107
+ alignItems: "flex-end",
108
+ px: isMobile ? "18px" : "24px",
109
+ gap: "24px",
110
+ overflowX: "auto",
111
+ width: "100%",
112
+ "&::-webkit-scrollbar": { display: "none" },
113
+ msOverflowStyle: "none",
114
+ scrollbarWidth: "none",
115
+ }, children: [sections.map((section, index) => {
116
+ const isActive = index === activeSectionIndex;
117
+ const selectedCount = selectedCounts[section.id] || 0;
118
+ return (_jsxs(Box, { "data-index": index, onClick: () => onSectionChange(index), sx: {
119
+ display: "flex",
120
+ alignItems: "center",
121
+ pb: "12px",
122
+ pt: "12px",
123
+ cursor: "pointer",
124
+ borderBottom: isActive ? "2px solid" : "2px solid transparent",
125
+ borderColor: (isEditorSelectionMode ? (isActive ? colors.background : "transparent") : (isActive ? colors.onSurface : "transparent")),
126
+ whiteSpace: "nowrap",
127
+ transition: "all 0.2s ease-in-out",
128
+ }, children: [_jsx(Typography, { sx: {
129
+ ...typography.bodyMedium,
130
+ color: (isEditorSelectionMode ? (isActive ? colors.background : colors.background) : (isActive ? colors.onSurface : colors.onSurfaceVariant1)),
131
+ }, children: section.label }), selectedCount > 0 && (_jsx(Box, { sx: {
132
+ ml: "6px",
133
+ backgroundColor: isEditorSelectionMode ? colors.background : colors.onSurface,
134
+ color: isEditorSelectionMode ? colors.onSurface : colors.surface,
135
+ borderRadius: "10px",
136
+ minWidth: "18px",
137
+ height: "18px",
138
+ display: "flex",
139
+ alignItems: "center",
140
+ justifyContent: "center",
141
+ px: "5px",
142
+ }, children: _jsx(Typography, { sx: {
143
+ ...typography.labelSmall,
144
+ color: isEditorSelectionMode ? colors.onSurface : colors.surface,
145
+ lineHeight: 1,
146
+ fontSize: "11px",
147
+ // fontWeight: 600,
148
+ }, children: selectedCount }) }))] }, section.id));
149
+ }), isLoadingMoreSections && (_jsx(Box, { sx: {
150
+ flexShrink: 0,
151
+ display: "flex",
152
+ alignItems: "center",
153
+ height: "44px",
154
+ px: "4px",
155
+ opacity: 0.4,
156
+ }, children: _jsx(Skeleton, { variant: "rectangular", width: 80, height: 24 }) }))] }), !isMobile && showRightButton && (_jsx(Box, { onClick: () => handleScroll("right"), sx: {
157
+ position: "absolute",
158
+ right: 0,
159
+ top: 0,
160
+ bottom: "1px",
161
+ display: "flex",
162
+ alignItems: "center",
163
+ background: isEditorSelectionMode ? colors.onBackground : colors.surface,
164
+ paddingLeft: "24px",
165
+ paddingRight: "16px",
166
+ zIndex: 2,
167
+ }, children: isEditorSelectionMode ?
168
+ _jsx(IcChevronArrowLightColor, { sx: {
169
+ width: "20px",
170
+ height: "20px",
171
+ color: colors.onSurface,
172
+ } }) :
173
+ _jsx(IcChevronArrowDarkColor, { sx: {
174
+ width: "20px",
175
+ height: "20px",
176
+ color: colors.onSurface,
177
+ transform: "rotate(270deg)",
178
+ } }) }))] }));
179
+ };
180
+ export default AlbumSectionTabsSelectMode;
@@ -0,0 +1,24 @@
1
+ import React, { ReactElement } from "react";
2
+ interface Props {
3
+ title: React.ReactNode;
4
+ description: React.ReactNode | ReactElement;
5
+ onClose?: () => void;
6
+ action?: React.ReactNode;
7
+ paddingTop?: string;
8
+ hasFullWidthContent?: boolean;
9
+ isCentered?: boolean;
10
+ }
11
+ export declare function HBaseDialog(props: Props): import("react/jsx-runtime").JSX.Element;
12
+ export declare function HBaseDialogMiddle(props: Props): import("react/jsx-runtime").JSX.Element;
13
+ interface ButtonProps {
14
+ text: string;
15
+ onClick: () => void;
16
+ color?: string;
17
+ backgroundColor?: string;
18
+ backgroundColorHover?: string;
19
+ padding?: string;
20
+ disabled?: boolean;
21
+ }
22
+ export declare function PositiveButton(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function NegativeButton(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
24
+ export {};