@open-mercato/ui 0.6.6-develop.5536.1.7cfc9c28a1 → 0.6.6-develop.5548.1.ce0de513a9
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/backend/DataTable.js +6 -6
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/primitives/pagination.js +3 -3
- package/dist/primitives/pagination.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/DataTable.tsx +10 -6
- package/src/backend/__tests__/DataTable.stickyResponsive.test.tsx +99 -0
- package/src/primitives/__tests__/pagination.test.tsx +31 -0
- package/src/primitives/pagination.tsx +3 -3
|
@@ -114,7 +114,7 @@ const Pagination = React.forwardRef(
|
|
|
114
114
|
ref,
|
|
115
115
|
"data-slot": "pagination",
|
|
116
116
|
"aria-label": props["aria-label"] ?? t("ui.pagination.landmark.ariaLabel", "Pagination"),
|
|
117
|
-
className: cn("flex w-full items-center justify-between gap-6", className),
|
|
117
|
+
className: cn("flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2", className),
|
|
118
118
|
...props,
|
|
119
119
|
children: [
|
|
120
120
|
showInfo ? /* @__PURE__ */ jsx(
|
|
@@ -129,7 +129,7 @@ const Pagination = React.forwardRef(
|
|
|
129
129
|
"div",
|
|
130
130
|
{
|
|
131
131
|
"data-slot": "pagination-controls",
|
|
132
|
-
className: "
|
|
132
|
+
className: "flex flex-wrap items-center justify-center gap-2",
|
|
133
133
|
children: [
|
|
134
134
|
showFirstLast ? /* @__PURE__ */ jsx(
|
|
135
135
|
"button",
|
|
@@ -159,7 +159,7 @@ const Pagination = React.forwardRef(
|
|
|
159
159
|
"ol",
|
|
160
160
|
{
|
|
161
161
|
"data-slot": "pagination-pages",
|
|
162
|
-
className: "
|
|
162
|
+
className: "flex flex-wrap items-center justify-center gap-2 list-none",
|
|
163
163
|
children: items.map((entry, index) => {
|
|
164
164
|
if (entry === "ellipsis-left" || entry === "ellipsis-right") {
|
|
165
165
|
return /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/pagination.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CompactSelectTrigger,\n Select,\n SelectContent,\n SelectItem,\n SelectValue,\n} from './compact-select'\n\n/**\n * Page navigation primitive per Figma `Pagination Group [1.1]` (DS Open\n * Mercato componentSet `199985:4135`).\n *\n * Layout (Figma `Basic` variant):\n *\n * [Left] \"Page 2 of 16\"\n * [Center] \u23EE \u25C0 [1][2]\u2026[N-1][N] \u25B6 \u23ED\n * [Right] \"7 / page\" CompactSelect\n *\n * Figma's three boolean variant props are mapped 1:1 to React props:\n *\n * \uD83E\uDD47 First / Last \u2192 `showFirstLast` (default true)\n * \u23ED\uFE0F Next / Previous \u2192 `showPrevNext` (default true)\n * \uD83E\uDDEA Advanced \u2192 `showInfo` + `showPageSize` (default both true)\n *\n * The numeric page list uses the same ellipsis algorithm Material /\n * MUI / shadcn ship: `boundaryCount` pages at each end (default 1),\n * `siblingCount` pages on either side of the current page (default 1),\n * collapse the rest into `\u2026` placeholders.\n *\n * ```tsx\n * const [page, setPage] = React.useState(1)\n * const [pageSize, setPageSize] = React.useState(25)\n * <Pagination\n * page={page}\n * pageSize={pageSize}\n * total={items.length}\n * onPageChange={setPage}\n * onPageSizeChange={setPageSize}\n * />\n *\n * // Compact (no first/last, no page-size select)\n * <Pagination\n * page={page}\n * pageSize={20}\n * total={120}\n * onPageChange={setPage}\n * showFirstLast={false}\n * showPageSize={false}\n * />\n * ```\n */\n\n/**\n * Build a stable list of pages + ellipsis placeholders that fits a\n * pagination row of `boundaryCount + 2 + siblingCount * 2 + 1` cells.\n * Returns `number` entries (1-indexed) and `'ellipsis-left'` /\n * `'ellipsis-right'` placeholders that the renderer paints as `\u2026`.\n *\n * Exported for tests + consumer reuse (e.g. a server-side rendered\n * pagination indicator that needs the same shape).\n */\nexport function buildPaginationItems(\n page: number,\n totalPages: number,\n siblingCount: number,\n boundaryCount: number,\n): Array<number | 'ellipsis-left' | 'ellipsis-right'> {\n if (totalPages <= 0) return []\n // When everything fits, just list every page.\n const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1)\n }\n const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1)\n const endPages = Array.from(\n { length: boundaryCount },\n (_, i) => totalPages - boundaryCount + 1 + i,\n )\n const siblingStart = Math.max(\n Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingEnd = Math.min(\n Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : totalPages - 1,\n )\n const middle: Array<number> = []\n for (let i = siblingStart; i <= siblingEnd; i += 1) middle.push(i)\n\n // Bridge between the start boundary and the sibling window. The\n // ellipsis is only worth showing when \u22652 page numbers fall between\n // them; if exactly 1 page is missing, render that single number\n // instead of \"\u2026\" (cleaner UX \u2014 same width, no information loss).\n const result: Array<number | 'ellipsis-left' | 'ellipsis-right'> = []\n for (const p of startPages) result.push(p)\n if (siblingStart > boundaryCount + 2) {\n result.push('ellipsis-left')\n } else if (siblingStart === boundaryCount + 2) {\n result.push(boundaryCount + 1)\n }\n for (const p of middle) result.push(p)\n const firstEnd = endPages[0] ?? totalPages\n if (siblingEnd < firstEnd - 2) {\n result.push('ellipsis-right')\n } else if (siblingEnd === firstEnd - 2) {\n result.push(firstEnd - 1)\n }\n for (const p of endPages) result.push(p)\n return result\n}\n\nconst cellVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-medium outline-none transition-colors tabular-nums ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n selected: {\n true: 'bg-muted text-foreground',\n false: 'bg-background text-foreground hover:bg-muted/40',\n },\n },\n defaultVariants: { selected: false },\n },\n)\n\nconst navButtonVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors ' +\n 'hover:bg-muted/40 hover:text-foreground ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground',\n)\n\nexport type PaginationProps = React.HTMLAttributes<HTMLDivElement> & {\n /** Current 1-indexed page. */\n page: number\n /** Items per page. */\n pageSize: number\n /** Total item count. Used to derive `Math.ceil(total / pageSize)` pages. */\n total: number\n /** Called when the user changes page. */\n onPageChange: (next: number) => void\n /** Called when the user changes page size. Optional \u2014 when omitted,\n * the \"X / page\" select is hidden. */\n onPageSizeChange?: (next: number) => void\n /** Page size options for the select. Default `[10, 25, 50, 100]`. */\n pageSizeOptions?: readonly number[]\n /** Show the left \"Page X of Y\" indicator. Default `true`. */\n showInfo?: boolean\n /** Show the right \"X / page\" select. Default `true` when\n * `onPageSizeChange` is provided; ignored otherwise. */\n showPageSize?: boolean\n /** Show \u23EE / \u23ED first / last buttons. Default `true`. */\n showFirstLast?: boolean\n /** Show \u25C0 / \u25B6 prev / next buttons. Default `true`. */\n showPrevNext?: boolean\n /** Pages on either side of the current page in the page list. Default `1`. */\n siblingCount?: number\n /** Pages pinned at each end of the page list. Default `1`. */\n boundaryCount?: number\n /** Block all interactions. Default `false`. */\n disabled?: boolean\n /** ARIA label for the navigation landmark. Default `\"Pagination\"`. */\n 'aria-label'?: string\n /** Format the \"Page X of Y\" label. */\n formatPageInfo?: (page: number, totalPages: number) => string\n /** Format the \"X / page\" label. */\n formatPageSizeLabel?: (pageSize: number) => string\n}\n\nexport const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(\n (\n {\n className,\n page,\n pageSize,\n total,\n onPageChange,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n showInfo = true,\n showPageSize: showPageSizeProp,\n showFirstLast = true,\n showPrevNext = true,\n siblingCount = 1,\n boundaryCount = 1,\n disabled = false,\n formatPageInfo,\n formatPageSizeLabel,\n ...props\n },\n ref,\n ) => {\n const t = useT()\n const resolvedFormatPageInfo =\n formatPageInfo ??\n ((p: number, total: number) =>\n t('ui.pagination.info.pageOf', 'Page {page} of {total}', { page: p, total }))\n const resolvedFormatPageSizeLabel =\n formatPageSizeLabel ??\n ((size: number) =>\n t('ui.pagination.itemsPerPage.label', '{size} / page', { size }))\n const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)))\n const safePage = Math.min(Math.max(1, page), totalPages)\n const items = React.useMemo(\n () => buildPaginationItems(safePage, totalPages, siblingCount, boundaryCount),\n [safePage, totalPages, siblingCount, boundaryCount],\n )\n const showPageSize = showPageSizeProp ?? Boolean(onPageSizeChange)\n\n const goTo = React.useCallback(\n (next: number) => {\n if (disabled) return\n const bounded = Math.min(Math.max(1, next), totalPages)\n if (bounded !== safePage) onPageChange(bounded)\n },\n [disabled, onPageChange, safePage, totalPages],\n )\n\n return (\n <nav\n ref={ref}\n data-slot=\"pagination\"\n aria-label={props['aria-label'] ?? t('ui.pagination.landmark.ariaLabel', 'Pagination')}\n className={cn('flex w-full items-center justify-between gap-6', className)}\n {...props}\n >\n {showInfo ? (\n <div\n data-slot=\"pagination-info\"\n className=\"shrink-0 text-sm text-muted-foreground tabular-nums\"\n >\n {resolvedFormatPageInfo(safePage, totalPages)}\n </div>\n ) : (\n <div />\n )}\n\n <div\n data-slot=\"pagination-controls\"\n className=\"inline-flex items-center gap-2\"\n >\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-first\"\n aria-label={t('ui.pagination.first.ariaLabel', 'First page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(1)}\n className={cn(navButtonVariants())}\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-prev\"\n aria-label={t('ui.pagination.previous.ariaLabel', 'Previous page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(safePage - 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n\n <ol\n data-slot=\"pagination-pages\"\n className=\"inline-flex items-center gap-2 list-none\"\n >\n {items.map((entry, index) => {\n if (entry === 'ellipsis-left' || entry === 'ellipsis-right') {\n return (\n <li\n key={`${entry}-${index}`}\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className=\"inline-flex size-8 items-center justify-center text-sm text-muted-foreground\"\n >\n \u2026\n </li>\n )\n }\n const selected = entry === safePage\n return (\n <li key={entry}>\n <button\n type=\"button\"\n data-slot=\"pagination-page\"\n data-state={selected ? 'on' : 'off'}\n aria-current={selected ? 'page' : undefined}\n aria-label={\n selected\n ? t('ui.pagination.page.currentAriaLabel', 'Page {page}, current page', { page: entry })\n : t('ui.pagination.page.goToAriaLabel', 'Go to page {page}', { page: entry })\n }\n disabled={disabled}\n onClick={() => goTo(entry)}\n className={cn(cellVariants({ selected }))}\n >\n {entry}\n </button>\n </li>\n )\n })}\n </ol>\n\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-next\"\n aria-label={t('ui.pagination.next.ariaLabel', 'Next page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(safePage + 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-last\"\n aria-label={t('ui.pagination.last.ariaLabel', 'Last page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(totalPages)}\n className={cn(navButtonVariants())}\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n </div>\n\n {showPageSize && onPageSizeChange ? (\n <div data-slot=\"pagination-page-size\" className=\"shrink-0\">\n <Select\n value={String(pageSize)}\n onValueChange={(next) => onPageSizeChange(Number(next))}\n disabled={disabled}\n >\n <CompactSelectTrigger aria-label={t('ui.pagination.itemsPerPage.ariaLabel', 'Items per page')}>\n <SelectValue />\n </CompactSelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {resolvedFormatPageSizeLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : (\n <div />\n )}\n </nav>\n )\n },\n)\nPagination.displayName = 'Pagination'\n\nexport { cellVariants as paginationCellVariants, navButtonVariants as paginationNavVariants }\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CompactSelectTrigger,\n Select,\n SelectContent,\n SelectItem,\n SelectValue,\n} from './compact-select'\n\n/**\n * Page navigation primitive per Figma `Pagination Group [1.1]` (DS Open\n * Mercato componentSet `199985:4135`).\n *\n * Layout (Figma `Basic` variant):\n *\n * [Left] \"Page 2 of 16\"\n * [Center] \u23EE \u25C0 [1][2]\u2026[N-1][N] \u25B6 \u23ED\n * [Right] \"7 / page\" CompactSelect\n *\n * Figma's three boolean variant props are mapped 1:1 to React props:\n *\n * \uD83E\uDD47 First / Last \u2192 `showFirstLast` (default true)\n * \u23ED\uFE0F Next / Previous \u2192 `showPrevNext` (default true)\n * \uD83E\uDDEA Advanced \u2192 `showInfo` + `showPageSize` (default both true)\n *\n * The numeric page list uses the same ellipsis algorithm Material /\n * MUI / shadcn ship: `boundaryCount` pages at each end (default 1),\n * `siblingCount` pages on either side of the current page (default 1),\n * collapse the rest into `\u2026` placeholders.\n *\n * ```tsx\n * const [page, setPage] = React.useState(1)\n * const [pageSize, setPageSize] = React.useState(25)\n * <Pagination\n * page={page}\n * pageSize={pageSize}\n * total={items.length}\n * onPageChange={setPage}\n * onPageSizeChange={setPageSize}\n * />\n *\n * // Compact (no first/last, no page-size select)\n * <Pagination\n * page={page}\n * pageSize={20}\n * total={120}\n * onPageChange={setPage}\n * showFirstLast={false}\n * showPageSize={false}\n * />\n * ```\n */\n\n/**\n * Build a stable list of pages + ellipsis placeholders that fits a\n * pagination row of `boundaryCount + 2 + siblingCount * 2 + 1` cells.\n * Returns `number` entries (1-indexed) and `'ellipsis-left'` /\n * `'ellipsis-right'` placeholders that the renderer paints as `\u2026`.\n *\n * Exported for tests + consumer reuse (e.g. a server-side rendered\n * pagination indicator that needs the same shape).\n */\nexport function buildPaginationItems(\n page: number,\n totalPages: number,\n siblingCount: number,\n boundaryCount: number,\n): Array<number | 'ellipsis-left' | 'ellipsis-right'> {\n if (totalPages <= 0) return []\n // When everything fits, just list every page.\n const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1)\n }\n const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1)\n const endPages = Array.from(\n { length: boundaryCount },\n (_, i) => totalPages - boundaryCount + 1 + i,\n )\n const siblingStart = Math.max(\n Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingEnd = Math.min(\n Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : totalPages - 1,\n )\n const middle: Array<number> = []\n for (let i = siblingStart; i <= siblingEnd; i += 1) middle.push(i)\n\n // Bridge between the start boundary and the sibling window. The\n // ellipsis is only worth showing when \u22652 page numbers fall between\n // them; if exactly 1 page is missing, render that single number\n // instead of \"\u2026\" (cleaner UX \u2014 same width, no information loss).\n const result: Array<number | 'ellipsis-left' | 'ellipsis-right'> = []\n for (const p of startPages) result.push(p)\n if (siblingStart > boundaryCount + 2) {\n result.push('ellipsis-left')\n } else if (siblingStart === boundaryCount + 2) {\n result.push(boundaryCount + 1)\n }\n for (const p of middle) result.push(p)\n const firstEnd = endPages[0] ?? totalPages\n if (siblingEnd < firstEnd - 2) {\n result.push('ellipsis-right')\n } else if (siblingEnd === firstEnd - 2) {\n result.push(firstEnd - 1)\n }\n for (const p of endPages) result.push(p)\n return result\n}\n\nconst cellVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-medium outline-none transition-colors tabular-nums ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n selected: {\n true: 'bg-muted text-foreground',\n false: 'bg-background text-foreground hover:bg-muted/40',\n },\n },\n defaultVariants: { selected: false },\n },\n)\n\nconst navButtonVariants = cva(\n 'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors ' +\n 'hover:bg-muted/40 hover:text-foreground ' +\n 'focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground',\n)\n\nexport type PaginationProps = React.HTMLAttributes<HTMLDivElement> & {\n /** Current 1-indexed page. */\n page: number\n /** Items per page. */\n pageSize: number\n /** Total item count. Used to derive `Math.ceil(total / pageSize)` pages. */\n total: number\n /** Called when the user changes page. */\n onPageChange: (next: number) => void\n /** Called when the user changes page size. Optional \u2014 when omitted,\n * the \"X / page\" select is hidden. */\n onPageSizeChange?: (next: number) => void\n /** Page size options for the select. Default `[10, 25, 50, 100]`. */\n pageSizeOptions?: readonly number[]\n /** Show the left \"Page X of Y\" indicator. Default `true`. */\n showInfo?: boolean\n /** Show the right \"X / page\" select. Default `true` when\n * `onPageSizeChange` is provided; ignored otherwise. */\n showPageSize?: boolean\n /** Show \u23EE / \u23ED first / last buttons. Default `true`. */\n showFirstLast?: boolean\n /** Show \u25C0 / \u25B6 prev / next buttons. Default `true`. */\n showPrevNext?: boolean\n /** Pages on either side of the current page in the page list. Default `1`. */\n siblingCount?: number\n /** Pages pinned at each end of the page list. Default `1`. */\n boundaryCount?: number\n /** Block all interactions. Default `false`. */\n disabled?: boolean\n /** ARIA label for the navigation landmark. Default `\"Pagination\"`. */\n 'aria-label'?: string\n /** Format the \"Page X of Y\" label. */\n formatPageInfo?: (page: number, totalPages: number) => string\n /** Format the \"X / page\" label. */\n formatPageSizeLabel?: (pageSize: number) => string\n}\n\nexport const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(\n (\n {\n className,\n page,\n pageSize,\n total,\n onPageChange,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n showInfo = true,\n showPageSize: showPageSizeProp,\n showFirstLast = true,\n showPrevNext = true,\n siblingCount = 1,\n boundaryCount = 1,\n disabled = false,\n formatPageInfo,\n formatPageSizeLabel,\n ...props\n },\n ref,\n ) => {\n const t = useT()\n const resolvedFormatPageInfo =\n formatPageInfo ??\n ((p: number, total: number) =>\n t('ui.pagination.info.pageOf', 'Page {page} of {total}', { page: p, total }))\n const resolvedFormatPageSizeLabel =\n formatPageSizeLabel ??\n ((size: number) =>\n t('ui.pagination.itemsPerPage.label', '{size} / page', { size }))\n const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)))\n const safePage = Math.min(Math.max(1, page), totalPages)\n const items = React.useMemo(\n () => buildPaginationItems(safePage, totalPages, siblingCount, boundaryCount),\n [safePage, totalPages, siblingCount, boundaryCount],\n )\n const showPageSize = showPageSizeProp ?? Boolean(onPageSizeChange)\n\n const goTo = React.useCallback(\n (next: number) => {\n if (disabled) return\n const bounded = Math.min(Math.max(1, next), totalPages)\n if (bounded !== safePage) onPageChange(bounded)\n },\n [disabled, onPageChange, safePage, totalPages],\n )\n\n return (\n <nav\n ref={ref}\n data-slot=\"pagination\"\n aria-label={props['aria-label'] ?? t('ui.pagination.landmark.ariaLabel', 'Pagination')}\n className={cn('flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2', className)}\n {...props}\n >\n {showInfo ? (\n <div\n data-slot=\"pagination-info\"\n className=\"shrink-0 text-sm text-muted-foreground tabular-nums\"\n >\n {resolvedFormatPageInfo(safePage, totalPages)}\n </div>\n ) : (\n <div />\n )}\n\n <div\n data-slot=\"pagination-controls\"\n className=\"flex flex-wrap items-center justify-center gap-2\"\n >\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-first\"\n aria-label={t('ui.pagination.first.ariaLabel', 'First page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(1)}\n className={cn(navButtonVariants())}\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-prev\"\n aria-label={t('ui.pagination.previous.ariaLabel', 'Previous page')}\n disabled={disabled || safePage <= 1}\n onClick={() => goTo(safePage - 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronLeft aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n\n <ol\n data-slot=\"pagination-pages\"\n className=\"flex flex-wrap items-center justify-center gap-2 list-none\"\n >\n {items.map((entry, index) => {\n if (entry === 'ellipsis-left' || entry === 'ellipsis-right') {\n return (\n <li\n key={`${entry}-${index}`}\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className=\"inline-flex size-8 items-center justify-center text-sm text-muted-foreground\"\n >\n \u2026\n </li>\n )\n }\n const selected = entry === safePage\n return (\n <li key={entry}>\n <button\n type=\"button\"\n data-slot=\"pagination-page\"\n data-state={selected ? 'on' : 'off'}\n aria-current={selected ? 'page' : undefined}\n aria-label={\n selected\n ? t('ui.pagination.page.currentAriaLabel', 'Page {page}, current page', { page: entry })\n : t('ui.pagination.page.goToAriaLabel', 'Go to page {page}', { page: entry })\n }\n disabled={disabled}\n onClick={() => goTo(entry)}\n className={cn(cellVariants({ selected }))}\n >\n {entry}\n </button>\n </li>\n )\n })}\n </ol>\n\n {showPrevNext ? (\n <button\n type=\"button\"\n data-slot=\"pagination-next\"\n aria-label={t('ui.pagination.next.ariaLabel', 'Next page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(safePage + 1)}\n className={cn(navButtonVariants())}\n >\n <ChevronRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n {showFirstLast ? (\n <button\n type=\"button\"\n data-slot=\"pagination-last\"\n aria-label={t('ui.pagination.last.ariaLabel', 'Last page')}\n disabled={disabled || safePage >= totalPages}\n onClick={() => goTo(totalPages)}\n className={cn(navButtonVariants())}\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-5\" />\n </button>\n ) : null}\n </div>\n\n {showPageSize && onPageSizeChange ? (\n <div data-slot=\"pagination-page-size\" className=\"shrink-0\">\n <Select\n value={String(pageSize)}\n onValueChange={(next) => onPageSizeChange(Number(next))}\n disabled={disabled}\n >\n <CompactSelectTrigger aria-label={t('ui.pagination.itemsPerPage.ariaLabel', 'Items per page')}>\n <SelectValue />\n </CompactSelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {resolvedFormatPageSizeLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : (\n <div />\n )}\n </nav>\n )\n },\n)\nPagination.displayName = 'Pagination'\n\nexport { cellVariants as paginationCellVariants, navButtonVariants as paginationNavVariants }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["total"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.5548.1.ce0de513a9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -154,13 +154,13 @@
|
|
|
154
154
|
"remark-gfm": "^4.0.1"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
157
|
+
"@open-mercato/shared": "0.6.6-develop.5548.1.ce0de513a9",
|
|
158
158
|
"react": ">=18.0.0",
|
|
159
159
|
"react-dom": ">=18.0.0",
|
|
160
160
|
"react-is": ">=18.0.0"
|
|
161
161
|
},
|
|
162
162
|
"devDependencies": {
|
|
163
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
163
|
+
"@open-mercato/shared": "0.6.6-develop.5548.1.ce0de513a9",
|
|
164
164
|
"@testing-library/dom": "^10.4.1",
|
|
165
165
|
"@testing-library/jest-dom": "^6.9.1",
|
|
166
166
|
"@testing-library/react": "^16.3.1",
|
|
@@ -367,10 +367,14 @@ const EMPTY_FILTER_VALUES: FilterValues = Object.freeze({}) as FilterValues
|
|
|
367
367
|
// sticky right-0 → shadow falls to the LEFT (use `before:` + `-left-2` + `to-l`)
|
|
368
368
|
// sticky left-0 → shadow falls to the RIGHT (use `after:` + `-right-2` + `to-r`)
|
|
369
369
|
// `foreground/8` matches the `--shadow-md` token opacity (8%) and is theme-aware.
|
|
370
|
+
// Column pinning (and these shadows) is md-and-up only: below `md` the pinned
|
|
371
|
+
// first column + actions column can be wider than the whole viewport, which
|
|
372
|
+
// leaves the scrollable middle columns no visible window at all — narrow
|
|
373
|
+
// screens fall back to plain horizontal scroll so every column stays reachable.
|
|
370
374
|
const STICKY_RIGHT_SHADOW_CLASS =
|
|
371
|
-
'before:absolute before:inset-y-0 before:-left-2 before:w-2 before:bg-gradient-to-l before:from-foreground/8 before:to-transparent before:pointer-events-none'
|
|
375
|
+
'md:before:absolute md:before:inset-y-0 md:before:-left-2 md:before:w-2 md:before:bg-gradient-to-l md:before:from-foreground/8 md:before:to-transparent md:before:pointer-events-none'
|
|
372
376
|
const STICKY_LEFT_SHADOW_CLASS =
|
|
373
|
-
'after:absolute after:inset-y-0 after:-right-2 after:w-2 after:bg-gradient-to-r after:from-foreground/8 after:to-transparent after:pointer-events-none'
|
|
377
|
+
'md:after:absolute md:after:inset-y-0 md:after:-right-2 md:after:w-2 md:after:bg-gradient-to-r md:after:from-foreground/8 md:after:to-transparent md:after:pointer-events-none'
|
|
374
378
|
|
|
375
379
|
type BulkActionExecuteResult = {
|
|
376
380
|
ok: boolean
|
|
@@ -2708,7 +2712,7 @@ export function DataTable<T>({
|
|
|
2708
2712
|
const columnMeta = (header.column.columnDef as any)?.meta
|
|
2709
2713
|
const priority = resolvePriority(header.column)
|
|
2710
2714
|
const isFirstDataColumn = headerIndex === 0
|
|
2711
|
-
const stickyClass = stickyFirstColumn && isFirstDataColumn ? ` sticky left-0 z-10 bg-background ${STICKY_LEFT_SHADOW_CLASS}` : ''
|
|
2715
|
+
const stickyClass = stickyFirstColumn && isFirstDataColumn ? ` md:sticky md:left-0 md:z-10 md:bg-background ${STICKY_LEFT_SHADOW_CLASS}` : ''
|
|
2712
2716
|
const headerCellContent = header.isPlaceholder ? null : (
|
|
2713
2717
|
<Button
|
|
2714
2718
|
variant="ghost"
|
|
@@ -2741,7 +2745,7 @@ export function DataTable<T>({
|
|
|
2741
2745
|
<TableHead
|
|
2742
2746
|
className={cn(
|
|
2743
2747
|
actionsColumnAlign === 'center' ? 'w-0 text-center' : 'w-0 text-right',
|
|
2744
|
-
stickyActionsColumn && `sticky right-0 z-20 bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2748
|
+
stickyActionsColumn && `md:sticky md:right-0 md:z-20 md:bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2745
2749
|
)}
|
|
2746
2750
|
>
|
|
2747
2751
|
{t('ui.dataTable.actionsColumn', 'Actions')}
|
|
@@ -2863,7 +2867,7 @@ export function DataTable<T>({
|
|
|
2863
2867
|
) : content
|
|
2864
2868
|
|
|
2865
2869
|
return (
|
|
2866
|
-
<TableCell key={cell.id} className={responsiveClass(priority, columnMeta?.hidden) + (isStickyCell ? ` sticky left-0 z-10 bg-background ${STICKY_LEFT_SHADOW_CLASS}` : '')}>
|
|
2870
|
+
<TableCell key={cell.id} className={responsiveClass(priority, columnMeta?.hidden) + (isStickyCell ? ` md:sticky md:left-0 md:z-10 md:bg-background ${STICKY_LEFT_SHADOW_CLASS}` : '')}>
|
|
2867
2871
|
{wrappedContent}
|
|
2868
2872
|
</TableCell>
|
|
2869
2873
|
)
|
|
@@ -2872,7 +2876,7 @@ export function DataTable<T>({
|
|
|
2872
2876
|
<TableCell
|
|
2873
2877
|
className={cn(
|
|
2874
2878
|
actionsColumnAlign === 'center' ? 'text-center whitespace-nowrap' : 'text-right whitespace-nowrap',
|
|
2875
|
-
stickyActionsColumn && `sticky right-0 z-10 bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2879
|
+
stickyActionsColumn && `md:sticky md:right-0 md:z-10 md:bg-background ${STICKY_RIGHT_SHADOW_CLASS}`,
|
|
2876
2880
|
)}
|
|
2877
2881
|
data-actions-cell
|
|
2878
2882
|
>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { DataTable } from '../DataTable'
|
|
4
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
5
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
6
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { render } from '@testing-library/react'
|
|
8
|
+
import { RowActions } from '../RowActions'
|
|
9
|
+
|
|
10
|
+
jest.mock('next/navigation', () => ({
|
|
11
|
+
useRouter: () => ({ push: jest.fn(), replace: jest.fn(), prefetch: jest.fn() }),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
15
|
+
useInjectionDataWidgets: () => ({ widgets: [], isLoading: false }),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
type Row = { id: string; title: string; status: string }
|
|
19
|
+
|
|
20
|
+
const tokensOf = (el: Element | null): string[] =>
|
|
21
|
+
(el?.getAttribute('class') ?? '').split(/\s+/).filter(Boolean)
|
|
22
|
+
|
|
23
|
+
// Pinned columns must never consume the viewport on phones: an unconditionally
|
|
24
|
+
// `sticky` first column (often ~300px wide) plus a sticky actions column can
|
|
25
|
+
// exceed a small screen entirely, leaving the middle columns scrolling
|
|
26
|
+
// invisibly underneath with no reachable window. Pinning (position, offsets,
|
|
27
|
+
// z-index, opaque background, edge shadows) must therefore apply only from the
|
|
28
|
+
// `md` breakpoint up, so narrow viewports fall back to the documented plain
|
|
29
|
+
// horizontal-scroll behavior where every column can be swiped into view.
|
|
30
|
+
describe('DataTable sticky columns are viewport-gated', () => {
|
|
31
|
+
function renderStickyTable() {
|
|
32
|
+
const columns: ColumnDef<Row>[] = [
|
|
33
|
+
{ accessorKey: 'title', header: 'Title' },
|
|
34
|
+
{ accessorKey: 'status', header: 'Status' },
|
|
35
|
+
]
|
|
36
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 0 } } })
|
|
37
|
+
const result = render(
|
|
38
|
+
<QueryClientProvider client={queryClient}>
|
|
39
|
+
<I18nProvider locale="en" dict={{}}>
|
|
40
|
+
<DataTable
|
|
41
|
+
columns={columns}
|
|
42
|
+
data={[{ id: '1', title: 'Solar rollout', status: 'open' }]}
|
|
43
|
+
stickyFirstColumn
|
|
44
|
+
stickyActionsColumn
|
|
45
|
+
rowActions={() => (
|
|
46
|
+
<RowActions items={[{ id: 'edit', label: 'Edit', onSelect: () => {} }]} />
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
</I18nProvider>
|
|
50
|
+
</QueryClientProvider>,
|
|
51
|
+
)
|
|
52
|
+
return { ...result, queryClient }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('pins the first data column only from md up', () => {
|
|
56
|
+
const { container, queryClient } = renderStickyTable()
|
|
57
|
+
try {
|
|
58
|
+
const headerCells = container.querySelectorAll('thead th')
|
|
59
|
+
const firstHeader = headerCells[0]
|
|
60
|
+
const firstBodyCell = container.querySelector('tbody tr td')
|
|
61
|
+
|
|
62
|
+
for (const cell of [firstHeader, firstBodyCell]) {
|
|
63
|
+
const tokens = tokensOf(cell)
|
|
64
|
+
expect(tokens).toEqual(
|
|
65
|
+
expect.arrayContaining(['md:sticky', 'md:left-0', 'md:bg-background', 'md:after:absolute']),
|
|
66
|
+
)
|
|
67
|
+
expect(tokens).not.toContain('sticky')
|
|
68
|
+
expect(tokens).not.toContain('left-0')
|
|
69
|
+
expect(tokens).not.toContain('bg-background')
|
|
70
|
+
expect(tokens).not.toContain('after:absolute')
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
queryClient.clear()
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('pins the actions column only from md up', () => {
|
|
78
|
+
const { container, queryClient } = renderStickyTable()
|
|
79
|
+
try {
|
|
80
|
+
const headerCells = container.querySelectorAll('thead th')
|
|
81
|
+
const actionsHeader = headerCells[headerCells.length - 1]
|
|
82
|
+
const actionsBodyCell = container.querySelector('tbody tr td[data-actions-cell]')
|
|
83
|
+
expect(actionsBodyCell).not.toBeNull()
|
|
84
|
+
|
|
85
|
+
for (const cell of [actionsHeader, actionsBodyCell]) {
|
|
86
|
+
const tokens = tokensOf(cell)
|
|
87
|
+
expect(tokens).toEqual(
|
|
88
|
+
expect.arrayContaining(['md:sticky', 'md:right-0', 'md:bg-background', 'md:before:absolute']),
|
|
89
|
+
)
|
|
90
|
+
expect(tokens).not.toContain('sticky')
|
|
91
|
+
expect(tokens).not.toContain('right-0')
|
|
92
|
+
expect(tokens).not.toContain('bg-background')
|
|
93
|
+
expect(tokens).not.toContain('before:absolute')
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
queryClient.clear()
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -60,6 +60,37 @@ describe('Pagination', () => {
|
|
|
60
60
|
expect(nav.getAttribute('aria-label')).toBe('Pagination')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
// The info text, page buttons, and page-size select together need ~600px of
|
|
64
|
+
// min-content (info and the select are shrink-0). Without flex-wrap the row
|
|
65
|
+
// cannot compress on a phone-width container, overflows its card, and drags
|
|
66
|
+
// the whole page into horizontal scroll. Every level that holds a fixed-width,
|
|
67
|
+
// shrink-0 button run must be allowed to wrap: the root row, the controls
|
|
68
|
+
// group (first/prev/pages/next/last ≈ 392px at 6 pages — wider than a phone
|
|
69
|
+
// on its own), and the page-number list itself (so a long run wraps instead
|
|
70
|
+
// of overflowing). Wrapping just the outer row leaves the ~392px controls
|
|
71
|
+
// group on a single unbreakable line that still overflows.
|
|
72
|
+
it('lets every fixed-width control row wrap on narrow containers', () => {
|
|
73
|
+
const { container } = render(
|
|
74
|
+
<Pagination
|
|
75
|
+
page={3}
|
|
76
|
+
pageSize={20}
|
|
77
|
+
total={120}
|
|
78
|
+
onPageChange={() => {}}
|
|
79
|
+
onPageSizeChange={() => {}}
|
|
80
|
+
/>,
|
|
81
|
+
)
|
|
82
|
+
const tokensOf = (selector: string) => {
|
|
83
|
+
const el = container.querySelector(selector) as HTMLElement | null
|
|
84
|
+
expect(el).not.toBeNull()
|
|
85
|
+
return (el!.getAttribute('class') ?? '').split(/\s+/).filter(Boolean)
|
|
86
|
+
}
|
|
87
|
+
expect(tokensOf('[data-slot="pagination"]')).toEqual(
|
|
88
|
+
expect.arrayContaining(['flex-wrap', 'justify-between']),
|
|
89
|
+
)
|
|
90
|
+
expect(tokensOf('[data-slot="pagination-controls"]')).toContain('flex-wrap')
|
|
91
|
+
expect(tokensOf('[data-slot="pagination-pages"]')).toContain('flex-wrap')
|
|
92
|
+
})
|
|
93
|
+
|
|
63
94
|
it('renders "Page X of Y" info by default', () => {
|
|
64
95
|
const { container } = render(
|
|
65
96
|
<Pagination page={2} pageSize={10} total={100} onPageChange={() => {}} />,
|
|
@@ -235,7 +235,7 @@ export const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(
|
|
|
235
235
|
ref={ref}
|
|
236
236
|
data-slot="pagination"
|
|
237
237
|
aria-label={props['aria-label'] ?? t('ui.pagination.landmark.ariaLabel', 'Pagination')}
|
|
238
|
-
className={cn('flex w-full items-center justify-between gap-6', className)}
|
|
238
|
+
className={cn('flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2', className)}
|
|
239
239
|
{...props}
|
|
240
240
|
>
|
|
241
241
|
{showInfo ? (
|
|
@@ -251,7 +251,7 @@ export const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(
|
|
|
251
251
|
|
|
252
252
|
<div
|
|
253
253
|
data-slot="pagination-controls"
|
|
254
|
-
className="
|
|
254
|
+
className="flex flex-wrap items-center justify-center gap-2"
|
|
255
255
|
>
|
|
256
256
|
{showFirstLast ? (
|
|
257
257
|
<button
|
|
@@ -280,7 +280,7 @@ export const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(
|
|
|
280
280
|
|
|
281
281
|
<ol
|
|
282
282
|
data-slot="pagination-pages"
|
|
283
|
-
className="
|
|
283
|
+
className="flex flex-wrap items-center justify-center gap-2 list-none"
|
|
284
284
|
>
|
|
285
285
|
{items.map((entry, index) => {
|
|
286
286
|
if (entry === 'ellipsis-left' || entry === 'ellipsis-right') {
|