@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.
Files changed (60) hide show
  1. package/dist/_internal/shared/actions/action-registry.js +8 -0
  2. package/dist/client.d.ts +2 -2
  3. package/dist/client.js +1 -4
  4. package/dist/constants/api-endpoints.d.ts +3 -0
  5. package/dist/constants/api-endpoints.js +1 -0
  6. package/dist/features/account/hooks/useDataViewMode.d.ts +17 -0
  7. package/dist/features/account/hooks/useDataViewMode.js +55 -0
  8. package/dist/features/account/hooks/useProfile.d.ts +2 -0
  9. package/dist/features/admin/components/AdminBundlesView.d.ts +3 -4
  10. package/dist/features/admin/components/AdminBundlesView.js +2 -2
  11. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  12. package/dist/features/admin/components/AdminCategoriesView.d.ts +2 -6
  13. package/dist/features/admin/components/AdminCategoriesView.js +1 -1
  14. package/dist/features/admin/components/AdminFaqsView.d.ts +0 -2
  15. package/dist/features/admin/components/AdminPrizeDrawsView.js +1 -1
  16. package/dist/features/admin/components/DataListingView.d.ts +8 -2
  17. package/dist/features/admin/components/DataListingView.js +7 -3
  18. package/dist/features/admin/components/DataTable.d.ts +10 -2
  19. package/dist/features/admin/components/DataTable.js +4 -2
  20. package/dist/features/admin/hooks/useAdminListing.d.ts +2 -2
  21. package/dist/features/admin/hooks/useAdminListing.js +6 -1
  22. package/dist/features/categories/components/BrandDetailTabs.js +2 -12
  23. package/dist/features/categories/components/CategoryDetailTabs.js +2 -12
  24. package/dist/features/events/components/AdminEventsView.d.ts +1 -5
  25. package/dist/features/faq/components/FAQAccordion.js +3 -2
  26. package/dist/features/faq/components/RelatedFAQs.js +1 -1
  27. package/dist/features/homepage/components/FAQSection.js +3 -3
  28. package/dist/features/layout/FooterLayout.js +2 -2
  29. package/dist/features/lottery/schemas/zod.d.ts +4 -4
  30. package/dist/features/payments/schemas/firestore.d.ts +6 -0
  31. package/dist/features/payments/schemas/firestore.js +2 -0
  32. package/dist/features/seller/components/SellerBundlesView.d.ts +3 -4
  33. package/dist/features/seller/components/SellerBundlesView.js +2 -2
  34. package/dist/features/seller/components/SellerPayoutsView.d.ts +2 -5
  35. package/dist/features/seller/components/SellerPayoutsView.js +100 -21
  36. package/dist/features/seller/components/SellerPreOrdersView.js +1 -1
  37. package/dist/features/seller/components/SellerPrizeDrawsView.js +1 -1
  38. package/dist/features/seller/components/SellerProductsView.js +6 -2
  39. package/dist/features/shipments/components/AdminShipmentsView.d.ts +1 -5
  40. package/dist/index.d.ts +3 -5
  41. package/dist/index.js +3 -3
  42. package/dist/react/index.d.ts +0 -2
  43. package/dist/react/index.js +0 -2
  44. package/dist/seed/faq-seed-data.js +11 -11
  45. package/dist/seed/homepage-sections-seed-data.js +1 -1
  46. package/dist/styles.css +30 -99
  47. package/dist/tailwind-utilities.css +1 -1
  48. package/dist/ui/DataTable.js +1 -1
  49. package/dist/ui/DataTable.style.css +10 -0
  50. package/dist/ui/components/DetailPageTabs.d.ts +1 -1
  51. package/dist/ui/components/DetailPageTabs.js +6 -30
  52. package/dist/ui/components/HorizontalScroller.d.ts +1 -1
  53. package/dist/ui/components/HorizontalScroller.js +78 -197
  54. package/dist/ui/components/Tabs.d.ts +9 -1
  55. package/dist/ui/components/Tabs.js +13 -8
  56. package/dist/ui/components/Tabs.style.css +19 -0
  57. package/dist/ui/components/index.style.css +0 -1
  58. package/dist/ui/index.d.ts +0 -2
  59. package/dist/ui/index.js +0 -1
  60. package/package.json +1 -1
