@recursica/adapter-mantine-v8 1.2.1 → 1.3.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/CHANGELOG.md +18 -0
- package/OVERSTYLING.md +1 -1
- package/SETUP.md +36 -5
- package/dist/adapter-mantine-v8.cjs +2 -2
- package/dist/adapter-mantine-v8.cjs.map +1 -1
- package/dist/adapter-mantine-v8.css +1 -1
- package/dist/adapter-mantine-v8.js +3157 -3002
- package/dist/adapter-mantine-v8.js.map +1 -1
- package/dist/index.d.ts +191 -164
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/recursica_brand.json +2 -2
- package/recursica_tokens.json +2 -2
- package/recursica_ui-kit.json +2 -2
- package/recursica_variables_scoped.css +7 -3
- package/src/components/Container/USAGE.md +0 -1
- package/src/components/Flex/USAGE.md +2 -1
- package/src/components/Group/USAGE.md +2 -1
- package/src/components/LayoutGrid/LAYOUT_GRID_IMPLEMENTATION_NOTES.md +88 -0
- package/src/components/LayoutGrid/LayoutGrid.columns.dom.test.tsx +66 -0
- package/src/components/LayoutGrid/LayoutGrid.module.css +48 -0
- package/src/components/LayoutGrid/LayoutGrid.stories.tsx +61 -0
- package/src/components/LayoutGrid/LayoutGrid.test.tsx +1 -0
- package/src/components/LayoutGrid/LayoutGrid.tsx +309 -0
- package/src/components/LayoutGrid/USAGE.md +76 -0
- package/src/components/Pagination/IMPLEMENTATION_NOTES.md +29 -8
- package/src/components/Pagination/Pagination.icons.tsx +0 -32
- package/src/components/Pagination/Pagination.manifest.dom.test.tsx +108 -0
- package/src/components/Pagination/Pagination.module.css +25 -155
- package/src/components/Pagination/Pagination.tsx +328 -197
- package/src/components/Pagination/USAGE.md +1 -0
- package/src/components/Stack/USAGE.md +2 -1
- package/src/components/index.ts +1 -1
- package/src/index.ts +1 -0
- package/src/utils/breakpointsFromRecManifest.test.ts +40 -0
- package/src/utils/breakpointsFromRecManifest.ts +75 -0
- package/src/utils/index.ts +1 -0
- package/src/components/Grid/GRID_IMPLEMENTATION_NOTES.md +0 -85
- package/src/components/Grid/Grid.module.css +0 -41
- package/src/components/Grid/Grid.stories.tsx +0 -200
- package/src/components/Grid/Grid.test.tsx +0 -1
- package/src/components/Grid/Grid.tsx +0 -179
- package/src/components/Grid/USAGE.md +0 -61
|
@@ -1,247 +1,381 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import React, { createContext, forwardRef, useContext } from "react";
|
|
2
|
+
import { usePagination } from "@mantine/hooks";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
type PaginationControlProps as MantinePaginationControlProps,
|
|
8
|
-
type PaginationDotsProps as MantinePaginationDotsProps,
|
|
9
|
-
} from "@mantine/core";
|
|
4
|
+
useRecursicaManifest,
|
|
5
|
+
type RecursicaPaginationProps,
|
|
6
|
+
} from "@recursica/adapter-common";
|
|
10
7
|
import {
|
|
11
8
|
filterStylingProps,
|
|
12
|
-
withCallerOverride,
|
|
13
|
-
mergeClassNames,
|
|
14
9
|
type RecursicaOverStyled,
|
|
15
10
|
} from "../../utils/filterStylingProps";
|
|
11
|
+
import { Button, type ButtonProps } from "../Button/Button";
|
|
16
12
|
import styles from "./Pagination.module.css";
|
|
17
|
-
import {
|
|
18
|
-
NextWithLabel,
|
|
19
|
-
PrevWithLabel,
|
|
20
|
-
FirstWithLabel,
|
|
21
|
-
LastWithLabel,
|
|
22
|
-
PaginationIcon,
|
|
23
|
-
} from "./Pagination.icons";
|
|
24
|
-
|
|
25
|
-
import { type RecursicaPaginationProps } from "@recursica/adapter-common";
|
|
13
|
+
import { PaginationIcon } from "./Pagination.icons";
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
>;
|
|
15
|
+
type PaginationRole = "active-pages" | "inactive-pages" | "navigation-controls";
|
|
16
|
+
type NavControl = "first" | "previous" | "next" | "last";
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
interface RoleVariant {
|
|
19
|
+
variant: NonNullable<ButtonProps["variant"]>;
|
|
20
|
+
size: NonNullable<ButtonProps["size"]>;
|
|
21
|
+
}
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
/** Reads which Button style and size the manifest selects for a Pagination role. */
|
|
24
|
+
function usePaginationRole(role: PaginationRole): RoleVariant {
|
|
25
|
+
const manifest = useRecursicaManifest();
|
|
26
|
+
const selected = (
|
|
27
|
+
manifest as {
|
|
28
|
+
"ui-kit"?: {
|
|
29
|
+
components?: {
|
|
30
|
+
pagination?: {
|
|
31
|
+
properties?: Record<
|
|
32
|
+
string,
|
|
33
|
+
{
|
|
34
|
+
$extensions?: {
|
|
35
|
+
"recursica.component"?: {
|
|
36
|
+
"selected-variants"?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
>;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
)["ui-kit"]?.components?.pagination?.properties?.[role]?.$extensions?.[
|
|
46
|
+
"recursica.component"
|
|
47
|
+
]?.["selected-variants"];
|
|
48
|
+
if (!selected) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Pagination: manifest has no ui-kit.components.pagination.properties.${role} selected-variants`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
variant: selected.style as RoleVariant["variant"],
|
|
55
|
+
size: selected.size as RoleVariant["size"],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
59
|
+
interface PaginationContextValue {
|
|
60
|
+
total: number;
|
|
61
|
+
active: number;
|
|
62
|
+
range: (number | "dots")[];
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
setPage: (page: number) => void;
|
|
65
|
+
first: () => void;
|
|
66
|
+
previous: () => void;
|
|
67
|
+
next: () => void;
|
|
68
|
+
last: () => void;
|
|
69
|
+
getItemProps?: (page: number) => Record<string, unknown>;
|
|
70
|
+
getControlProps?: (control: NavControl) => Record<string, unknown>;
|
|
71
|
+
dotsIcon?: React.ReactNode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const PaginationContext = createContext<PaginationContextValue | null>(null);
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
function usePaginationContext(): PaginationContextValue {
|
|
77
|
+
const ctx = useContext(PaginationContext);
|
|
78
|
+
if (!ctx) {
|
|
79
|
+
throw new Error("Pagination parts must be rendered inside Pagination.Root");
|
|
80
|
+
}
|
|
81
|
+
return ctx;
|
|
82
|
+
}
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
interface PaginationBaseProps {
|
|
85
|
+
/** Total number of pages */
|
|
86
|
+
total: number;
|
|
87
|
+
/** Active page (controlled) */
|
|
88
|
+
value?: number;
|
|
89
|
+
/** Initial active page (uncontrolled) */
|
|
90
|
+
defaultValue?: number;
|
|
91
|
+
/** Called when the active page changes */
|
|
92
|
+
onChange?: (page: number) => void;
|
|
93
|
+
/** Pages shown on each side of the active page */
|
|
94
|
+
siblings?: number;
|
|
95
|
+
/** Pages always shown at the start and end */
|
|
96
|
+
boundaries?: number;
|
|
97
|
+
disabled?: boolean;
|
|
98
|
+
/** Extra props for each page button */
|
|
99
|
+
getItemProps?: (page: number) => Record<string, unknown>;
|
|
100
|
+
/** Extra props for each first/previous/next/last button */
|
|
101
|
+
getControlProps?: (control: NavControl) => Record<string, unknown>;
|
|
102
|
+
/** Replaces the dots between page ranges */
|
|
103
|
+
dotsIcon?: React.ReactNode;
|
|
62
104
|
}
|
|
63
105
|
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
export type PaginationRootProps = RecursicaOverStyled<
|
|
107
|
+
PaginationBaseProps &
|
|
108
|
+
Omit<React.ComponentPropsWithoutRef<"nav">, "onChange" | "children"> & {
|
|
109
|
+
children?: React.ReactNode;
|
|
110
|
+
}
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
const _PaginationRoot = forwardRef<HTMLElement, PaginationRootProps>(
|
|
114
|
+
function PaginationRoot(
|
|
115
|
+
{
|
|
116
|
+
overStyled = false,
|
|
117
|
+
total,
|
|
118
|
+
value,
|
|
119
|
+
defaultValue,
|
|
120
|
+
onChange,
|
|
121
|
+
siblings,
|
|
122
|
+
boundaries,
|
|
123
|
+
disabled,
|
|
124
|
+
getItemProps,
|
|
125
|
+
getControlProps,
|
|
126
|
+
dotsIcon,
|
|
127
|
+
children,
|
|
128
|
+
...rest
|
|
129
|
+
},
|
|
130
|
+
ref,
|
|
131
|
+
) {
|
|
132
|
+
const pagination = usePagination({
|
|
133
|
+
total,
|
|
134
|
+
page: value,
|
|
135
|
+
initialPage: defaultValue,
|
|
136
|
+
onChange,
|
|
137
|
+
siblings,
|
|
138
|
+
boundaries,
|
|
139
|
+
});
|
|
66
140
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
67
|
-
const
|
|
68
|
-
sanitizedProps as Record<string, unknown>,
|
|
69
|
-
);
|
|
141
|
+
const classNameProp = (sanitizedProps as { className?: string }).className;
|
|
70
142
|
|
|
71
143
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
144
|
+
<PaginationContext.Provider
|
|
145
|
+
value={{
|
|
146
|
+
total,
|
|
147
|
+
active: pagination.active,
|
|
148
|
+
range: pagination.range,
|
|
149
|
+
disabled,
|
|
150
|
+
setPage: pagination.setPage,
|
|
151
|
+
first: pagination.first,
|
|
152
|
+
previous: pagination.previous,
|
|
153
|
+
next: pagination.next,
|
|
154
|
+
last: pagination.last,
|
|
155
|
+
getItemProps,
|
|
156
|
+
getControlProps,
|
|
157
|
+
dotsIcon,
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<nav
|
|
161
|
+
ref={ref}
|
|
162
|
+
aria-label="Pagination"
|
|
163
|
+
{...sanitizedProps}
|
|
164
|
+
className={
|
|
165
|
+
classNameProp ? `${styles.root} ${classNameProp}` : styles.root
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</nav>
|
|
170
|
+
</PaginationContext.Provider>
|
|
78
171
|
);
|
|
79
172
|
},
|
|
80
173
|
);
|
|
81
174
|
_PaginationRoot.displayName = "Pagination.Root";
|
|
82
175
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
>
|
|
87
|
-
|
|
88
|
-
ref,
|
|
89
|
-
) {
|
|
90
|
-
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
91
|
-
const computedIcon = withLabel ? NextWithLabel : undefined;
|
|
92
|
-
return (
|
|
93
|
-
<MantinePagination.Next
|
|
94
|
-
ref={ref}
|
|
95
|
-
{...sanitizedProps}
|
|
96
|
-
data-variant="text"
|
|
97
|
-
icon={withCallerOverride(computedIcon, icon) as any}
|
|
98
|
-
/>
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
_PaginationNext.displayName = "Pagination.Next";
|
|
102
|
-
|
|
103
|
-
const _PaginationPrevious = forwardRef<
|
|
104
|
-
HTMLButtonElement,
|
|
105
|
-
PaginationEdgeProps<typeof MantinePagination.Previous>
|
|
106
|
-
>(function PaginationPrevious(
|
|
107
|
-
{ overStyled = false, withLabel, icon, ...rest },
|
|
108
|
-
ref,
|
|
109
|
-
) {
|
|
110
|
-
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
111
|
-
const computedIcon = withLabel ? PrevWithLabel : undefined;
|
|
112
|
-
return (
|
|
113
|
-
<MantinePagination.Previous
|
|
114
|
-
ref={ref}
|
|
115
|
-
{...sanitizedProps}
|
|
116
|
-
data-variant="text"
|
|
117
|
-
icon={withCallerOverride(computedIcon, icon) as any}
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
_PaginationPrevious.displayName = "Pagination.Previous";
|
|
176
|
+
type ButtonElementProps = Omit<
|
|
177
|
+
React.ComponentPropsWithoutRef<"button">,
|
|
178
|
+
"style" | "className"
|
|
179
|
+
> &
|
|
180
|
+
Pick<ButtonProps, "overStyled">;
|
|
122
181
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
) {
|
|
130
|
-
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
131
|
-
const computedIcon = withLabel ? FirstWithLabel : undefined;
|
|
132
|
-
return (
|
|
133
|
-
<MantinePagination.First
|
|
134
|
-
ref={ref}
|
|
135
|
-
{...sanitizedProps}
|
|
136
|
-
data-variant="text"
|
|
137
|
-
icon={withCallerOverride(computedIcon, icon) as any}
|
|
138
|
-
/>
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
_PaginationFirst.displayName = "Pagination.First";
|
|
182
|
+
export type PaginationControlProps = RecursicaOverStyled<
|
|
183
|
+
ButtonElementProps & {
|
|
184
|
+
/** Whether this is the active page: uses the manifest's `active-pages` Button variant. */
|
|
185
|
+
active?: boolean;
|
|
186
|
+
}
|
|
187
|
+
>;
|
|
142
188
|
|
|
143
|
-
const
|
|
189
|
+
const _PaginationControl = forwardRef<
|
|
144
190
|
HTMLButtonElement,
|
|
145
|
-
|
|
146
|
-
>(function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
)
|
|
150
|
-
const
|
|
151
|
-
const computedIcon = withLabel ? LastWithLabel : undefined;
|
|
191
|
+
PaginationControlProps
|
|
192
|
+
>(function PaginationControl({ active = false, disabled, ...rest }, ref) {
|
|
193
|
+
const ctx = usePaginationContext();
|
|
194
|
+
const activeRole = usePaginationRole("active-pages");
|
|
195
|
+
const inactiveRole = usePaginationRole("inactive-pages");
|
|
196
|
+
const role = active ? activeRole : inactiveRole;
|
|
152
197
|
return (
|
|
153
|
-
<
|
|
198
|
+
<Button
|
|
154
199
|
ref={ref}
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
variant={role.variant}
|
|
201
|
+
size={role.size}
|
|
202
|
+
disabled={disabled || ctx.disabled}
|
|
203
|
+
aria-current={active ? "page" : undefined}
|
|
204
|
+
{...(rest as ButtonProps)}
|
|
158
205
|
/>
|
|
159
206
|
);
|
|
160
207
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const _Pagination = forwardRef<HTMLDivElement, PaginationProps>(
|
|
164
|
-
function Pagination(
|
|
165
|
-
{ overStyled = false, getControlProps, withLabels, ...rest },
|
|
166
|
-
ref,
|
|
167
|
-
) {
|
|
168
|
-
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
169
|
-
const stylingParams = usePaginationClassNames(
|
|
170
|
-
sanitizedProps as Record<string, unknown>,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const mergedGetControlProps = (
|
|
174
|
-
control: "first" | "previous" | "last" | "next",
|
|
175
|
-
) => {
|
|
176
|
-
const baseProps = { "data-variant": "text" };
|
|
177
|
-
if (getControlProps) {
|
|
178
|
-
return { ...baseProps, ...getControlProps(control) };
|
|
179
|
-
}
|
|
180
|
-
return baseProps;
|
|
181
|
-
};
|
|
208
|
+
_PaginationControl.displayName = "Pagination.Control";
|
|
182
209
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
previousIcon: PrevWithLabel,
|
|
187
|
-
firstIcon: FirstWithLabel,
|
|
188
|
-
lastIcon: LastWithLabel,
|
|
189
|
-
}
|
|
190
|
-
: {};
|
|
210
|
+
export type PaginationDotsProps = React.ComponentPropsWithoutRef<"div"> & {
|
|
211
|
+
icon?: React.ReactNode;
|
|
212
|
+
};
|
|
191
213
|
|
|
214
|
+
const _PaginationDots = forwardRef<HTMLDivElement, PaginationDotsProps>(
|
|
215
|
+
function PaginationDots({ icon, ...rest }, ref) {
|
|
216
|
+
const { size } = usePaginationRole("inactive-pages");
|
|
192
217
|
return (
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{...(sanitizedProps as MantinePaginationProps)}
|
|
197
|
-
className={stylingParams.className}
|
|
198
|
-
classNames={stylingParams.classNames}
|
|
199
|
-
getControlProps={mergedGetControlProps}
|
|
200
|
-
/>
|
|
218
|
+
<div ref={ref} className={styles.dots} data-size={size} {...rest}>
|
|
219
|
+
{icon ?? "…"}
|
|
220
|
+
</div>
|
|
201
221
|
);
|
|
202
222
|
},
|
|
203
223
|
);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
export type PaginationControlProps =
|
|
207
|
-
RecursicaOverStyled<MantinePaginationControlProps>;
|
|
224
|
+
_PaginationDots.displayName = "Pagination.Dots";
|
|
208
225
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
PaginationControlProps
|
|
212
|
-
>(function PaginationControl({ overStyled = false, ...rest }, ref) {
|
|
213
|
-
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
226
|
+
function PaginationItems() {
|
|
227
|
+
const ctx = usePaginationContext();
|
|
214
228
|
return (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
<>
|
|
230
|
+
{ctx.range.map((page, index) =>
|
|
231
|
+
page === "dots" ? (
|
|
232
|
+
<_PaginationDots key={index} icon={ctx.dotsIcon} />
|
|
233
|
+
) : (
|
|
234
|
+
<_PaginationControl
|
|
235
|
+
key={index}
|
|
236
|
+
active={page === ctx.active}
|
|
237
|
+
aria-label={`Page ${page}`}
|
|
238
|
+
onClick={() => ctx.setPage(page)}
|
|
239
|
+
{...ctx.getItemProps?.(page)}
|
|
240
|
+
>
|
|
241
|
+
{(ctx.getItemProps?.(page)?.children as React.ReactNode) ?? page}
|
|
242
|
+
</_PaginationControl>
|
|
243
|
+
),
|
|
244
|
+
)}
|
|
245
|
+
</>
|
|
219
246
|
);
|
|
220
|
-
}
|
|
221
|
-
|
|
247
|
+
}
|
|
248
|
+
PaginationItems.displayName = "Pagination.Items";
|
|
222
249
|
|
|
223
|
-
export type
|
|
224
|
-
|
|
250
|
+
export type PaginationEdgeProps = RecursicaOverStyled<
|
|
251
|
+
Omit<ButtonElementProps, "children" | "rightSection"> & {
|
|
252
|
+
/** If set to true, displays text labels alongside the icon. */
|
|
253
|
+
withLabel?: boolean;
|
|
254
|
+
icon?: React.ReactNode;
|
|
255
|
+
}
|
|
256
|
+
>;
|
|
225
257
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
258
|
+
const NAV_ARIA_LABEL: Record<NavControl, string> = {
|
|
259
|
+
first: "First page",
|
|
260
|
+
previous: "Previous page",
|
|
261
|
+
next: "Next page",
|
|
262
|
+
last: "Last page",
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const NAV_LABEL: Record<NavControl, string> = {
|
|
266
|
+
first: "First",
|
|
267
|
+
previous: "Prev",
|
|
268
|
+
next: "Next",
|
|
269
|
+
last: "Last",
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function createNavControl(
|
|
273
|
+
control: NavControl,
|
|
274
|
+
iconType: "first" | "prev" | "next" | "last",
|
|
275
|
+
iconAfterLabel: boolean,
|
|
276
|
+
displayName: string,
|
|
277
|
+
) {
|
|
278
|
+
const Component = forwardRef<HTMLButtonElement, PaginationEdgeProps>(
|
|
279
|
+
function PaginationNav({ withLabel, icon, disabled, ...rest }, ref) {
|
|
280
|
+
const ctx = usePaginationContext();
|
|
281
|
+
const role = usePaginationRole("navigation-controls");
|
|
282
|
+
const atStart = ctx.active === 1;
|
|
283
|
+
const atEnd = ctx.active === ctx.total;
|
|
284
|
+
const isDisabled =
|
|
285
|
+
disabled ||
|
|
286
|
+
ctx.disabled ||
|
|
287
|
+
((control === "first" || control === "previous") && atStart) ||
|
|
288
|
+
((control === "next" || control === "last") && atEnd);
|
|
289
|
+
const iconElement = icon ?? <PaginationIcon type={iconType} />;
|
|
290
|
+
const sections = withLabel
|
|
291
|
+
? iconAfterLabel
|
|
292
|
+
? {
|
|
293
|
+
rightSection: (
|
|
294
|
+
<span className={styles.rightIcon}>{iconElement}</span>
|
|
295
|
+
),
|
|
296
|
+
}
|
|
297
|
+
: { icon: iconElement }
|
|
298
|
+
: { icon: iconElement };
|
|
299
|
+
return (
|
|
300
|
+
<Button
|
|
301
|
+
ref={ref}
|
|
302
|
+
variant={role.variant}
|
|
303
|
+
size={role.size}
|
|
304
|
+
aria-label={NAV_ARIA_LABEL[control]}
|
|
305
|
+
disabled={isDisabled}
|
|
306
|
+
onClick={ctx[control]}
|
|
307
|
+
{...({
|
|
308
|
+
...sections,
|
|
309
|
+
...ctx.getControlProps?.(control),
|
|
310
|
+
...rest,
|
|
311
|
+
} as ButtonProps)}
|
|
312
|
+
>
|
|
313
|
+
{withLabel ? NAV_LABEL[control] : undefined}
|
|
314
|
+
</Button>
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
Component.displayName = displayName;
|
|
319
|
+
return Component;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const _PaginationFirst = createNavControl(
|
|
323
|
+
"first",
|
|
324
|
+
"first",
|
|
325
|
+
false,
|
|
326
|
+
"Pagination.First",
|
|
327
|
+
);
|
|
328
|
+
const _PaginationPrevious = createNavControl(
|
|
329
|
+
"previous",
|
|
330
|
+
"prev",
|
|
331
|
+
false,
|
|
332
|
+
"Pagination.Previous",
|
|
333
|
+
);
|
|
334
|
+
const _PaginationNext = createNavControl(
|
|
335
|
+
"next",
|
|
336
|
+
"next",
|
|
337
|
+
true,
|
|
338
|
+
"Pagination.Next",
|
|
339
|
+
);
|
|
340
|
+
const _PaginationLast = createNavControl(
|
|
341
|
+
"last",
|
|
342
|
+
"last",
|
|
343
|
+
true,
|
|
344
|
+
"Pagination.Last",
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
export type PaginationProps = RecursicaOverStyled<
|
|
348
|
+
RecursicaPaginationProps &
|
|
349
|
+
PaginationBaseProps &
|
|
350
|
+
Omit<React.ComponentPropsWithoutRef<"nav">, "onChange" | "children">
|
|
351
|
+
>;
|
|
352
|
+
|
|
353
|
+
const _Pagination = forwardRef<HTMLElement, PaginationProps>(
|
|
354
|
+
function Pagination(
|
|
355
|
+
{ withEdges, withControls = true, withLabels, ...rest },
|
|
356
|
+
ref,
|
|
357
|
+
) {
|
|
229
358
|
return (
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
|
|
359
|
+
<_PaginationRoot ref={ref} {...(rest as PaginationRootProps)}>
|
|
360
|
+
{withEdges && <_PaginationFirst withLabel={withLabels} />}
|
|
361
|
+
{withControls && <_PaginationPrevious withLabel={withLabels} />}
|
|
362
|
+
<PaginationItems />
|
|
363
|
+
{withControls && <_PaginationNext withLabel={withLabels} />}
|
|
364
|
+
{withEdges && <_PaginationLast withLabel={withLabels} />}
|
|
365
|
+
</_PaginationRoot>
|
|
234
366
|
);
|
|
235
367
|
},
|
|
236
368
|
);
|
|
237
|
-
|
|
369
|
+
_Pagination.displayName = "Pagination";
|
|
238
370
|
|
|
239
371
|
/**
|
|
240
|
-
* Recursica Pagination
|
|
372
|
+
* Recursica Pagination. Its page and navigation buttons are Recursica `Button`s whose style and
|
|
373
|
+
* size come from the manifest's `ui-kit.components.pagination` selected variants. Requires the
|
|
374
|
+
* `manifest` prop on `RecursicaThemeProvider`.
|
|
241
375
|
*/
|
|
242
376
|
export const Pagination = _Pagination as typeof _Pagination & {
|
|
243
377
|
Root: typeof _PaginationRoot;
|
|
244
|
-
Items: typeof
|
|
378
|
+
Items: typeof PaginationItems;
|
|
245
379
|
Control: typeof _PaginationControl;
|
|
246
380
|
Dots: typeof _PaginationDots;
|
|
247
381
|
Next: typeof _PaginationNext;
|
|
@@ -252,10 +386,7 @@ export const Pagination = _Pagination as typeof _Pagination & {
|
|
|
252
386
|
};
|
|
253
387
|
|
|
254
388
|
Pagination.Root = _PaginationRoot;
|
|
255
|
-
|
|
256
|
-
// `style`/`className`/BoxProps), so re-exporting Mantine's implementation
|
|
257
|
-
// directly is not a styling-gate gap.
|
|
258
|
-
Pagination.Items = MantinePagination.Items;
|
|
389
|
+
Pagination.Items = PaginationItems;
|
|
259
390
|
Pagination.Control = _PaginationControl;
|
|
260
391
|
Pagination.Dots = _PaginationDots;
|
|
261
392
|
Pagination.Next = _PaginationNext;
|
|
@@ -31,6 +31,7 @@ All Recursica components in the `@recursica/adapter-mantine-v8` package adhere s
|
|
|
31
31
|
|
|
32
32
|
> [!IMPORTANT]
|
|
33
33
|
>
|
|
34
|
+
> - **Requires the manifest**: Pagination reads which Button `style` and `size` to use for its pages and navigation controls from the Forge manifest, so `RecursicaThemeProvider` must be given `manifest={manifest}` (the parsed `recursica_manifest.json`). It throws without it. See [SETUP.md](../../../SETUP.md).
|
|
34
35
|
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
35
36
|
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
36
37
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
@@ -36,10 +36,11 @@ All Recursica components in the `@recursica/adapter-mantine-v8` package adhere s
|
|
|
36
36
|
|
|
37
37
|
> [!IMPORTANT]
|
|
38
38
|
>
|
|
39
|
-
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
40
39
|
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
41
40
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
42
41
|
|
|
42
|
+
> [!NOTE] > **Responsive props use your Mantine theme's breakpoints, not Forge's.** Keys such as `{ base, sm, md }` come from `theme.breakpoints`; Forge's layout grids switch via plain CSS `@media` and never edit the Mantine theme. Build your theme with `breakpointsFromRecManifest` so the two agree (see [LayoutGrid](../LayoutGrid/USAGE.md)).
|
|
43
|
+
|
|
43
44
|
---
|
|
44
45
|
|
|
45
46
|
## 4. Key Integration Features & Constraints
|
package/src/components/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export * from "./FileUpload/FileUpload";
|
|
|
23
23
|
export type { RecursicaFileUploadProps } from "./FileUpload/FileUpload";
|
|
24
24
|
export * from "./Flex/Flex";
|
|
25
25
|
export * from "./FormControlLayout/FormControlLayout";
|
|
26
|
-
export * from "./
|
|
26
|
+
export * from "./LayoutGrid/LayoutGrid";
|
|
27
27
|
export * from "./Group/Group";
|
|
28
28
|
export * from "./Heading/Heading";
|
|
29
29
|
export * from "./HoverCard/HoverCard";
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { breakpointsFromRecManifest } from "./breakpointsFromRecManifest";
|
|
3
|
+
|
|
4
|
+
const grid = (width: Record<string, number>) =>
|
|
5
|
+
Object.fromEntries(Object.entries(width).map(([k, v]) => [k, { $value: v }]));
|
|
6
|
+
|
|
7
|
+
describe("breakpointsFromRecManifest", () => {
|
|
8
|
+
it("starts each grid at its min-width and places default after the widest max-width", () => {
|
|
9
|
+
const manifest = {
|
|
10
|
+
brand: {
|
|
11
|
+
"layout-grids": {
|
|
12
|
+
default: {},
|
|
13
|
+
mobile: grid({ "max-width": 480 }),
|
|
14
|
+
tablet: grid({ "min-width": 481, "max-width": 780 }),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
expect(breakpointsFromRecManifest(manifest)).toEqual({
|
|
19
|
+
mobile: "0px",
|
|
20
|
+
tablet: "481px",
|
|
21
|
+
default: "781px",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns an empty object when default is the only grid", () => {
|
|
26
|
+
expect(
|
|
27
|
+
breakpointsFromRecManifest({
|
|
28
|
+
brand: { "layout-grids": { default: {} } },
|
|
29
|
+
}),
|
|
30
|
+
).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("throws for a non-default grid with no width", () => {
|
|
34
|
+
expect(() =>
|
|
35
|
+
breakpointsFromRecManifest({
|
|
36
|
+
brand: { "layout-grids": { default: {}, odd: {} } },
|
|
37
|
+
}),
|
|
38
|
+
).toThrow(/neither min-width nor max-width/);
|
|
39
|
+
});
|
|
40
|
+
});
|