@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.
@@ -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: "inline-flex items-center gap-2",
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: "inline-flex items-center gap-2 list-none",
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.5536.1.7cfc9c28a1",
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.5536.1.7cfc9c28a1",
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.5536.1.7cfc9c28a1",
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="inline-flex items-center gap-2"
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="inline-flex items-center gap-2 list-none"
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') {