@celestia-island/hikari 0.55.310 → 0.55.312

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.
@@ -1,198 +0,0 @@
1
- /**
2
- * useHeightRide — the family-level "height ride" measurement primitive.
3
- *
4
- * Several hikari surfaces want one specific open/close motion: the SURFACE
5
- * FRAME itself grows from a fraction of its natural height to the full
6
- * height, while the chrome (title bar, footer action row, any pinned band)
7
- * keeps its natural size and stays legible. Scaling the whole frame
8
- * (`transform: scaleY()`) is the wrong tool for that — it squashes the
9
- * title glyphs, the buttons and the corner radii along with everything
10
- * else (2026-11-07 user report on the modal ride), and it re-rasters text
11
- * for the entire ride.
12
- *
13
- * The ride is therefore a LAYOUT-height animation:
14
- *
15
- * frame height : chrome + content × factor ⇄ chrome + content
16
- * content region: scaleY(factor) ⇄ scaleY(1) (origin top)
17
- *
18
- * The content region keeps its natural layout box (so nothing inside it
19
- * reflows per frame) and is only visually compressed about its own TOP
20
- * edge, which makes the compressed paint land exactly in the shorter slot
21
- * the frame leaves for it: the frame's bottom edge and the content's
22
- * bottom edge coincide at every t. Chrome is never inside the transform,
23
- * so its type, icons and padding render at natural size throughout.
24
- *
25
- * This composable owns the MEASUREMENT half only: it publishes
26
- *
27
- * --hk-ride-chrome px the part of the frame the ride must not squeeze
28
- * --hk-ride-content px the region that does get compressed
29
- * --hk-ride-factor the fraction (default 0.9)
30
- *
31
- * on the frame, and leaves the interpolation to CSS. That split is what
32
- * keeps it composable with the surfaces' existing state machines: the
33
- * consumer keeps flipping its own enter/leave classes (HkModal's surface
34
- * machine, HkRideBox's `riding` prop) and the sheet resolves
35
- * `height: calc(chrome + content × factor)` from the published numbers.
36
- * No timers, no class ownership, no fight with a consumer's own height pin
37
- * — a state rule carrying `!important` legitimately outranks the inline
38
- * px the size-morph pins while a ride is in flight.
39
- *
40
- * Scroll-container compatibility (the reason this lives upstream rather
41
- * than inside HkModal): every hikari custom scroller (`HkScrollContainer`,
42
- * `HkScrollPin`, the overlay scrollbar, the windowed scroller) measures
43
- * LAYOUT boxes — `offsetHeight` / `clientHeight` / `scrollHeight` — which
44
- * a transform never touches. Because the ride animates the FRAME's height
45
- * and leaves the content box natural, those measurements, the scroll
46
- * offsets, the pins and the fade bands stay coherent for the whole ride;
47
- * nothing has to be re-measured per frame, and nothing drifts when the
48
- * ride is interrupted.
49
- */
50
- import { onBeforeUnmount, type Ref } from "vue";
51
-
52
- /** Class/attribute vocabulary shared by the sheet and the contract tests. */
53
- export const RIDE_CLASS = "hk-ride";
54
- export const RIDE_FROM_CLASS = "hk-ride--from";
55
- export const RIDE_TO_CLASS = "hk-ride--to";
56
- export const RIDE_CONTENT_CLASS = "hk-ride-content";
57
- export const RIDE_ATTR = "data-hk-riding";
58
- /** Present only once the chrome/content split has been measured. */
59
- export const RIDE_READY_ATTR = "data-hk-ride-ready";
60
-
61
- export interface HeightRideOptions {
62
- /** Content height at the ride's extreme, as a fraction of natural.
63
- * Default 0.9 (a 10% vertical reveal). */
64
- factor?: number;
65
- }
66
-
67
- export interface HeightRide {
68
- /** Measure the frame's chrome/content split and publish the variables.
69
- * Safe to call at any time OUTSIDE a ride (rest geometry is what the
70
- * ride needs); calling it mid-ride re-measures the live, compressed
71
- * box and is therefore a no-op unless `force` is passed. */
72
- refresh(force?: boolean): boolean;
73
- /** Inline style vars for the CURRENT cache, or null when nothing has
74
- * been measured yet. Consumers bind this in their render so the very
75
- * first painted frame of a remount already carries the ride (a post-
76
- * mount publish is one paint too late: the enter-from frame would show
77
- * a full-height frame with unscaled content — read as "clipping"). */
78
- styleVars(): Record<string, string> | null;
79
- /** True when a measured split exists (drives the ready attribute). */
80
- hasCache(): boolean;
81
- /** Re-apply the LAST measured split without touching the DOM boxes.
82
- * A surface that unmounts its frame on close loses the inline variables
83
- * with it; republishing on the next mount is what makes the very first
84
- * painted frame of the next ride already correct. */
85
- publish(): boolean;
86
- /** The chrome height (px) from the last successful measure. */
87
- chromeHeight(): number;
88
- /** The content height (px) from the last successful measure. */
89
- contentHeight(): number;
90
- /** Drop the published variables (the sheet then falls back to `auto`,
91
- * i.e. no ride — never to a wrong box). */
92
- release(): void;
93
- }
94
-
95
- /** Any ride class means the box is (or is heading) somewhere other than its
96
- * rest height, so a measurement taken then would poison the next ride. The
97
- * guard therefore covers BOTH ends; consumers measure at rest. */
98
- function isInFlight(frame: HTMLElement): boolean {
99
- return (
100
- frame.classList.contains(RIDE_FROM_CLASS) || frame.classList.contains(RIDE_TO_CLASS)
101
- );
102
- }
103
-
104
- /**
105
- * Wire a frame (whose height rides) to the content region it compresses.
106
- *
107
- * `frame` must carry `.hk-ride` (directly, or via `HkRideBox`); `content`
108
- * must carry `.hk-ride-content`. Both are applied by the sheet/component,
109
- * so consumers normally only supply the refs.
110
- */
111
- export function useHeightRide(
112
- frame: Ref<HTMLElement | null | undefined>,
113
- content: Ref<HTMLElement | null | undefined>,
114
- options: HeightRideOptions = {},
115
- ): HeightRide {
116
- // Read live: a consumer may change `factor` after mount.
117
- const factorOf = (): number => options.factor ?? 0.9;
118
- let chrome = 0;
119
- let contentH = 0;
120
-
121
- const release = (): void => {
122
- const f = frame.value;
123
- if (!f) return;
124
- f.style.removeProperty("--hk-ride-chrome");
125
- f.style.removeProperty("--hk-ride-content");
126
- f.style.removeProperty("--hk-ride-factor");
127
- f.removeAttribute(RIDE_READY_ATTR);
128
- };
129
-
130
- const refresh = (force = false): boolean => {
131
- const f = frame.value;
132
- const c = content.value;
133
- if (!f || !c) return false;
134
- // Once the split is known, re-measuring DURING a ride would read the
135
- // compressed boxes and shrink again on the next flight. The first
136
- // measurement is exempt: the sheet keeps the frame at `auto` until the
137
- // split is published, so that one is taken on honest natural geometry.
138
- if (!force && f.hasAttribute(RIDE_READY_ATTR) && isInFlight(f)) return false;
139
- // Neither side reads the FRAME's height (that is the number the ride
140
- // computes): the content region's own layout box, plus the frame's OTHER
141
- // element children (the pinned chrome bands, always `flex-shrink: 0`).
142
- // Phones/WebViews that report 0 publish nothing at all rather than a
143
- // wrong box.
144
- let chromeNatural = 0;
145
- for (const child of Array.from(f.children)) {
146
- if (child === c || !(child instanceof HTMLElement)) continue;
147
- chromeNatural += child.offsetHeight;
148
- }
149
- // The content region's own layout box is the ONLY honest source. With
150
- // no ride class the body is flex-natural; while a ride class is present
151
- // the box is locked to the published var (stable, no new information —
152
- // and the frame's height is the ride's own output, so deriving the
153
- // content from it would feed every early error forward forever).
154
- const measured = c.offsetHeight;
155
- if (measured <= 0) return false;
156
- contentH = measured;
157
- chrome = chromeNatural;
158
- f.style.setProperty("--hk-ride-chrome", `${chrome}px`);
159
- f.style.setProperty("--hk-ride-content", `${contentH}px`);
160
- f.style.setProperty("--hk-ride-factor", String(factorOf()));
161
- // Readiness flag: the sheet only overrides the frame's height once the
162
- // split is real, so an unmeasured ride degrades to a plain fade instead
163
- // of collapsing to `calc(0px + 0px)`.
164
- f.setAttribute(RIDE_READY_ATTR, "");
165
- return true;
166
- };
167
-
168
- onBeforeUnmount(() => release());
169
-
170
- const publish = (): boolean => {
171
- const f = frame.value;
172
- if (!f || contentH <= 0) return false;
173
- f.style.setProperty("--hk-ride-chrome", `${chrome}px`);
174
- f.style.setProperty("--hk-ride-content", `${contentH}px`);
175
- f.style.setProperty("--hk-ride-factor", String(factorOf()));
176
- f.setAttribute(RIDE_READY_ATTR, "");
177
- return true;
178
- };
179
-
180
- const styleVars = (): Record<string, string> | null =>
181
- contentH <= 0
182
- ? null
183
- : {
184
- "--hk-ride-chrome": `${chrome}px`,
185
- "--hk-ride-content": `${contentH}px`,
186
- "--hk-ride-factor": String(factorOf()),
187
- };
188
-
189
- return {
190
- refresh,
191
- styleVars,
192
- hasCache: () => contentH > 0,
193
- publish,
194
- chromeHeight: () => chrome,
195
- contentHeight: () => contentH,
196
- release,
197
- };
198
- }