@livx.cc/bare-v3 0.1.0-alpha.24 → 0.1.0-alpha.25
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/dist/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.0-alpha.
|
|
2
|
+
"version": "0.1.0-alpha.25",
|
|
3
3
|
"cssSha256": "bcc34a659395a996278e980419d5817fb4ea1a4b5c5f85d66be17ed37fcb5e91",
|
|
4
4
|
"classes": [
|
|
5
5
|
"b3-accordion",
|
|
@@ -453,6 +453,7 @@
|
|
|
453
453
|
"editable-filtered-rows.md",
|
|
454
454
|
"icons.md",
|
|
455
455
|
"motion.md",
|
|
456
|
+
"scroll-entry.md",
|
|
456
457
|
"start.md"
|
|
457
458
|
]
|
|
458
459
|
}
|
|
@@ -37,6 +37,8 @@ Native `dialog` plus the mounted shared helper provides modal focus/inertness, b
|
|
|
37
37
|
|
|
38
38
|
The playground's motion studio shows shared geometry, real DOM reordering, direct pointer tracking and spring return, staged scene entrance, cancellable exit/entry exchange, and a collection sheet. The adjacent collapsed feedback recipes cover pending/error/retry, undo/focus recovery and loading with reserved space. Their illustrative content and simulated operations are not runtime services.
|
|
39
39
|
|
|
40
|
+
For scroll entrances, default to once per page visit. Observe a stationary section wrapper, animate its children, remember entered sections and unobserve them after entry. Do not reset them when scrolling away, returning to the tab or restoring the page. Repeat only a specifically chosen interaction or an explicit Replay action. Prepare the starting visual state before the first paint; waiting for fonts or starting a hidden frame only after intersection causes visible content to disappear and reappear. Keep content visible if setup cannot run. Reduced motion and keyboard focus must expose waiting content immediately. For the complete once-only lifecycle recipe, read scroll-entry.md on demand. The landing-studio composition demonstrates the policy.
|
|
41
|
+
|
|
40
42
|
For an expanding card beside text: exit the neighboring copy before moving into its space; reveal the copy only after the return layout completes. Guard each completion against newer input. FLIP preserves geometry but does not prevent sibling intersections.
|
|
41
43
|
|
|
42
44
|
For view replacement: start an exit; guard its completion with a monotonically increasing request id; if it still owns the transition, update content and enter. Rapid navigation must not commit an obsolete view. Do not await a confirmation animation before enabling the next action.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Once-only scroll entrances
|
|
2
|
+
|
|
3
|
+
For a client-rendered page, call this in the framework's post-render hook before paint. For static HTML, use a synchronous script immediately after the section markup. Calling it after fonts, a network request or a paint can cause the original flash. This snippet is not a solution for asynchronously hydrated visible HTML.
|
|
4
|
+
|
|
5
|
+
Choose one delivery location. On a static page, define and call the routine once in the synchronous script after the markup; do not also generate a fallback TypeScript copy. In a client-rendered app, put it in one module and call it from the pre-paint post-render hook instead. Keep the returned cleanup with that owner. The two delivery choices are alternatives, not parallel implementations.
|
|
6
|
+
|
|
7
|
+
Mark stationary section wrappers with `data-entry-section` and their animated children with `data-entry-part`. Keep the final layout visible in CSS. The routine prepares paused animations synchronously; setup failure, reduced motion and focus expose final content. Scroll entry is once per mount, with no tab-return reset. Explicit replay belongs to a separate user action.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
function prepareEntrances(root) {
|
|
11
|
+
const preference = matchMedia('(prefers-reduced-motion: reduce)');
|
|
12
|
+
const groups = new Map();
|
|
13
|
+
let observer;
|
|
14
|
+
let disposed = false;
|
|
15
|
+
const expose = (section) => {
|
|
16
|
+
const group = groups.get(section);
|
|
17
|
+
if (!group) return;
|
|
18
|
+
group.entered = true;
|
|
19
|
+
group.animations.forEach(animation => animation.cancel());
|
|
20
|
+
observer?.unobserve(section);
|
|
21
|
+
};
|
|
22
|
+
const exposeAll = () => groups.forEach((_, section) => expose(section));
|
|
23
|
+
const onPreference = () => { if (preference.matches) exposeAll(); };
|
|
24
|
+
const onFocus = event => {
|
|
25
|
+
for (const section of groups.keys()) {
|
|
26
|
+
if (section.contains(event.target)) expose(section);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
for (const section of root.querySelectorAll('[data-entry-section]')) {
|
|
31
|
+
const group = { entered: preference.matches, animations: [] };
|
|
32
|
+
groups.set(section, group);
|
|
33
|
+
if (preference.matches) continue;
|
|
34
|
+
for (const [index, part] of [...section.querySelectorAll('[data-entry-part]')].entries()) {
|
|
35
|
+
const animation = part.animate([
|
|
36
|
+
{ opacity: 0, transform: 'translateY(12px)' },
|
|
37
|
+
{ opacity: 1, transform: 'none' }
|
|
38
|
+
], { duration: 440, delay: Math.min(index * 70, 210),
|
|
39
|
+
easing: 'cubic-bezier(.16,1,.3,1)', fill: 'backwards' });
|
|
40
|
+
group.animations.push(animation);
|
|
41
|
+
animation.pause();
|
|
42
|
+
animation.currentTime = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
observer = new IntersectionObserver(entries => {
|
|
46
|
+
if (disposed) return;
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const group = groups.get(entry.target);
|
|
49
|
+
if (!entry.isIntersecting || group.entered) continue;
|
|
50
|
+
group.entered = true;
|
|
51
|
+
group.animations.forEach(animation => animation.play());
|
|
52
|
+
observer.unobserve(entry.target);
|
|
53
|
+
}
|
|
54
|
+
}, { threshold: 0 });
|
|
55
|
+
groups.forEach((group, section) => { if (!group.entered) observer.observe(section); });
|
|
56
|
+
preference.addEventListener('change', onPreference);
|
|
57
|
+
root.addEventListener('focusin', onFocus);
|
|
58
|
+
// A framework may restore focus before this setup hook runs.
|
|
59
|
+
onFocus({ target: (root.ownerDocument || root).activeElement });
|
|
60
|
+
} catch {
|
|
61
|
+
// Failure must leave the page usable; no hidden CSS depends on successful JS.
|
|
62
|
+
exposeAll();
|
|
63
|
+
observer?.disconnect();
|
|
64
|
+
}
|
|
65
|
+
return () => {
|
|
66
|
+
disposed = true;
|
|
67
|
+
exposeAll();
|
|
68
|
+
observer?.disconnect();
|
|
69
|
+
preference.removeEventListener('change', onPreference);
|
|
70
|
+
root.removeEventListener('focusin', onFocus);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Assumes window scrolling and untransformed child resting styles. For a scroll container, pass the actual scroll root to IntersectionObserver. Use an inner wrapper when authored transforms must be preserved. Do not infer animation state from truthiness of data attributes. Verify initial offscreen state, first entry, reentry, focus and reduced-motion changes; visible final content alone does not prove entrances ran.
|