@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/OVERSTYLING.md +1 -1
  3. package/SETUP.md +36 -5
  4. package/dist/adapter-mantine-v8.cjs +2 -2
  5. package/dist/adapter-mantine-v8.cjs.map +1 -1
  6. package/dist/adapter-mantine-v8.css +1 -1
  7. package/dist/adapter-mantine-v8.js +3157 -3002
  8. package/dist/adapter-mantine-v8.js.map +1 -1
  9. package/dist/index.d.ts +191 -164
  10. package/llms.txt +1 -1
  11. package/package.json +3 -3
  12. package/recursica_brand.json +2 -2
  13. package/recursica_tokens.json +2 -2
  14. package/recursica_ui-kit.json +2 -2
  15. package/recursica_variables_scoped.css +7 -3
  16. package/src/components/Container/USAGE.md +0 -1
  17. package/src/components/Flex/USAGE.md +2 -1
  18. package/src/components/Group/USAGE.md +2 -1
  19. package/src/components/LayoutGrid/LAYOUT_GRID_IMPLEMENTATION_NOTES.md +88 -0
  20. package/src/components/LayoutGrid/LayoutGrid.columns.dom.test.tsx +66 -0
  21. package/src/components/LayoutGrid/LayoutGrid.module.css +48 -0
  22. package/src/components/LayoutGrid/LayoutGrid.stories.tsx +61 -0
  23. package/src/components/LayoutGrid/LayoutGrid.test.tsx +1 -0
  24. package/src/components/LayoutGrid/LayoutGrid.tsx +309 -0
  25. package/src/components/LayoutGrid/USAGE.md +76 -0
  26. package/src/components/Pagination/IMPLEMENTATION_NOTES.md +29 -8
  27. package/src/components/Pagination/Pagination.icons.tsx +0 -32
  28. package/src/components/Pagination/Pagination.manifest.dom.test.tsx +108 -0
  29. package/src/components/Pagination/Pagination.module.css +25 -155
  30. package/src/components/Pagination/Pagination.tsx +328 -197
  31. package/src/components/Pagination/USAGE.md +1 -0
  32. package/src/components/Stack/USAGE.md +2 -1
  33. package/src/components/index.ts +1 -1
  34. package/src/index.ts +1 -0
  35. package/src/utils/breakpointsFromRecManifest.test.ts +40 -0
  36. package/src/utils/breakpointsFromRecManifest.ts +75 -0
  37. package/src/utils/index.ts +1 -0
  38. package/src/components/Grid/GRID_IMPLEMENTATION_NOTES.md +0 -85
  39. package/src/components/Grid/Grid.module.css +0 -41
  40. package/src/components/Grid/Grid.stories.tsx +0 -200
  41. package/src/components/Grid/Grid.test.tsx +0 -1
  42. package/src/components/Grid/Grid.tsx +0 -179
  43. package/src/components/Grid/USAGE.md +0 -61
