@konce-pt/motion 0.0.0-stage → 0.15.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.
@@ -0,0 +1,44 @@
1
+ import { layoutMotionAllowed } from "./dom.js";
2
+ const STEPPER_ID = 'kpt-stepper-light';
3
+ /**
4
+ * Światło przejścia kreatora wg `stepperSchedule`: kometa (`comet`, pozycjonowana względem tego
5
+ * samego przodka co linie) jedzie po kolejnych odcinkach `connectors`, gaśnie na ich końcu,
6
+ * a znacznik, do którego dociera kolor, wypuszcza falę - `::before` animowany z WAAPI, więc
7
+ * nie potrzebuje dodatkowego węzła. Wołaj po renderze nowego stanu. Przy "ogranicz ruch" nic
8
+ * nie robi.
9
+ */
10
+ export function playStepperLight(schedule, comet, connectors, markers) {
11
+ if (!layoutMotionAllowed())
12
+ return;
13
+ if (comet)
14
+ for (const a of comet.getAnimations())
15
+ if (a.id === STEPPER_ID)
16
+ a.cancel();
17
+ if (!schedule.direction)
18
+ return;
19
+ const parent = comet?.offsetParent?.getBoundingClientRect();
20
+ if (comet && parent) {
21
+ schedule.lines.forEach((line, k) => {
22
+ const el = connectors[k];
23
+ if (!line.duration || !el)
24
+ return;
25
+ const r = el.getBoundingClientRect();
26
+ const y = r.top + r.height / 2 - parent.top;
27
+ const [a, b] = schedule.direction > 0 ? [r.left, r.right] : [r.right, r.left];
28
+ const x = (p) => a + (b - a) * p - parent.left;
29
+ comet.animate([
30
+ { transform: `translate(${x(0)}px, ${y}px)`, opacity: 0 },
31
+ { transform: `translate(${x(0.15)}px, ${y}px)`, opacity: 1, offset: 0.15 },
32
+ { transform: `translate(${x(0.85)}px, ${y}px)`, opacity: 1, offset: 0.85 },
33
+ { transform: `translate(${x(1)}px, ${y}px)`, opacity: 0 },
34
+ ], { id: STEPPER_ID, delay: line.delay, duration: line.duration, easing: 'cubic-bezier(.4, 0, .2, 1)' });
35
+ });
36
+ }
37
+ const target = schedule.arrival ? markers[schedule.arrival.index] : undefined;
38
+ if (target && schedule.arrival) {
39
+ target.animate([
40
+ { opacity: 0.9, transform: 'scale(1)' },
41
+ { opacity: 0, transform: 'scale(2)' },
42
+ ], { id: STEPPER_ID, delay: schedule.arrival.at, duration: 640, easing: 'ease-out', pseudoElement: '::before' });
43
+ }
44
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Harmonogram przełączenia kroku w kreatorze (`kpt-stepper`): kolor płynie po linii od kroku
3
+ * do kroku, a znacznik zmienia wygląd dopiero, gdy kolor do niego dotrze. Przy skoku o kilka
4
+ * kroków odcinki jadą po kolei, a całość mieści się w budżecie czasu.
5
+ *
6
+ * Wynik to same liczby - porty wpisują je w zmienne CSS (opóźnienia przejść) i w WAAPI (kometa,
7
+ * fala). Stan komponentu zmienia się od razu; harmonogram mówi tylko, kiedy to widać.
8
+ */
9
+ export interface KptStepTiming {
10
+ /** Start w ms od zmiany. */
11
+ delay: number;
12
+ /** Czas trwania w ms. */
13
+ duration: number;
14
+ }
15
+ export interface KptStepperSchedule {
16
+ /** Opóźnienie zmiany wyglądu znacznika (indeks kroku -> ms); 0 dla nieruszanych. */
17
+ markers: number[];
18
+ /** Odcinek linii między krokiem `k` a `k + 1`. */
19
+ lines: KptStepTiming[];
20
+ /** Znacznik, do którego kolor dociera na końcu (nowy aktywny krok), i kiedy. */
21
+ arrival: {
22
+ index: number;
23
+ at: number;
24
+ } | null;
25
+ /** Kierunek: 1 do przodu, -1 wstecz, 0 bez zmiany. */
26
+ direction: 1 | -1 | 0;
27
+ /** Czas całego przejścia w ms. */
28
+ total: number;
29
+ }
30
+ export interface KptStepperScheduleOptions {
31
+ /** Najdłuższy pojedynczy odcinek (ms). */
32
+ segment?: number;
33
+ /** Najkrótszy odcinek przy długim skoku (ms). */
34
+ minSegment?: number;
35
+ /** Budżet całego skoku - dłuższe skoki skracają odcinki (ms). */
36
+ budget?: number;
37
+ /** Przerwa między zalaniem znacznika a ruszeniem linii (ms). */
38
+ gap?: number;
39
+ }
40
+ /**
41
+ * @param count liczba kroków
42
+ * @param from krok przed zmianą
43
+ * @param to krok po zmianie
44
+ */
45
+ export declare function stepperSchedule(count: number, from: number, to: number, options?: KptStepperScheduleOptions): KptStepperSchedule;
46
+ /**
47
+ * Tempo linii z jednego czasu (`animationDuration` kreatora), żeby ten sam input sterował całym
48
+ * ruchem, a nie tylko wejściem treści. Dla 500 ms daje wartości domyślne `stepperSchedule`;
49
+ * 0 ms - wszystko od razu.
50
+ */
51
+ export declare function stepperScheduleOptions(duration: number): KptStepperScheduleOptions;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Harmonogram przełączenia kroku w kreatorze (`kpt-stepper`): kolor płynie po linii od kroku
3
+ * do kroku, a znacznik zmienia wygląd dopiero, gdy kolor do niego dotrze. Przy skoku o kilka
4
+ * kroków odcinki jadą po kolei, a całość mieści się w budżecie czasu.
5
+ *
6
+ * Wynik to same liczby - porty wpisują je w zmienne CSS (opóźnienia przejść) i w WAAPI (kometa,
7
+ * fala). Stan komponentu zmienia się od razu; harmonogram mówi tylko, kiedy to widać.
8
+ */
9
+ /**
10
+ * @param count liczba kroków
11
+ * @param from krok przed zmianą
12
+ * @param to krok po zmianie
13
+ */
14
+ export function stepperSchedule(count, from, to, options = {}) {
15
+ const { segment = 420, minSegment = 200, budget = 900, gap = 60 } = options;
16
+ const markers = Array.from({ length: Math.max(0, count) }, () => 0);
17
+ const lines = Array.from({ length: Math.max(0, count - 1) }, () => ({ delay: 0, duration: 0 }));
18
+ const last = count - 1;
19
+ from = Math.max(0, Math.min(from, last));
20
+ to = Math.max(0, Math.min(to, last));
21
+ if (count < 2 || from === to)
22
+ return { markers, lines, arrival: null, direction: 0, total: 0 };
23
+ const n = Math.abs(to - from);
24
+ const dur = Math.round(Math.max(minSegment, Math.min(segment, budget / n)));
25
+ let t = 0;
26
+ if (to > from) {
27
+ // Krok k zalewa się kolorem, po chwili rusza linia, a jej koniec zapala krok k + 1.
28
+ const g = n > 1 ? Math.round(gap / 2) : gap;
29
+ for (let k = from; k < to; k++) {
30
+ markers[k] = t;
31
+ lines[k] = { delay: t + g, duration: dur };
32
+ t += g + dur;
33
+ markers[k + 1] = t;
34
+ }
35
+ return { markers, lines, arrival: { index: to, at: t }, direction: 1, total: t };
36
+ }
37
+ // Wstecz: krok k + 1 gaśnie, linia zwija się szybciej, a jej koniec przywraca krok k.
38
+ const back = Math.round(dur * 0.8);
39
+ for (let k = from - 1; k >= to; k--) {
40
+ markers[k + 1] = t;
41
+ lines[k] = { delay: t, duration: back };
42
+ t += back;
43
+ markers[k] = t;
44
+ }
45
+ return { markers, lines, arrival: { index: to, at: t }, direction: -1, total: t };
46
+ }
47
+ /**
48
+ * Tempo linii z jednego czasu (`animationDuration` kreatora), żeby ten sam input sterował całym
49
+ * ruchem, a nie tylko wejściem treści. Dla 500 ms daje wartości domyślne `stepperSchedule`;
50
+ * 0 ms - wszystko od razu.
51
+ */
52
+ export function stepperScheduleOptions(duration) {
53
+ const d = Math.max(0, duration);
54
+ return {
55
+ segment: Math.round(d * 0.84),
56
+ minSegment: Math.round(Math.min(200, d * 0.4)),
57
+ budget: Math.round(d * 1.8),
58
+ gap: Math.round(Math.min(60, d * 0.12)),
59
+ };
60
+ }
package/dist/swap.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Podmiana treści w miejscu (krok kreatora, zakładka): stara treść wychodzi w kierunku przeciwnym
3
+ * do ruchu i rozmywa się, a pojemnik płynnie zmienia wysokość. Wejście nowej treści zostaje po
4
+ * stronie CSS komponentu (`@keyframes` na nowym elemencie).
5
+ *
6
+ * Framework zamienia treść od razu, więc starej już nie ma po renderze - dlatego wołasz to PRZED
7
+ * zmianą (zdejmuje klon starej treści i mierzy wysokość), a zwróconą funkcję po renderze
8
+ * (Angular: `afterNextRender`, React: `useLayoutEffect`). Warstwa DOM bez testów.
9
+ *
10
+ * @param container pojemnik treści - musi być pozycjonowany (`position: relative`)
11
+ * @param direction 1 - ruch do przodu (stara treść ucieka w lewo), -1 - wstecz
12
+ */
13
+ export declare function preparePanelSwap(container: HTMLElement, direction: 1 | -1): () => void;
package/dist/swap.js ADDED
@@ -0,0 +1,61 @@
1
+ import { layoutMotionAllowed } from "./dom.js";
2
+ import { springEasing } from "./spring.js";
3
+ /**
4
+ * Podmiana treści w miejscu (krok kreatora, zakładka): stara treść wychodzi w kierunku przeciwnym
5
+ * do ruchu i rozmywa się, a pojemnik płynnie zmienia wysokość. Wejście nowej treści zostaje po
6
+ * stronie CSS komponentu (`@keyframes` na nowym elemencie).
7
+ *
8
+ * Framework zamienia treść od razu, więc starej już nie ma po renderze - dlatego wołasz to PRZED
9
+ * zmianą (zdejmuje klon starej treści i mierzy wysokość), a zwróconą funkcję po renderze
10
+ * (Angular: `afterNextRender`, React: `useLayoutEffect`). Warstwa DOM bez testów.
11
+ *
12
+ * @param container pojemnik treści - musi być pozycjonowany (`position: relative`)
13
+ * @param direction 1 - ruch do przodu (stara treść ucieka w lewo), -1 - wstecz
14
+ */
15
+ export function preparePanelSwap(container, direction) {
16
+ if (!layoutMotionAllowed())
17
+ return () => undefined;
18
+ const current = container.firstElementChild;
19
+ // Ślady poprzedniej podmiany: przerwana zmiana nie zostawia ducha ani wysokości w stylu.
20
+ for (const a of container.getAnimations())
21
+ a.cancel();
22
+ for (const old of container.querySelectorAll(':scope > [data-kpt-swap-ghost]'))
23
+ old.remove();
24
+ const h0 = container.offsetHeight;
25
+ const ghost = current instanceof HTMLElement ? current.cloneNode(true) : null;
26
+ const top = current instanceof HTMLElement ? current.offsetTop : 0;
27
+ return () => {
28
+ if (ghost instanceof HTMLElement) {
29
+ ghost.setAttribute('data-kpt-swap-ghost', '');
30
+ ghost.setAttribute('aria-hidden', 'true');
31
+ ghost.inert = true;
32
+ for (const node of [ghost, ...ghost.querySelectorAll('[id]')])
33
+ node.removeAttribute('id');
34
+ Object.assign(ghost.style, {
35
+ position: 'absolute',
36
+ insetInline: '0',
37
+ top: `${top}px`,
38
+ pointerEvents: 'none',
39
+ animation: 'none',
40
+ });
41
+ container.appendChild(ghost);
42
+ ghost
43
+ .animate([
44
+ { opacity: 1, transform: 'none', filter: 'blur(0)' },
45
+ { opacity: 0, transform: `translateX(${-direction * 14}px)`, filter: 'blur(4px)' },
46
+ ], { duration: 180, easing: 'ease-in', fill: 'forwards' })
47
+ .finished.then(() => ghost.remove(), () => ghost.remove());
48
+ }
49
+ const h1 = container.offsetHeight;
50
+ if (Math.abs(h1 - h0) < 1)
51
+ return;
52
+ const overflow = container.style.overflow;
53
+ container.style.overflow = 'clip';
54
+ const restore = () => {
55
+ container.style.overflow = overflow;
56
+ };
57
+ container
58
+ .animate([{ height: `${h0}px` }, { height: `${h1}px` }], { duration: 440, easing: springEasing(0.18) })
59
+ .finished.then(restore, restore);
60
+ };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@konce-pt/motion",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.15.0",
4
+ "description": "Motion core for Koncept UI - spring curves sampled to linear() and FLIP flight plans for layout changes. Framework-free TypeScript, zero runtime dependencies.",
5
+ "license": "MIT",
6
+ "author": "konce.pt",
7
+ "homepage": "https://ui.konce.pt/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://gitlab.com/konce-pt/koncept-ui.git",
11
+ "directory": "packages/motion"
12
+ },
13
+ "bugs": {
14
+ "url": "https://gitlab.com/konce-pt/koncept-ui/-/issues"
15
+ },
16
+ "keywords": [
17
+ "motion",
18
+ "animation",
19
+ "flip",
20
+ "spring",
21
+ "design-system",
22
+ "koncept-ui",
23
+ "kpt"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "typescript": "~6.0.3"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json",
43
+ "test": "node --test \"src/**/*.test.ts\"",
44
+ "lint": "eslint .",
45
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
46
+ }
6
47
  }