@primitiv-ui/react 0.1.32 → 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.32",
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",
@@ -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",
@@ -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. Cover Flow
154
- * layouts where slides are narrower than the Viewport).
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. Cover Flow layouts
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