@@ -1,247 +1,381 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import React, { forwardRef } from "react";
1
+ import React, { createContext, forwardRef, useContext } from "react";
2
+ import { usePagination } from "@mantine/hooks";
3
3
  import {
4
- Pagination as MantinePagination,
5
- type PaginationProps as MantinePaginationProps,
6
- type PaginationRootProps as MantinePaginationRootProps,
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
- export type PaginationProps = RecursicaOverStyled<
28
- MantinePaginationProps & RecursicaPaginationProps
29
- >;
15
+ type PaginationRole = "active-pages" | "inactive-pages" | "navigation-controls";
16
+ type NavControl = "first" | "previous" | "next" | "last";
30
17
 
31
- export type PaginationRootProps =
32
- RecursicaOverStyled<MantinePaginationRootProps>;
18
+ interface RoleVariant {
19
+ variant: NonNullable<ButtonProps["variant"]>;
20
+ size: NonNullable<ButtonProps["size"]>;
21
+ }
33
22
 
34
- export type PaginationEdgeProps<T extends React.ElementType> =
35
- RecursicaOverStyled<
36
- React.ComponentProps<T> & {
37
- /** If set to true, displays text labels alongside the icon. */
38
- withLabel?: boolean;
39
- icon?: any;
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
- function usePaginationClassNames(restRecord: Record<string, unknown>): {
44
- className: string;
45
- classNames: Partial<Record<string, string>>;
46
- } {
47
- const mergedClassNames = mergeClassNames(
48
- {
49
- root: styles.root,
50
- control: styles.control,
51
- dots: styles.dots,
52
- },
53
- restRecord.classNames as Partial<Record<string, string>> | undefined,
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
- const classNameProp = restRecord.className as string | undefined;
57
- const finalClass = classNameProp
58
- ? `${styles.root} ${classNameProp}`
59
- : styles.root;
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
- return { className: finalClass, classNames: mergedClassNames };
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
- const _PaginationRoot = forwardRef<HTMLDivElement, PaginationRootProps>(
65
- function PaginationRoot({ overStyled = false, ...rest }, ref) {
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 stylingParams = usePaginationClassNames(
68
- sanitizedProps as Record<string, unknown>,
69
- );
141
+ const classNameProp = (sanitizedProps as { className?: string }).className;
70
142
 
71
143
  return (
72
- <MantinePagination.Root
73
- ref={ref}
74
- {...(sanitizedProps as MantinePaginationRootProps)}
75
- className={stylingParams.className}
76
- classNames={stylingParams.classNames}
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
- const _PaginationNext = forwardRef<
84
- HTMLButtonElement,
85
- PaginationEdgeProps<typeof MantinePagination.Next>
86
- >(function PaginationNext(
87
- { overStyled = false, withLabel, icon, ...rest },
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
- const _PaginationFirst = forwardRef<
124
- HTMLButtonElement,
125
- PaginationEdgeProps<typeof MantinePagination.First>
126
- >(function PaginationFirst(
127
- { overStyled = false, withLabel, icon, ...rest },
128
- ref,
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 _PaginationLast = forwardRef<
189
+ const _PaginationControl = forwardRef<
144
190
  HTMLButtonElement,
145
- PaginationEdgeProps<typeof MantinePagination.Last>
146
- >(function PaginationLast(
147
- { overStyled = false, withLabel, icon, ...rest },
148
- ref,
149
- ) {
150
- const sanitizedProps = filterStylingProps(rest, overStyled);
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
- <MantinePagination.Last
198
+ <Button
154
199
  ref={ref}
155
- {...sanitizedProps}
156
- data-variant="text"
157
- icon={withCallerOverride(computedIcon, icon) as any}
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
- _PaginationLast.displayName = "Pagination.Last";
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
- const labelsIcons = withLabels
184
- ? {
185
- nextIcon: NextWithLabel,
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
- <MantinePagination
194
- ref={ref}
195
- {...labelsIcons}
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
- _Pagination.displayName = "Pagination";
205
-
206
- export type PaginationControlProps =
207
- RecursicaOverStyled<MantinePaginationControlProps>;
224
+ _PaginationDots.displayName = "Pagination.Dots";
208
225
 
209
- const _PaginationControl = forwardRef<
210
- HTMLButtonElement,
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
- <MantinePagination.Control
216
- ref={ref}
217
- {...(sanitizedProps as unknown as MantinePaginationControlProps)}
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
- _PaginationControl.displayName = "Pagination.Control";
247
+ }
248
+ PaginationItems.displayName = "Pagination.Items";
222
249
 
223
- export type PaginationDotsProps =
224
- RecursicaOverStyled<MantinePaginationDotsProps>;
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 _PaginationDots = forwardRef<HTMLDivElement, PaginationDotsProps>(
227
- function PaginationDots({ overStyled = false, ...rest }, ref) {
228
- const sanitizedProps = filterStylingProps(rest, overStyled);
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
- <MantinePagination.Dots
231
- ref={ref}
232
- {...(sanitizedProps as unknown as MantinePaginationDotsProps)}
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
- _PaginationDots.displayName = "Pagination.Dots";
369
+ _Pagination.displayName = "Pagination";
238
370
 
239
371
  /**
240
- * Recursica Pagination component wrapping Mantine's 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 MantinePagination.Items;
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
- // Pagination.Items has no styling props of its own (just a `dotsIcon` prop, no
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
@@ -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 "./Grid/Grid";
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
@@ -1,3 +1,4 @@
1
1
  import "@recursica/adapter-common/style.css";
2
2
 
3
3
  export * from "./components";
4
+ export * from "./utils/breakpointsFromRecManifest";
@@ -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
+ });