@celestia-island/hikari 0.31.0 → 0.31.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
|
@@ -23,13 +23,23 @@
|
|
|
23
23
|
background: var(--hi-color-border, #ddd);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/* The component's own wrapper is `display: contents` (measurement anchor +
|
|
27
|
+
CSS-var inheritance) — layout-wise the buttons remain direct children of
|
|
28
|
+
HkAuthCard's `.s-auth-methods` slot container. */
|
|
29
|
+
.s-auth-methods-list {
|
|
30
|
+
display: contents;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
/* Fixed icon + label columns: the two spans keep the same width on every
|
|
27
34
|
row, so icons and labels align across buttons while each button stays a
|
|
28
35
|
centered hk-btn-block. HkButton has a fragment root, so inbound classes
|
|
29
36
|
don't fall through — scope everything off the `.s-auth-methods`
|
|
30
|
-
container
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
container (descendant match: the component wrapper is display:contents).
|
|
38
|
+
The label column width is MEASURED by the component (widest label text,
|
|
39
|
+
published as `--auth-methods-label-width` on its wrapper, inherited into
|
|
40
|
+
the rows); the 8em fallback only covers the pre-measure frame, and apps
|
|
41
|
+
may still override the var explicitly. */
|
|
42
|
+
.s-auth-methods .hk-btn .s-auth-methods-icon {
|
|
33
43
|
width: 20px;
|
|
34
44
|
flex: none;
|
|
35
45
|
display: inline-flex;
|
|
@@ -37,7 +47,7 @@
|
|
|
37
47
|
justify-content: center;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
.s-auth-methods
|
|
50
|
+
.s-auth-methods .hk-btn .s-auth-methods-label {
|
|
41
51
|
width: var(--auth-methods-label-width, 8em);
|
|
42
52
|
flex: none;
|
|
43
53
|
text-align: start;
|
|
@@ -29,13 +29,14 @@ const methods = [
|
|
|
29
29
|
describe("HkAuthMethodList", () => {
|
|
30
30
|
it("renders one fixed-column button per method and an optional divider", () => {
|
|
31
31
|
// The card's `methods` slot owns the `.s-auth-methods` container —
|
|
32
|
-
// reproduce that context here (the
|
|
32
|
+
// reproduce that context here (the buttons are layout children of it
|
|
33
|
+
// through the component's display:contents wrapper).
|
|
33
34
|
const c = mount(
|
|
34
35
|
h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { divider: "其他方式登录", methods })),
|
|
35
36
|
);
|
|
36
37
|
const divider = c.querySelector(".s-auth-methods-divider");
|
|
37
38
|
expect(divider?.textContent).toContain("其他方式登录");
|
|
38
|
-
const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods
|
|
39
|
+
const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .hk-btn");
|
|
39
40
|
expect(buttons.length).toBe(2);
|
|
40
41
|
const labels = [...buttons].map(
|
|
41
42
|
(b) => b.querySelector<HTMLElement>(".s-auth-methods-label")?.textContent,
|
|
@@ -57,9 +58,41 @@ describe("HkAuthMethodList", () => {
|
|
|
57
58
|
},
|
|
58
59
|
})),
|
|
59
60
|
);
|
|
60
|
-
const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods
|
|
61
|
+
const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .hk-btn");
|
|
61
62
|
buttons[1]!.click();
|
|
62
63
|
await nextTick();
|
|
63
64
|
expect(picked).toBe("linuxdo");
|
|
64
65
|
});
|
|
66
|
+
|
|
67
|
+
it("publishes the widest label text as the label-column custom property", async () => {
|
|
68
|
+
const c = mount(h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })));
|
|
69
|
+
await nextTick();
|
|
70
|
+
const wrapper = c.querySelector<HTMLElement>(".s-auth-methods-list")!;
|
|
71
|
+
expect(wrapper).toBeTruthy();
|
|
72
|
+
// Layout engines in test DOM report zero text metrics; stub scrollWidth
|
|
73
|
+
// on the label spans to a deterministic widest-wins pair (LinuxDo wider).
|
|
74
|
+
const spans = [...wrapper.querySelectorAll<HTMLElement>(".s-auth-methods-label")];
|
|
75
|
+
expect(spans.length).toBe(2);
|
|
76
|
+
Object.defineProperty(spans[0]!, "scrollWidth", { configurable: true, value: 61 });
|
|
77
|
+
Object.defineProperty(spans[1]!, "scrollWidth", { configurable: true, value: 59 });
|
|
78
|
+
// Trigger the measurement through a fresh label change (watch path).
|
|
79
|
+
await nextTick();
|
|
80
|
+
const { app } = mounts[mounts.length - 1]!;
|
|
81
|
+
// Re-run measurement via a re-mount-less route: the component exposes
|
|
82
|
+
// measure() — reach it through the rendered component instance.
|
|
83
|
+
const instance = (wrapper as unknown as { __vueParentComponent?: { exposed?: Record<string, unknown> } })
|
|
84
|
+
.__vueParentComponent?.exposed;
|
|
85
|
+
expect(typeof instance?.measure).toBe("function");
|
|
86
|
+
(instance!.measure as () => void)();
|
|
87
|
+
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("61px");
|
|
88
|
+
void app;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("leaves the column fallback intact when no text metrics are available", async () => {
|
|
92
|
+
const c = mount(h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })));
|
|
93
|
+
await nextTick();
|
|
94
|
+
const wrapper = c.querySelector<HTMLElement>(".s-auth-methods-list")!;
|
|
95
|
+
// Zero scrollWidth (no layout) must not publish a 0px column.
|
|
96
|
+
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("");
|
|
97
|
+
});
|
|
65
98
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent,
|
|
1
|
+
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
2
2
|
import HkButton from "./HkButton";
|
|
3
3
|
import "./HkAuthMethodList.scss";
|
|
4
4
|
|
|
@@ -7,15 +7,20 @@ import "./HkAuthMethodList.scss";
|
|
|
7
7
|
* auth cards (the ERP login's provider-button grammar, componentized).
|
|
8
8
|
*
|
|
9
9
|
* Every button renders [icon | label] with a FIXED icon column and a FIXED
|
|
10
|
-
* label column
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* label column, so the icons and the label text start at the same x on
|
|
11
|
+
* every row regardless of label length; because every content block is
|
|
12
|
+
* then the same width, the row can stay centered like any HkButton
|
|
13
|
+
* without the columns drifting.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* The label column is auto-sized: after mount (and whenever the label set
|
|
16
|
+
* or the document fonts change) the component measures the widest label
|
|
17
|
+
* text and publishes it as `--auth-methods-label-width` on its wrapper,
|
|
18
|
+
* so the aligned text region hugs the longest label instead of a blind
|
|
19
|
+
* hardcoded width. Consumers can still override the var explicitly.
|
|
20
|
+
*
|
|
21
|
+
* The wrapper is `display: contents`: the component keeps a DOM node for
|
|
22
|
+
* measurement and CSS-var inheritance while the buttons remain direct
|
|
23
|
+
* layout children of HkAuthCard's `.s-auth-methods` slot container.
|
|
19
24
|
*/
|
|
20
25
|
export default defineComponent({
|
|
21
26
|
name: "HkAuthMethodList",
|
|
@@ -45,9 +50,45 @@ export default defineComponent({
|
|
|
45
50
|
/** A method button was clicked. */
|
|
46
51
|
select: (_key: string) => true,
|
|
47
52
|
},
|
|
48
|
-
setup(props, { emit }) {
|
|
53
|
+
setup(props, { emit, expose }) {
|
|
54
|
+
const listRef = ref<HTMLDivElement | null>(null);
|
|
55
|
+
|
|
56
|
+
/** Measure the widest label text and publish it as the label-column
|
|
57
|
+
* width custom property. `scrollWidth` reports the text width even
|
|
58
|
+
* while the fixed column clips it, so this is correct both before
|
|
59
|
+
* the variable is first set and after any label change. */
|
|
60
|
+
function measure() {
|
|
61
|
+
const root = listRef.value;
|
|
62
|
+
if (!root) return;
|
|
63
|
+
let widest = 0;
|
|
64
|
+
for (const label of root.querySelectorAll<HTMLElement>(".s-auth-methods-label")) {
|
|
65
|
+
widest = Math.max(widest, label.scrollWidth);
|
|
66
|
+
}
|
|
67
|
+
if (widest > 0) root.style.setProperty("--auth-methods-label-width", `${widest}px`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
void nextTick(measure);
|
|
72
|
+
// Webfonts change text metrics after first paint; re-measure when
|
|
73
|
+
// they finish loading (no-op in environments without document.fonts).
|
|
74
|
+
if (typeof document !== "undefined" && document.fonts?.ready) {
|
|
75
|
+
void document.fonts.ready.then(measure).catch(() => {});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
onBeforeUnmount(() => {
|
|
79
|
+
listRef.value?.style.removeProperty("--auth-methods-label-width");
|
|
80
|
+
});
|
|
81
|
+
watch(
|
|
82
|
+
() => props.methods.map((method) => method.label).join("\u0000"),
|
|
83
|
+
() => {
|
|
84
|
+
void nextTick(measure);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expose({ measure });
|
|
89
|
+
|
|
49
90
|
return () => (
|
|
50
|
-
|
|
91
|
+
<div class="s-auth-methods-list" ref={listRef}>
|
|
51
92
|
{props.divider && (
|
|
52
93
|
<div class="s-auth-methods-divider">
|
|
53
94
|
<span>{props.divider}</span>
|
|
@@ -66,7 +107,7 @@ export default defineComponent({
|
|
|
66
107
|
<span class="s-auth-methods-label">{method.label}</span>
|
|
67
108
|
</HkButton>
|
|
68
109
|
))}
|
|
69
|
-
|
|
110
|
+
</div>
|
|
70
111
|
);
|
|
71
112
|
},
|
|
72
113
|
});
|
|
@@ -340,9 +340,11 @@ export default defineComponent({
|
|
|
340
340
|
zoomPercent={zoomPercent.value}
|
|
341
341
|
canZoomIn={canZoomIn.value}
|
|
342
342
|
canZoomOut={canZoomOut.value}
|
|
343
|
+
zoomStepPercent={5}
|
|
344
|
+
minZoomPercent={Math.max(1, Math.round(fit.value * 100))}
|
|
345
|
+
maxZoomPercent={Math.round(MAX_ZOOM * 100)}
|
|
343
346
|
showReset
|
|
344
|
-
|
|
345
|
-
onZoomOut={() => setZoom(zoom.value / 1.2)}
|
|
347
|
+
onZoomTo={(percent: number) => setZoom(percent / 100)}
|
|
346
348
|
onReset={resetView}
|
|
347
349
|
onPanDelta={onPanDelta}
|
|
348
350
|
/>
|
|
@@ -31,9 +31,9 @@ export interface MinimapRect {
|
|
|
31
31
|
* `transform-origin: 0 0` — the same model HImageViewer uses.
|
|
32
32
|
*
|
|
33
33
|
* The zoom bar is governed by default: ±5% fixed steps clamped to the
|
|
34
|
-
* 50–200% range,
|
|
35
|
-
* percent
|
|
36
|
-
*
|
|
34
|
+
* 50–200% range, each press emitting `zoomTo` with the clamped target
|
|
35
|
+
* percent. Consumers with a wider native camera range override
|
|
36
|
+
* `minZoomPercent`/`maxZoomPercent`.
|
|
37
37
|
*/
|
|
38
38
|
export default defineComponent({
|
|
39
39
|
name: "HkMinimap",
|
|
@@ -66,17 +66,13 @@ export default defineComponent({
|
|
|
66
66
|
* when a reset handler was wired up). */
|
|
67
67
|
showReset: { type: Boolean, default: false },
|
|
68
68
|
/** Optional prop-callback surface (alternative to the emits). */
|
|
69
|
-
onZoomIn: { type: Function as PropType<() => void>, default: undefined },
|
|
70
|
-
onZoomOut: { type: Function as PropType<() => void>, default: undefined },
|
|
71
69
|
onZoomTo: { type: Function as PropType<(percent: number) => void>, default: undefined },
|
|
72
70
|
onReset: { type: Function as PropType<() => void>, default: undefined },
|
|
73
71
|
onPanDelta: { type: Function as PropType<(dx: number, dy: number) => void>, default: undefined },
|
|
74
72
|
},
|
|
75
73
|
emits: {
|
|
76
|
-
zoomIn: () => true,
|
|
77
|
-
zoomOut: () => true,
|
|
78
74
|
/** Fixed-step zoom request carrying the clamped target percent —
|
|
79
|
-
* consumers
|
|
75
|
+
* consumers animate their camera toward this target. */
|
|
80
76
|
zoomTo: (_percent: number) => true,
|
|
81
77
|
reset: () => true,
|
|
82
78
|
panDelta: (_dx: number, _dy: number) => true,
|
|
@@ -132,14 +128,12 @@ export default defineComponent({
|
|
|
132
128
|
props.canZoomOut && props.zoomPercent > props.minZoomPercent + 1e-9);
|
|
133
129
|
|
|
134
130
|
/** Step the zoom bar by the fixed increment, clamped to [min, max];
|
|
135
|
-
* emits
|
|
136
|
-
*
|
|
131
|
+
* emits `zoomTo` with the exact clamped target percent. No-op (and
|
|
132
|
+
* stays disabled) at the bounds. */
|
|
137
133
|
function stepZoom(dir: number) {
|
|
138
134
|
const raw = props.zoomPercent + dir * props.zoomStepPercent;
|
|
139
135
|
const next = Math.min(props.maxZoomPercent, Math.max(props.minZoomPercent, raw));
|
|
140
136
|
if (Math.abs(next - props.zoomPercent) < 1e-9) return;
|
|
141
|
-
if (dir > 0) emit("zoomIn");
|
|
142
|
-
else emit("zoomOut");
|
|
143
137
|
emit("zoomTo", next);
|
|
144
138
|
}
|
|
145
139
|
|