@uniweb/runtime 1.1.12 → 1.1.14

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/src/sticky.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Where a section's `sticky` setting lands — pure, so the browser's renderer
3
+ * (`components/Layout.jsx`, `BlockRenderer.jsx`) and the prerender's (`ssr-renderer.js`) make the
4
+ * same boxes sticky, and a prerendered page does not change when the browser renders over it.
5
+ *
6
+ * ⭐ NO BOX A COMPONENT DRAWS CAN STICK. `position: sticky` moves an element only within its
7
+ * parent's box, and a component's markup sits in boxes as tall as itself: the section's wrapper,
8
+ * the layout area's wrapper (`area-wrappers.js`), the built-in layout's zone (`default-layout.js`).
9
+ * So a section asks — `sticky:`, one of a section's own settings (`@uniweb/schemas/section`),
10
+ * defaulted by its type's `meta.js` — and the runtime makes sticky the box that can:
11
+ *
12
+ * - in the body, the section's own wrapper, whose parent, the body, spans every section;
13
+ * - a child section rendered as a section (`<ChildBlocks wrapAs>`, kit's `ChildGrid`), its own
14
+ * wrapper too, which then stays in view within the box its parent places it in — in
15
+ * `ChildGrid`, its cell, as tall as the row — and no further. Top-aligned, so a flex or grid
16
+ * item has room. At one column (a phone) a child is its own row and does not stick, which is
17
+ * what a side panel stacked under the content should do;
18
+ * - in any other area, the area's box when any of its sections is sticky: the zone, in the
19
+ * built-in layout; the area wrapper, in a foundation's layout. That box takes the area's
20
+ * layer, and a sticky area's layer is above every area that scrolls under it
21
+ * (`resolveLayoutLayers`).
22
+ *
23
+ * ⭐ A section stays in view BELOW what is stuck at the top. A sticky area sticks at `0px`; a
24
+ * sticky section with no `top` of its own sticks at `--uw-sticky-top`, how far down the page's
25
+ * stuck chrome reaches, which the browser measures (`sticky-top.js`) — so a side panel or a
26
+ * sub-navigation clears a stuck header whatever its height. Unmeasured — a prerendered page
27
+ * before the browser runs, a page with nothing stuck — it is `0px`.
28
+ *
29
+ * Every box this module makes sticky says so, `data-uw-sticky` = `area`, `section` or `child`,
30
+ * for what reads them in the browser: the measurement, and `uniweb dev`'s check
31
+ * (`sticky-check.js`).
32
+ *
33
+ * ⛔ A layout that wraps an area in an element of its own confines the area wrapper, as the zone
34
+ * does in the built-in layout. That element is the layout's, so the layout makes it sticky, when
35
+ * `page.isSticky(area)`.
36
+ *
37
+ * ⛔ Until 2026-10-19 there was no setting, and a header's own `sticky top-0` — the starter
38
+ * templates' — scrolled away with the page. Until 2026-10-20 a child section's setting did
39
+ * nothing, and a sticky section stuck at `0px`, under a stuck header.
40
+ */
41
+
42
+ /** The very top of the viewport — a sticky area with no `top` of its own. */
43
+ export const STICKY_TOP = '0px'
44
+
45
+ /**
46
+ * Where a sticky section with no `top` of its own stays: below what is stuck at the top
47
+ * (`sticky-top.js` measures it), else the very top.
48
+ */
49
+ export const STICKY_SECTION_TOP = 'var(--uw-sticky-top, 0px)'
50
+
51
+ /**
52
+ * The style that keeps a box in view.
53
+ *
54
+ * @param {string|null} [top] - how far below the viewport's top, a CSS length
55
+ * @param {number|null} [zIndex] - its layer, when it has one
56
+ * @returns {{ position: 'sticky', top: string, zIndex?: number }}
57
+ */
58
+ export function stickyStyle(top, zIndex) {
59
+ const style = { position: 'sticky', top: top || STICKY_TOP }
60
+ if (zIndex != null) style.zIndex = zIndex
61
+ return style
62
+ }
63
+
64
+ /**
65
+ * Whether an area stays in view — when any of its sections is sticky — and how far below the
66
+ * top: the first sticky section's.
67
+ *
68
+ * @param {Array<Object>|null|undefined} blocks - the area's blocks, their components initialized
69
+ * @returns {{ top: string }|null}
70
+ */
71
+ export function areaStickiness(blocks) {
72
+ const first = (blocks || []).find((block) => block?.sticky)
73
+ return first ? { top: first.stickyTop || STICKY_TOP } : null
74
+ }
75
+
76
+ /**
77
+ * Each layout area's stickiness, read from its sections — what both renderers compute before the
78
+ * areas' layers, since a sticky area is lifted above the rest (`resolveLayoutLayers`).
79
+ *
80
+ * @param {Object<string, Array<Object>>} areas - area name → its blocks, their components initialized
81
+ * @returns {Object<string, { top: string }|null>}
82
+ */
83
+ export function layoutStickiness(areas) {
84
+ const sticky = {}
85
+ for (const [name, blocks] of Object.entries(areas || {})) sticky[name] = areaStickiness(blocks)
86
+ return sticky
87
+ }
88
+
89
+ /**
90
+ * The props a built-in layout zone takes when its area sticks — or null.
91
+ *
92
+ * @param {{ top: string }|null|undefined} stickiness - the area's (`areaStickiness`)
93
+ * @param {number|null} [layer] - the area's layer (`resolveLayoutLayers`)
94
+ * @returns {{ style: Object, 'data-uw-sticky': 'area' }|null}
95
+ */
96
+ export function zoneProps(stickiness, layer) {
97
+ return stickiness ? { style: stickyStyle(stickiness.top, layer), 'data-uw-sticky': 'area' } : null
98
+ }
99
+
100
+ /**
101
+ * The style a sticky section's own wrapper takes — a body section's, or a child section's — or
102
+ * null. Below what is stuck at the top unless its type says how far down; layer 1, so it paints
103
+ * over the sections after it, which are positioned only when they have a background; and
104
+ * top-aligned, so that as a flex or grid item it is not stretched to the row, which would leave
105
+ * it no room to move (in normal flow `align-self` does nothing).
106
+ *
107
+ * @param {Object} block - its component initialized
108
+ * @returns {Object|null}
109
+ */
110
+ export function sectionStickyStyle(block) {
111
+ return block?.sticky ? { ...stickyStyle(block.stickyTop || STICKY_SECTION_TOP, 1), alignSelf: 'start' } : null
112
+ }
113
+
114
+ /**
115
+ * What a section's wrapper adds when it sticks: its style, and which kind of box it is.
116
+ *
117
+ * @param {Object} block - its component initialized
118
+ * @param {'section'|'child'|boolean} kind - a body section (`true` reads as one), or a child section
119
+ * @returns {{ style: Object, 'data-uw-sticky': string }|null}
120
+ */
121
+ export function sectionStickyProps(block, kind) {
122
+ const style = kind ? sectionStickyStyle(block) : null
123
+ return style ? { style, 'data-uw-sticky': kind === 'child' ? 'child' : 'section' } : null
124
+ }