@primitiv-ui/react 0.1.33 → 0.1.34
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitiv-ui/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Headless, accessible React components built on the WAI-ARIA authoring patterns. Zero styles ship with this package — bring your own (CSS, Tailwind, CSS-in-JS, design tokens).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
package/src/Carousel/README.md
CHANGED
|
@@ -1445,81 +1445,3 @@ So `scroll-snap-align: center`/`"end"` end up centering/ending the whole
|
|
|
1445
1445
|
page, matching what the programmatic `scrollTo` already does. This is
|
|
1446
1446
|
automatic and needs no consumer CSS; it's a no-op when `slidesPerPage` is
|
|
1447
1447
|
1 (the leading slide already *is* the whole page).
|
|
1448
|
-
|
|
1449
|
-
### Cover Flow (scroll-driven 3D)
|
|
1450
|
-
|
|
1451
|
-
A "cover flow" — narrow snap units with cards that tilt away in 3D
|
|
1452
|
-
and stack with depth — can be built entirely in CSS, with no extra JS,
|
|
1453
|
-
using scroll-driven animations (`view-timeline` + `animation-timeline`).
|
|
1454
|
-
The full working source is the workbench example at
|
|
1455
|
-
`apps/workbench/src/pages/CarouselExample/examples/_coverFlow.scss`;
|
|
1456
|
-
this section documents the design so it can be reused or ported.
|
|
1457
|
-
|
|
1458
|
-
**Structure.** Each slide is a narrow snap unit that owns the named
|
|
1459
|
-
timelines. Inside it, a card-sized `visual` box is centred over the
|
|
1460
|
-
snap unit (and overflows it so neighbours interleave) and handles the
|
|
1461
|
-
`translateX`; inside that, a `card` element handles the `rotateY`.
|
|
1462
|
-
Keep `perspective()` *inside* the keyframe transform — a `overflow-x`
|
|
1463
|
-
scroll container flattens a `perspective` CSS property, but the
|
|
1464
|
-
function form is immune.
|
|
1465
|
-
|
|
1466
|
-
**Two timelines, one slide.** The rotate/translate run on a tight
|
|
1467
|
-
centred band (`view-timeline-inset` shrinks the scrollport) over
|
|
1468
|
-
`animation-range: contain`. The depth order runs on a *second*,
|
|
1469
|
-
full-scrollport timeline (`inset: 0`) over `animation-range: cover`.
|
|
1470
|
-
The wide range matters: `z-index` is an animatable integer, so a
|
|
1471
|
-
`1 → peak → 1` keyframe driven by each slide's own timeline makes the
|
|
1472
|
-
most-centred card win the stack generically — no per-slide numbers.
|
|
1473
|
-
A narrow range would clamp every off-centre slide to the boundary
|
|
1474
|
-
`z-index`, leaving DOM order to (incorrectly) break the ties on one
|
|
1475
|
-
side.
|
|
1476
|
-
|
|
1477
|
-
**Customising.** Drive every dimension from a small set of custom
|
|
1478
|
-
properties on the root, and derive the rest with `calc()`:
|
|
1479
|
-
|
|
1480
|
-
```css
|
|
1481
|
-
.cover-flow {
|
|
1482
|
-
/* Geometry knobs */
|
|
1483
|
-
--cf-viewport-w: 38rem; /* visible carousel width */
|
|
1484
|
-
--cf-card-w: 180px; /* rendered card width */
|
|
1485
|
-
--cf-aspect: 1.168; /* width ÷ height — 1 = square, >1 = wide */
|
|
1486
|
-
--cf-snap: 90px; /* centre-to-centre spacing (< card-w → overlap) */
|
|
1487
|
-
--cf-track-pad: 0px; /* grey track shown above/below the cards */
|
|
1488
|
-
--cf-radius: 10px;
|
|
1489
|
-
/* Motion knobs */
|
|
1490
|
-
--cf-spread: 1.4; /* tilt band width, in multiples of --cf-snap */
|
|
1491
|
-
--cf-tilt: 55deg; /* max rotateY of the side cards */
|
|
1492
|
-
--cf-perspective: 500px;
|
|
1493
|
-
--cf-shift: 30%; /* translateX of the card toward centre */
|
|
1494
|
-
--cf-lift: 10; /* peak z-index of the centred card */
|
|
1495
|
-
|
|
1496
|
-
/* Derived — leave alone */
|
|
1497
|
-
--cf-card-h: calc(var(--cf-card-w) / var(--cf-aspect));
|
|
1498
|
-
--cf-viewport-h: calc(var(--cf-card-h) + var(--cf-track-pad) * 2);
|
|
1499
|
-
--cf-center-pad: calc(50% - var(--cf-snap) / 2); /* centres first/last card */
|
|
1500
|
-
--cf-band: calc(var(--cf-snap) * var(--cf-spread));
|
|
1501
|
-
}
|
|
1502
|
-
```
|
|
1503
|
-
|
|
1504
|
-
With that in place the three common tweaks are one line each:
|
|
1505
|
-
|
|
1506
|
-
- **Viewport size** — set `--cf-viewport-w`. The height follows the
|
|
1507
|
-
cards automatically (`card-h + 2·track-pad`), so you rarely set it.
|
|
1508
|
-
- **Padding / spacing** — `--cf-track-pad` adds grey track above and
|
|
1509
|
-
below the cards; `--cf-snap` is the centre-to-centre spacing — the
|
|
1510
|
-
smaller it is relative to `--cf-card-w`, the more the cards overlap
|
|
1511
|
-
(it also sets the centring gutter).
|
|
1512
|
-
- **Slide shape** — `--cf-aspect` is width ÷ height (`1` = exact
|
|
1513
|
-
square, `>1` = landscape, `<1` = portrait); `--cf-card-w` is the
|
|
1514
|
-
rendered size. A 240px square is `--cf-card-w: 240px; --cf-aspect: 1`.
|
|
1515
|
-
|
|
1516
|
-
Two gotchas:
|
|
1517
|
-
|
|
1518
|
-
- `--cf-aspect` must resolve to a *number*, not a fraction token.
|
|
1519
|
-
`--cf-aspect: 16 / 9` lands inside `card-w / aspect` and divides
|
|
1520
|
-
twice; use `--cf-aspect: calc(16 / 9)` (or a literal like `1.778`).
|
|
1521
|
-
- The whole recipe relies on `animation-timeline` and `calc()` length
|
|
1522
|
-
division. Wrap it in `@supports (animation-timeline: --x)` and keep
|
|
1523
|
-
a static `[data-state="active"] { z-index: var(--cf-lift) }` fallback
|
|
1524
|
-
plus `@media (prefers-reduced-motion: reduce) { animation: none }` so
|
|
1525
|
-
the carousel degrades to a plain scroll-snap row.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
import { Carousel } from "../Carousel.tsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Viewport chose its scroll behaviour by reading
|
|
8
|
+
* `window.matchMedia` in a `useMemo` — during render, so a server render
|
|
9
|
+
* (no `window`) threw `ReferenceError: window is not defined`. Guarded with a
|
|
10
|
+
* `typeof window` check, the primitive renders a static snapshot on the server.
|
|
11
|
+
*/
|
|
12
|
+
describe("Carousel SSR", () => {
|
|
13
|
+
it("should render to static markup when there is no window", () => {
|
|
14
|
+
expect(typeof window).toBe("undefined");
|
|
15
|
+
|
|
16
|
+
const markup = renderToStaticMarkup(
|
|
17
|
+
<Carousel.Root ariaLabel="Featured">
|
|
18
|
+
<Carousel.Viewport>
|
|
19
|
+
<Carousel.Slide>First</Carousel.Slide>
|
|
20
|
+
<Carousel.Slide>Second</Carousel.Slide>
|
|
21
|
+
</Carousel.Viewport>
|
|
22
|
+
</Carousel.Root>,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(markup).toContain("First");
|
|
26
|
+
expect(markup).toContain("Second");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -319,8 +319,13 @@ export function useCarouselViewport() {
|
|
|
319
319
|
|
|
320
320
|
// Read prefers-reduced-motion once on mount; choose scrollTo
|
|
321
321
|
// behavior accordingly so we don't fight the OS-level setting.
|
|
322
|
+
// `useMemo` runs during render, so this must be SSR-safe: on the server
|
|
323
|
+
// there is no `window`, and reading it unguarded threw. Default to "smooth"
|
|
324
|
+
// there — the first client scroll re-reads it anyway (nothing scrolls on the
|
|
325
|
+
// server), so the resolved behaviour is never actually server-rendered.
|
|
322
326
|
const scrollBehavior = useMemo<ScrollBehavior>(
|
|
323
327
|
() =>
|
|
328
|
+
typeof window !== "undefined" &&
|
|
324
329
|
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
325
330
|
? "instant"
|
|
326
331
|
: "smooth",
|
package/src/Carousel/types.ts
CHANGED
|
@@ -150,8 +150,8 @@ export type CarouselLoopMode = "none" | "wrap" | "infinite";
|
|
|
150
150
|
* - `"center"` — scrolls so the first slide of the target page is
|
|
151
151
|
* **centred** in the Viewport. The target is offset inward by
|
|
152
152
|
* `(viewportWidth − slideWidth) / 2`. Use this with
|
|
153
|
-
* `scroll-snap-align: center` in consumer CSS (e.g.
|
|
154
|
-
*
|
|
153
|
+
* `scroll-snap-align: center` in consumer CSS (e.g. a peek
|
|
154
|
+
* layout where slides are narrower than the Viewport).
|
|
155
155
|
* - `"end"` — scrolls so the first slide of the target page's
|
|
156
156
|
* **trailing** edge aligns with the Viewport's trailing edge. The
|
|
157
157
|
* target is offset inward by the full `viewportWidth − slideWidth`.
|
|
@@ -373,7 +373,7 @@ export type CarouselRootProps = Omit<
|
|
|
373
373
|
/** Scroll-snap alignment the Viewport targets when programmatically
|
|
374
374
|
* scrolling to a page — see {@link CarouselSnapAlign}.
|
|
375
375
|
* Defaults to `"start"`. Set to `"center"` when consumer CSS uses
|
|
376
|
-
* `scroll-snap-align: center` on slides (e.g.
|
|
376
|
+
* `scroll-snap-align: center` on slides (e.g. a peek layout
|
|
377
377
|
* where slides are narrower than the Viewport). */
|
|
378
378
|
snapAlign?: CarouselSnapAlign;
|
|
379
379
|
/** Axis the carousel scrolls and paginates along — see
|