@@ -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", 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 &&
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: activeTabProp, defaultTab, onTabChange, className, }: DetailPageTabsProps): React.JSX.Element;
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 { Span } from "./Typography";
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: activeTabProp, defaultTab, onTabChange, className, }) {
18
- const [internalActive, setInternalActive] = useState(defaultTab ?? tabs[0]?.id ?? "");
19
- const activeTab = activeTabProp ?? internalActive;
20
- const active = tabs.find((t) => t.id === activeTab) ?? tabs[0];
21
- const handleChange = (id) => {
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
- /** Infinite-loop mode: circular slot rendering with scroll teleport no array cloning */
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
- // Prevents re-entrant teleports during the instantaneous scrollLeft reset
35
- const isJumping = useRef(false);
36
- // Prevents re-running the initial scroll-to-real-start on every itemWidth update
37
- const loopInitialized = useRef(false);
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
- const gridLoopActive = loop && gridMode && gridSlideCount > 1;
57
- // On first paint after itemWidth resolves, scroll to the first *real* item
58
- // (skipping the left clone slots that provide backward-wrap buffer).
59
- useEffect(() => {
60
- if (!loop || loopCloneCount === 0 || itemWidth === undefined)
61
- return;
62
- const el = containerRef.current;
63
- if (!el || loopInitialized.current)
64
- return;
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 = stride;
119
- requestAnimationFrame(() => {
58
+ el.scrollLeft = left;
59
+ instantScrollRaf.current = requestAnimationFrame(() => {
120
60
  el.style.scrollBehavior = "";
61
+ instantScrollRaf.current = undefined;
121
62
  });
122
- }, [gridLoopActive, gap, containerRef]);
123
- // Circular teleporter for grid mode same idea as handleScrollLoop, at slide granularity.
124
- const handleGridScrollLoop = useCallback(() => {
125
- if (!gridLoopActive)
126
- return;
127
- const el = containerRef.current;
128
- if (!el || isJumping.current)
129
- return;
130
- const stride = el.clientWidth + gap;
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
- // Always advance; handleScrollLoop teleports when clone zone is reached
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 && !loop)
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
- if (perView) {
205
- const count = resolvePerView(perView, w);
206
- if (count > 0) {
207
- setColCount(count);
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, loop, gap, containerRef, updateExtents]);
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
- // Loop mode prepends a clone of the last slide and appends a clone of the
232
- // first slide; handleGridScrollLoop teleports across a full cycle at the
233
- // boundary so the strip appears to scroll infinitely.
234
- (() => {
235
- const renderSlide = (slideIndex, key, isClone) => {
236
- const slideItems = normalizedItems.slice(slideIndex * gridCardsPerSlide, (slideIndex + 1) * gridCardsPerSlide);
237
- return (_jsx("div", { className: "appkit-hscroller__slide", style: {
238
- display: "grid",
239
- gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
240
- gap: `${gap}px`,
241
- width: "100%",
242
- flexShrink: 0,
243
- }, "aria-hidden": isClone ? true : undefined, children: slideItems.map((item, idx) => (_jsx("div", { className: [
244
- "appkit-hscroller__item",
245
- snapToItems ? "appkit-hscroller__item--snap" : "",
246
- itemClassName,
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
- const hoverHandlers = pauseOnHover
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
- if (gridMode) {
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 effectiveLoop = gridMode ? gridLoopActive : loop;
319
- const prevDisabled = !effectiveLoop && atStart;
320
- const nextDisabled = !effectiveLoop && atEnd;
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, ...hoverHandlers, "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" }) }))] }));
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, ...hoverHandlers, "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 })] }));
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
- value: item.props.value,
39
- label: typeof item.props.children === "string" ? item.props.children : item.props.value,
40
- disabled: item.props.disabled,
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 (_jsx("button", { role: "tab", type: "button", disabled: disabled, "aria-selected": isActive, onClick: () => setActive(value), className: [UI_TABS.trigger, className].filter(Boolean).join(" "), children: children }));
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;
@@ -22,7 +22,6 @@
22
22
  @import "./PriceDisplay.style.css";
23
23
  @import "./ItemRow.style.css";
24
24
  @import "./Tooltip.style.css";
25
- @import "./TabStrip.style.css";
26
25
  @import "./Layout.style.css";
27
26
  @import "./SortDropdown.style.css";
28
27
  @import "./TablePagination.style.css";
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "3.9.1",
3
+ "version": "4.0.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"