@burtson-labs/ui 0.12.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/components/checkbox-card.d.ts +25 -0
- package/dist/components/checkbox-card.js +65 -0
- package/dist/components/checkbox-card.js.map +1 -0
- package/dist/components/data-table.d.ts +53 -3
- package/dist/components/data-table.js +345 -112
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dialog.d.ts +6 -1
- package/dist/components/dialog.js +21 -10
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/pagination.d.ts +104 -3
- package/dist/components/pagination.js +244 -58
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/sheet.d.ts +6 -1
- package/dist/components/sheet.js +2 -2
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/steps.d.ts +15 -2
- package/dist/components/steps.js +68 -22
- package/dist/components/steps.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { Button } from "./button.js";
|
|
3
|
-
import "
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
4
|
+
import * as React from "react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import ChevronRight from "@burtson-labs/icons/react/chevron-right";
|
|
6
7
|
import ChevronLeft from "@burtson-labs/icons/react/chevron-left";
|
|
@@ -25,74 +26,259 @@ function paginationRange(page, pageCount, siblings = 1) {
|
|
|
25
26
|
out.push(pageCount);
|
|
26
27
|
return out;
|
|
27
28
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/** The page sizes offered by default: 25, 50 or 100 rows. */
|
|
30
|
+
var DEFAULT_PAGE_SIZES = [
|
|
31
|
+
25,
|
|
32
|
+
50,
|
|
33
|
+
100
|
|
34
|
+
];
|
|
35
|
+
/** Pages needed for `total` rows, never less than 1. */
|
|
36
|
+
function pageCountFor(total, pageSize) {
|
|
37
|
+
if (!Number.isFinite(total) || total <= 0) return 1;
|
|
38
|
+
return Math.max(1, Math.ceil(total / (Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : 1)));
|
|
39
|
+
}
|
|
40
|
+
/** A requested page clamped to 1…pageCount. Anything unreadable ("abc", NaN) is page 1. */
|
|
41
|
+
function clampPage(page, pageCount) {
|
|
42
|
+
const n = typeof page === "number" ? page : Number(page);
|
|
43
|
+
if (!Number.isFinite(n)) return 1;
|
|
44
|
+
return Math.min(Math.max(1, Math.floor(n)), Math.max(1, Math.floor(pageCount) || 1));
|
|
45
|
+
}
|
|
46
|
+
/** Where page `page` of `total` rows starts and ends, for server-side paging. */
|
|
47
|
+
function pageWindow({ page, pageSize, total }) {
|
|
48
|
+
const size = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : 1;
|
|
49
|
+
const safeTotal = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
|
|
50
|
+
const pageCount = pageCountFor(safeTotal, size);
|
|
51
|
+
const p = clampPage(page, pageCount);
|
|
52
|
+
const start = (p - 1) * size;
|
|
53
|
+
const to = Math.min(start + size, safeTotal);
|
|
54
|
+
return {
|
|
55
|
+
page: p,
|
|
56
|
+
pageCount,
|
|
57
|
+
pageSize: size,
|
|
58
|
+
total: safeTotal,
|
|
59
|
+
from: safeTotal ? start + 1 : 0,
|
|
60
|
+
to: safeTotal ? to : 0
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** One page of `rows`, with the numbers for the page controls. */
|
|
64
|
+
function paginate(rows, page, pageSize) {
|
|
65
|
+
const w = pageWindow({
|
|
66
|
+
page,
|
|
67
|
+
pageSize,
|
|
68
|
+
total: rows.length
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
...w,
|
|
72
|
+
rows: rows.slice(w.from ? w.from - 1 : 0, w.to)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* "1–25 of 312", "7 of 7" when everything fits, "0 of 0" when empty. Numbers
|
|
77
|
+
* use the locale's grouping ("1,025"); pass `locale` to pin it.
|
|
78
|
+
*/
|
|
79
|
+
function paginationSummary({ from, to, total }, locale) {
|
|
80
|
+
const fmt = new Intl.NumberFormat(locale);
|
|
81
|
+
if (!total) return "0 of 0";
|
|
82
|
+
if (from === 1 && to === total) return `${fmt.format(total)} of ${fmt.format(total)}`;
|
|
83
|
+
return `${fmt.format(from)}–${fmt.format(to)} of ${fmt.format(total)}`;
|
|
84
|
+
}
|
|
85
|
+
var storedSizeKey = (key) => `bl-page-size:${key}`;
|
|
86
|
+
function readStoredSize(key, options) {
|
|
87
|
+
if (!key) return void 0;
|
|
88
|
+
try {
|
|
89
|
+
const n = Number(globalThis.localStorage?.getItem(storedSizeKey(key)));
|
|
90
|
+
return options.includes(n) ? n : void 0;
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function writeStoredSize(key, size) {
|
|
96
|
+
if (!key) return;
|
|
97
|
+
try {
|
|
98
|
+
globalThis.localStorage?.setItem(storedSizeKey(key), String(size));
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Headless paging for a list or table: slices `rows`, clamps the page,
|
|
103
|
+
* returns to page 1 when `resetKey` changes, and optionally remembers the
|
|
104
|
+
* page size. Controlled `page` / `pageSize` let an app keep them in the URL.
|
|
105
|
+
*/
|
|
106
|
+
function usePagination(rows, options = {}) {
|
|
107
|
+
const { page: pageProp, defaultPage = 1, onPageChange, pageSize: pageSizeProp, defaultPageSize, onPageSizeChange, pageSizeOptions = DEFAULT_PAGE_SIZES, resetKey, storageKey, total: serverTotal, locale } = options;
|
|
108
|
+
const [pageState, setPageState] = React.useState(defaultPage);
|
|
109
|
+
const [sizeState, setSizeState] = React.useState(() => readStoredSize(storageKey, pageSizeOptions) ?? defaultPageSize ?? pageSizeOptions[0] ?? 25);
|
|
110
|
+
const page = pageProp ?? pageState;
|
|
111
|
+
const pageSize = pageSizeProp ?? sizeState;
|
|
112
|
+
const latest = React.useRef({
|
|
113
|
+
onPageChange,
|
|
114
|
+
onPageSizeChange,
|
|
115
|
+
pageProp,
|
|
116
|
+
storageKey
|
|
117
|
+
});
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
latest.current = {
|
|
120
|
+
onPageChange,
|
|
121
|
+
onPageSizeChange,
|
|
122
|
+
pageProp,
|
|
123
|
+
storageKey
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const setPage = React.useCallback((next) => {
|
|
127
|
+
setPageState(next);
|
|
128
|
+
latest.current.onPageChange?.(next);
|
|
129
|
+
}, []);
|
|
130
|
+
const setPageSize = React.useCallback((next) => {
|
|
131
|
+
writeStoredSize(latest.current.storageKey, next);
|
|
132
|
+
setSizeState(next);
|
|
133
|
+
setPageState(1);
|
|
134
|
+
latest.current.onPageSizeChange?.(next);
|
|
135
|
+
latest.current.onPageChange?.(1);
|
|
136
|
+
}, []);
|
|
137
|
+
const [lastKey, setLastKey] = React.useState(resetKey);
|
|
138
|
+
if (!Object.is(lastKey, resetKey)) {
|
|
139
|
+
setLastKey(resetKey);
|
|
140
|
+
setPageState(1);
|
|
141
|
+
}
|
|
142
|
+
const committedKey = React.useRef(resetKey);
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (Object.is(committedKey.current, resetKey)) return;
|
|
145
|
+
committedKey.current = resetKey;
|
|
146
|
+
const { pageProp: controlled, onPageChange: report } = latest.current;
|
|
147
|
+
if (controlled !== void 0 && controlled !== 1) report?.(1);
|
|
148
|
+
}, [resetKey]);
|
|
149
|
+
const w = serverTotal === void 0 ? paginate(rows, page, pageSize) : {
|
|
150
|
+
...pageWindow({
|
|
151
|
+
page,
|
|
152
|
+
pageSize,
|
|
153
|
+
total: serverTotal
|
|
154
|
+
}),
|
|
155
|
+
rows: [...rows]
|
|
156
|
+
};
|
|
157
|
+
const summary = paginationSummary(w, locale);
|
|
158
|
+
const hasPages = w.total > Math.min(...pageSizeOptions, w.pageSize);
|
|
159
|
+
return {
|
|
160
|
+
...w,
|
|
161
|
+
pageSizeOptions,
|
|
162
|
+
summary,
|
|
163
|
+
hasPages,
|
|
164
|
+
setPage,
|
|
165
|
+
setPageSize,
|
|
166
|
+
paginationProps: {
|
|
167
|
+
page: w.page,
|
|
168
|
+
pageCount: w.pageCount,
|
|
169
|
+
onPageChange: setPage,
|
|
170
|
+
pageSize: w.pageSize,
|
|
171
|
+
pageSizeOptions,
|
|
172
|
+
onPageSizeChange: setPageSize,
|
|
173
|
+
summary
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Page controls for a table or list: a summary, the pages, and optionally a
|
|
179
|
+
* rows-per-page menu. Below 640px it becomes "Page 2 of 13" between large
|
|
180
|
+
* previous and next buttons. Buttons are real buttons, keyboard-reachable,
|
|
181
|
+
* and 44px on touch screens.
|
|
182
|
+
*/
|
|
183
|
+
function Pagination({ page: requestedPage, pageCount: requestedCount, onPageChange, siblings = 1, summary, total, pageSize, pageSizeOptions = DEFAULT_PAGE_SIZES, onPageSizeChange, pageSizeLabel = "Rows per page", formatPageSize = (size) => `${size} per page`, className, ...props }) {
|
|
30
184
|
const pageCount = Number.isFinite(requestedCount) ? Math.max(0, Math.floor(requestedCount)) : 0;
|
|
31
185
|
const page = pageCount === 0 ? 0 : Number.isFinite(requestedPage) ? Math.max(1, Math.min(pageCount, Math.floor(requestedPage))) : 1;
|
|
32
186
|
const pages = paginationRange(page, pageCount, siblings);
|
|
33
187
|
const go = (p) => onPageChange(Math.min(Math.max(1, p), Math.max(1, pageCount)));
|
|
188
|
+
const shownSummary = summary ?? (total !== void 0 && pageSize ? paginationSummary(pageWindow({
|
|
189
|
+
page: page || 1,
|
|
190
|
+
pageSize,
|
|
191
|
+
total
|
|
192
|
+
})) : null);
|
|
193
|
+
const sizes = pageSize !== void 0 && !pageSizeOptions.includes(pageSize) ? [...pageSizeOptions, pageSize].sort((a, b) => a - b) : pageSizeOptions;
|
|
194
|
+
const position = `Page ${page} of ${pageCount}`;
|
|
34
195
|
return /* @__PURE__ */ jsxs("nav", {
|
|
35
196
|
"aria-label": "Pagination",
|
|
36
197
|
"data-slot": "pagination",
|
|
37
|
-
className: cn("flex flex-wrap items-center justify-between gap-3", className),
|
|
198
|
+
className: cn("flex flex-wrap items-center justify-between gap-x-3 gap-y-2", className),
|
|
38
199
|
...props,
|
|
39
|
-
children: [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"aria-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ jsx("div", {
|
|
202
|
+
"data-slot": "pagination-summary",
|
|
203
|
+
className: "min-w-0 text-xs text-muted-foreground tabular-nums max-sm:order-2",
|
|
204
|
+
children: shownSummary
|
|
205
|
+
}),
|
|
206
|
+
/* @__PURE__ */ jsx("span", {
|
|
207
|
+
role: "status",
|
|
208
|
+
className: "sr-only",
|
|
209
|
+
children: pageCount > 0 ? position : ""
|
|
210
|
+
}),
|
|
211
|
+
/* @__PURE__ */ jsxs("ul", {
|
|
212
|
+
className: "flex items-center gap-1 max-sm:order-1 max-sm:w-full max-sm:justify-between",
|
|
213
|
+
children: [
|
|
214
|
+
/* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Button, {
|
|
215
|
+
variant: "ghost",
|
|
216
|
+
size: "sm",
|
|
217
|
+
onClick: () => go(page - 1),
|
|
218
|
+
disabled: page <= 1,
|
|
219
|
+
"aria-label": "Previous page",
|
|
220
|
+
className: "max-sm:size-11 max-sm:border max-sm:border-border max-sm:px-0",
|
|
221
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", {
|
|
222
|
+
className: "hidden sm:inline",
|
|
223
|
+
children: "Previous"
|
|
224
|
+
})]
|
|
225
|
+
}) }),
|
|
226
|
+
pages.map((p, i) => p === "ellipsis" ? /* @__PURE__ */ jsx("li", {
|
|
227
|
+
"aria-hidden": true,
|
|
228
|
+
className: "hidden px-1 text-muted-foreground sm:block",
|
|
229
|
+
children: "…"
|
|
230
|
+
}, `e${i}`) : /* @__PURE__ */ jsx("li", {
|
|
231
|
+
className: "hidden sm:block",
|
|
232
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
233
|
+
variant: p === page ? "outline" : "ghost",
|
|
234
|
+
size: "icon-sm",
|
|
235
|
+
onClick: () => go(p),
|
|
236
|
+
"aria-current": p === page ? "page" : void 0,
|
|
237
|
+
"aria-label": `Page ${p}`,
|
|
238
|
+
className: cn("tabular-nums", p === page && "border-brand/40 text-brand"),
|
|
239
|
+
children: p
|
|
240
|
+
})
|
|
241
|
+
}, p)),
|
|
242
|
+
/* @__PURE__ */ jsx("li", {
|
|
243
|
+
"aria-hidden": true,
|
|
244
|
+
"data-slot": "pagination-position",
|
|
245
|
+
className: "px-2 text-sm font-medium tabular-nums sm:hidden",
|
|
246
|
+
children: position
|
|
247
|
+
}),
|
|
248
|
+
/* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Button, {
|
|
249
|
+
variant: "ghost",
|
|
250
|
+
size: "sm",
|
|
251
|
+
onClick: () => go(page + 1),
|
|
252
|
+
disabled: page >= pageCount,
|
|
253
|
+
"aria-label": "Next page",
|
|
254
|
+
className: "max-sm:size-11 max-sm:border max-sm:border-border max-sm:px-0",
|
|
255
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
256
|
+
className: "hidden sm:inline",
|
|
257
|
+
children: "Next"
|
|
258
|
+
}), /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": true })]
|
|
259
|
+
}) })
|
|
260
|
+
]
|
|
261
|
+
}),
|
|
262
|
+
onPageSizeChange && /* @__PURE__ */ jsxs(Select, {
|
|
263
|
+
value: pageSize !== void 0 ? String(pageSize) : void 0,
|
|
264
|
+
onValueChange: (v) => onPageSizeChange(Number(v)),
|
|
265
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
82
266
|
size: "sm",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
267
|
+
"aria-label": pageSizeLabel,
|
|
268
|
+
className: "w-auto gap-1.5 text-xs tabular-nums max-sm:order-3 pointer-coarse:h-11",
|
|
269
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
270
|
+
}), /* @__PURE__ */ jsx(SelectContent, {
|
|
271
|
+
align: "end",
|
|
272
|
+
children: sizes.map((s) => /* @__PURE__ */ jsx(SelectItem, {
|
|
273
|
+
value: String(s),
|
|
274
|
+
children: formatPageSize(s)
|
|
275
|
+
}, s))
|
|
276
|
+
})]
|
|
277
|
+
})
|
|
278
|
+
]
|
|
93
279
|
});
|
|
94
280
|
}
|
|
95
281
|
//#endregion
|
|
96
|
-
export { Pagination, paginationRange };
|
|
282
|
+
export { DEFAULT_PAGE_SIZES, Pagination, clampPage, pageCountFor, pageWindow, paginate, paginationRange, paginationSummary, usePagination };
|
|
97
283
|
|
|
98
284
|
//# sourceMappingURL=pagination.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.js","names":[],"sources":["../../src/components/pagination.tsx"],"sourcesContent":["import ChevronLeft from '@burtson-labs/icons/react/chevron-left';\nimport ChevronRight from '@burtson-labs/icons/react/chevron-right';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\n/**\n * The page numbers to show: always the first and last page, `siblings` pages\n * either side of the current one, and 'ellipsis' for the gaps.\n */\nexport function paginationRange(\n page: number,\n pageCount: number,\n siblings = 1,\n): (number | 'ellipsis')[] {\n pageCount = Number.isFinite(pageCount) ? Math.max(0, Math.floor(pageCount)) : 0;\n siblings = Number.isFinite(siblings) ? Math.min(5, Math.max(0, Math.floor(siblings))) : 1;\n page = Number.isFinite(page) ? Math.max(1, Math.min(pageCount, Math.floor(page))) : 1;\n if (pageCount <= 0) return [];\n const window = siblings * 2 + 5; // first, last, current, siblings, two gaps\n if (pageCount <= window) return Array.from({ length: pageCount }, (_, i) => i + 1);\n const start = Math.max(2, page - siblings);\n const end = Math.min(pageCount - 1, page + siblings);\n const out: (number | 'ellipsis')[] = [1];\n if (start > 2) out.push('ellipsis');\n for (let p = start; p <= end; p += 1) out.push(p);\n if (end < pageCount - 1) out.push('ellipsis');\n out.push(pageCount);\n return out;\n}\n\nexport interface PaginationProps extends Omit<React.ComponentProps<'nav'>, 'onChange'> {\n /** 1-based current page. */\n page: number;\n pageCount: number;\n onPageChange: (page: number) => void;\n siblings?: number;\n /** Optional summary on the left, e.g. \"1–25 of 312\". */\n summary?: React.ReactNode;\n}\n\n/** Page controls for a table or list. Buttons are real buttons, keyboard-reachable. */\nfunction Pagination({\n page: requestedPage,\n pageCount: requestedCount,\n onPageChange,\n siblings = 1,\n summary,\n className,\n ...props\n}: PaginationProps) {\n const pageCount = Number.isFinite(requestedCount) ? Math.max(0, Math.floor(requestedCount)) : 0;\n const page =\n pageCount === 0\n ? 0\n : Number.isFinite(requestedPage)\n ? Math.max(1, Math.min(pageCount, Math.floor(requestedPage)))\n : 1;\n const pages = paginationRange(page, pageCount, siblings);\n const go = (p: number) => onPageChange(Math.min(Math.max(1, p), Math.max(1, pageCount)));\n return (\n <nav\n aria-label=\"Pagination\"\n data-slot=\"pagination\"\n className={cn('flex flex-wrap items-center justify-between gap-3', className)}\n {...props}\n >\n <div className=\"text-xs text-muted-foreground tabular-nums\">{summary}</div>\n <ul className=\"flex items-center gap-1\">\n <li>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => go(page - 1)}\n disabled={page <= 1}\n aria-label=\"Previous page\"\n >\n <ChevronLeft aria-hidden />\n <span className=\"hidden sm:inline\">Previous</span>\n </Button>\n </li>\n {pages.map((p, i) =>\n p === 'ellipsis' ? (\n <li key={`e${i}`} aria-hidden className=\"px-1 text-muted-foreground\">\n …\n </li>\n ) : (\n <li key={p} className=\"hidden sm:block\">\n <Button\n variant={p === page ? 'outline' : 'ghost'}\n size=\"icon-sm\"\n onClick={() => go(p)}\n aria-current={p === page ? 'page' : undefined}\n aria-label={`Page ${p}`}\n className={cn('tabular-nums', p === page && 'border-brand/40 text-brand')}\n >\n {p}\n </Button>\n </li>\n ),\n )}\n <li className=\"px-2 text-xs text-muted-foreground tabular-nums sm:hidden\">\n {page} / {pageCount}\n </li>\n <li>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => go(page + 1)}\n disabled={page >= pageCount}\n aria-label=\"Next page\"\n >\n <span className=\"hidden sm:inline\">Next</span>\n <ChevronRight aria-hidden />\n </Button>\n </li>\n </ul>\n </nav>\n );\n}\n\nexport { Pagination };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pagination.js","names":[],"sources":["../../src/components/pagination.tsx"],"sourcesContent":["import ChevronLeft from '@burtson-labs/icons/react/chevron-left';\nimport ChevronRight from '@burtson-labs/icons/react/chevron-right';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';\n\n/**\n * The page numbers to show: always the first and last page, `siblings` pages\n * either side of the current one, and 'ellipsis' for the gaps.\n */\nexport function paginationRange(\n page: number,\n pageCount: number,\n siblings = 1,\n): (number | 'ellipsis')[] {\n pageCount = Number.isFinite(pageCount) ? Math.max(0, Math.floor(pageCount)) : 0;\n siblings = Number.isFinite(siblings) ? Math.min(5, Math.max(0, Math.floor(siblings))) : 1;\n page = Number.isFinite(page) ? Math.max(1, Math.min(pageCount, Math.floor(page))) : 1;\n if (pageCount <= 0) return [];\n const window = siblings * 2 + 5; // first, last, current, siblings, two gaps\n if (pageCount <= window) return Array.from({ length: pageCount }, (_, i) => i + 1);\n const start = Math.max(2, page - siblings);\n const end = Math.min(pageCount - 1, page + siblings);\n const out: (number | 'ellipsis')[] = [1];\n if (start > 2) out.push('ellipsis');\n for (let p = start; p <= end; p += 1) out.push(p);\n if (end < pageCount - 1) out.push('ellipsis');\n out.push(pageCount);\n return out;\n}\n\n/* ------------------------------------------------------------------------ */\n/* Paging arithmetic: no React, for any list, table or server query. */\n/* ------------------------------------------------------------------------ */\n\n/** The page sizes offered by default: 25, 50 or 100 rows. */\nexport const DEFAULT_PAGE_SIZES: readonly number[] = [25, 50, 100];\n\nexport interface PageWindow {\n /** 1-based, clamped to the pages that exist (1 when there are no rows). */\n page: number;\n /** At least 1, so \"Page 1 of 1\" reads sensibly for an empty list. */\n pageCount: number;\n pageSize: number;\n total: number;\n /** 1-based index of the first row on this page; 0 when there are none. */\n from: number;\n /** 1-based index of the last row on this page; 0 when there are none. */\n to: number;\n}\n\n/** Pages needed for `total` rows, never less than 1. */\nexport function pageCountFor(total: number, pageSize: number): number {\n if (!Number.isFinite(total) || total <= 0) return 1;\n const size = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : 1;\n return Math.max(1, Math.ceil(total / size));\n}\n\n/** A requested page clamped to 1…pageCount. Anything unreadable (\"abc\", NaN) is page 1. */\nexport function clampPage(page: unknown, pageCount: number): number {\n const n = typeof page === 'number' ? page : Number(page);\n if (!Number.isFinite(n)) return 1;\n return Math.min(Math.max(1, Math.floor(n)), Math.max(1, Math.floor(pageCount) || 1));\n}\n\n/** Where page `page` of `total` rows starts and ends, for server-side paging. */\nexport function pageWindow({\n page,\n pageSize,\n total,\n}: {\n page: unknown;\n pageSize: number;\n total: number;\n}): PageWindow {\n const size = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : 1;\n const safeTotal = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;\n const pageCount = pageCountFor(safeTotal, size);\n const p = clampPage(page, pageCount);\n const start = (p - 1) * size;\n const to = Math.min(start + size, safeTotal);\n return {\n page: p,\n pageCount,\n pageSize: size,\n total: safeTotal,\n from: safeTotal ? start + 1 : 0,\n to: safeTotal ? to : 0,\n };\n}\n\n/** One page of `rows`, with the numbers for the page controls. */\nexport function paginate<T>(\n rows: readonly T[],\n page: unknown,\n pageSize: number,\n): PageWindow & { rows: T[] } {\n const w = pageWindow({ page, pageSize, total: rows.length });\n return { ...w, rows: rows.slice(w.from ? w.from - 1 : 0, w.to) };\n}\n\n/**\n * \"1–25 of 312\", \"7 of 7\" when everything fits, \"0 of 0\" when empty. Numbers\n * use the locale's grouping (\"1,025\"); pass `locale` to pin it.\n */\nexport function paginationSummary(\n { from, to, total }: Pick<PageWindow, 'from' | 'to' | 'total'>,\n locale?: string,\n): string {\n const fmt = new Intl.NumberFormat(locale);\n if (!total) return '0 of 0';\n if (from === 1 && to === total) return `${fmt.format(total)} of ${fmt.format(total)}`;\n return `${fmt.format(from)}–${fmt.format(to)} of ${fmt.format(total)}`;\n}\n\nconst storedSizeKey = (key: string) => `bl-page-size:${key}`;\n\nfunction readStoredSize(key: string | undefined, options: readonly number[]): number | undefined {\n if (!key) return undefined;\n try {\n const n = Number(globalThis.localStorage?.getItem(storedSizeKey(key)));\n return options.includes(n) ? n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredSize(key: string | undefined, size: number) {\n if (!key) return;\n try {\n globalThis.localStorage?.setItem(storedSizeKey(key), String(size));\n } catch {\n // Storage blocked (private mode, a sandboxed frame): the size is not remembered.\n }\n}\n\nexport interface UsePaginationOptions {\n /** Controlled page (e.g. read from the URL). Leave out to let the hook keep it. */\n page?: number;\n /** Starting page when uncontrolled. */\n defaultPage?: number;\n onPageChange?: (page: number) => void;\n /** Controlled page size. */\n pageSize?: number;\n /** Starting page size when uncontrolled; the first option by default. */\n defaultPageSize?: number;\n onPageSizeChange?: (pageSize: number) => void;\n /** Sizes offered by the page-size menu. */\n pageSizeOptions?: readonly number[];\n /**\n * Whatever decides which rows are listed (search text, a filter, a tab).\n * When it changes, paging goes back to page 1. The first render never\n * resets, so a link to page 3 stays on page 3.\n */\n resetKey?: unknown;\n /** Remembers the chosen page size in localStorage under this key. */\n storageKey?: string;\n /**\n * The full row count when the server pages: `rows` is then already the\n * current page and is not sliced.\n */\n total?: number;\n /** Locale for the summary's numbers. */\n locale?: string;\n}\n\nexport interface UsePaginationResult<T> extends PageWindow {\n /** The rows to render on this page. */\n rows: T[];\n pageSizeOptions: readonly number[];\n /** \"1–25 of 312\". */\n summary: string;\n /** More rows than the smallest page size: worth showing controls for. */\n hasPages: boolean;\n setPage: (page: number) => void;\n /** Changes the size and goes back to page 1. */\n setPageSize: (pageSize: number) => void;\n /** Spread onto Pagination (or pass to DataTable's paging props). */\n paginationProps: Pick<\n PaginationProps,\n 'page' | 'pageCount' | 'onPageChange' | 'pageSize' | 'pageSizeOptions' | 'onPageSizeChange'\n > & { summary: string };\n}\n\n/**\n * Headless paging for a list or table: slices `rows`, clamps the page,\n * returns to page 1 when `resetKey` changes, and optionally remembers the\n * page size. Controlled `page` / `pageSize` let an app keep them in the URL.\n */\nexport function usePagination<T>(\n rows: readonly T[],\n options: UsePaginationOptions = {},\n): UsePaginationResult<T> {\n const {\n page: pageProp,\n defaultPage = 1,\n onPageChange,\n pageSize: pageSizeProp,\n defaultPageSize,\n onPageSizeChange,\n pageSizeOptions = DEFAULT_PAGE_SIZES,\n resetKey,\n storageKey,\n total: serverTotal,\n locale,\n } = options;\n\n const [pageState, setPageState] = React.useState(defaultPage);\n const [sizeState, setSizeState] = React.useState<number>(\n () =>\n readStoredSize(storageKey, pageSizeOptions) ?? defaultPageSize ?? pageSizeOptions[0] ?? 25,\n );\n const page = pageProp ?? pageState;\n const pageSize = pageSizeProp ?? sizeState;\n\n // Keep the latest callbacks without making the setters change every render.\n const latest = React.useRef({ onPageChange, onPageSizeChange, pageProp, storageKey });\n React.useEffect(() => {\n latest.current = { onPageChange, onPageSizeChange, pageProp, storageKey };\n });\n\n const setPage = React.useCallback((next: number) => {\n setPageState(next);\n latest.current.onPageChange?.(next);\n }, []);\n const setPageSize = React.useCallback((next: number) => {\n writeStoredSize(latest.current.storageKey, next);\n setSizeState(next);\n setPageState(1);\n latest.current.onPageSizeChange?.(next);\n latest.current.onPageChange?.(1);\n }, []);\n\n // A new filter starts again at page 1: during render for local state (no\n // flash of an empty page), and after it for a controlled page.\n const [lastKey, setLastKey] = React.useState(resetKey);\n if (!Object.is(lastKey, resetKey)) {\n setLastKey(resetKey);\n setPageState(1);\n }\n const committedKey = React.useRef(resetKey);\n React.useEffect(() => {\n if (Object.is(committedKey.current, resetKey)) return;\n committedKey.current = resetKey;\n const { pageProp: controlled, onPageChange: report } = latest.current;\n if (controlled !== undefined && controlled !== 1) report?.(1);\n }, [resetKey]);\n\n const w =\n serverTotal === undefined\n ? paginate(rows, page, pageSize)\n : { ...pageWindow({ page, pageSize, total: serverTotal }), rows: [...rows] };\n const summary = paginationSummary(w, locale);\n const hasPages = w.total > Math.min(...pageSizeOptions, w.pageSize);\n\n return {\n ...w,\n pageSizeOptions,\n summary,\n hasPages,\n setPage,\n setPageSize,\n paginationProps: {\n page: w.page,\n pageCount: w.pageCount,\n onPageChange: setPage,\n pageSize: w.pageSize,\n pageSizeOptions,\n onPageSizeChange: setPageSize,\n summary,\n },\n };\n}\n\n/* ------------------------------------------------------------------------ */\n/* The controls. */\n/* ------------------------------------------------------------------------ */\n\nexport interface PaginationProps extends Omit<React.ComponentProps<'nav'>, 'onChange'> {\n /** 1-based current page. */\n page: number;\n pageCount: number;\n onPageChange: (page: number) => void;\n siblings?: number;\n /** Summary on the left, e.g. \"1–25 of 312\". Worked out from `total` when left out. */\n summary?: React.ReactNode;\n /** Total rows: with `pageSize`, gives the default summary. */\n total?: number;\n /** Rows per page, shown in the page-size menu. */\n pageSize?: number;\n /** Sizes in the page-size menu (25, 50, 100 by default). */\n pageSizeOptions?: readonly number[];\n /** Adds a rows-per-page menu. */\n onPageSizeChange?: (pageSize: number) => void;\n /** The page-size menu's accessible name. */\n pageSizeLabel?: string;\n /** An option's text, e.g. \"25 per page\". */\n formatPageSize?: (size: number) => string;\n}\n\n/**\n * Page controls for a table or list: a summary, the pages, and optionally a\n * rows-per-page menu. Below 640px it becomes \"Page 2 of 13\" between large\n * previous and next buttons. Buttons are real buttons, keyboard-reachable,\n * and 44px on touch screens.\n */\nfunction Pagination({\n page: requestedPage,\n pageCount: requestedCount,\n onPageChange,\n siblings = 1,\n summary,\n total,\n pageSize,\n pageSizeOptions = DEFAULT_PAGE_SIZES,\n onPageSizeChange,\n pageSizeLabel = 'Rows per page',\n formatPageSize = (size) => `${size} per page`,\n className,\n ...props\n}: PaginationProps) {\n const pageCount = Number.isFinite(requestedCount) ? Math.max(0, Math.floor(requestedCount)) : 0;\n const page =\n pageCount === 0\n ? 0\n : Number.isFinite(requestedPage)\n ? Math.max(1, Math.min(pageCount, Math.floor(requestedPage)))\n : 1;\n const pages = paginationRange(page, pageCount, siblings);\n const go = (p: number) => onPageChange(Math.min(Math.max(1, p), Math.max(1, pageCount)));\n const shownSummary =\n summary ??\n (total !== undefined && pageSize\n ? paginationSummary(pageWindow({ page: page || 1, pageSize, total }))\n : null);\n const sizes =\n pageSize !== undefined && !pageSizeOptions.includes(pageSize)\n ? [...pageSizeOptions, pageSize].sort((a, b) => a - b)\n : pageSizeOptions;\n const position = `Page ${page} of ${pageCount}`;\n return (\n <nav\n aria-label=\"Pagination\"\n data-slot=\"pagination\"\n className={cn('flex flex-wrap items-center justify-between gap-x-3 gap-y-2', className)}\n {...props}\n >\n <div\n data-slot=\"pagination-summary\"\n className=\"min-w-0 text-xs text-muted-foreground tabular-nums max-sm:order-2\"\n >\n {shownSummary}\n </div>\n {/* Announced when the page changes; the visible \"Page 2 of 13\" is for phones. */}\n <span role=\"status\" className=\"sr-only\">\n {pageCount > 0 ? position : ''}\n </span>\n <ul className=\"flex items-center gap-1 max-sm:order-1 max-sm:w-full max-sm:justify-between\">\n <li>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => go(page - 1)}\n disabled={page <= 1}\n aria-label=\"Previous page\"\n className=\"max-sm:size-11 max-sm:border max-sm:border-border max-sm:px-0\"\n >\n <ChevronLeft aria-hidden />\n <span className=\"hidden sm:inline\">Previous</span>\n </Button>\n </li>\n {pages.map((p, i) =>\n p === 'ellipsis' ? (\n <li key={`e${i}`} aria-hidden className=\"hidden px-1 text-muted-foreground sm:block\">\n …\n </li>\n ) : (\n <li key={p} className=\"hidden sm:block\">\n <Button\n variant={p === page ? 'outline' : 'ghost'}\n size=\"icon-sm\"\n onClick={() => go(p)}\n aria-current={p === page ? 'page' : undefined}\n aria-label={`Page ${p}`}\n className={cn('tabular-nums', p === page && 'border-brand/40 text-brand')}\n >\n {p}\n </Button>\n </li>\n ),\n )}\n <li\n aria-hidden\n data-slot=\"pagination-position\"\n className=\"px-2 text-sm font-medium tabular-nums sm:hidden\"\n >\n {position}\n </li>\n <li>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => go(page + 1)}\n disabled={page >= pageCount}\n aria-label=\"Next page\"\n className=\"max-sm:size-11 max-sm:border max-sm:border-border max-sm:px-0\"\n >\n <span className=\"hidden sm:inline\">Next</span>\n <ChevronRight aria-hidden />\n </Button>\n </li>\n </ul>\n {onPageSizeChange && (\n <Select\n value={pageSize !== undefined ? String(pageSize) : undefined}\n onValueChange={(v) => onPageSizeChange(Number(v))}\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={pageSizeLabel}\n className=\"w-auto gap-1.5 text-xs tabular-nums max-sm:order-3 pointer-coarse:h-11\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent align=\"end\">\n {sizes.map((s) => (\n <SelectItem key={s} value={String(s)}>\n {formatPageSize(s)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </nav>\n );\n}\n\nexport { Pagination };\n"],"mappings":"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"}
|
|
@@ -9,8 +9,13 @@ declare const sideClasses: {
|
|
|
9
9
|
readonly top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]";
|
|
10
10
|
readonly bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]";
|
|
11
11
|
};
|
|
12
|
-
declare function SheetContent({ className, children, side, handle, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
12
|
+
declare function SheetContent({ className, children, side, handle, showCloseButton, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
13
13
|
side?: keyof typeof sideClasses;
|
|
14
|
+
/**
|
|
15
|
+
* The X in the corner. Turn it off when the sheet draws its own header
|
|
16
|
+
* with a close control (a SheetClose), so there are not two.
|
|
17
|
+
*/
|
|
18
|
+
showCloseButton?: boolean;
|
|
14
19
|
/**
|
|
15
20
|
* A grab bar at the top edge, the cue that this is a drawer. Visual only:
|
|
16
21
|
* it does not drag; close with the button, Escape or the overlay. On by
|
package/dist/components/sheet.js
CHANGED
|
@@ -28,7 +28,7 @@ var sideClasses = {
|
|
|
28
28
|
top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]",
|
|
29
29
|
bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]"
|
|
30
30
|
};
|
|
31
|
-
function SheetContent({ className, children, side = "right", handle = side === "bottom", ...props }) {
|
|
31
|
+
function SheetContent({ className, children, side = "right", handle = side === "bottom", showCloseButton = true, ...props }) {
|
|
32
32
|
return /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, {
|
|
33
33
|
"data-slot": "sheet-overlay",
|
|
34
34
|
className: "fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in"
|
|
@@ -43,7 +43,7 @@ function SheetContent({ className, children, side = "right", handle = side === "
|
|
|
43
43
|
className: "mx-auto mt-2 -mb-2 h-1 w-10 shrink-0 rounded-full bg-border-strong"
|
|
44
44
|
}),
|
|
45
45
|
children,
|
|
46
|
-
/* @__PURE__ */ jsxs(Dialog.Close, {
|
|
46
|
+
showCloseButton && /* @__PURE__ */ jsxs(Dialog.Close, {
|
|
47
47
|
className: cn("absolute right-3 grid size-8 place-items-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 pointer-coarse:size-11 [&_svg]:size-4 pointer-coarse:[&_svg]:size-5", side === "top" ? "top-[calc(env(safe-area-inset-top)+0.75rem)]" : "top-3"),
|
|
48
48
|
children: [/* @__PURE__ */ jsx(X, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", {
|
|
49
49
|
className: "sr-only",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sheet.js","names":["SheetPrimitive"],"sources":["../../src/components/sheet.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Dialog as SheetPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction Sheet(props: React.ComponentProps<typeof SheetPrimitive.Root>) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger(props: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Close>) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nconst sideClasses = {\n right: 'inset-y-0 right-0 h-full w-3/4 border-l [--bl-sheet-x:100%] sm:max-w-sm',\n left: 'inset-y-0 left-0 h-full w-3/4 border-r [--bl-sheet-x:-100%] sm:max-w-sm',\n // Top and bottom sheets are the mobile drawers. Each keeps clear of the\n // notch or home indicator on its edge (safe-area insets).\n top: 'inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]',\n bottom:\n 'inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]',\n} as const;\n\nfunction SheetContent({\n className,\n children,\n side = 'right',\n handle = side === 'bottom',\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n side?: keyof typeof sideClasses;\n /**\n * A grab bar at the top edge, the cue that this is a drawer. Visual only:\n * it does not drag; close with the button, Escape or the overlay. On by\n * default for bottom sheets.\n */\n handle?: boolean;\n}) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className=\"fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in\"\n />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n 'fixed z-50 max-h-dvh overflow-y-auto overscroll-contain flex flex-col gap-4 border-border-strong bg-surface-raised text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-sheet-out data-[state=open]:animate-sheet-in',\n sideClasses[side],\n className,\n )}\n {...props}\n >\n {handle && (\n <div\n data-slot=\"sheet-handle\"\n aria-hidden\n className=\"mx-auto mt-2 -mb-2 h-1 w-10 shrink-0 rounded-full bg-border-strong\"\n />\n )}\n {children}\n {/* 44px on touch screens, 32px with a mouse. */}\n <SheetPrimitive.Close\n
|
|
1
|
+
{"version":3,"file":"sheet.js","names":["SheetPrimitive"],"sources":["../../src/components/sheet.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Dialog as SheetPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction Sheet(props: React.ComponentProps<typeof SheetPrimitive.Root>) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger(props: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Close>) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nconst sideClasses = {\n right: 'inset-y-0 right-0 h-full w-3/4 border-l [--bl-sheet-x:100%] sm:max-w-sm',\n left: 'inset-y-0 left-0 h-full w-3/4 border-r [--bl-sheet-x:-100%] sm:max-w-sm',\n // Top and bottom sheets are the mobile drawers. Each keeps clear of the\n // notch or home indicator on its edge (safe-area insets).\n top: 'inset-x-0 top-0 max-h-[85dvh] rounded-b-xl border-b pt-[env(safe-area-inset-top)] [--bl-sheet-x:0] [--bl-sheet-y:-100%]',\n bottom:\n 'inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t pb-[env(safe-area-inset-bottom)] [--bl-sheet-x:0] [--bl-sheet-y:100%]',\n} as const;\n\nfunction SheetContent({\n className,\n children,\n side = 'right',\n handle = side === 'bottom',\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n side?: keyof typeof sideClasses;\n /**\n * The X in the corner. Turn it off when the sheet draws its own header\n * with a close control (a SheetClose), so there are not two.\n */\n showCloseButton?: boolean;\n /**\n * A grab bar at the top edge, the cue that this is a drawer. Visual only:\n * it does not drag; close with the button, Escape or the overlay. On by\n * default for bottom sheets.\n */\n handle?: boolean;\n}) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className=\"fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in\"\n />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n 'fixed z-50 max-h-dvh overflow-y-auto overscroll-contain flex flex-col gap-4 border-border-strong bg-surface-raised text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-sheet-out data-[state=open]:animate-sheet-in',\n sideClasses[side],\n className,\n )}\n {...props}\n >\n {handle && (\n <div\n data-slot=\"sheet-handle\"\n aria-hidden\n className=\"mx-auto mt-2 -mb-2 h-1 w-10 shrink-0 rounded-full bg-border-strong\"\n />\n )}\n {children}\n {/* 44px on touch screens, 32px with a mouse. */}\n {showCloseButton && (\n <SheetPrimitive.Close\n className={cn(\n 'absolute right-3 grid size-8 place-items-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 pointer-coarse:size-11 [&_svg]:size-4 pointer-coarse:[&_svg]:size-5',\n side === 'top' ? 'top-[calc(env(safe-area-inset-top)+0.75rem)]' : 'top-3',\n )}\n >\n <X aria-hidden />\n <span className=\"sr-only\">Close</span>\n </SheetPrimitive.Close>\n )}\n </SheetPrimitive.Content>\n </SheetPrimitive.Portal>\n );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn('flex flex-col gap-1.5 p-4', className)}\n {...props}\n />\n );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn('mt-auto flex flex-col gap-2 p-4', className)}\n {...props}\n />\n );\n}\n\nfunction SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn('text-base font-semibold tracking-[-0.02em] text-foreground', className)}\n {...props}\n />\n );\n}\n\nfunction SheetDescription({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n};\n"],"mappings":";;;;;;AAMA,SAAS,MAAM,OAAyD;CACtE,OAAO,oBAACA,OAAe,MAAhB;EAAqB,aAAU;EAAQ,GAAI;CAAQ,CAAA;AAC5D;AAEA,SAAS,aAAa,OAA4D;CAChF,OAAO,oBAACA,OAAe,SAAhB;EAAwB,aAAU;EAAgB,GAAI;CAAQ,CAAA;AACvE;AAEA,SAAS,WAAW,OAA0D;CAC5E,OAAO,oBAACA,OAAe,OAAhB;EAAsB,aAAU;EAAc,GAAI;CAAQ,CAAA;AACnE;AAEA,IAAM,cAAc;CAClB,OAAO;CACP,MAAM;CAGN,KAAK;CACL,QACE;AACJ;AAEA,SAAS,aAAa,EACpB,WACA,UACA,OAAO,SACP,SAAS,SAAS,UAClB,kBAAkB,MAClB,GAAG,SAcF;CACD,OACE,qBAACA,OAAe,QAAhB,EAAA,UAAA,CACE,oBAACA,OAAe,SAAhB;EACE,aAAU;EACV,WAAU;CACX,CAAA,GACD,qBAACA,OAAe,SAAhB;EACE,aAAU;EACV,WAAW,GACT,mQACA,YAAY,OACZ,SACF;EACA,GAAI;EAPN,UAAA;GASG,UACC,oBAAC,OAAD;IACE,aAAU;IACV,eAAA;IACA,WAAU;GACX,CAAA;GAEF;GAEA,mBACC,qBAACA,OAAe,OAAhB;IACE,WAAW,GACT,gRACA,SAAS,QAAQ,iDAAiD,OACpE;IAJF,UAAA,CAME,oBAAC,GAAD,EAAG,eAAA,KAAa,CAAA,GAChB,oBAAC,QAAD;KAAM,WAAU;KAAU,UAAA;IAAW,CAAA,CACjB;;EAEF;CACH,CAAA,CAAA,EAAA,CAAA;AAE3B;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,6BAA6B,SAAS;EACpD,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,mCAAmC,SAAS;EAC1D,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,SAA4D;CAC9F,OACE,oBAACA,OAAe,OAAhB;EACE,aAAU;EACV,WAAW,GAAG,8DAA8D,SAAS;EACrF,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,iBAAiB,EACxB,WACA,GAAG,SACuD;CAC1D,OACE,oBAACA,OAAe,aAAhB;EACE,aAAU;EACV,WAAW,GAAG,iCAAiC,SAAS;EACxD,GAAI;CACL,CAAA;AAEL"}
|
|
@@ -3,12 +3,25 @@ export interface StepItem {
|
|
|
3
3
|
title: React.ReactNode;
|
|
4
4
|
description?: React.ReactNode;
|
|
5
5
|
}
|
|
6
|
+
export type StepsBreakpoint = 'sm' | 'md' | 'lg';
|
|
6
7
|
export interface StepsProps extends React.ComponentProps<'ol'> {
|
|
7
8
|
items: StepItem[];
|
|
8
9
|
/** 0-based index of the step in progress; earlier steps show as done. */
|
|
9
10
|
current: number;
|
|
10
11
|
orientation?: 'horizontal' | 'vertical';
|
|
12
|
+
/**
|
|
13
|
+
* Below this width a horizontal Steps shows only where you are: the step's
|
|
14
|
+
* title, "Step 2 of 5" and a segmented progress bar. `sm` (640px) by
|
|
15
|
+
* default for horizontal steps; `false` keeps every step visible.
|
|
16
|
+
*/
|
|
17
|
+
compactBelow?: StepsBreakpoint | false;
|
|
18
|
+
/** The compact counter's text. */
|
|
19
|
+
formatCounter?: (step: number, total: number) => string;
|
|
11
20
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Progress through a short multi-step flow (a wizard, an onboarding). On a
|
|
23
|
+
* phone a horizontal Steps collapses to the current step, "Step 2 of 5" and
|
|
24
|
+
* a progress bar, instead of squeezing every title into a few letters.
|
|
25
|
+
*/
|
|
26
|
+
declare function Steps({ items, current, orientation, compactBelow, formatCounter, className, ...props }: StepsProps): React.JSX.Element;
|
|
14
27
|
export { Steps };
|
package/dist/components/steps.js
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import "react";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import Check from "@burtson-labs/icons/react/check";
|
|
5
5
|
//#region src/components/steps.tsx
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
var compactClasses = {
|
|
7
|
+
sm: {
|
|
8
|
+
other: "max-sm:hidden",
|
|
9
|
+
item: "max-sm:grid max-sm:grid-cols-[minmax(0,1fr)_auto] max-sm:items-baseline max-sm:gap-x-3 max-sm:gap-y-2",
|
|
10
|
+
marker: "max-sm:hidden",
|
|
11
|
+
counter: "hidden max-sm:block",
|
|
12
|
+
bar: "hidden max-sm:flex max-sm:col-span-2",
|
|
13
|
+
text: "max-sm:pt-0"
|
|
14
|
+
},
|
|
15
|
+
md: {
|
|
16
|
+
other: "max-md:hidden",
|
|
17
|
+
item: "max-md:grid max-md:grid-cols-[minmax(0,1fr)_auto] max-md:items-baseline max-md:gap-x-3 max-md:gap-y-2",
|
|
18
|
+
marker: "max-md:hidden",
|
|
19
|
+
counter: "hidden max-md:block",
|
|
20
|
+
bar: "hidden max-md:flex max-md:col-span-2",
|
|
21
|
+
text: "max-md:pt-0"
|
|
22
|
+
},
|
|
23
|
+
lg: {
|
|
24
|
+
other: "max-lg:hidden",
|
|
25
|
+
item: "max-lg:grid max-lg:grid-cols-[minmax(0,1fr)_auto] max-lg:items-baseline max-lg:gap-x-3 max-lg:gap-y-2",
|
|
26
|
+
marker: "max-lg:hidden",
|
|
27
|
+
counter: "hidden max-lg:block",
|
|
28
|
+
bar: "hidden max-lg:flex max-lg:col-span-2",
|
|
29
|
+
text: "max-lg:pt-0"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Progress through a short multi-step flow (a wizard, an onboarding). On a
|
|
34
|
+
* phone a horizontal Steps collapses to the current step, "Step 2 of 5" and
|
|
35
|
+
* a progress bar, instead of squeezing every title into a few letters.
|
|
36
|
+
*/
|
|
37
|
+
function Steps({ items, current, orientation = "horizontal", compactBelow = orientation === "horizontal" ? "sm" : false, formatCounter = (step, total) => `Step ${step} of ${total}`, className, ...props }) {
|
|
38
|
+
const compact = compactBelow ? compactClasses[compactBelow] : null;
|
|
39
|
+
const focus = Math.min(Math.max(0, current), items.length - 1);
|
|
8
40
|
return /* @__PURE__ */ jsx("ol", {
|
|
9
41
|
"data-slot": "steps",
|
|
10
42
|
"data-orientation": orientation,
|
|
@@ -12,29 +44,43 @@ function Steps({ items, current, orientation = "horizontal", className, ...props
|
|
|
12
44
|
...props,
|
|
13
45
|
children: items.map((item, i) => {
|
|
14
46
|
const state = i < current ? "complete" : i === current ? "current" : "upcoming";
|
|
47
|
+
const isFocus = compact && i === focus;
|
|
15
48
|
return /* @__PURE__ */ jsxs("li", {
|
|
16
49
|
"data-state": state,
|
|
17
50
|
"aria-current": state === "current" ? "step" : void 0,
|
|
18
|
-
className: cn("flex min-w-0 items-start gap-2.5", orientation === "horizontal" && "flex-1"),
|
|
19
|
-
children: [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
className: cn("
|
|
29
|
-
children: [
|
|
30
|
-
className: "
|
|
31
|
-
children:
|
|
51
|
+
className: cn("flex min-w-0 items-start gap-2.5", orientation === "horizontal" && "flex-1", compact && (isFocus ? compact.item : compact.other)),
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", {
|
|
54
|
+
className: cn("grid size-6 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums", state === "complete" && "border-primary bg-primary text-primary-foreground", state === "current" && "border-brand bg-brand-soft text-brand", state === "upcoming" && "border-border-strong text-muted-foreground", isFocus && compact.marker),
|
|
55
|
+
children: state === "complete" ? /* @__PURE__ */ jsx(Check, {
|
|
56
|
+
className: "size-3.5",
|
|
57
|
+
"aria-hidden": true
|
|
58
|
+
}) : i + 1
|
|
59
|
+
}),
|
|
60
|
+
/* @__PURE__ */ jsxs("span", {
|
|
61
|
+
className: cn("grid min-w-0 gap-0.5 pt-0.5", isFocus && compact.text),
|
|
62
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
63
|
+
className: cn("break-words text-[13px] font-semibold", state === "upcoming" && "text-muted-foreground"),
|
|
64
|
+
children: [item.title, /* @__PURE__ */ jsx("span", {
|
|
65
|
+
className: "sr-only",
|
|
66
|
+
children: state === "complete" ? " (done)" : state === "current" ? " (current)" : ""
|
|
67
|
+
})]
|
|
68
|
+
}), item.description && /* @__PURE__ */ jsx("span", {
|
|
69
|
+
className: "text-xs text-muted-foreground",
|
|
70
|
+
children: item.description
|
|
32
71
|
})]
|
|
33
|
-
}),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
}),
|
|
73
|
+
isFocus && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
74
|
+
"data-slot": "steps-counter",
|
|
75
|
+
className: cn("text-xs whitespace-nowrap text-muted-foreground tabular-nums", compact.counter),
|
|
76
|
+
children: formatCounter(focus + 1, items.length)
|
|
77
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
78
|
+
"data-slot": "steps-bar",
|
|
79
|
+
"aria-hidden": true,
|
|
80
|
+
className: cn("gap-1", compact.bar),
|
|
81
|
+
children: items.map((_, j) => /* @__PURE__ */ jsx("span", { className: cn("h-1 flex-1 rounded-full", j <= current ? "bg-brand" : "bg-muted") }, j))
|
|
82
|
+
})] })
|
|
83
|
+
]
|
|
38
84
|
}, i);
|
|
39
85
|
})
|
|
40
86
|
});
|