@celestia-island/hikari 0.35.1 → 0.36.1
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/package.json
CHANGED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import { HkAuthCard } from "./HkAuthCard";
|
|
5
|
+
|
|
6
|
+
/** Injectable ResizeObserver: captures the callback so tests can fire
|
|
7
|
+
* content changes deterministically (happy-dom's own RO never fires —
|
|
8
|
+
* there is no layout engine). Same harness as useSizeMorph.test.ts. */
|
|
9
|
+
class FakeResizeObserver {
|
|
10
|
+
static instances: FakeResizeObserver[] = [];
|
|
11
|
+
callback: () => void;
|
|
12
|
+
observed: Element[] = [];
|
|
13
|
+
disconnected = false;
|
|
14
|
+
|
|
15
|
+
constructor(callback: () => void) {
|
|
16
|
+
this.callback = callback;
|
|
17
|
+
FakeResizeObserver.instances.push(this);
|
|
18
|
+
}
|
|
19
|
+
observe(el: Element): void {
|
|
20
|
+
this.observed.push(el);
|
|
21
|
+
}
|
|
22
|
+
disconnect(): void {
|
|
23
|
+
this.disconnected = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const originalRO = globalThis.ResizeObserver;
|
|
28
|
+
|
|
29
|
+
/** The prototype that actually defines `offsetHeight` in this DOM
|
|
30
|
+
* implementation, so the stub below is portable across environments. */
|
|
31
|
+
function offsetHeightOwner(): object {
|
|
32
|
+
let proto = Object.getPrototypeOf(document.createElement("div")) as object | null;
|
|
33
|
+
while (proto) {
|
|
34
|
+
if (Object.getOwnPropertyDescriptor(proto, "offsetHeight")) return proto;
|
|
35
|
+
proto = Object.getPrototypeOf(proto);
|
|
36
|
+
}
|
|
37
|
+
return HTMLElement.prototype;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
|
|
41
|
+
offsetHeightOwner(),
|
|
42
|
+
"offsetHeight",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Natural height every element reports for `offsetHeight` in tests. */
|
|
46
|
+
let naturalHeight = 0;
|
|
47
|
+
|
|
48
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
49
|
+
|
|
50
|
+
function mount(node: ReturnType<typeof h>) {
|
|
51
|
+
const container = document.createElement("div");
|
|
52
|
+
document.body.appendChild(container);
|
|
53
|
+
const app = createApp({ render: () => node });
|
|
54
|
+
app.mount(container);
|
|
55
|
+
mounts.push({ app, container });
|
|
56
|
+
return container;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cardOf(c: HTMLElement) {
|
|
60
|
+
return c.querySelector<HTMLElement>(".s-auth-card")!;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function wrapperOf(c: HTMLElement) {
|
|
64
|
+
return c.querySelector<HTMLElement>(".s-auth-card-height")!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fireTransitionEnd(el: HTMLElement, propertyName: string) {
|
|
68
|
+
const event = new Event("transitionend", { bubbles: true });
|
|
69
|
+
Object.defineProperty(event, "propertyName", { value: propertyName });
|
|
70
|
+
el.dispatchEvent(event);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
FakeResizeObserver.instances = [];
|
|
75
|
+
naturalHeight = 140;
|
|
76
|
+
globalThis.ResizeObserver = FakeResizeObserver as unknown as typeof ResizeObserver;
|
|
77
|
+
Object.defineProperty(offsetHeightOwner(), "offsetHeight", {
|
|
78
|
+
configurable: true,
|
|
79
|
+
get: () => naturalHeight,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
globalThis.ResizeObserver = originalRO;
|
|
85
|
+
if (originalOffsetHeight) {
|
|
86
|
+
Object.defineProperty(offsetHeightOwner(), "offsetHeight", originalOffsetHeight);
|
|
87
|
+
} else {
|
|
88
|
+
delete (offsetHeightOwner() as { offsetHeight?: number }).offsetHeight;
|
|
89
|
+
}
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
vi.useRealTimers();
|
|
92
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
93
|
+
app.unmount();
|
|
94
|
+
container.remove();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("HkAuthCard", () => {
|
|
99
|
+
it("renders title, subtitle and every slot inside the unchanged card shell", () => {
|
|
100
|
+
const c = mount(
|
|
101
|
+
h(HkAuthCard, { title: "Sign in", subtitle: "Welcome back" }, {
|
|
102
|
+
default: () => h("input", { name: "probe-field" }),
|
|
103
|
+
methods: () => h("div", { class: "probe-methods" }, "methods"),
|
|
104
|
+
footer: () => h("div", { class: "probe-footer" }, "footer"),
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
// The measuring wrapper is present and the card is still `.s-auth-card`.
|
|
108
|
+
const wrapper = wrapperOf(c);
|
|
109
|
+
const card = cardOf(c);
|
|
110
|
+
expect(wrapper).toBeTruthy();
|
|
111
|
+
expect(card).toBeTruthy();
|
|
112
|
+
expect(c.querySelector(".s-auth-title")?.textContent).toBe("Sign in");
|
|
113
|
+
expect(c.querySelector(".s-auth-subtitle")?.textContent).toBe("Welcome back");
|
|
114
|
+
expect(card.querySelector(".s-auth-form input[name='probe-field']")).toBeTruthy();
|
|
115
|
+
expect(card.querySelector(".s-auth-methods .probe-methods")).toBeTruthy();
|
|
116
|
+
expect(card.querySelector(".s-auth-footer .probe-footer")).toBeTruthy();
|
|
117
|
+
// The wrapper is transparent to descendant styling: the header, form,
|
|
118
|
+
// methods and footer blocks all remain children of `.s-auth-card`.
|
|
119
|
+
expect(wrapper.querySelector(".s-auth-card")).toBe(card);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("seeds the wrapper height from the card on mount without animating", () => {
|
|
123
|
+
naturalHeight = 140;
|
|
124
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
125
|
+
const wrapper = wrapperOf(c);
|
|
126
|
+
expect(wrapper.style.height).toBe("140px");
|
|
127
|
+
// Seeding must not animate: the measuring class is absent on first paint.
|
|
128
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
129
|
+
// The observer watches the card element itself, not the wrapper.
|
|
130
|
+
expect(FakeResizeObserver.instances).toHaveLength(1);
|
|
131
|
+
expect(FakeResizeObserver.instances[0]!.observed).toEqual([cardOf(c)]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("animates growth through the measuring class and releases it on the backstop timer", () => {
|
|
135
|
+
vi.useFakeTimers();
|
|
136
|
+
naturalHeight = 140;
|
|
137
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
138
|
+
const wrapper = wrapperOf(c);
|
|
139
|
+
expect(wrapper.style.height).toBe("140px");
|
|
140
|
+
|
|
141
|
+
naturalHeight = 240;
|
|
142
|
+
FakeResizeObserver.instances[0]!.callback();
|
|
143
|
+
expect(wrapper.style.height).toBe("240px");
|
|
144
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(true);
|
|
145
|
+
|
|
146
|
+
vi.advanceTimersByTime(449);
|
|
147
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(true);
|
|
148
|
+
vi.advanceTimersByTime(1);
|
|
149
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("releases the measuring class when the height transition finishes", () => {
|
|
153
|
+
naturalHeight = 100;
|
|
154
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
155
|
+
const wrapper = wrapperOf(c);
|
|
156
|
+
|
|
157
|
+
naturalHeight = 180;
|
|
158
|
+
FakeResizeObserver.instances[0]!.callback();
|
|
159
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(true);
|
|
160
|
+
|
|
161
|
+
// An unrelated property finishing its transition must not release.
|
|
162
|
+
fireTransitionEnd(wrapper, "color");
|
|
163
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(true);
|
|
164
|
+
|
|
165
|
+
fireTransitionEnd(wrapper, "height");
|
|
166
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("ignores height transitionsend events bubbling up from descendant content", () => {
|
|
170
|
+
naturalHeight = 100;
|
|
171
|
+
const c = mount(
|
|
172
|
+
h(HkAuthCard, { title: "T" }, { default: () => h("div", { class: "probe-anim" }) }),
|
|
173
|
+
);
|
|
174
|
+
const wrapper = wrapperOf(c);
|
|
175
|
+
const child = c.querySelector<HTMLElement>(".probe-anim")!;
|
|
176
|
+
|
|
177
|
+
naturalHeight = 180;
|
|
178
|
+
FakeResizeObserver.instances[0]!.callback();
|
|
179
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(true);
|
|
180
|
+
|
|
181
|
+
// A descendant slot element's own height transition bubbling up must
|
|
182
|
+
// NOT release the clip — it did not open this measuring window.
|
|
183
|
+
fireTransitionEnd(child, "height");
|
|
184
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(true);
|
|
185
|
+
|
|
186
|
+
// Only the wrapper's own height transition may end it.
|
|
187
|
+
fireTransitionEnd(wrapper, "height");
|
|
188
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("snaps unclipped under prefers-reduced-motion without arming the backstop", () => {
|
|
192
|
+
vi.useFakeTimers();
|
|
193
|
+
vi.spyOn(window, "matchMedia").mockReturnValue({ matches: true } as unknown as MediaQueryList);
|
|
194
|
+
naturalHeight = 140;
|
|
195
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
196
|
+
const wrapper = wrapperOf(c);
|
|
197
|
+
|
|
198
|
+
naturalHeight = 240;
|
|
199
|
+
FakeResizeObserver.instances[0]!.callback();
|
|
200
|
+
// Height still tracks the content…
|
|
201
|
+
expect(wrapper.style.height).toBe("240px");
|
|
202
|
+
// …but with the transition disabled there is nothing to animate, so
|
|
203
|
+
// no measuring window opens: no clip, no pending backstop timer.
|
|
204
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
205
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("ignores observer firings that move the height by less than a pixel", () => {
|
|
209
|
+
naturalHeight = 140;
|
|
210
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
211
|
+
const wrapper = wrapperOf(c);
|
|
212
|
+
|
|
213
|
+
naturalHeight = 140.6;
|
|
214
|
+
FakeResizeObserver.instances[0]!.callback();
|
|
215
|
+
expect(wrapper.style.height).toBe("140px");
|
|
216
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("with smoothHeight=false renders the wrapper but installs no measuring", () => {
|
|
220
|
+
naturalHeight = 140;
|
|
221
|
+
const c = mount(h(HkAuthCard, { title: "T", smoothHeight: false }));
|
|
222
|
+
const wrapper = wrapperOf(c);
|
|
223
|
+
// The wrapper still exists (stable DOM shape for consumers) but is
|
|
224
|
+
// completely unmanaged: no explicit height and no observer.
|
|
225
|
+
expect(wrapper).toBeTruthy();
|
|
226
|
+
expect(wrapper.style.height).toBe("");
|
|
227
|
+
expect(FakeResizeObserver.instances).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("degrades to an unmanaged wrapper when ResizeObserver is unavailable", () => {
|
|
231
|
+
(globalThis as { ResizeObserver?: unknown }).ResizeObserver = undefined;
|
|
232
|
+
naturalHeight = 140;
|
|
233
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
234
|
+
const wrapper = wrapperOf(c);
|
|
235
|
+
expect(wrapper).toBeTruthy();
|
|
236
|
+
expect(wrapper.style.height).toBe("");
|
|
237
|
+
expect(wrapper.classList.contains("s-auth-card--measuring")).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("disconnects the observer and clears the backstop on unmount", () => {
|
|
241
|
+
vi.useFakeTimers();
|
|
242
|
+
naturalHeight = 140;
|
|
243
|
+
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
244
|
+
const ro = FakeResizeObserver.instances[0]!;
|
|
245
|
+
|
|
246
|
+
naturalHeight = 200;
|
|
247
|
+
ro.callback();
|
|
248
|
+
expect(wrapperOf(c).classList.contains("s-auth-card--measuring")).toBe(true);
|
|
249
|
+
|
|
250
|
+
// Pop the entry so the shared afterEach does not unmount it twice.
|
|
251
|
+
const entry = mounts.pop()!;
|
|
252
|
+
entry.app.unmount();
|
|
253
|
+
expect(ro.disconnected).toBe(true);
|
|
254
|
+
// The pending backstop timer was cleared on unmount: nothing is left
|
|
255
|
+
// scheduled (a leaked timer here would also fire releaseClip on a
|
|
256
|
+
// dead element).
|
|
257
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { defineComponent,
|
|
1
|
+
import { defineComponent, onBeforeUnmount, onMounted, ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/** Grace window that force-releases the `--measuring` clip if the height
|
|
4
|
+
* `transitionend` never arrives (interrupted transition, backgrounded
|
|
5
|
+
* tab, reduced-motion). Slightly longer than the 0.3s CSS height
|
|
6
|
+
* transition so a normal completion is always released by the event. */
|
|
7
|
+
const MEASURE_BACKSTOP_MS = 450;
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* HkAuthCard — the shared shell of every Celestia auth screen (login,
|
|
@@ -19,42 +25,162 @@ import { defineComponent, type PropType } from "vue";
|
|
|
19
25
|
* per-row centering; content that must sit on one row belongs inside
|
|
20
26
|
* one child element.
|
|
21
27
|
* - `logo` swaps the header's logo slot; `default` is the form body.
|
|
28
|
+
*
|
|
29
|
+
* Smooth height contract:
|
|
30
|
+
* The card's content is consumer-driven and changes at runtime — channel
|
|
31
|
+
* tabs swap the OAuth `methods` list (3 buttons ↔ 1), validation banners
|
|
32
|
+
* appear, locale switches reflow labels — and the card's height would
|
|
33
|
+
* otherwise snap between the two layouts. The root card is therefore
|
|
34
|
+
* wrapped in a `.s-auth-card-height` measuring wrapper that owns an
|
|
35
|
+
* explicit `height` and animates toward whatever natural height the card
|
|
36
|
+
* grows or shrinks to:
|
|
37
|
+
*
|
|
38
|
+
* - On mount the wrapper seeds its `height` from the card's current
|
|
39
|
+
* `offsetHeight` while the animating state is impossible, so the first
|
|
40
|
+
* paint never animates.
|
|
41
|
+
* - A `ResizeObserver` on the card element (not the wrapper) fires when
|
|
42
|
+
* the content-driven natural height changes. Changes smaller than 1px
|
|
43
|
+
* are ignored to keep rounding noise from starting a cycle. A real
|
|
44
|
+
* change adds the `s-auth-card--measuring` class — the ONLY place
|
|
45
|
+
* `overflow: hidden` is applied — then rewrites the height, and the CSS
|
|
46
|
+
* `height` transition animates the wrapper between the two sizes.
|
|
47
|
+
* - The clipping class is released by the wrapper's `transitionend`
|
|
48
|
+
* (propertyName === "height") and, if that event never fires, by a
|
|
49
|
+
* 450ms backstop timer. Between animations the card carries no clip,
|
|
50
|
+
* so its shadow, focus rings and popovers are never cut off at rest.
|
|
51
|
+
* - `smoothHeight: false` opts out entirely: the wrapper still renders
|
|
52
|
+
* (the DOM shape stays stable for consumers) but no height style, no
|
|
53
|
+
* observer and no listeners are installed, and height changes snap as
|
|
54
|
+
* before. The prop is read ONCE at mount time — runtime toggles are
|
|
55
|
+
* ignored, making it a static opt-out rather than a reactive switch
|
|
56
|
+
* (remount with a changed `:key` to apply a new value).
|
|
57
|
+
* - Environments without `ResizeObserver` degrade the same way: instant
|
|
58
|
+
* snap, no measurement, no clipping.
|
|
59
|
+
* - `prefers-reduced-motion: reduce` disables the transition in CSS, so
|
|
60
|
+
* the height still tracks the content, just without animation. While
|
|
61
|
+
* it matches, the observer updates the height directly WITHOUT opening
|
|
62
|
+
* a measuring window — no clip and no backstop timer — because a clip
|
|
63
|
+
* held for the backstop would cut off the card's shadow with nothing
|
|
64
|
+
* animating to mask it.
|
|
65
|
+
*
|
|
66
|
+
* The wrapper is transparent to descendant styling: nothing inside
|
|
67
|
+
* `.s-auth-card` changes, and no hikari stylesheet targets the card as a
|
|
68
|
+
* direct child. Only consumer styles that select `.s-auth-card` as a
|
|
69
|
+
* DIRECT child of a specific parent must account for the extra wrapper
|
|
70
|
+
* (e.g. `.us-main > .s-auth-card` becomes
|
|
71
|
+
* `.us-main > .s-auth-card-height > .s-auth-card`).
|
|
22
72
|
*/
|
|
23
73
|
export const HkAuthCard = defineComponent({
|
|
24
74
|
name: "HkAuthCard",
|
|
25
75
|
props: {
|
|
26
76
|
title: { type: String, required: true },
|
|
27
77
|
subtitle: { type: String, default: "" },
|
|
78
|
+
smoothHeight: { type: Boolean, default: true },
|
|
28
79
|
},
|
|
29
80
|
setup(props, { slots }) {
|
|
81
|
+
const heightEl = ref<HTMLElement | null>(null);
|
|
82
|
+
const cardEl = ref<HTMLElement | null>(null);
|
|
83
|
+
|
|
84
|
+
let observer: ResizeObserver | null = null;
|
|
85
|
+
let backstopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
+
|
|
87
|
+
/** End the current measuring window: drop the clip and the backstop. */
|
|
88
|
+
const releaseClip = () => {
|
|
89
|
+
if (backstopTimer !== null) {
|
|
90
|
+
clearTimeout(backstopTimer);
|
|
91
|
+
backstopTimer = null;
|
|
92
|
+
}
|
|
93
|
+
heightEl.value?.classList.remove("s-auth-card--measuring");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onTransitionEnd = (event: TransitionEvent) => {
|
|
97
|
+
// Only the wrapper's OWN height transition may release the clip —
|
|
98
|
+
// a descendant slot element's height transition bubbling up must
|
|
99
|
+
// not end a measuring window it did not open.
|
|
100
|
+
if (event.target !== heightEl.value) return;
|
|
101
|
+
if (event.propertyName !== "height") return;
|
|
102
|
+
releaseClip();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
if (!props.smoothHeight) return;
|
|
107
|
+
const wrapper = heightEl.value;
|
|
108
|
+
const card = cardEl.value;
|
|
109
|
+
if (!wrapper || !card || typeof ResizeObserver === "undefined") return;
|
|
110
|
+
|
|
111
|
+
// Seed the explicit height BEFORE any animating class exists, so
|
|
112
|
+
// the first paint (wrapper already at the natural size) never
|
|
113
|
+
// animates.
|
|
114
|
+
wrapper.style.height = `${card.offsetHeight}px`;
|
|
115
|
+
wrapper.addEventListener("transitionend", onTransitionEnd);
|
|
116
|
+
|
|
117
|
+
observer = new ResizeObserver(() => {
|
|
118
|
+
const next = card.offsetHeight;
|
|
119
|
+
const current = parseFloat(wrapper.style.height) || 0;
|
|
120
|
+
// Ignore sub-pixel churn — rounding noise must not start a
|
|
121
|
+
// clip-and-animate cycle.
|
|
122
|
+
if (Math.abs(next - current) < 1) return;
|
|
123
|
+
|
|
124
|
+
// Reduced motion: the CSS transition is `none`, so transitionend
|
|
125
|
+
// will never fire and the clip would just sit there for the
|
|
126
|
+
// backstop window, cutting off the shadow with nothing to mask.
|
|
127
|
+
// Snap unclipped instead — no class, no timer.
|
|
128
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
129
|
+
wrapper.style.height = `${next}px`;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Clip ONLY while a height transition is actually running; the
|
|
134
|
+
// class is released by transitionend or the backstop timer.
|
|
135
|
+
wrapper.classList.add("s-auth-card--measuring");
|
|
136
|
+
wrapper.style.height = `${next}px`;
|
|
137
|
+
|
|
138
|
+
if (backstopTimer !== null) clearTimeout(backstopTimer);
|
|
139
|
+
backstopTimer = setTimeout(releaseClip, MEASURE_BACKSTOP_MS);
|
|
140
|
+
});
|
|
141
|
+
observer.observe(card);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
onBeforeUnmount(() => {
|
|
145
|
+
observer?.disconnect();
|
|
146
|
+
observer = null;
|
|
147
|
+
heightEl.value?.removeEventListener("transitionend", onTransitionEnd);
|
|
148
|
+
if (backstopTimer !== null) {
|
|
149
|
+
clearTimeout(backstopTimer);
|
|
150
|
+
backstopTimer = null;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
30
154
|
return () => (
|
|
31
|
-
<div class="s-auth-card">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
155
|
+
<div class="s-auth-card-height" ref={heightEl}>
|
|
156
|
+
<div class="s-auth-card" ref={cardEl}>
|
|
157
|
+
{slots.logo && (
|
|
158
|
+
<div class="s-auth-header">
|
|
159
|
+
{slots.logo()}
|
|
160
|
+
<h1 class="s-auth-title">{props.title}</h1>
|
|
161
|
+
{props.subtitle && <p class="s-auth-subtitle">{props.subtitle}</p>}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
{!slots.logo && (
|
|
165
|
+
<div class="s-auth-header">
|
|
166
|
+
<h1 class="s-auth-title">{props.title}</h1>
|
|
167
|
+
{props.subtitle && <p class="s-auth-subtitle">{props.subtitle}</p>}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
<div class="s-auth-form">
|
|
171
|
+
{slots.default?.()}
|
|
43
172
|
</div>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
173
|
+
{slots.methods && (
|
|
174
|
+
<div class="s-auth-methods">
|
|
175
|
+
{slots.methods()}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
{slots.footer && (
|
|
179
|
+
<div class="s-auth-footer">
|
|
180
|
+
{slots.footer()}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
47
183
|
</div>
|
|
48
|
-
{slots.methods && (
|
|
49
|
-
<div class="s-auth-methods">
|
|
50
|
-
{slots.methods()}
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
53
|
-
{slots.footer && (
|
|
54
|
-
<div class="s-auth-footer">
|
|
55
|
-
{slots.footer()}
|
|
56
|
-
</div>
|
|
57
|
-
)}
|
|
58
184
|
</div>
|
|
59
185
|
);
|
|
60
186
|
},
|
|
@@ -195,42 +195,53 @@
|
|
|
195
195
|
word-break: break-word;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/* Syntax highlighting palette. Values are app-overridable through the
|
|
199
|
+
* --code-* custom properties (dark = One Dark, light = GitHub Light in
|
|
200
|
+
* themes that define the family — e.g. celestia webui theme.scss);
|
|
201
|
+
* the fallbacks below are the One Dark originals so standalone
|
|
202
|
+
* consumers render exactly as before. */
|
|
198
203
|
.s-tool-code-block .hljs-keyword,
|
|
199
|
-
.s-tool-code-block .hljs-selector-tag
|
|
204
|
+
.s-tool-code-block .hljs-selector-tag {
|
|
205
|
+
color: var(--code-keyword, #c678dd);
|
|
206
|
+
}
|
|
207
|
+
|
|
200
208
|
.s-tool-code-block .hljs-built_in {
|
|
201
|
-
color: #c678dd;
|
|
209
|
+
color: var(--code-builtin, #c678dd);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.s-tool-code-block .hljs-string {
|
|
213
|
+
color: var(--code-string, #98c379);
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
.s-tool-code-block .hljs-string,
|
|
205
216
|
.s-tool-code-block .hljs-attr {
|
|
206
|
-
color: #
|
|
217
|
+
color: var(--code-number, #d19a66);
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
.s-tool-code-block .hljs-number,
|
|
210
221
|
.s-tool-code-block .hljs-literal {
|
|
211
|
-
color: #d19a66;
|
|
222
|
+
color: var(--code-number, #d19a66);
|
|
212
223
|
}
|
|
213
224
|
|
|
214
225
|
.s-tool-code-block .hljs-comment,
|
|
215
226
|
.s-tool-code-block .hljs-doctag {
|
|
216
|
-
color: #5c6370;
|
|
227
|
+
color: var(--code-comment, #5c6370);
|
|
217
228
|
font-style: italic;
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
.s-tool-code-block .hljs-function .hljs-title,
|
|
221
232
|
.s-tool-code-block .hljs-title.function_ {
|
|
222
|
-
color: #61afef;
|
|
233
|
+
color: var(--code-function, #61afef);
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
.s-tool-code-block .hljs-type,
|
|
226
237
|
.s-tool-code-block .hljs-class .hljs-title {
|
|
227
|
-
color: #e5c07b;
|
|
238
|
+
color: var(--code-type, #e5c07b);
|
|
228
239
|
}
|
|
229
240
|
|
|
230
241
|
.s-tool-code-block .hljs-variable,
|
|
231
242
|
.s-tool-code-block .hljs-template-variable,
|
|
232
243
|
.s-tool-code-block .hljs-property {
|
|
233
|
-
color: #e06c75;
|
|
244
|
+
color: var(--code-variable, #e06c75);
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
.s-tool-code-block .hljs-params {
|
|
@@ -352,20 +363,20 @@
|
|
|
352
363
|
}
|
|
353
364
|
|
|
354
365
|
.s-jv-str {
|
|
355
|
-
color: #98c379;
|
|
366
|
+
color: var(--code-string, #98c379);
|
|
356
367
|
word-break: break-all;
|
|
357
368
|
}
|
|
358
369
|
|
|
359
370
|
.s-jv-num {
|
|
360
|
-
color: #d19a66;
|
|
371
|
+
color: var(--code-number, #d19a66);
|
|
361
372
|
}
|
|
362
373
|
|
|
363
374
|
.s-jv-bool {
|
|
364
|
-
color: #c678dd;
|
|
375
|
+
color: var(--code-keyword, #c678dd);
|
|
365
376
|
}
|
|
366
377
|
|
|
367
378
|
.s-jv-str-raw {
|
|
368
|
-
color: #98c379;
|
|
379
|
+
color: var(--code-string, #98c379);
|
|
369
380
|
white-space: pre-wrap;
|
|
370
381
|
word-break: break-all;
|
|
371
382
|
}
|
|
@@ -527,6 +527,30 @@
|
|
|
527
527
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/* Measuring wrapper (HkAuthCard): owns an explicit height so content-
|
|
531
|
+
driven height changes (channel tabs swapping the methods list, error
|
|
532
|
+
banners, …) animate instead of snapping. The transition lives here,
|
|
533
|
+
always on; the `--measuring` clip below is applied by HkAuthCard only
|
|
534
|
+
for the duration of a height transition, so an idle card never clips
|
|
535
|
+
its shadow, focus rings or popovers. */
|
|
536
|
+
.s-auth-card-height {
|
|
537
|
+
/* The wrapper is sized from the card's offsetHeight (border-box); an
|
|
538
|
+
explicit box-sizing keeps that mapping honest on hosts without a
|
|
539
|
+
global border-box reset. */
|
|
540
|
+
box-sizing: border-box;
|
|
541
|
+
transition: height 0.3s var(--ease-out-expo, cubic-bezier(0.19, 1, 0.22, 1));
|
|
542
|
+
|
|
543
|
+
&.s-auth-card--measuring {
|
|
544
|
+
overflow: hidden;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@media (prefers-reduced-motion: reduce) {
|
|
549
|
+
.s-auth-card-height {
|
|
550
|
+
transition: none;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
530
554
|
.s-auth-card input {
|
|
531
555
|
transition: color calc(infinity * 1s) step-end, background-color calc(infinity * 1s) step-end;
|
|
532
556
|
appearance: none;
|