@mohasinac/appkit 3.9.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/shared/actions/action-registry.js +8 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -4
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/features/account/hooks/useDataViewMode.d.ts +17 -0
- package/dist/features/account/hooks/useDataViewMode.js +55 -0
- package/dist/features/account/hooks/useProfile.d.ts +2 -0
- package/dist/features/admin/components/AdminBundlesView.d.ts +3 -4
- package/dist/features/admin/components/AdminBundlesView.js +2 -2
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminCategoriesView.d.ts +2 -6
- package/dist/features/admin/components/AdminCategoriesView.js +1 -1
- package/dist/features/admin/components/AdminFaqsView.d.ts +0 -2
- package/dist/features/admin/components/AdminPrizeDrawsView.js +1 -1
- package/dist/features/admin/components/DataListingView.d.ts +8 -2
- package/dist/features/admin/components/DataListingView.js +7 -3
- package/dist/features/admin/components/DataTable.d.ts +10 -2
- package/dist/features/admin/components/DataTable.js +4 -2
- package/dist/features/admin/hooks/useAdminListing.d.ts +2 -2
- package/dist/features/admin/hooks/useAdminListing.js +6 -1
- package/dist/features/categories/components/BrandDetailTabs.js +2 -12
- package/dist/features/categories/components/CategoryDetailTabs.js +2 -12
- package/dist/features/events/components/AdminEventsView.d.ts +1 -5
- package/dist/features/faq/components/FAQAccordion.js +3 -2
- package/dist/features/faq/components/RelatedFAQs.js +1 -1
- package/dist/features/homepage/components/FAQSection.js +3 -3
- package/dist/features/layout/FooterLayout.js +2 -2
- package/dist/features/lottery/schemas/zod.d.ts +4 -4
- package/dist/features/payments/schemas/firestore.d.ts +6 -0
- package/dist/features/payments/schemas/firestore.js +2 -0
- package/dist/features/seller/components/SellerBundlesView.d.ts +3 -4
- package/dist/features/seller/components/SellerBundlesView.js +2 -2
- package/dist/features/seller/components/SellerPayoutsView.d.ts +2 -5
- package/dist/features/seller/components/SellerPayoutsView.js +100 -21
- package/dist/features/seller/components/SellerPreOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPrizeDrawsView.js +1 -1
- package/dist/features/seller/components/SellerProductsView.js +6 -2
- package/dist/features/shipments/components/AdminShipmentsView.d.ts +1 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +3 -3
- package/dist/react/index.d.ts +0 -2
- package/dist/react/index.js +0 -2
- package/dist/seed/faq-seed-data.js +11 -11
- package/dist/seed/homepage-sections-seed-data.js +1 -1
- package/dist/styles.css +30 -99
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +10 -0
- package/dist/ui/components/DetailPageTabs.d.ts +1 -1
- package/dist/ui/components/DetailPageTabs.js +6 -30
- package/dist/ui/components/HorizontalScroller.d.ts +1 -1
- package/dist/ui/components/HorizontalScroller.js +78 -197
- package/dist/ui/components/Tabs.d.ts +9 -1
- package/dist/ui/components/Tabs.js +13 -8
- package/dist/ui/components/Tabs.style.css +19 -0
- package/dist/ui/components/index.style.css +0 -1
- package/dist/ui/index.d.ts +0 -2
- package/dist/ui/index.js +0 -1
- package/package.json +1 -1
package/dist/ui/DataTable.js
CHANGED
|
@@ -123,7 +123,7 @@ pageSize: pageSizeProp, stickyHeader: stickyHeaderProp, striped: stripedProp, sh
|
|
|
123
123
|
mobileCardRender &&
|
|
124
124
|
renderCardGrid(activeViewMode), activeViewMode === "table" && mobileCardRender && (_jsx(Div, { className: "appkit-data-table__mobile-cards", children: paginatedData.map((item) => (_jsx(SelectableCard, { id: keyExtractor(item), selectable: selectable, selected: selectedIds.includes(keyExtractor(item)), onToggle: (id, checked) => onSelectionChange?.(checked
|
|
125
125
|
? [...selectedIds, id]
|
|
126
|
-
: selectedIds.filter((s) => s !== id)), children: mobileCardRender(item) }, keyExtractor(item)))) })), activeViewMode === "table" && (_jsx(Div, { className: "appkit-data-table__wrapper"
|
|
126
|
+
: selectedIds.filter((s) => s !== id)), children: mobileCardRender(item) }, keyExtractor(item)))) })), activeViewMode === "table" && (_jsx(Div, { className: `appkit-data-table__wrapper ${mobileCardRender ? "appkit-data-table__wrapper--has-mobile-cards" : ""}`, children: _jsx(Div, { className: `appkit-data-table__scroll ${stickyHeader ? "appkit-data-table__scroll--sticky" : ""}`, children: _jsxs("table", { className: "appkit-data-table__table", children: [_jsx("thead", { className: `appkit-data-table__thead ${stickyHeader ? "appkit-data-table__thead--sticky" : ""}`, children: _jsxs("tr", { children: [selectable && (_jsx("th", { scope: "col", className: "px-4 py-3 w-8", children: _jsx(Checkbox, { bare: true, className: CLS_CHECKBOX, "aria-label": "Select all on page", checked: paginatedData.length > 0 &&
|
|
127
127
|
paginatedData.every((item) => selectedIds.includes(keyExtractor(item))), onChange: (e) => {
|
|
128
128
|
const pageIds = paginatedData.map(keyExtractor);
|
|
129
129
|
onSelectionChange?.(e.target.checked
|
|
@@ -190,3 +190,13 @@
|
|
|
190
190
|
display: none;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
|
|
194
|
+
/* When mobileCardRender is supplied, the desktop table wrapper must hide
|
|
195
|
+
below the same breakpoint the mobile-cards block uses above — otherwise
|
|
196
|
+
both render simultaneously on small screens. Wrapper has no modifier
|
|
197
|
+
(and always renders) when mobileCardRender is absent. */
|
|
198
|
+
@media (max-width: 767px) {
|
|
199
|
+
.appkit-data-table__wrapper--has-mobile-cards {
|
|
200
|
+
display: none;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -31,4 +31,4 @@ export interface DetailPageTabsProps {
|
|
|
31
31
|
*
|
|
32
32
|
* W1-14 — extracted 2026-05-23.
|
|
33
33
|
*/
|
|
34
|
-
export declare function DetailPageTabs({ tabs, activeTab
|
|
34
|
+
export declare function DetailPageTabs({ tabs, activeTab, defaultTab, onTabChange, className, }: DetailPageTabsProps): React.JSX.Element;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
4
3
|
import { Div } from "./Div";
|
|
5
|
-
import {
|
|
6
|
-
const __O = {
|
|
7
|
-
xAuto: "overflow-x-auto",
|
|
8
|
-
};
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs";
|
|
9
5
|
/**
|
|
10
6
|
* `DetailPageTabs` — tab shell used by Product, Auction and Event detail
|
|
11
7
|
* pages. Renders an accessible trigger strip + a single visible panel.
|
|
@@ -14,29 +10,9 @@ const __O = {
|
|
|
14
10
|
*
|
|
15
11
|
* W1-14 — extracted 2026-05-23.
|
|
16
12
|
*/
|
|
17
|
-
export function DetailPageTabs({ tabs, activeTab
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (activeTabProp === undefined)
|
|
23
|
-
setInternalActive(id);
|
|
24
|
-
onTabChange?.(id);
|
|
25
|
-
};
|
|
26
|
-
return (_jsxs(Div, { className: [
|
|
27
|
-
"appkit-detail-tabs",
|
|
28
|
-
"space-y-4",
|
|
29
|
-
className,
|
|
30
|
-
]
|
|
31
|
-
.filter(Boolean)
|
|
32
|
-
.join(" "), children: [_jsx(Div, { role: "tablist", "aria-orientation": "horizontal", className: `flex gap-2 border-b border-[var(--appkit-color-border)] ${__O.xAuto}`, children: tabs.map((tab) => {
|
|
33
|
-
const isActive = tab.id === active?.id;
|
|
34
|
-
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": isActive, "aria-controls": `detail-tab-panel-${tab.id}`, id: `detail-tab-trigger-${tab.id}`, disabled: tab.disabled, onClick: () => handleChange(tab.id), className: [
|
|
35
|
-
"flex-shrink-0 px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
|
|
36
|
-
isActive
|
|
37
|
-
? "border-[var(--appkit-color-primary)] text-[var(--appkit-color-text)]"
|
|
38
|
-
: "border-transparent text-[var(--appkit-color-text-muted)] hover:text-zinc-700 hover:text-[var(--appkit-color-text-muted)]",
|
|
39
|
-
tab.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
|
|
40
|
-
].join(" "), children: [tab.label, typeof tab.count === "number" ? (_jsx(Span, { size: "xs", className: "ml-2 inline-flex items-center justify-center rounded-full bg-[var(--appkit-color-surface)] px-2 py-0.5", children: tab.count })) : null] }, tab.id));
|
|
41
|
-
}) }), active ? (_jsx(Div, { role: "tabpanel", id: `detail-tab-panel-${active.id}`, "aria-labelledby": `detail-tab-trigger-${active.id}`, children: active.content })) : null] }));
|
|
13
|
+
export function DetailPageTabs({ tabs, activeTab, defaultTab, onTabChange, className, }) {
|
|
14
|
+
return (_jsxs(Tabs, { value: activeTab, defaultValue: defaultTab ?? tabs[0]?.id ?? "", onChange: onTabChange, className: ["appkit-detail-tabs", "space-y-4", className].filter(Boolean).join(" "), children: [_jsx(TabsList, { children: tabs.map((tab) => {
|
|
15
|
+
const label = typeof tab.label === "string" ? tab.label : undefined;
|
|
16
|
+
return (_jsx(TabsTrigger, { value: tab.id, disabled: tab.disabled, badge: tab.count, label: label, children: tab.label }, tab.id));
|
|
17
|
+
}) }), tabs.map((tab) => (_jsx(TabsContent, { value: tab.id, children: _jsx(Div, { children: tab.content }) }, tab.id)))] }));
|
|
42
18
|
}
|
|
@@ -29,7 +29,7 @@ export interface HorizontalScrollerProps<T = unknown> {
|
|
|
29
29
|
minItemWidth?: number;
|
|
30
30
|
pauseOnHover?: boolean;
|
|
31
31
|
itemClassName?: string;
|
|
32
|
-
/**
|
|
32
|
+
/** When the scroller reaches the last item, snap instantly back to the first instead of stopping. */
|
|
33
33
|
loop?: boolean;
|
|
34
34
|
}
|
|
35
35
|
export declare function HorizontalScroller<T = unknown>({ children, className, gap, snapToItems, showArrows, arrowSize, showScrollbar, showFadeEdges, scrollContainerRef: externalRef, onScroll, items, renderItem, keyExtractor, perView, rows, autoScroll, autoScrollInterval, minItemWidth, pauseOnHover, itemClassName, loop, }: HorizontalScrollerProps<T>): import("react").JSX.Element;
|
|
@@ -31,132 +31,60 @@ export function HorizontalScroller({ children, className = "", gap = 16, snapToI
|
|
|
31
31
|
const containerRef = (externalRef ??
|
|
32
32
|
internalRef);
|
|
33
33
|
const autoScrollTimer = useRef(undefined);
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
const
|
|
34
|
+
// Tracks the pending "restore smooth scrolling" rAF so a fast reset (autoScroll
|
|
35
|
+
// firing again, or a manual arrow click) can't leave a stale callback that fires
|
|
36
|
+
// after — and clobbers — a newer scroll-behavior change.
|
|
37
|
+
const instantScrollRaf = useRef(undefined);
|
|
38
38
|
const normalizedItems = Array.isArray(items) ? items : [];
|
|
39
39
|
const itemsMode = Array.isArray(items) && renderItem != null;
|
|
40
40
|
const itemCount = normalizedItems.length;
|
|
41
|
-
// Number of clone slots on each side — fixed size, computed once from perView hint.
|
|
42
|
-
// Using Math.min so tiny lists don't double-render more slots than they have items.
|
|
43
|
-
const loopCloneCount = loop && itemsMode && rows <= 1 && itemCount > 0
|
|
44
|
-
? Math.min(itemCount, typeof perView === "number" ? perView : 3)
|
|
45
|
-
: 0;
|
|
46
|
-
// Grid mode (rows > 1): items are grouped into full-width "slides". Looping here
|
|
47
|
-
// clones exactly one slide on each side (the slide itself is the full stride, so a
|
|
48
|
-
// single buffer slide is enough) and teleports across a full cycle at the boundary —
|
|
49
|
-
// same technique as the single-item loop above, just at slide granularity.
|
|
50
41
|
const gridMode = itemsMode && rows > 1;
|
|
51
42
|
const gridCols = colCount > 0 ? colCount : 3;
|
|
52
43
|
const gridCardsPerSlide = rows * gridCols;
|
|
53
44
|
const gridSlideCount = gridMode
|
|
54
45
|
? Math.ceil(itemCount / gridCardsPerSlide)
|
|
55
46
|
: 0;
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
loopInitialized.current = true;
|
|
66
|
-
el.style.scrollBehavior = "auto";
|
|
67
|
-
el.scrollLeft = loopCloneCount * (itemWidth + gap);
|
|
68
|
-
requestAnimationFrame(() => {
|
|
69
|
-
el.style.scrollBehavior = "";
|
|
70
|
-
});
|
|
71
|
-
}, [loop, loopCloneCount, itemWidth, gap, containerRef]);
|
|
72
|
-
// Circular teleporter: when scroll enters a clone zone, jump by exactly one
|
|
73
|
-
// full cycle so the viewport content is unchanged but we're back in real territory.
|
|
74
|
-
const handleScrollLoop = useCallback(() => {
|
|
75
|
-
if (!loop || loopCloneCount === 0 || itemWidth === undefined)
|
|
76
|
-
return;
|
|
77
|
-
const el = containerRef.current;
|
|
78
|
-
if (!el || isJumping.current)
|
|
79
|
-
return;
|
|
80
|
-
const stride = itemWidth + gap;
|
|
81
|
-
const cycleWidth = itemCount * stride;
|
|
82
|
-
// scrollLeft where real items start and end
|
|
83
|
-
const realStart = loopCloneCount * stride;
|
|
84
|
-
const realEnd = realStart + cycleWidth;
|
|
85
|
-
if (el.scrollLeft < realStart - stride * 0.5) {
|
|
86
|
-
// Entered left clone zone — jump forward one cycle
|
|
87
|
-
isJumping.current = true;
|
|
88
|
-
el.style.scrollBehavior = "auto";
|
|
89
|
-
el.scrollLeft += cycleWidth;
|
|
90
|
-
requestAnimationFrame(() => {
|
|
91
|
-
isJumping.current = false;
|
|
92
|
-
el.style.scrollBehavior = "";
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
else if (el.scrollLeft > realEnd - el.clientWidth + stride * 0.5) {
|
|
96
|
-
// Entered right clone zone — jump backward one cycle
|
|
97
|
-
isJumping.current = true;
|
|
98
|
-
el.style.scrollBehavior = "auto";
|
|
99
|
-
el.scrollLeft -= cycleWidth;
|
|
100
|
-
requestAnimationFrame(() => {
|
|
101
|
-
isJumping.current = false;
|
|
102
|
-
el.style.scrollBehavior = "";
|
|
103
|
-
});
|
|
47
|
+
// Instantly jumps to a scroll position — used to wrap the loop back to the start
|
|
48
|
+
// (or to the end, for prev-at-start) instead of layering a teleport on top of an
|
|
49
|
+
// in-flight smooth scroll, which is what produced the old flicker/oscillation.
|
|
50
|
+
// Cancels any previously-scheduled "restore smooth scrolling" rAF first so a
|
|
51
|
+
// rapid-fire reset (autoScroll ticking again, or an arrow click) can't leave a
|
|
52
|
+
// stale callback that fires later and clobbers a newer scroll-behavior change.
|
|
53
|
+
const instantScrollTo = useCallback((el, left) => {
|
|
54
|
+
if (instantScrollRaf.current !== undefined) {
|
|
55
|
+
cancelAnimationFrame(instantScrollRaf.current);
|
|
104
56
|
}
|
|
105
|
-
}, [loop, loopCloneCount, itemWidth, gap, itemCount, containerRef]);
|
|
106
|
-
// Prevents re-running the initial scroll-to-real-start on every resize in grid loop mode
|
|
107
|
-
const gridLoopInitialized = useRef(false);
|
|
108
|
-
// On first paint, scroll past the prepended clone slide to the first *real* slide.
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!gridLoopActive)
|
|
111
|
-
return;
|
|
112
|
-
const el = containerRef.current;
|
|
113
|
-
if (!el || gridLoopInitialized.current)
|
|
114
|
-
return;
|
|
115
|
-
gridLoopInitialized.current = true;
|
|
116
|
-
const stride = el.clientWidth + gap;
|
|
117
57
|
el.style.scrollBehavior = "auto";
|
|
118
|
-
el.scrollLeft =
|
|
119
|
-
requestAnimationFrame(() => {
|
|
58
|
+
el.scrollLeft = left;
|
|
59
|
+
instantScrollRaf.current = requestAnimationFrame(() => {
|
|
120
60
|
el.style.scrollBehavior = "";
|
|
61
|
+
instantScrollRaf.current = undefined;
|
|
121
62
|
});
|
|
122
|
-
}, [
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const cycleWidth = gridSlideCount * stride;
|
|
132
|
-
const realStart = stride; // after the 1 prepended clone slide
|
|
133
|
-
const realEnd = realStart + cycleWidth;
|
|
134
|
-
if (el.scrollLeft < realStart - stride * 0.5) {
|
|
135
|
-
isJumping.current = true;
|
|
136
|
-
el.style.scrollBehavior = "auto";
|
|
137
|
-
el.scrollLeft += cycleWidth;
|
|
138
|
-
requestAnimationFrame(() => {
|
|
139
|
-
isJumping.current = false;
|
|
140
|
-
el.style.scrollBehavior = "";
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
else if (el.scrollLeft > realEnd - el.clientWidth + stride * 0.5) {
|
|
144
|
-
isJumping.current = true;
|
|
145
|
-
el.style.scrollBehavior = "auto";
|
|
146
|
-
el.scrollLeft -= cycleWidth;
|
|
147
|
-
requestAnimationFrame(() => {
|
|
148
|
-
isJumping.current = false;
|
|
149
|
-
el.style.scrollBehavior = "";
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}, [gridLoopActive, gap, gridSlideCount, containerRef]);
|
|
63
|
+
}, []);
|
|
64
|
+
// Cancel any pending rAF on unmount so it never fires against a detached node.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
if (instantScrollRaf.current !== undefined) {
|
|
68
|
+
cancelAnimationFrame(instantScrollRaf.current);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
153
72
|
const scrollBy = useCallback((direction) => {
|
|
154
73
|
const el = containerRef.current;
|
|
155
74
|
if (!el)
|
|
156
75
|
return;
|
|
157
76
|
const width = el.clientWidth;
|
|
77
|
+
const maxScroll = el.scrollWidth - width;
|
|
78
|
+
if (loop && direction === 1 && el.scrollLeft >= maxScroll - 1) {
|
|
79
|
+
instantScrollTo(el, 0);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (loop && direction === -1 && el.scrollLeft <= 1) {
|
|
83
|
+
instantScrollTo(el, maxScroll);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
158
86
|
el.scrollBy({ left: direction * width * 0.8, behavior: "smooth" });
|
|
159
|
-
}, [containerRef]);
|
|
87
|
+
}, [containerRef, loop, instantScrollTo]);
|
|
160
88
|
const updateExtents = useCallback(() => {
|
|
161
89
|
const el = containerRef.current;
|
|
162
90
|
if (!el)
|
|
@@ -188,98 +116,57 @@ export function HorizontalScroller({ children, className = "", gap = 16, snapToI
|
|
|
188
116
|
const el = containerRef.current;
|
|
189
117
|
if (!el)
|
|
190
118
|
return;
|
|
191
|
-
|
|
119
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
120
|
+
const atRealEnd = maxScroll <= 1 || el.scrollLeft >= maxScroll - 1;
|
|
121
|
+
if (atRealEnd) {
|
|
122
|
+
if (loop)
|
|
123
|
+
instantScrollTo(el, 0);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
192
126
|
el.scrollBy({ left: el.clientWidth * 0.8, behavior: "smooth" });
|
|
193
127
|
}, autoScrollInterval);
|
|
194
128
|
return () => clearInterval(autoScrollTimer.current);
|
|
195
|
-
}, [autoScroll, isPaused, autoScrollInterval, containerRef]);
|
|
129
|
+
}, [autoScroll, isPaused, autoScrollInterval, loop, instantScrollTo, containerRef]);
|
|
196
130
|
useEffect(() => {
|
|
197
|
-
if (!perView
|
|
131
|
+
if (!perView)
|
|
198
132
|
return;
|
|
199
133
|
const el = containerRef.current;
|
|
200
134
|
if (!el)
|
|
201
135
|
return;
|
|
202
136
|
const observer = new ResizeObserver(([entry]) => {
|
|
203
137
|
const w = entry.contentRect.width;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
setItemWidth((w - (count - 1) * gap) / count);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
// loop is true but no perView hint was given: items render at their
|
|
213
|
-
// natural width, so measure a real rendered item to get the scroll
|
|
214
|
-
// stride the clone-buffer offset / edge teleporter need — without
|
|
215
|
-
// this, itemWidth never resolves and loop mode never initializes.
|
|
216
|
-
const item = el.querySelector(".appkit-hscroller__item");
|
|
217
|
-
if (item)
|
|
218
|
-
setItemWidth(item.getBoundingClientRect().width);
|
|
138
|
+
const count = resolvePerView(perView, w);
|
|
139
|
+
if (count > 0) {
|
|
140
|
+
setColCount(count);
|
|
141
|
+
setItemWidth((w - (count - 1) * gap) / count);
|
|
219
142
|
}
|
|
220
143
|
updateExtents();
|
|
221
144
|
});
|
|
222
145
|
observer.observe(el);
|
|
223
146
|
return () => observer.disconnect();
|
|
224
|
-
}, [perView,
|
|
147
|
+
}, [perView, gap, containerRef, updateExtents]);
|
|
225
148
|
// Recompute extents when content size changes (itemWidth resolved, items count changes).
|
|
226
149
|
useEffect(() => {
|
|
227
150
|
updateExtents();
|
|
228
151
|
}, [updateExtents, itemWidth, itemCount]);
|
|
229
152
|
const content = itemsMode ? (rows > 1 ? (
|
|
230
153
|
// Grid mode: group items into slides of (rows × colCount) cards.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
]
|
|
248
|
-
.filter(Boolean)
|
|
249
|
-
.join(" "), style: minItemWidth ? { minWidth: minItemWidth } : undefined, children: renderItem(item, slideIndex * gridCardsPerSlide + idx) }, keyExtractor ? keyExtractor(item, slideIndex * gridCardsPerSlide + idx) : slideIndex * gridCardsPerSlide + idx))) }, key));
|
|
250
|
-
};
|
|
251
|
-
const realSlides = Array.from({ length: gridSlideCount }, (_, slideIndex) => renderSlide(slideIndex, `slide-${slideIndex}`, false));
|
|
252
|
-
if (!gridLoopActive)
|
|
253
|
-
return realSlides;
|
|
254
|
-
return [
|
|
255
|
-
renderSlide(gridSlideCount - 1, "slide-clone-start", true),
|
|
256
|
-
...realSlides,
|
|
257
|
-
renderSlide(0, "slide-clone-end", true),
|
|
258
|
-
];
|
|
259
|
-
})()) : loop && itemCount > 0 ? (
|
|
260
|
-
// Circular loop: fixed (itemCount + 2×loopCloneCount) DOM slots.
|
|
261
|
-
// Each slot maps to a real item via modulo — no array cloning, no list growth.
|
|
262
|
-
// Left slots [0 .. loopCloneCount-1] → tail of real list (left buffer)
|
|
263
|
-
// Real slots [loopCloneCount .. loopCloneCount+n-1] → items[0..n-1]
|
|
264
|
-
// Right slots [loopCloneCount+n .. end] → head of real list (right buffer)
|
|
265
|
-
Array.from({ length: itemCount + 2 * loopCloneCount }, (_, i) => {
|
|
266
|
-
const realIndex = ((i - loopCloneCount) % itemCount + itemCount) % itemCount;
|
|
267
|
-
const item = normalizedItems[realIndex];
|
|
268
|
-
const isClone = i < loopCloneCount || i >= loopCloneCount + itemCount;
|
|
269
|
-
return (_jsx("div", { className: [
|
|
270
|
-
"appkit-hscroller__item",
|
|
271
|
-
snapToItems ? "appkit-hscroller__item--snap" : "",
|
|
272
|
-
itemClassName,
|
|
273
|
-
]
|
|
274
|
-
.filter(Boolean)
|
|
275
|
-
.join(" "), style: itemWidth !== undefined
|
|
276
|
-
? { width: itemWidth, flexShrink: 0 }
|
|
277
|
-
: minItemWidth
|
|
278
|
-
? { minWidth: minItemWidth }
|
|
279
|
-
: undefined, "aria-hidden": isClone ? true : undefined, children: renderItem(item, realIndex) }, `loop-slot-${i}`));
|
|
280
|
-
})) : (
|
|
281
|
-
// Normal single row (no loop)
|
|
282
|
-
normalizedItems.map((item, i) => (_jsx("div", { className: [
|
|
154
|
+
Array.from({ length: gridSlideCount }, (_, slideIndex) => {
|
|
155
|
+
const slideItems = normalizedItems.slice(slideIndex * gridCardsPerSlide, (slideIndex + 1) * gridCardsPerSlide);
|
|
156
|
+
return (_jsx("div", { className: "appkit-hscroller__slide", style: {
|
|
157
|
+
display: "grid",
|
|
158
|
+
gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
|
|
159
|
+
gap: `${gap}px`,
|
|
160
|
+
width: "100%",
|
|
161
|
+
flexShrink: 0,
|
|
162
|
+
}, children: slideItems.map((item, idx) => (_jsx("div", { className: [
|
|
163
|
+
"appkit-hscroller__item",
|
|
164
|
+
snapToItems ? "appkit-hscroller__item--snap" : "",
|
|
165
|
+
itemClassName,
|
|
166
|
+
]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(" "), style: minItemWidth ? { minWidth: minItemWidth } : undefined, children: renderItem(item, slideIndex * gridCardsPerSlide + idx) }, keyExtractor ? keyExtractor(item, slideIndex * gridCardsPerSlide + idx) : slideIndex * gridCardsPerSlide + idx))) }, `slide-${slideIndex}`));
|
|
169
|
+
})) : (normalizedItems.map((item, i) => (_jsx("div", { className: [
|
|
283
170
|
"appkit-hscroller__item",
|
|
284
171
|
snapToItems ? "appkit-hscroller__item--snap" : "",
|
|
285
172
|
itemClassName,
|
|
@@ -290,40 +177,34 @@ export function HorizontalScroller({ children, className = "", gap = 16, snapToI
|
|
|
290
177
|
: minItemWidth
|
|
291
178
|
? { minWidth: minItemWidth }
|
|
292
179
|
: undefined, children: renderItem(item, i) }, keyExtractor ? keyExtractor(item, i) : i))))) : (children);
|
|
293
|
-
|
|
180
|
+
// Pauses autoScroll for the duration of any user interaction — hover, touch,
|
|
181
|
+
// keyboard focus (arrow-key nav), or an active wheel/drag scroll — so autoplay
|
|
182
|
+
// never fights a scroll the user is in the middle of driving themselves.
|
|
183
|
+
const interactionHandlers = pauseOnHover
|
|
294
184
|
? {
|
|
295
185
|
onMouseEnter: () => setIsPaused(true),
|
|
296
186
|
onMouseLeave: () => setIsPaused(false),
|
|
297
187
|
onTouchStart: () => setIsPaused(true),
|
|
298
188
|
onTouchEnd: () => setIsPaused(false),
|
|
299
189
|
onTouchCancel: () => setIsPaused(false),
|
|
190
|
+
onFocus: () => setIsPaused(true),
|
|
191
|
+
onBlur: () => setIsPaused(false),
|
|
192
|
+
onWheel: () => setIsPaused(true),
|
|
300
193
|
}
|
|
301
194
|
: {};
|
|
302
195
|
const combinedOnScroll = () => {
|
|
303
|
-
|
|
304
|
-
if (gridLoopActive)
|
|
305
|
-
handleGridScrollLoop();
|
|
306
|
-
else
|
|
307
|
-
updateExtents();
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
if (loop)
|
|
311
|
-
handleScrollLoop();
|
|
312
|
-
if (!loop)
|
|
313
|
-
updateExtents();
|
|
314
|
-
}
|
|
196
|
+
updateExtents();
|
|
315
197
|
onScroll?.();
|
|
316
198
|
};
|
|
317
199
|
if (showArrows) {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
const arrowsHidden = !effectiveLoop && atStart && atEnd; // no scrollable overflow
|
|
200
|
+
const prevDisabled = !loop && atStart;
|
|
201
|
+
const nextDisabled = !loop && atEnd;
|
|
202
|
+
const arrowsHidden = !loop && atStart && atEnd; // no scrollable overflow
|
|
322
203
|
return (_jsxs("div", { className: ["appkit-hscroller appkit-hscroller--with-arrows", className]
|
|
323
204
|
.filter(Boolean)
|
|
324
|
-
.join(" "), tabIndex: 0, onKeyDown: handleKeyDown, ...
|
|
205
|
+
.join(" "), tabIndex: 0, onKeyDown: handleKeyDown, ...interactionHandlers, "data-section": "horizontalscroller-div-511", children: [showFadeEdges && (_jsxs(_Fragment, { children: [_jsx("div", { className: "appkit-hscroller__fade appkit-hscroller__fade--left" }), _jsx("div", { className: "appkit-hscroller__fade appkit-hscroller__fade--right" })] })), !arrowsHidden && (_jsx("button", { type: "button", onClick: () => scrollBy(-1), "aria-label": "Previous", "aria-disabled": prevDisabled || undefined, disabled: prevDisabled, className: `appkit-hscroller__arrow appkit-hscroller__arrow--prev appkit-hscroller__arrow--${arrowSize}`, children: _jsx(ChevronLeft, { className: "w-4 h-4" }) })), _jsx("div", { ref: containerRef, onScroll: combinedOnScroll, className: scrollerCls(snapToItems, showScrollbar), style: { gap: `${gap}px`, paddingLeft: 36, paddingRight: 36 }, "data-section": "horizontalscroller-div-512", children: content }), !arrowsHidden && (_jsx("button", { type: "button", onClick: () => scrollBy(1), "aria-label": "Next", "aria-disabled": nextDisabled || undefined, disabled: nextDisabled, className: `appkit-hscroller__arrow appkit-hscroller__arrow--next appkit-hscroller__arrow--${arrowSize}`, children: _jsx(ChevronRight, { className: "w-4 h-4" }) }))] }));
|
|
325
206
|
}
|
|
326
|
-
return (_jsxs("div", { className: ["appkit-hscroller", className].filter(Boolean).join(" "), tabIndex: 0, onKeyDown: handleKeyDown, ...
|
|
207
|
+
return (_jsxs("div", { className: ["appkit-hscroller", className].filter(Boolean).join(" "), tabIndex: 0, onKeyDown: handleKeyDown, ...interactionHandlers, "data-section": "horizontalscroller-div-513", children: [showFadeEdges && (_jsxs(_Fragment, { children: [_jsx("div", { className: "appkit-hscroller__fade appkit-hscroller__fade--left" }), _jsx("div", { className: "appkit-hscroller__fade appkit-hscroller__fade--right" })] })), _jsx("div", { ref: containerRef, onScroll: combinedOnScroll, className: scrollerCls(snapToItems, showScrollbar), style: { gap: `${gap}px` }, "data-section": "horizontalscroller-div-514", children: content })] }));
|
|
327
208
|
}
|
|
328
209
|
function scrollerCls(snapToItems, showScrollbar) {
|
|
329
210
|
return [
|
|
@@ -15,6 +15,14 @@ export interface TabsTriggerProps {
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
className?: string;
|
|
17
17
|
children?: React.ReactNode;
|
|
18
|
+
/** Optional count badge rendered as a small pill after the label. */
|
|
19
|
+
badge?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Plain-text label to use for the collapsed mobile dropdown's option text
|
|
22
|
+
* when `children` isn't a plain string (e.g. it renders a badge pill).
|
|
23
|
+
* Falls back to `value` when omitted and `children` isn't a string.
|
|
24
|
+
*/
|
|
25
|
+
label?: string;
|
|
18
26
|
}
|
|
19
27
|
export interface TabsContentProps {
|
|
20
28
|
value: string;
|
|
@@ -23,6 +31,6 @@ export interface TabsContentProps {
|
|
|
23
31
|
}
|
|
24
32
|
export declare function Tabs({ defaultValue, value, onChange, className, children, }: TabsProps): React.JSX.Element;
|
|
25
33
|
export declare function TabsList({ className, children }: TabsListProps): React.JSX.Element;
|
|
26
|
-
export declare function TabsTrigger({ value, disabled, className, children, }: TabsTriggerProps): React.JSX.Element;
|
|
34
|
+
export declare function TabsTrigger({ value, disabled, className, children, badge, }: TabsTriggerProps): React.JSX.Element;
|
|
27
35
|
export declare function TabsContent({ value, className, children, }: TabsContentProps): React.JSX.Element | null;
|
|
28
36
|
export default Tabs;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { createContext, useContext, useMemo, useState } from "react";
|
|
4
4
|
import { Select } from "./Select";
|
|
5
5
|
const UI_TABS = {
|
|
@@ -7,6 +7,7 @@ const UI_TABS = {
|
|
|
7
7
|
listDropdown: "appkit-tabs-list--dropdown",
|
|
8
8
|
trigger: "appkit-tabs-trigger",
|
|
9
9
|
content: "appkit-tabs-content",
|
|
10
|
+
badge: "appkit-tabs-trigger__badge",
|
|
10
11
|
};
|
|
11
12
|
/** Past this many items, TabsList renders a <Select> dropdown instead of a
|
|
12
13
|
* horizontal row — a scrollable row of 6+ tabs is unreadable/unreachable on
|
|
@@ -34,19 +35,23 @@ export function TabsList({ className = "", children }) {
|
|
|
34
35
|
const { active, setActive } = useContext(TabsContext);
|
|
35
36
|
const items = React.Children.toArray(children).filter((child) => React.isValidElement(child));
|
|
36
37
|
if (items.length > TABS_DROPDOWN_THRESHOLD) {
|
|
37
|
-
const options = items.map((item) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const options = items.map((item) => {
|
|
39
|
+
const baseLabel = item.props.label ??
|
|
40
|
+
(typeof item.props.children === "string" ? item.props.children : item.props.value);
|
|
41
|
+
return {
|
|
42
|
+
value: item.props.value,
|
|
43
|
+
label: item.props.badge ? `${baseLabel} (${item.props.badge})` : baseLabel,
|
|
44
|
+
disabled: item.props.disabled,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
42
47
|
return (_jsx(Select, { bare: true, "aria-label": "Tabs", role: "tablist", options: options, value: active, onValueChange: setActive, className: [UI_TABS.list, UI_TABS.listDropdown, className].filter(Boolean).join(" ") }));
|
|
43
48
|
}
|
|
44
49
|
return (_jsx("div", { role: "tablist", className: [UI_TABS.list, className].filter(Boolean).join(" "), "data-section": "tabs-div-615", children: children }));
|
|
45
50
|
}
|
|
46
|
-
export function TabsTrigger({ value, disabled, className = "", children, }) {
|
|
51
|
+
export function TabsTrigger({ value, disabled, className = "", children, badge, }) {
|
|
47
52
|
const { active, setActive } = useContext(TabsContext);
|
|
48
53
|
const isActive = active === value;
|
|
49
|
-
return (
|
|
54
|
+
return (_jsxs("button", { role: "tab", type: "button", disabled: disabled, "aria-selected": isActive, onClick: () => setActive(value), className: [UI_TABS.trigger, className].filter(Boolean).join(" "), children: [children, typeof badge === "number" && badge > 0 && (_jsx("span", { className: UI_TABS.badge, children: badge > 99 ? "99+" : badge }))] }));
|
|
50
55
|
}
|
|
51
56
|
export function TabsContent({ value, className = "", children, }) {
|
|
52
57
|
const { active } = useContext(TabsContext);
|
|
@@ -49,6 +49,25 @@
|
|
|
49
49
|
opacity: 0.5;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
.appkit-tabs-trigger__badge {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
margin-left: 0.5rem;
|
|
57
|
+
padding: 0 0.4rem;
|
|
58
|
+
min-width: 1.25rem;
|
|
59
|
+
height: 1.25rem;
|
|
60
|
+
border-radius: 9999px;
|
|
61
|
+
font-size: 0.6875rem;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
background: var(--appkit-color-surface-elevated);
|
|
64
|
+
color: var(--appkit-color-text-muted);
|
|
65
|
+
}
|
|
66
|
+
.appkit-tabs-trigger[aria-selected="true"] .appkit-tabs-trigger__badge {
|
|
67
|
+
background: color-mix(in srgb, var(--appkit-color-primary) 15%, transparent);
|
|
68
|
+
color: var(--appkit-color-primary);
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
.appkit-tabs-content {
|
|
53
72
|
padding-top: 1rem;
|
|
54
73
|
padding-bottom: 1rem;
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -113,8 +113,6 @@ export type { ItemRowProps } from "./components/ItemRow";
|
|
|
113
113
|
export { ItemRow } from "./components/ItemRow";
|
|
114
114
|
export type { HorizontalScrollerProps, PerViewConfig, } from "./components/HorizontalScroller";
|
|
115
115
|
export { HorizontalScroller } from "./components/HorizontalScroller";
|
|
116
|
-
export type { TabStripProps, TabStripTab } from "./components/TabStrip";
|
|
117
|
-
export { TabStrip } from "./components/TabStrip";
|
|
118
116
|
export type { FormGridProps, FormFieldProps } from "./components/FormGrid";
|
|
119
117
|
export { FormGrid, FormField as FormGridField } from "./components/FormGrid";
|
|
120
118
|
export type { SettingsSectionProps, ToggleRowProps } from "./components/SettingsSection";
|
package/dist/ui/index.js
CHANGED
|
@@ -59,7 +59,6 @@ export { SummaryCard } from "./components/SummaryCard";
|
|
|
59
59
|
export { CountdownDisplay } from "./components/CountdownDisplay";
|
|
60
60
|
export { ItemRow } from "./components/ItemRow";
|
|
61
61
|
export { HorizontalScroller } from "./components/HorizontalScroller";
|
|
62
|
-
export { TabStrip } from "./components/TabStrip";
|
|
63
62
|
export { FormGrid, FormField as FormGridField } from "./components/FormGrid";
|
|
64
63
|
export { SettingsSection, ToggleRow } from "./components/SettingsSection";
|
|
65
64
|
export { DetailPageHero } from "./components/DetailPageHero";
|