@griddle/svelte 0.1.0 → 0.1.2

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 CHANGED
@@ -38,6 +38,31 @@ shown above). `svelte` (^4 or ^5) is expected to already be in your app.
38
38
  </GriddleGrid>
39
39
  ```
40
40
 
41
+ ## Animation configuration
42
+
43
+ Tile repositioning and lift animations share `config.animation` with the other
44
+ Griddle adapters. Repositioning defaults to a smooth 320 ms ease-out and rapid
45
+ repacks continue from each tile's current visual position.
46
+
47
+ ```js
48
+ const api = createGriddle({
49
+ config: {
50
+ cols: 12, rows: 12, unitWidth: 75, unitHeight: 75,
51
+ animation: {
52
+ repositionDurationMs: 320,
53
+ repositionEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
54
+ liftDurationMs: 160,
55
+ liftEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
56
+ respectReducedMotion: true,
57
+ },
58
+ },
59
+ tiles,
60
+ });
61
+ ```
62
+
63
+ Use `animation.enabled: false` to disable all adapter animations. A duration of
64
+ `0` disables only that transition.
65
+
41
66
  ## Loop mode (infinite gallery)
42
67
 
43
68
  Enable `loop` in the config and the content repeats endlessly with drag-to-pan
@@ -1,6 +1,7 @@
1
1
  <script>import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
2
2
  import { visibleRange, visibleTiles, gridContentSize, DragController, computeTileLayout, resolveStickyStacking, isOutOfFlow, pixelsToPin, } from '@griddle/core';
3
3
  import LoopGrid from './LoopGrid.svelte';
4
+ import { animateReposition, liftTransition } from './animation.js';
4
5
  export let api;
5
6
  export let height = '100%';
6
7
  export let showGrid = true;
@@ -16,6 +17,10 @@ $: tilesAll = $tilesStore;
16
17
  $: ver = $versionStore;
17
18
  // Loop mode delegates rendering to LoopGrid.
18
19
  $: loopOn = cfg.loop?.enabled === true;
20
+ // 'none' scroll mode: the grid sizes to content and lets the host page own
21
+ // scrolling/panning — no internal scroll box, no touch-action lock.
22
+ $: contained = cfg.scroll !== 'none';
23
+ $: heightCss = typeof height === 'number' ? height + 'px' : height;
19
24
  $: colSize = cfg.unitWidth + (cfg.gap ?? 0);
20
25
  $: rowSize = cfg.unitHeight + (cfg.gap ?? 0);
21
26
  $: halfGap = (cfg.gap ?? 0) / 2;
@@ -259,12 +264,7 @@ async function runFlip(_v) {
259
264
  const dx = p.x - x;
260
265
  const dy = p.y - y;
261
266
  if (dx !== 0 || dy !== 0) {
262
- node.style.transition = 'none';
263
- node.style.transform = `translate(${dx}px, ${dy}px)`;
264
- requestAnimationFrame(() => {
265
- node.style.transition = 'transform 220ms cubic-bezier(.2,.7,.2,1)';
266
- node.style.transform = 'translate(0,0)';
267
- });
267
+ animateReposition(node, dx, dy, cfg.animation);
268
268
  }
269
269
  }
270
270
  prevRects.set(t.id, { x, y });
@@ -363,7 +363,9 @@ $: indicatorRect = (() => {
363
363
  <div
364
364
  bind:this={scrollEl}
365
365
  class="griddle-scroll"
366
- style:height={typeof height === 'number' ? height + 'px' : height}
366
+ style:overflow={contained ? 'auto' : 'visible'}
367
+ style:touch-action={contained ? 'none' : 'auto'}
368
+ style:height={contained ? heightCss : 'auto'}
367
369
  >
368
370
  <div
369
371
  class="griddle-content"
@@ -402,6 +404,7 @@ $: indicatorRect = (() => {
402
404
  style:width={layout.width + 'px'}
403
405
  style:height={layout.height + 'px'}
404
406
  style:transform={layout.transform ?? ''}
407
+ style:transition={isDragging(tile.id) ? liftTransition(cfg.animation) : ''}
405
408
  style:z-index={layout.zIndex}
406
409
  >
407
410
  <slot name="tile" {tile} />
@@ -422,9 +425,8 @@ $: indicatorRect = (() => {
422
425
 
423
426
  <style>
424
427
  .griddle-scroll {
428
+ /* overflow / touch-action / height are set inline, driven by cfg.scroll. */
425
429
  position: relative;
426
- overflow: auto;
427
- touch-action: none;
428
430
  }
429
431
  .griddle-content {
430
432
  position: relative;
@@ -442,7 +444,7 @@ $: indicatorRect = (() => {
442
444
  box-sizing: border-box;
443
445
  cursor: grab;
444
446
  user-select: none;
445
- will-change: transform;
447
+ will-change: transform, translate;
446
448
  z-index: 1;
447
449
  }
448
450
  .griddle-tile.griddle-resizing {
@@ -458,7 +460,6 @@ $: indicatorRect = (() => {
458
460
  is set inline (cursor delta), so the scale is folded into a CSS filter
459
461
  to avoid clobbering it. */
460
462
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.18));
461
- transition: filter 120ms ease-out, opacity 120ms ease-out;
462
463
  }
463
464
  .griddle-drop-indicator {
464
465
  position: absolute;
@@ -4,6 +4,7 @@
4
4
  // NO native scrolling (wheel deltas feed the camera directly).
5
5
  import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
6
6
  import { DragController, PanController, loopInstances, loopPeriod, resolveLoop, } from '@griddle/core';
7
+ import { animateReposition } from './animation.js';
7
8
  export let api;
8
9
  export let height = '100%';
9
10
  export let showGrid = true;
@@ -329,12 +330,7 @@ async function runFlip(_v) {
329
330
  if ((dx !== 0 || dy !== 0) &&
330
331
  Math.abs(dx) < period.width / 2 &&
331
332
  Math.abs(dy) < period.height / 2) {
332
- node.style.transition = 'none';
333
- node.style.transform = `translate(${dx}px, ${dy}px)`;
334
- requestAnimationFrame(() => {
335
- node.style.transition = 'transform 220ms cubic-bezier(.2,.7,.2,1)';
336
- node.style.transform = 'translate(0,0)';
337
- });
333
+ animateReposition(node, dx, dy, cfg.animation);
338
334
  }
339
335
  }
340
336
  }
@@ -472,6 +468,7 @@ function instanceLayout(inst, r) {
472
468
  position: absolute;
473
469
  box-sizing: border-box;
474
470
  user-select: none;
471
+ will-change: translate;
475
472
  z-index: 1;
476
473
  border-radius: var(--griddle-tile-radius, 4px);
477
474
  }
@@ -0,0 +1,4 @@
1
+ import { type GridAnimationConfig } from '@griddle/core';
2
+ /** Continue rapid repacks from the tile's current visual position. */
3
+ export declare function animateReposition(node: HTMLElement, deltaX: number, deltaY: number, config?: GridAnimationConfig): void;
4
+ export declare function liftTransition(config?: GridAnimationConfig): string;
@@ -0,0 +1,49 @@
1
+ import { resolveAnimationConfig, } from '@griddle/core';
2
+ const pendingFrames = new WeakMap();
3
+ function reducedMotionRequested(config) {
4
+ const animation = resolveAnimationConfig(config);
5
+ return animation.respectReducedMotion &&
6
+ typeof window !== 'undefined' &&
7
+ typeof window.matchMedia === 'function' &&
8
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
9
+ }
10
+ function currentTranslation(node) {
11
+ if (typeof window === 'undefined')
12
+ return { x: 0, y: 0 };
13
+ const translate = window.getComputedStyle(node).translate;
14
+ if (!translate || translate === 'none')
15
+ return { x: 0, y: 0 };
16
+ const [x = '0', y = '0'] = translate.split(/\s+/);
17
+ return { x: Number.parseFloat(x) || 0, y: Number.parseFloat(y) || 0 };
18
+ }
19
+ /** Continue rapid repacks from the tile's current visual position. */
20
+ export function animateReposition(node, deltaX, deltaY, config) {
21
+ const pending = pendingFrames.get(node);
22
+ if (pending !== undefined)
23
+ cancelAnimationFrame(pending);
24
+ const animation = resolveAnimationConfig(config);
25
+ if (!animation.enabled || animation.repositionDurationMs === 0 || reducedMotionRequested(config)) {
26
+ node.style.transition = 'none';
27
+ node.style.translate = '0px 0px';
28
+ pendingFrames.delete(node);
29
+ return;
30
+ }
31
+ const current = currentTranslation(node);
32
+ node.style.transition = 'none';
33
+ node.style.translate = `${deltaX + current.x}px ${deltaY + current.y}px`;
34
+ void node.offsetWidth;
35
+ const frame = requestAnimationFrame(() => {
36
+ pendingFrames.delete(node);
37
+ node.style.transition =
38
+ `translate ${animation.repositionDurationMs}ms ${animation.repositionEasing}`;
39
+ node.style.translate = '0px 0px';
40
+ });
41
+ pendingFrames.set(node, frame);
42
+ }
43
+ export function liftTransition(config) {
44
+ const animation = resolveAnimationConfig(config);
45
+ const duration = !animation.enabled || reducedMotionRequested(config)
46
+ ? 0
47
+ : animation.liftDurationMs;
48
+ return `filter ${duration}ms ${animation.liftEasing}, opacity ${duration}ms ${animation.liftEasing}`;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@griddle/svelte",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Svelte bindings for Griddle — a headless, zero-dependency grid/canvas engine.",
5
5
  "keywords": [
6
6
  "grid",
@@ -26,7 +26,10 @@
26
26
  "license": "MIT",
27
27
  "author": "Trustybits",
28
28
  "type": "module",
29
- "sideEffects": ["**/*.svelte", "**/*.css"],
29
+ "sideEffects": [
30
+ "**/*.svelte",
31
+ "**/*.css"
32
+ ],
30
33
  "svelte": "./dist/index.js",
31
34
  "types": "./dist/index.d.ts",
32
35
  "exports": {
@@ -35,7 +38,9 @@
35
38
  "svelte": "./dist/index.js"
36
39
  }
37
40
  },
38
- "files": ["dist"],
41
+ "files": [
42
+ "dist"
43
+ ],
39
44
  "engines": {
40
45
  "node": ">=18"
41
46
  },
@@ -44,14 +49,15 @@
44
49
  },
45
50
  "scripts": {
46
51
  "build": "svelte-package -i src -o dist",
47
- "prepublishOnly": "npm run build"
52
+ "test": "node --test test/animation.test.mjs",
53
+ "prepublishOnly": "npm run build && npm test"
48
54
  },
49
55
  "peerDependencies": {
50
- "@griddle/core": "^0.1.0",
56
+ "@griddle/core": "^0.1.2",
51
57
  "svelte": "^4.0.0 || ^5.0.0"
52
58
  },
53
59
  "devDependencies": {
54
- "@griddle/core": "^0.1.0",
60
+ "@griddle/core": "^0.1.2",
55
61
  "@sveltejs/package": "^2.5.8",
56
62
  "svelte": "^4.2.0",
57
63
  "svelte-check": "^4.7.2",