@konce-pt/motion 0.0.0-stage → 0.14.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.
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/config.d.ts +71 -0
- package/dist/config.js +54 -0
- package/dist/dom.d.ts +86 -0
- package/dist/dom.js +210 -0
- package/dist/flow-diff.d.ts +7 -0
- package/dist/flow-diff.js +17 -0
- package/dist/flow.d.ts +22 -0
- package/dist/flow.js +84 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +13 -0
- package/dist/keyed.d.ts +64 -0
- package/dist/keyed.js +221 -0
- package/dist/layout.d.ts +65 -0
- package/dist/layout.js +115 -0
- package/dist/loader-dom.d.ts +48 -0
- package/dist/loader-dom.js +821 -0
- package/dist/loader.d.ts +177 -0
- package/dist/loader.js +400 -0
- package/dist/spring.d.ts +16 -0
- package/dist/spring.js +41 -0
- package/dist/stepper-dom.d.ts +9 -0
- package/dist/stepper-dom.js +44 -0
- package/dist/stepper.d.ts +51 -0
- package/dist/stepper.js +60 -0
- package/dist/swap.d.ts +13 -0
- package/dist/swap.js +61 -0
- package/package.json +44 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 konce.pt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI - Angular component library" width="100%" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# @konce-pt/motion
|
|
6
|
+
|
|
7
|
+
Rdzeń ruchu [Koncept UI](https://gitlab.com/konce-pt/koncept-ui): krzywe sprężyn do `linear()`,
|
|
8
|
+
plany lotów FLIP przy zmianie układu, globalna konfiguracja ruchu i mała warstwa DOM, która te loty
|
|
9
|
+
odgrywa. Czysty TypeScript, **zero zależności runtime**, bez wiązania z frameworkiem.
|
|
10
|
+
|
|
11
|
+
Zwykle nie instalujesz go samodzielnie: to zależność `@konce-pt/angular` i `@konce-pt/react`,
|
|
12
|
+
a konfigurację ustawia się przez `provideKptMotion` / `KptMotionProvider` (patrz README portów).
|
|
13
|
+
Sięgnij po niego wprost, gdy własny widok ma się ruszać tak samo jak komponenty biblioteki.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @konce-pt/motion
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { planLayoutMotion, resolveLayoutMotion, springEasing } from '@konce-pt/motion';
|
|
21
|
+
|
|
22
|
+
// Sprężyna jako krzywa CSS - do transition-timing-function.
|
|
23
|
+
springEasing(0.32); // 'linear(0, ..., 1)'
|
|
24
|
+
|
|
25
|
+
// Jaki ruch dostaje komponent: wyłączenie globalne > input > domyślny z konfiguracji > spring.
|
|
26
|
+
resolveLayoutMotion(undefined, { layout: 'arc' }); // 'arc'
|
|
27
|
+
resolveLayoutMotion('arc', { enabled: false }); // 'none'
|
|
28
|
+
|
|
29
|
+
// Klatki lotu FLIP z prostokątów przed i po zmianie układu.
|
|
30
|
+
const flights = planLayoutMotion(before, after, { motion: 'spring', radius: 8 });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Konfiguracja
|
|
34
|
+
|
|
35
|
+
`KptMotionConfig` ma pięć opcjonalnych kluczy: `enabled` oraz domyślny rodzaj ruchu dla czterech grup
|
|
36
|
+
komponentów - `layout` (`spring | arc | morph | none`), `collapse`, `enter` i `toggle`
|
|
37
|
+
(`spring | fade | none`). Funkcje `resolve*Motion(input, config)` rozstrzygają, co dostaje komponent:
|
|
38
|
+
`enabled: false` > input komponentu > wartość z konfiguracji > `spring`. `applyMotionConfig` przy
|
|
39
|
+
`enabled: false` stawia na korzeniu `data-kpt-motion="off"` i zeruje `--kpt-motion-scale` oraz czasy
|
|
40
|
+
`--kpt-motion-duration-*` - tak gaśnie ruch zapisany w samym CSS.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
- **Konfiguracja** - `resolveLayoutMotion`, `resolveCollapseMotion`, `resolveEnterMotion`, `resolveToggleMotion`,
|
|
45
|
+
`motionRootStyle`, stałe `KPT_DEFAULT_*_MOTION`, typy `KptMotionConfig`, `KptLayoutMotion`,
|
|
46
|
+
`KptCollapseMotion`, `KptEnterMotion`, `KptToggleMotion`
|
|
47
|
+
- **Sprężyny** - `springProgress`, `springEasing`
|
|
48
|
+
- **Loty układu** - `planLayoutMotion`, `layoutMotionDuration`, typy `KptRect`, `KptLayoutFlight`
|
|
49
|
+
- **Warstwa DOM** - `snapshotLayout`, `playLayoutMotion`, `stopLayoutMotion`, `morphLayout`,
|
|
50
|
+
`applyMotionConfig`, `layoutMotionAllowed`, `layoutMorphSupported`
|
|
51
|
+
- **Mikroruch** - `playEnterMotion`, `playShakeMotion`, `playPopMotion`, `playInvalidMotion`,
|
|
52
|
+
`playIndicatorMotion`
|
|
53
|
+
- **Listy z kluczem** - `snapshotKeyed`, `playKeyedMotion`, `stopKeyedMotion`, `morphKeyed`,
|
|
54
|
+
`isVisibleInScroller`, `revealInScroller`
|
|
55
|
+
- **Zawijany wiersz (tagi, chipy)** - `prepareFlowMotion`, `flowChangeOf`, typ `KptFlowChange`
|
|
56
|
+
- **Kreator i podmiana treści** - `stepperSchedule`, `stepperScheduleOptions`, `playStepperLight`, `preparePanelSwap`, typy
|
|
57
|
+
`KptStepperSchedule`, `KptStepTiming`
|
|
58
|
+
|
|
59
|
+
Pełny opis: [`llms.txt`](https://ui.konce.pt/llms/motion/llms.txt) (EN)
|
|
60
|
+
i [`llms-pl.txt`](https://ui.konce.pt/llms/motion/llms-pl.txt) (PL).
|
|
61
|
+
|
|
62
|
+
## Testy
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm --filter @konce-pt/motion exec node --test "src/**/*.test.ts"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Licencja
|
|
69
|
+
|
|
70
|
+
MIT © [konce.pt](https://konce.pt)
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { KptLayoutMotion } from './layout.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Ruch zwijania sekcji (`kpt-accordion`, `kpt-fieldset`, `kpt-panel`): `spring` - wysokość na
|
|
4
|
+
* miękkiej sprężynie, treść wjeżdża z przesunięciem i rozmyciem, a przy zwijaniu najpierw gaśnie;
|
|
5
|
+
* `fade` - wysokość na zwykłej krzywej, treść się przenika; `none` - bez ruchu.
|
|
6
|
+
*/
|
|
7
|
+
export type KptCollapseMotion = 'spring' | 'fade' | 'none';
|
|
8
|
+
/**
|
|
9
|
+
* Ruch pojawiania się (nowa wiadomość czatu, reakcja, okno docka): `spring` - element wyrasta
|
|
10
|
+
* z miejsca, z którego przychodzi, na sprężynie; `fade` - tylko się przenika; `none` - bez ruchu.
|
|
11
|
+
*/
|
|
12
|
+
export type KptEnterMotion = 'spring' | 'fade' | 'none';
|
|
13
|
+
/**
|
|
14
|
+
* Ruch przełączania (`kpt-checkbox`, `kpt-radio-group`, `kpt-switch`): `spring` - pole wskakuje,
|
|
15
|
+
* ptaszek rysuje się kreską, kciuk jedzie na sprężynie; `fade` - spokojne przejście koloru i znaku;
|
|
16
|
+
* `none` - bez ruchu.
|
|
17
|
+
*/
|
|
18
|
+
export type KptToggleMotion = 'spring' | 'fade' | 'none';
|
|
19
|
+
/**
|
|
20
|
+
* Globalne ustawienie ruchu Koncept UI - jedno dla całej aplikacji (Angular:
|
|
21
|
+
* `provideKptMotion(...)`, React: `<KptMotionProvider>`). Brak konfiguracji = ruch włączony
|
|
22
|
+
* z wartościami domyślnymi; systemowe "ogranicz ruch" jest respektowane zawsze.
|
|
23
|
+
*/
|
|
24
|
+
export interface KptMotionConfig {
|
|
25
|
+
/**
|
|
26
|
+
* `false` wyłącza ruch wszystkich komponentów: przejścia CSS dostają czas 0, a ruch sterowany
|
|
27
|
+
* z kodu (np. zmiana układu `kpt-data-view`) się nie odpala. Wskaźniki ładowania (spinner,
|
|
28
|
+
* nieokreślony postęp, połysk szkieletu) zwalniają zamiast stanąć. Domyślnie `true`.
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
/** Domyślny ruch zmiany układu dla komponentów, które go mają (`kpt-data-view`). Domyślnie `spring`. */
|
|
32
|
+
layout?: KptLayoutMotion;
|
|
33
|
+
/** Domyślny ruch zwijania sekcji (`kpt-accordion`, `kpt-fieldset`, `kpt-panel`). Domyślnie `spring`. */
|
|
34
|
+
collapse?: KptCollapseMotion;
|
|
35
|
+
/** Domyślny ruch pojawiania się (czat: wiadomości, reakcje, status, dock). Domyślnie `spring`. */
|
|
36
|
+
enter?: KptEnterMotion;
|
|
37
|
+
/** Domyślny ruch przełączania (checkbox, radio, switch). Domyślnie `spring`. */
|
|
38
|
+
toggle?: KptToggleMotion;
|
|
39
|
+
}
|
|
40
|
+
/** Co konfiguracja nakłada na korzeń dokumentu: atrybut i zmienne CSS. */
|
|
41
|
+
export interface KptMotionRootStyle {
|
|
42
|
+
/** Wartość `data-kpt-motion` albo `null`, gdy atrybutu ma nie być. */
|
|
43
|
+
attribute: 'off' | null;
|
|
44
|
+
properties: Readonly<Record<string, string>>;
|
|
45
|
+
}
|
|
46
|
+
export declare const KPT_DEFAULT_LAYOUT_MOTION: KptLayoutMotion;
|
|
47
|
+
export declare const KPT_DEFAULT_COLLAPSE_MOTION: KptCollapseMotion;
|
|
48
|
+
export declare const KPT_DEFAULT_ENTER_MOTION: KptEnterMotion;
|
|
49
|
+
export declare const KPT_DEFAULT_TOGGLE_MOTION: KptToggleMotion;
|
|
50
|
+
/**
|
|
51
|
+
* Ruch zmiany układu dla jednego komponentu. Kolejność: wyłączenie globalne wygrywa ze wszystkim,
|
|
52
|
+
* potem jawny input komponentu, potem domyślny `layout` z konfiguracji, na końcu `spring`.
|
|
53
|
+
* Systemowe "ogranicz ruch" sprawdza warstwa DOM w chwili zmiany (może się zmienić w trakcie).
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveLayoutMotion(input?: KptLayoutMotion | null, config?: KptMotionConfig | null): KptLayoutMotion;
|
|
56
|
+
/**
|
|
57
|
+
* Ruch zwijania dla jednej sekcji. Kolejność: wyłączenie globalne, potem dawne `animated: false`
|
|
58
|
+
* komponentu (zostaje jako wyłącznik), potem jawny input, domyślny `collapse` z konfiguracji
|
|
59
|
+
* i na końcu `spring`. Systemowe "ogranicz ruch" gasi go w CSS (`@media`), nie tutaj.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveCollapseMotion(input?: KptCollapseMotion | null, config?: KptMotionConfig | null, animated?: boolean): KptCollapseMotion;
|
|
62
|
+
/** Ruch pojawiania się: wyłączenie globalne > input komponentu > `enter` z konfiguracji > `spring`. */
|
|
63
|
+
export declare function resolveEnterMotion(input?: KptEnterMotion | null, config?: KptMotionConfig | null): KptEnterMotion;
|
|
64
|
+
/** Ruch przełączania: wyłączenie globalne > input komponentu > `toggle` z konfiguracji > `spring`. */
|
|
65
|
+
export declare function resolveToggleMotion(input?: KptToggleMotion | null, config?: KptMotionConfig | null): KptToggleMotion;
|
|
66
|
+
/**
|
|
67
|
+
* Zmienne nakładane na `:root` przy wyłączonym ruchu. `--kpt-motion-scale` mnoży czasy wpisane
|
|
68
|
+
* w komponentach (`calc(0.2s * var(--kpt-motion-scale, 1))`), a zerowe tokeny czasu gaszą
|
|
69
|
+
* resztę - razem bez jednego `!important` i bez dodatkowego arkusza.
|
|
70
|
+
*/
|
|
71
|
+
export declare function motionRootStyle(config?: KptMotionConfig | null): KptMotionRootStyle;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const KPT_DEFAULT_LAYOUT_MOTION = 'spring';
|
|
2
|
+
export const KPT_DEFAULT_COLLAPSE_MOTION = 'spring';
|
|
3
|
+
export const KPT_DEFAULT_ENTER_MOTION = 'spring';
|
|
4
|
+
export const KPT_DEFAULT_TOGGLE_MOTION = 'spring';
|
|
5
|
+
/**
|
|
6
|
+
* Ruch zmiany układu dla jednego komponentu. Kolejność: wyłączenie globalne wygrywa ze wszystkim,
|
|
7
|
+
* potem jawny input komponentu, potem domyślny `layout` z konfiguracji, na końcu `spring`.
|
|
8
|
+
* Systemowe "ogranicz ruch" sprawdza warstwa DOM w chwili zmiany (może się zmienić w trakcie).
|
|
9
|
+
*/
|
|
10
|
+
export function resolveLayoutMotion(input, config) {
|
|
11
|
+
if (config?.enabled === false)
|
|
12
|
+
return 'none';
|
|
13
|
+
return input ?? config?.layout ?? KPT_DEFAULT_LAYOUT_MOTION;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Ruch zwijania dla jednej sekcji. Kolejność: wyłączenie globalne, potem dawne `animated: false`
|
|
17
|
+
* komponentu (zostaje jako wyłącznik), potem jawny input, domyślny `collapse` z konfiguracji
|
|
18
|
+
* i na końcu `spring`. Systemowe "ogranicz ruch" gasi go w CSS (`@media`), nie tutaj.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveCollapseMotion(input, config, animated = true) {
|
|
21
|
+
if (config?.enabled === false || !animated)
|
|
22
|
+
return 'none';
|
|
23
|
+
return input ?? config?.collapse ?? KPT_DEFAULT_COLLAPSE_MOTION;
|
|
24
|
+
}
|
|
25
|
+
/** Ruch pojawiania się: wyłączenie globalne > input komponentu > `enter` z konfiguracji > `spring`. */
|
|
26
|
+
export function resolveEnterMotion(input, config) {
|
|
27
|
+
if (config?.enabled === false)
|
|
28
|
+
return 'none';
|
|
29
|
+
return input ?? config?.enter ?? KPT_DEFAULT_ENTER_MOTION;
|
|
30
|
+
}
|
|
31
|
+
/** Ruch przełączania: wyłączenie globalne > input komponentu > `toggle` z konfiguracji > `spring`. */
|
|
32
|
+
export function resolveToggleMotion(input, config) {
|
|
33
|
+
if (config?.enabled === false)
|
|
34
|
+
return 'none';
|
|
35
|
+
return input ?? config?.toggle ?? KPT_DEFAULT_TOGGLE_MOTION;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Zmienne nakładane na `:root` przy wyłączonym ruchu. `--kpt-motion-scale` mnoży czasy wpisane
|
|
39
|
+
* w komponentach (`calc(0.2s * var(--kpt-motion-scale, 1))`), a zerowe tokeny czasu gaszą
|
|
40
|
+
* resztę - razem bez jednego `!important` i bez dodatkowego arkusza.
|
|
41
|
+
*/
|
|
42
|
+
export function motionRootStyle(config) {
|
|
43
|
+
if (config?.enabled !== false)
|
|
44
|
+
return { attribute: null, properties: {} };
|
|
45
|
+
return {
|
|
46
|
+
attribute: 'off',
|
|
47
|
+
properties: {
|
|
48
|
+
'--kpt-motion-scale': '0',
|
|
49
|
+
'--kpt-motion-duration-fast': '0ms',
|
|
50
|
+
'--kpt-motion-duration-base': '0ms',
|
|
51
|
+
'--kpt-motion-duration-slow': '0ms',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type KptMotionConfig } from './config.ts';
|
|
2
|
+
import { type KptRect } from './layout.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Warstwa DOM ruchu układu - wspólna dla obu portów, bo pomiar, odtworzenie lotów i morf są
|
|
5
|
+
* identyczne w Angularze i Reakcie; różni je tylko to, jak framework wymusza wyrenderowanie
|
|
6
|
+
* nowego stanu. Bez testów jednostkowych (potrzebuje przeglądarki); logika liczb siedzi
|
|
7
|
+
* w `layout.ts` i tam jest testowana.
|
|
8
|
+
*/
|
|
9
|
+
/** Stan układu przed zmianą, zmierzony z ekranu (razem z trwającym ruchem). */
|
|
10
|
+
export interface KptLayoutSnapshot {
|
|
11
|
+
rects: KptRect[];
|
|
12
|
+
height: number;
|
|
13
|
+
/** Środek elementu, który wywołał zmianę - stąd rusza fala w `arc`. */
|
|
14
|
+
origin?: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface KptPlayLayoutOptions {
|
|
20
|
+
motion: 'spring' | 'arc';
|
|
21
|
+
/** Klasa na czas lotu (np. `overflow: clip`, `z-index`); zdejmowana po wylądowaniu. */
|
|
22
|
+
movingClass: string;
|
|
23
|
+
/** Element kontr-skalowany wewnątrz elementu (zwykle jego jedyne dziecko). */
|
|
24
|
+
innerOf: (item: HTMLElement) => HTMLElement | null;
|
|
25
|
+
}
|
|
26
|
+
/** Ruch układu ma sens tylko w przeglądarce z WAAPI i bez systemowego "ogranicz ruch". */
|
|
27
|
+
export declare function layoutMotionAllowed(): boolean;
|
|
28
|
+
/** Morf wymaga View Transitions (same-document); bez nich zmiana dzieje się od razu. */
|
|
29
|
+
export declare function layoutMorphSupported(): boolean;
|
|
30
|
+
/** Mierzy elementy względem kontenera; `getBoundingClientRect` celowo z transformem. */
|
|
31
|
+
export declare function snapshotLayout(container: HTMLElement, items: readonly HTMLElement[], originEl?: Element | null): KptLayoutSnapshot;
|
|
32
|
+
/** Zatrzymuje trwający ruch układu. Wołać PO pomiarze - wtedy nowy lot startuje z ekranu. */
|
|
33
|
+
export declare function stopLayoutMotion(container: HTMLElement, movingClass: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Gra loty od `first` do bieżącego (już wyrenderowanego) układu. Zwraca czas całości w ms.
|
|
36
|
+
* Kolor cienia w `arc` i promień rogu bierze z obliczonych stylów, więc idą za motywem.
|
|
37
|
+
*/
|
|
38
|
+
export declare function playLayoutMotion(container: HTMLElement, items: readonly HTMLElement[], first: KptLayoutSnapshot, options: KptPlayLayoutOptions): number;
|
|
39
|
+
/**
|
|
40
|
+
* Natywny morf (View Transitions): nadaje elementom unikalne `view-transition-name` na czas
|
|
41
|
+
* przejścia, woła `update` w środku `startViewTransition` i zdejmuje nazwy po końcu.
|
|
42
|
+
* `update` MUSI zsynchronizować DOM przed powrotem (Angular: `detectChanges()`, React:
|
|
43
|
+
* `flushSync`) - przeglądarka robi zrzut nowego stanu zaraz po nim.
|
|
44
|
+
* Bez wsparcia przeglądarki albo przy "ogranicz ruch" po prostu woła `update`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function morphLayout(items: readonly HTMLElement[], namePrefix: string, className: string, update: () => void): void;
|
|
47
|
+
/**
|
|
48
|
+
* Nakłada globalną konfigurację ruchu na korzeń dokumentu (`data-kpt-motion` + zmienne CSS)
|
|
49
|
+
* i zwraca funkcję, która cofa dokładnie to, co ustawiła.
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyMotionConfig(root: HTMLElement, config?: KptMotionConfig | null): () => void;
|
|
52
|
+
/** Skąd element przychodzi: z dołu (własna wiadomość z pola pisania) albo od strony nadawcy. */
|
|
53
|
+
export type KptEnterFrom = 'below' | 'start' | 'end';
|
|
54
|
+
/**
|
|
55
|
+
* Wejście elementu, który już jest w DOM (WAAPI, bez klas): `spring` - wyrasta z kierunku `from`
|
|
56
|
+
* na sprężynie i wyostrza się z rozmycia, `fade` - tylko się przenika. `start`/`end` idą za
|
|
57
|
+
* kierunkiem pisma (RTL odwraca stronę). Bez WAAPI albo przy "ogranicz ruch" nic nie robi.
|
|
58
|
+
*/
|
|
59
|
+
export declare function playEnterMotion(element: HTMLElement, motion: 'spring' | 'fade', from: KptEnterFrom, delay?: number): void;
|
|
60
|
+
/**
|
|
61
|
+
* Potrząśnięcie w poziomie - sygnał "nie tak" (pole staje się nieprawidłowe). Krótkie i gasnące,
|
|
62
|
+
* żeby nie męczyło przy kolejnych próbach; amplituda w px, czas ~0,36 s. Bez WAAPI albo przy
|
|
63
|
+
* "ogranicz ruch" nic nie robi - stan i tak niesie kolor i komunikat.
|
|
64
|
+
*/
|
|
65
|
+
export declare function playShakeMotion(element: HTMLElement, amplitude?: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Wskoczenie (pop) - element przysiada i odbija na sprężynie. Odpalane z handlera zmiany, nie z CSS:
|
|
68
|
+
* reguła na `:checked` zagrałaby też przy pierwszym renderze. `squash` rozciąga element w poziomie
|
|
69
|
+
* zamiast go zmniejszać (kciuk przełącznika w ruchu). Przy "ogranicz ruch" nic nie robi.
|
|
70
|
+
*/
|
|
71
|
+
export declare function playPopMotion(element: HTMLElement, mode?: 'pop' | 'squash'): void;
|
|
72
|
+
/**
|
|
73
|
+
* Sygnał błędu kontrolki: pierścień w kolorze błędu rozchodzi się od elementu i gaśnie, a element
|
|
74
|
+
* krótko się potrząsa (\`shake: false\` - sam pierścień, np. gdy potrząsa już otaczający
|
|
75
|
+
* \`kpt-form-field\`). Kolor z tokenu \`--kpt-form-field-border-error\` (zapas: \`--kpt-color-danger\`),
|
|
76
|
+
* więc idzie za motywem. Przy "ogranicz ruch" nic nie robi - błąd i tak niesie kolor ramki.
|
|
77
|
+
*/
|
|
78
|
+
export declare function playInvalidMotion(element: HTMLElement, options?: {
|
|
79
|
+
shake?: boolean;
|
|
80
|
+
}): void;
|
|
81
|
+
/**
|
|
82
|
+
* Wskaźnik wyboru w kontrolce segmentowej: tło aktywnej pozycji (pseudo-element `pseudo`) przejeżdża
|
|
83
|
+
* z miejsca i szerokości poprzedniej pozycji (`from`, zmierzone przed zmianą) na nową - FLIP samego
|
|
84
|
+
* pseudo-elementu, bez dodatkowego węzła w DOM. Przy "ogranicz ruch" nic nie robi.
|
|
85
|
+
*/
|
|
86
|
+
export declare function playIndicatorMotion(from: DOMRect, to: HTMLElement, pseudo?: string): void;
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { motionRootStyle } from "./config.js";
|
|
2
|
+
import { layoutMotionDuration, planLayoutMotion } from "./layout.js";
|
|
3
|
+
import { springEasing } from "./spring.js";
|
|
4
|
+
/** Identyfikator naszych animacji - sprzątamy tylko je, nie ruch, który ma treść elementu. */
|
|
5
|
+
const ANIMATION_ID = 'kpt-layout-motion';
|
|
6
|
+
/** Ruch układu ma sens tylko w przeglądarce z WAAPI i bez systemowego "ogranicz ruch". */
|
|
7
|
+
export function layoutMotionAllowed() {
|
|
8
|
+
if (typeof window === 'undefined' || typeof Element === 'undefined')
|
|
9
|
+
return false;
|
|
10
|
+
if (typeof Element.prototype.animate !== 'function')
|
|
11
|
+
return false;
|
|
12
|
+
return !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
13
|
+
}
|
|
14
|
+
/** Morf wymaga View Transitions (same-document); bez nich zmiana dzieje się od razu. */
|
|
15
|
+
export function layoutMorphSupported() {
|
|
16
|
+
return layoutMotionAllowed() && typeof document.startViewTransition === 'function';
|
|
17
|
+
}
|
|
18
|
+
/** Mierzy elementy względem kontenera; `getBoundingClientRect` celowo z transformem. */
|
|
19
|
+
export function snapshotLayout(container, items, originEl) {
|
|
20
|
+
const box = container.getBoundingClientRect();
|
|
21
|
+
const rects = items.map((el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
return { x: r.left - box.left, y: r.top - box.top, width: r.width, height: r.height };
|
|
24
|
+
});
|
|
25
|
+
const snapshot = { rects, height: box.height };
|
|
26
|
+
if (originEl) {
|
|
27
|
+
const r = originEl.getBoundingClientRect();
|
|
28
|
+
snapshot.origin = { x: r.left - box.left + r.width / 2, y: r.top - box.top + r.height / 2 };
|
|
29
|
+
}
|
|
30
|
+
return snapshot;
|
|
31
|
+
}
|
|
32
|
+
/** Zatrzymuje trwający ruch układu. Wołać PO pomiarze - wtedy nowy lot startuje z ekranu. */
|
|
33
|
+
export function stopLayoutMotion(container, movingClass) {
|
|
34
|
+
for (const a of container.getAnimations({ subtree: true }))
|
|
35
|
+
if (a.id === ANIMATION_ID)
|
|
36
|
+
a.cancel();
|
|
37
|
+
for (const el of container.querySelectorAll(`.${movingClass}`))
|
|
38
|
+
el.classList.remove(movingClass);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Gra loty od `first` do bieżącego (już wyrenderowanego) układu. Zwraca czas całości w ms.
|
|
42
|
+
* Kolor cienia w `arc` i promień rogu bierze z obliczonych stylów, więc idą za motywem.
|
|
43
|
+
*/
|
|
44
|
+
export function playLayoutMotion(container, items, first, options) {
|
|
45
|
+
const last = snapshotLayout(container, items);
|
|
46
|
+
const sample = items[0];
|
|
47
|
+
const radius = sample ? Number.parseFloat(getComputedStyle(sample).borderTopLeftRadius) || 0 : 0;
|
|
48
|
+
const scrim = getComputedStyle(container).getPropertyValue('--kpt-color-overlay-scrim').trim();
|
|
49
|
+
const flights = planLayoutMotion(first.rects, last.rects, {
|
|
50
|
+
motion: options.motion,
|
|
51
|
+
radius,
|
|
52
|
+
...(first.origin ? { origin: first.origin } : {}),
|
|
53
|
+
...(scrim ? { shadowColor: `color-mix(in srgb, ${scrim} 55%, transparent)` } : {}),
|
|
54
|
+
});
|
|
55
|
+
for (const flight of flights) {
|
|
56
|
+
const el = items[flight.index];
|
|
57
|
+
if (!el)
|
|
58
|
+
continue;
|
|
59
|
+
const timing = {
|
|
60
|
+
id: ANIMATION_ID,
|
|
61
|
+
duration: flight.duration,
|
|
62
|
+
delay: flight.delay,
|
|
63
|
+
easing: 'linear',
|
|
64
|
+
fill: 'backwards',
|
|
65
|
+
};
|
|
66
|
+
el.classList.add(options.movingClass);
|
|
67
|
+
const anim = el.animate(flight.item.map((f) => ({ ...f })), timing);
|
|
68
|
+
options.innerOf(el)?.animate(flight.inner.map((f) => ({ ...f })), timing);
|
|
69
|
+
const land = () => {
|
|
70
|
+
const flying = el.getAnimations().some((a) => a.id === ANIMATION_ID && a.playState === 'running');
|
|
71
|
+
if (!flying)
|
|
72
|
+
el.classList.remove(options.movingClass);
|
|
73
|
+
};
|
|
74
|
+
anim.finished.then(land, () => undefined);
|
|
75
|
+
}
|
|
76
|
+
// Kontener zmienia wysokość razem z kartami, zamiast skakać pod nimi.
|
|
77
|
+
if (Math.abs(first.height - last.height) > 1) {
|
|
78
|
+
const easing = getComputedStyle(container).getPropertyValue('--kpt-motion-easing-emphasized').trim() || 'ease';
|
|
79
|
+
container.animate({ height: [`${first.height}px`, `${last.height}px`] }, { id: ANIMATION_ID, duration: options.motion === 'arc' ? 640 : 520, easing });
|
|
80
|
+
}
|
|
81
|
+
return layoutMotionDuration(flights);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Natywny morf (View Transitions): nadaje elementom unikalne `view-transition-name` na czas
|
|
85
|
+
* przejścia, woła `update` w środku `startViewTransition` i zdejmuje nazwy po końcu.
|
|
86
|
+
* `update` MUSI zsynchronizować DOM przed powrotem (Angular: `detectChanges()`, React:
|
|
87
|
+
* `flushSync`) - przeglądarka robi zrzut nowego stanu zaraz po nim.
|
|
88
|
+
* Bez wsparcia przeglądarki albo przy "ogranicz ruch" po prostu woła `update`.
|
|
89
|
+
*/
|
|
90
|
+
export function morphLayout(items, namePrefix, className, update) {
|
|
91
|
+
if (!layoutMorphSupported()) {
|
|
92
|
+
update();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
items.forEach((el, i) => {
|
|
96
|
+
el.style.viewTransitionName = `${namePrefix}-${i}`;
|
|
97
|
+
el.style.viewTransitionClass = className;
|
|
98
|
+
});
|
|
99
|
+
const clear = () => {
|
|
100
|
+
for (const el of items) {
|
|
101
|
+
el.style.viewTransitionName = '';
|
|
102
|
+
el.style.viewTransitionClass = '';
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
document.startViewTransition(update).finished.then(clear, clear);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Nakłada globalną konfigurację ruchu na korzeń dokumentu (`data-kpt-motion` + zmienne CSS)
|
|
109
|
+
* i zwraca funkcję, która cofa dokładnie to, co ustawiła.
|
|
110
|
+
*/
|
|
111
|
+
export function applyMotionConfig(root, config) {
|
|
112
|
+
const { attribute, properties } = motionRootStyle(config);
|
|
113
|
+
if (attribute)
|
|
114
|
+
root.setAttribute('data-kpt-motion', attribute);
|
|
115
|
+
for (const [name, value] of Object.entries(properties))
|
|
116
|
+
root.style.setProperty(name, value);
|
|
117
|
+
return () => {
|
|
118
|
+
if (attribute)
|
|
119
|
+
root.removeAttribute('data-kpt-motion');
|
|
120
|
+
for (const name of Object.keys(properties))
|
|
121
|
+
root.style.removeProperty(name);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Wejście elementu, który już jest w DOM (WAAPI, bez klas): `spring` - wyrasta z kierunku `from`
|
|
126
|
+
* na sprężynie i wyostrza się z rozmycia, `fade` - tylko się przenika. `start`/`end` idą za
|
|
127
|
+
* kierunkiem pisma (RTL odwraca stronę). Bez WAAPI albo przy "ogranicz ruch" nic nie robi.
|
|
128
|
+
*/
|
|
129
|
+
export function playEnterMotion(element, motion, from, delay = 0) {
|
|
130
|
+
if (!layoutMotionAllowed())
|
|
131
|
+
return;
|
|
132
|
+
const timing = { id: ANIMATION_ID, delay, fill: 'backwards' };
|
|
133
|
+
if (motion === 'fade') {
|
|
134
|
+
element.animate({ opacity: [0, 1] }, { ...timing, duration: 260, easing: 'ease-out' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const rtl = getComputedStyle(element).direction === 'rtl';
|
|
138
|
+
const side = (from === 'end') !== rtl ? 1 : -1;
|
|
139
|
+
const start = from === 'below' ? 'translate(0, 18px) scale(0.94)' : `translate(${side * 16}px, 6px) scale(0.97)`;
|
|
140
|
+
const origin = from === 'below' ? (rtl ? '0% 100%' : '100% 100%') : side > 0 ? '100% 100%' : '0% 100%';
|
|
141
|
+
element.animate({ transform: [start, 'none'], transformOrigin: [origin, origin] }, { ...timing, duration: 560, easing: springEasing(0.32) });
|
|
142
|
+
element.animate({ opacity: [0, 1], filter: ['blur(4px)', 'blur(0px)'] }, { ...timing, duration: 240, easing: 'ease-out' });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Potrząśnięcie w poziomie - sygnał "nie tak" (pole staje się nieprawidłowe). Krótkie i gasnące,
|
|
146
|
+
* żeby nie męczyło przy kolejnych próbach; amplituda w px, czas ~0,36 s. Bez WAAPI albo przy
|
|
147
|
+
* "ogranicz ruch" nic nie robi - stan i tak niesie kolor i komunikat.
|
|
148
|
+
*/
|
|
149
|
+
export function playShakeMotion(element, amplitude = 6) {
|
|
150
|
+
if (!layoutMotionAllowed())
|
|
151
|
+
return;
|
|
152
|
+
const a = amplitude;
|
|
153
|
+
element.animate({ translate: ['0', `${-a}px 0`, `${a * 0.8}px 0`, `${-a * 0.55}px 0`, `${a * 0.3}px 0`, '0'] }, { id: ANIMATION_ID, duration: 360, easing: 'ease-out' });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Wskoczenie (pop) - element przysiada i odbija na sprężynie. Odpalane z handlera zmiany, nie z CSS:
|
|
157
|
+
* reguła na `:checked` zagrałaby też przy pierwszym renderze. `squash` rozciąga element w poziomie
|
|
158
|
+
* zamiast go zmniejszać (kciuk przełącznika w ruchu). Przy "ogranicz ruch" nic nie robi.
|
|
159
|
+
*/
|
|
160
|
+
export function playPopMotion(element, mode = 'pop') {
|
|
161
|
+
if (!layoutMotionAllowed())
|
|
162
|
+
return;
|
|
163
|
+
const keyframes = mode === 'squash'
|
|
164
|
+
? { scale: ['1', '1.3 0.85', '1'] }
|
|
165
|
+
: { scale: ['0.8', '1'] };
|
|
166
|
+
element.animate(keyframes, {
|
|
167
|
+
id: ANIMATION_ID,
|
|
168
|
+
duration: mode === 'squash' ? 320 : 420,
|
|
169
|
+
easing: mode === 'squash' ? 'ease-out' : springEasing(0.5),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Sygnał błędu kontrolki: pierścień w kolorze błędu rozchodzi się od elementu i gaśnie, a element
|
|
174
|
+
* krótko się potrząsa (\`shake: false\` - sam pierścień, np. gdy potrząsa już otaczający
|
|
175
|
+
* \`kpt-form-field\`). Kolor z tokenu \`--kpt-form-field-border-error\` (zapas: \`--kpt-color-danger\`),
|
|
176
|
+
* więc idzie za motywem. Przy "ogranicz ruch" nic nie robi - błąd i tak niesie kolor ramki.
|
|
177
|
+
*/
|
|
178
|
+
export function playInvalidMotion(element, options = {}) {
|
|
179
|
+
if (!layoutMotionAllowed())
|
|
180
|
+
return;
|
|
181
|
+
const style = getComputedStyle(element);
|
|
182
|
+
const color = style.getPropertyValue('--kpt-form-field-border-error').trim() ||
|
|
183
|
+
style.getPropertyValue('--kpt-color-danger').trim() ||
|
|
184
|
+
'currentColor';
|
|
185
|
+
element.animate({
|
|
186
|
+
boxShadow: [
|
|
187
|
+
`0 0 0 0 color-mix(in srgb, ${color} 55%, transparent)`,
|
|
188
|
+
`0 0 0 0.45rem color-mix(in srgb, ${color} 0%, transparent)`,
|
|
189
|
+
],
|
|
190
|
+
}, { id: ANIMATION_ID, duration: 560, easing: 'ease-out' });
|
|
191
|
+
if (options.shake !== false)
|
|
192
|
+
playShakeMotion(element, 3);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Wskaźnik wyboru w kontrolce segmentowej: tło aktywnej pozycji (pseudo-element `pseudo`) przejeżdża
|
|
196
|
+
* z miejsca i szerokości poprzedniej pozycji (`from`, zmierzone przed zmianą) na nową - FLIP samego
|
|
197
|
+
* pseudo-elementu, bez dodatkowego węzła w DOM. Przy "ogranicz ruch" nic nie robi.
|
|
198
|
+
*/
|
|
199
|
+
export function playIndicatorMotion(from, to, pseudo = '::before') {
|
|
200
|
+
if (!layoutMotionAllowed())
|
|
201
|
+
return;
|
|
202
|
+
const r = to.getBoundingClientRect();
|
|
203
|
+
if (!r.width || !from.width)
|
|
204
|
+
return;
|
|
205
|
+
const dx = from.left - r.left;
|
|
206
|
+
const sx = from.width / r.width;
|
|
207
|
+
if (Math.abs(dx) < 0.5 && Math.abs(sx - 1) < 0.01)
|
|
208
|
+
return;
|
|
209
|
+
to.animate({ transform: [`translateX(${dx}px) scaleX(${sx})`, 'none'], transformOrigin: ['0 0', '0 0'] }, { id: ANIMATION_ID, duration: 480, easing: springEasing(0.18), pseudoElement: pseudo });
|
|
210
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KptFlowChange } from './flow.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Zmiana listy jako wejście dla `prepareFlowMotion`: indeks usuniętego klucza w starej liście
|
|
4
|
+
* i dodanego w nowej. Ruch ma sens tylko dla pojedynczej zmiany - przy kilku naraz (np. wyczyszczenie,
|
|
5
|
+
* podmiana całej wartości) albo przy zmianie kolejności zwraca `null` i lista zmienia się bez ruchu.
|
|
6
|
+
*/
|
|
7
|
+
export declare function flowChangeOf<K>(before: readonly K[], after: readonly K[]): KptFlowChange | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zmiana listy jako wejście dla `prepareFlowMotion`: indeks usuniętego klucza w starej liście
|
|
3
|
+
* i dodanego w nowej. Ruch ma sens tylko dla pojedynczej zmiany - przy kilku naraz (np. wyczyszczenie,
|
|
4
|
+
* podmiana całej wartości) albo przy zmianie kolejności zwraca `null` i lista zmienia się bez ruchu.
|
|
5
|
+
*/
|
|
6
|
+
export function flowChangeOf(before, after) {
|
|
7
|
+
const removed = before.filter((k) => !after.includes(k));
|
|
8
|
+
const added = after.filter((k) => !before.includes(k));
|
|
9
|
+
if (removed.length + added.length !== 1)
|
|
10
|
+
return null;
|
|
11
|
+
// Wspólne klucze muszą zachować kolejność - inaczej FLIP po indeksach poleciałby z złych miejsc.
|
|
12
|
+
const keptBefore = before.filter((k) => after.includes(k));
|
|
13
|
+
const keptAfter = after.filter((k) => before.includes(k));
|
|
14
|
+
if (keptBefore.some((k, i) => k !== keptAfter[i]))
|
|
15
|
+
return null;
|
|
16
|
+
return removed.length ? { removed: before.indexOf(removed[0]) } : { added: after.indexOf(added[0]) };
|
|
17
|
+
}
|
package/dist/flow.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruch listy "płynącej" (tagi w `kpt-chips-input`): elementy w jednym wierszu z zawijaniem, śledzone
|
|
3
|
+
* po indeksie. Po usunięciu środkowego framework kasuje OSTATNI węzeł i przepisuje treść pozostałych,
|
|
4
|
+
* więc wejście i wyjście w CSS pokazałyby zły element. Dlatego:
|
|
5
|
+
* - przed zmianą mierzymy pozycje i - przy usunięciu - klonujemy znikający element (duch),
|
|
6
|
+
* - po renderze sąsiedzi dosuwają się FLIP-em z pozycji, którą ich dane miały wcześniej,
|
|
7
|
+
* - duch gaśnie w miejscu, a nowy element wskakuje.
|
|
8
|
+
* Warstwa DOM bez testów (potrzebuje przeglądarki), wspólna dla obu portów.
|
|
9
|
+
*/
|
|
10
|
+
export interface KptFlowChange {
|
|
11
|
+
/** Indeks usuniętego elementu w liście PRZED zmianą. */
|
|
12
|
+
removed?: number;
|
|
13
|
+
/** Indeks dodanego elementu w liście PO zmianie. */
|
|
14
|
+
added?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Zapamiętuje stan listy przed zmianą. Zwraca funkcję do wywołania po wyrenderowaniu nowego stanu
|
|
18
|
+
* (Angular: `afterNextRender`, React: `useLayoutEffect` - przed malowaniem, inaczej mignie klatka
|
|
19
|
+
* z nowym układem). `items` to elementy w kolejności, łącznie z tymi za listą (np. pole tekstowe).
|
|
20
|
+
* `container` musi być pozycjonowany (`position: relative`) - w nim ląduje duch.
|
|
21
|
+
*/
|
|
22
|
+
export declare function prepareFlowMotion(container: HTMLElement, items: readonly HTMLElement[], change: KptFlowChange, motion: 'spring' | 'fade'): (after: readonly HTMLElement[]) => void;
|
package/dist/flow.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { layoutMotionAllowed } from "./dom.js";
|
|
2
|
+
import { springEasing } from "./spring.js";
|
|
3
|
+
const FLOW_ID = 'kpt-flow-motion';
|
|
4
|
+
/**
|
|
5
|
+
* Zapamiętuje stan listy przed zmianą. Zwraca funkcję do wywołania po wyrenderowaniu nowego stanu
|
|
6
|
+
* (Angular: `afterNextRender`, React: `useLayoutEffect` - przed malowaniem, inaczej mignie klatka
|
|
7
|
+
* z nowym układem). `items` to elementy w kolejności, łącznie z tymi za listą (np. pole tekstowe).
|
|
8
|
+
* `container` musi być pozycjonowany (`position: relative`) - w nim ląduje duch.
|
|
9
|
+
*/
|
|
10
|
+
export function prepareFlowMotion(container, items, change, motion) {
|
|
11
|
+
if (!layoutMotionAllowed())
|
|
12
|
+
return () => undefined;
|
|
13
|
+
const origin = container.getBoundingClientRect();
|
|
14
|
+
const rects = items.map((el) => el.getBoundingClientRect());
|
|
15
|
+
// Zmierzone razem z trwającym ruchem - teraz można go przerwać, nowy lot ruszy z tego miejsca.
|
|
16
|
+
for (const el of items)
|
|
17
|
+
for (const a of el.getAnimations())
|
|
18
|
+
if (a.id === FLOW_ID)
|
|
19
|
+
a.cancel();
|
|
20
|
+
const leaving = change.removed === undefined ? undefined : items[change.removed];
|
|
21
|
+
const leavingRect = change.removed === undefined ? undefined : rects[change.removed];
|
|
22
|
+
const ghost = leaving && leavingRect ? makeGhost(leaving, leavingRect, origin, container) : null;
|
|
23
|
+
return (after) => {
|
|
24
|
+
if (ghost) {
|
|
25
|
+
container.appendChild(ghost);
|
|
26
|
+
const out = motion === 'spring'
|
|
27
|
+
? { opacity: [1, 0], transform: ['none', 'scale(0.6)'], filter: ['blur(0px)', 'blur(3px)'] }
|
|
28
|
+
: { opacity: [1, 0] };
|
|
29
|
+
ghost
|
|
30
|
+
.animate(out, { id: FLOW_ID, duration: 220, easing: 'ease-in', fill: 'forwards' })
|
|
31
|
+
.finished.then(() => ghost.remove(), () => ghost.remove());
|
|
32
|
+
}
|
|
33
|
+
const now = container.getBoundingClientRect();
|
|
34
|
+
after.forEach((el, j) => {
|
|
35
|
+
if (j === change.added) {
|
|
36
|
+
playFlowEnter(el, motion);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Indeks, pod którym te same dane stały przed zmianą.
|
|
40
|
+
let i = j;
|
|
41
|
+
if (change.removed !== undefined && j >= change.removed)
|
|
42
|
+
i = j + 1;
|
|
43
|
+
if (change.added !== undefined && j > change.added)
|
|
44
|
+
i = j - 1;
|
|
45
|
+
const before = rects[i];
|
|
46
|
+
if (!before)
|
|
47
|
+
return;
|
|
48
|
+
const r = el.getBoundingClientRect();
|
|
49
|
+
const dx = before.left - origin.left - (r.left - now.left);
|
|
50
|
+
const dy = before.top - origin.top - (r.top - now.top);
|
|
51
|
+
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5)
|
|
52
|
+
return;
|
|
53
|
+
el.animate({ transform: [`translate(${dx}px, ${dy}px)`, 'none'] }, { id: FLOW_ID, duration: motion === 'spring' ? 480 : 240, easing: motion === 'spring' ? springEasing(0.18) : 'ease-out' });
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Wejście nowego elementu: wskakuje ze zmniejszenia i rozmycia (`fade`: samo przenikanie). */
|
|
58
|
+
function playFlowEnter(el, motion) {
|
|
59
|
+
if (motion === 'fade') {
|
|
60
|
+
el.animate({ opacity: [0, 1] }, { id: FLOW_ID, duration: 220, easing: 'ease-out' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
el.animate({ transform: ['scale(0.6)', 'none'] }, { id: FLOW_ID, duration: 520, easing: springEasing(0.32) });
|
|
64
|
+
el.animate({ opacity: [0, 1], filter: ['blur(3px)', 'blur(0px)'] }, { id: FLOW_ID, duration: 200, easing: 'ease-out' });
|
|
65
|
+
}
|
|
66
|
+
/** Nieinteraktywny klon w miejscu znikającego elementu, wewnątrz `container`. */
|
|
67
|
+
function makeGhost(source, rect, origin, container) {
|
|
68
|
+
const ghost = source.cloneNode(true);
|
|
69
|
+
for (const node of [ghost, ...ghost.querySelectorAll('[id]')])
|
|
70
|
+
node.removeAttribute('id');
|
|
71
|
+
ghost.removeAttribute('role');
|
|
72
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
73
|
+
ghost.inert = true;
|
|
74
|
+
Object.assign(ghost.style, {
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
left: `${rect.left - origin.left - container.clientLeft}px`,
|
|
77
|
+
top: `${rect.top - origin.top - container.clientTop}px`,
|
|
78
|
+
width: `${rect.width}px`,
|
|
79
|
+
height: `${rect.height}px`,
|
|
80
|
+
margin: '0',
|
|
81
|
+
pointerEvents: 'none',
|
|
82
|
+
});
|
|
83
|
+
return ghost;
|
|
84
|
+
}
|