@celestia-island/hikari 0.43.13 → 0.43.15
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 +1 -1
- package/src/components/HkAlert.scss +21 -21
- package/src/components/HkAltSignIn.scss +3 -3
- package/src/components/HkAuthMethodList.scss +2 -2
- package/src/components/HkAuthMethodList.test.tsx +24 -0
- package/src/components/HkAuthMethodList.tsx +8 -1
- package/src/components/HkAvatar.scss +1 -1
- package/src/components/HkBlockingToast.scss +6 -6
- package/src/components/HkBoard.scale.test.ts +136 -0
- package/src/components/HkBoard.tsx +27 -14
- package/src/components/HkCheckbox.scss +11 -11
- package/src/components/HkDatePicker.scss +12 -12
- package/src/components/HkDateTimePicker.scss +16 -16
- package/src/components/HkDivider.scss +3 -3
- package/src/components/HkDraggableGrid.scss +11 -11
- package/src/components/HkDraggableList.scss +6 -6
- package/src/components/HkDrawer.scss +24 -24
- package/src/components/HkDrawer.sheetfamily.test.ts +1 -1
- package/src/components/HkEmptyState.scss +6 -6
- package/src/components/HkFab.scss +12 -12
- package/src/components/HkFileField.scss +2 -2
- package/src/components/HkFilePickerField.scss +2 -2
- package/src/components/HkImagePreview.scss +8 -8
- package/src/components/HkImageViewer.tsx +37 -16
- package/src/components/HkImageViewerTouch.test.tsx +57 -0
- package/src/components/HkInput.scss +2 -2
- package/src/components/HkJsonTree.scss +2 -2
- package/src/components/HkKeywordSearchModal.scss +22 -22
- package/src/components/HkLabel.scss +3 -3
- package/src/components/HkListTransition.scss +23 -23
- package/src/components/HkLogo.scss +3 -3
- package/src/components/HkMarkdownRenderer.scss +9 -9
- package/src/components/HkMediaSlider.scss +1 -1
- package/src/components/HkMinimap.scale.test.ts +78 -0
- package/src/components/HkMinimap.scss +2 -2
- package/src/components/HkMinimap.tsx +16 -1
- package/src/components/HkModal.scss +14 -14
- package/src/components/HkModalBreadcrumb.scss +5 -5
- package/src/components/HkNavItem.scss +2 -2
- package/src/components/HkNumberInput.scss +3 -3
- package/src/components/HkPasswordInput.scss +29 -29
- package/src/components/HkPasswordInput.test.ts +48 -0
- package/src/components/HkPasswordInput.tsx +9 -1
- package/src/components/HkPhaseTransition.scss +5 -5
- package/src/components/HkPickerPane.scss +4 -4
- package/src/components/HkPopover.scss +2 -2
- package/src/components/HkProgressRing.scss +4 -4
- package/src/components/HkRadio.scss +4 -4
- package/src/components/HkRollingNumber.scss +1 -1
- package/src/components/HkScrollContainer.scss +5 -5
- package/src/components/HkScrollContainer.test.ts +48 -1
- package/src/components/HkScrollContainer.tsx +12 -1
- package/src/components/HkSearchInput.scss +3 -3
- package/src/components/HkSelect.scss +2 -2
- package/src/components/HkSelectionGrid.scss +20 -20
- package/src/components/HkSelectionWaterfall.scss +15 -15
- package/src/components/HkSidebar.scss +7 -7
- package/src/components/HkSkeleton.scss +8 -8
- package/src/components/HkSlider.scss +1 -1
- package/src/components/HkSplash.scss +3 -3
- package/src/components/HkStepFlow.scss +1 -1
- package/src/components/HkSwitch.scss +1 -1
- package/src/components/HkTable.scss +21 -21
- package/src/components/HkTag.scss +18 -18
- package/src/components/HkTextarea.scss +1 -1
- package/src/components/HkTimeline.scss +2 -2
- package/src/components/HkTitleBar.scss +2 -2
- package/src/components/HkToast.scss +15 -15
- package/src/components/HkTree.scss +16 -16
- package/src/components/HkWindowedItem.scale.test.ts +107 -0
- package/src/components/HkWindowedItem.tsx +9 -1
- package/src/components/window-close.scss +1 -1
- package/src/composables/layoutGeometry.test.ts +41 -0
- package/src/composables/layoutGeometry.ts +27 -0
- package/src/composables/useOverlayScrollbar.test.ts +33 -0
- package/src/composables/useOverlayScrollbar.ts +15 -1
- package/src/composables/useZoomPan.test.ts +90 -0
- package/src/composables/useZoomPan.ts +44 -19
- package/src/index.ts +16 -0
- package/src/scale.scss +76 -0
- package/src/styles/admin-tokens.scss +4 -37
- package/src/styles/index.scss +25 -11
- package/src/styles/theme/_glass.scss +1 -0
- package/src/styles/theme/_layout.scss +1 -0
- package/src/styles/theme/_scrollbar.scss +1 -0
- package/src/styles/theme/_tokens.scss +1 -0
- package/src/styles/theme/base.scss +1 -0
- package/src/styles/theme/channels.scss +23 -41
- package/src/styles/theme/foundation.scss +1 -0
- package/src/styles/theme/mixins.scss +1 -0
- package/src/styles/theme/scale.scss +78 -0
- package/src/styles/theme/themes.scss +1 -0
- package/src/styles/theme/variables.scss +1 -0
- package/src/styles/tokenInventory.test.ts +290 -0
- package/src/styles/vendoredSync.test.ts +91 -0
- package/src/tokens.scss +23 -41
package/package.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
align-items: flex-start;
|
|
4
4
|
gap: var(--space-12, 12px);
|
|
5
5
|
padding: var(--space-12, 12px) var(--space-16, 16px);
|
|
6
|
-
border-radius: var(--
|
|
6
|
+
border-radius: var(--radius-md, 8px);
|
|
7
7
|
border: 1px solid transparent;
|
|
8
8
|
border-inline-start-width: 4px;
|
|
9
|
-
color: var(--hi-color-text-primary, #
|
|
9
|
+
color: var(--hi-color-text-primary, #1e1e1e);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.hk-alert-banner {
|
|
@@ -45,27 +45,27 @@
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.hk-alert-warning {
|
|
48
|
-
background: color-mix(in srgb, var(--hi-color-warning, #
|
|
49
|
-
border-color: color-mix(in srgb, var(--hi-color-warning, #
|
|
50
|
-
border-inline-start-color: var(--hi-color-warning, #
|
|
48
|
+
background: color-mix(in srgb, var(--hi-color-warning, #eed677) 8%, transparent);
|
|
49
|
+
border-color: color-mix(in srgb, var(--hi-color-warning, #eed677) 15%, transparent);
|
|
50
|
+
border-inline-start-color: var(--hi-color-warning, #eed677);
|
|
51
51
|
|
|
52
|
-
.hk-alert-icon { color: var(--hi-color-warning, #
|
|
52
|
+
.hk-alert-icon { color: var(--hi-color-warning, #eed677); }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
.hk-alert-info {
|
|
56
|
-
background: color-mix(in srgb, var(--hi-color-primary, #
|
|
57
|
-
border-color: color-mix(in srgb, var(--hi-color-primary, #
|
|
58
|
-
border-inline-start-color: var(--hi-color-primary, #
|
|
56
|
+
background: color-mix(in srgb, var(--hi-color-primary, #7aa2f7) 8%, transparent);
|
|
57
|
+
border-color: color-mix(in srgb, var(--hi-color-primary, #7aa2f7) 15%, transparent);
|
|
58
|
+
border-inline-start-color: var(--hi-color-primary, #7aa2f7);
|
|
59
59
|
|
|
60
|
-
.hk-alert-icon { color: var(--hi-color-primary, #
|
|
60
|
+
.hk-alert-icon { color: var(--hi-color-primary, #7aa2f7); }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.hk-alert-success {
|
|
64
|
-
background: color-mix(in srgb, var(--hi-color-success, #
|
|
65
|
-
border-color: color-mix(in srgb, var(--hi-color-success, #
|
|
66
|
-
border-inline-start-color: var(--hi-color-success, #
|
|
64
|
+
background: color-mix(in srgb, var(--hi-color-success, #0eb840) 8%, transparent);
|
|
65
|
+
border-color: color-mix(in srgb, var(--hi-color-success, #0eb840) 15%, transparent);
|
|
66
|
+
border-inline-start-color: var(--hi-color-success, #0eb840);
|
|
67
67
|
|
|
68
|
-
.hk-alert-icon { color: var(--hi-color-success, #
|
|
68
|
+
.hk-alert-icon { color: var(--hi-color-success, #0eb840); }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/* ── Icon ──────────────────────────────────────── */
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
.hk-alert-text {
|
|
94
94
|
margin: 0;
|
|
95
95
|
line-height: 1.5;
|
|
96
|
-
color: color-mix(in srgb, var(--hi-color-text-secondary, #
|
|
96
|
+
color: color-mix(in srgb, var(--hi-color-text-secondary, #666666) 85%, transparent);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/* ── Close button ──────────────────────────────── */
|
|
@@ -107,19 +107,19 @@
|
|
|
107
107
|
height: 22px;
|
|
108
108
|
border: 0;
|
|
109
109
|
background: transparent;
|
|
110
|
-
border-radius: var(--
|
|
111
|
-
color: var(--hi-color-muted, #
|
|
110
|
+
border-radius: var(--radius-sm, 4px);
|
|
111
|
+
color: var(--hi-color-muted, #6c6c6c);
|
|
112
112
|
cursor: pointer;
|
|
113
113
|
padding: 0;
|
|
114
|
-
transition: background var(--
|
|
114
|
+
transition: background var(--duration-fast, 0.15s) ease, color var(--duration-fast, 0.15s) ease;
|
|
115
115
|
|
|
116
116
|
&:hover {
|
|
117
|
-
background: var(--hi-color-surface, #
|
|
118
|
-
color: var(--hi-color-text-primary, #
|
|
117
|
+
background: var(--hi-color-surface, #f0f4f8);
|
|
118
|
+
color: var(--hi-color-text-primary, #1e1e1e);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
&:focus-visible {
|
|
122
|
-
outline: 2px solid var(--hi-color-primary, #
|
|
122
|
+
outline: 2px solid var(--hi-color-primary, #7aa2f7);
|
|
123
123
|
outline-offset: 2px;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
background: transparent;
|
|
19
19
|
cursor: pointer;
|
|
20
20
|
font-size: var(--text-xs, 0.75rem);
|
|
21
|
-
color: var(--hi-color-muted, #
|
|
21
|
+
color: var(--hi-color-muted, #6c6c6c);
|
|
22
22
|
transition:
|
|
23
23
|
color var(--duration-normal, 0.3s) ease,
|
|
24
24
|
opacity var(--duration-normal, 0.3s) ease;
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
|
|
27
27
|
.hk-alt-signin__trigger:hover:not(:disabled),
|
|
28
28
|
.hk-alt-signin__trigger[aria-expanded="true"] {
|
|
29
|
-
color: var(--hi-color-text-secondary, #
|
|
29
|
+
color: var(--hi-color-text-secondary, #666666);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/* Keyboard position feedback on the full-width disclosure row. */
|
|
33
33
|
.hk-alt-signin__trigger:focus-visible:not(:disabled) {
|
|
34
|
-
outline: 2px solid var(--hi-color-primary, #
|
|
34
|
+
outline: 2px solid var(--hi-color-primary, #7aa2f7);
|
|
35
35
|
outline-offset: 2px;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
align-items: center;
|
|
10
10
|
gap: var(--space-10, 0.625rem);
|
|
11
11
|
margin: 0 0 var(--space-4, 0.25rem);
|
|
12
|
-
color: var(--hi-color-muted, #
|
|
12
|
+
color: var(--hi-color-muted, #6c6c6c);
|
|
13
13
|
font-size: var(--text-xs, 0.75rem);
|
|
14
14
|
text-align: center;
|
|
15
15
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
content: "";
|
|
20
20
|
flex: 1;
|
|
21
21
|
height: 1px;
|
|
22
|
-
background: var(--hi-color-border, #
|
|
22
|
+
background: var(--hi-color-border, #646464);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/* The component's own wrapper is `display: contents` (measurement anchor +
|
|
@@ -89,6 +89,30 @@ describe("HkAuthMethodList", () => {
|
|
|
89
89
|
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("63px");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
it("publishes a width in the column's own units inside a scaled root", async () => {
|
|
93
|
+
// The labels are measured where they are DRAWN while the custom property
|
|
94
|
+
// is a CSS length the column is laid out with: in a scaled or zoomed root
|
|
95
|
+
// the published column would come out that many times too wide.
|
|
96
|
+
const c = mount(h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })));
|
|
97
|
+
await nextTick();
|
|
98
|
+
const wrapper = c.querySelector<HTMLElement>(".s-auth-methods-list")!;
|
|
99
|
+
const spans = [...wrapper.querySelectorAll<HTMLElement>(".s-auth-methods-label")];
|
|
100
|
+
Object.defineProperty(spans[0]!, "getBoundingClientRect", { configurable: true, value: () => ({ width: 58 }) });
|
|
101
|
+
Object.defineProperty(spans[1]!, "getBoundingClientRect", { configurable: true, value: () => ({ width: 61.4 }) });
|
|
102
|
+
// The list is drawn twice the size it is laid out at.
|
|
103
|
+
Object.defineProperty(wrapper, "getBoundingClientRect", {
|
|
104
|
+
configurable: true,
|
|
105
|
+
value: () => ({ left: 0, top: 0, right: 400, bottom: 200, width: 400, height: 200, x: 0, y: 0 }) as DOMRect,
|
|
106
|
+
});
|
|
107
|
+
Object.defineProperty(wrapper, "offsetWidth", { configurable: true, get: () => 200 });
|
|
108
|
+
Object.defineProperty(wrapper, "offsetHeight", { configurable: true, get: () => 100 });
|
|
109
|
+
const instance = (wrapper as unknown as { __vueParentComponent?: { exposed?: Record<string, unknown> } })
|
|
110
|
+
.__vueParentComponent?.exposed;
|
|
111
|
+
(instance!.measure as () => void)();
|
|
112
|
+
// 61.4 drawn are 30.7 of the column's own: ceil 31, plus the 1px headroom.
|
|
113
|
+
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("32px");
|
|
114
|
+
});
|
|
115
|
+
|
|
92
116
|
it("leaves the column fallback intact when no text metrics are available", async () => {
|
|
93
117
|
const c = mount(h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })));
|
|
94
118
|
await nextTick();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
2
2
|
import HkButton from "./HkButton";
|
|
3
|
+
import { drawnScale } from "../composables/layoutGeometry";
|
|
3
4
|
import "./HkAuthMethodList.scss";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -81,10 +82,16 @@ export default defineComponent({
|
|
|
81
82
|
const root = listRef.value;
|
|
82
83
|
if (!root) return;
|
|
83
84
|
let widest = 0;
|
|
85
|
+
// The labels are measured where they are DRAWN, and the width is
|
|
86
|
+
// published as a CSS length the column is laid out with: in a scaled or
|
|
87
|
+
// zoomed root the two differ by the scale of the space, so the column
|
|
88
|
+
// would come out that many times too wide (see `layoutGeometry`).
|
|
89
|
+
const space = drawnScale(root);
|
|
90
|
+
const factor = root.offsetWidth > 0 ? space.x : root.offsetHeight > 0 ? space.y : 1;
|
|
84
91
|
for (const label of root.querySelectorAll<HTMLElement>(".s-auth-methods-label")) {
|
|
85
92
|
const previous = label.style.width;
|
|
86
93
|
label.style.width = "max-content";
|
|
87
|
-
widest = Math.max(widest, label.getBoundingClientRect().width);
|
|
94
|
+
widest = Math.max(widest, label.getBoundingClientRect().width / (factor > 0 ? factor : 1));
|
|
88
95
|
label.style.width = previous;
|
|
89
96
|
}
|
|
90
97
|
// Guard: only publish when real text metrics were observed, so a
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
align-items: flex-start;
|
|
33
33
|
gap: var(--space-8, 8px);
|
|
34
34
|
padding: var(--space-12, 12px) var(--space-16, 16px);
|
|
35
|
-
border-radius: var(--
|
|
35
|
+
border-radius: var(--radius-sm, 4px);
|
|
36
36
|
font-size: var(--text-base, 0.875rem);
|
|
37
37
|
line-height: 1.4;
|
|
38
38
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
@@ -77,12 +77,12 @@
|
|
|
77
77
|
// Variant backgrounds follow the HkToast conventions (including the
|
|
78
78
|
// dark-on-amber warning text for contrast).
|
|
79
79
|
.hk-blocking-toast-info {
|
|
80
|
-
background: var(--hi-color-info, #
|
|
80
|
+
background: var(--hi-color-info, #6abed6);
|
|
81
81
|
color: #fff;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
.hk-blocking-toast-warning {
|
|
85
|
-
background: var(--hi-color-warning, #
|
|
85
|
+
background: var(--hi-color-warning, #eed677);
|
|
86
86
|
color: #1c1c1e;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -95,13 +95,13 @@
|
|
|
95
95
|
// cards go absolute so the remaining ones glide up (hk-toast pattern).
|
|
96
96
|
.hk-blocking-toast-enter-active {
|
|
97
97
|
transition-property: background-color, border-color, color, box-shadow, opacity, transform;
|
|
98
|
-
transition-duration: var(--
|
|
98
|
+
transition-duration: var(--duration-normal, 0.3s);
|
|
99
99
|
transition-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.hk-blocking-toast-leave-active {
|
|
103
103
|
transition-property: background-color, border-color, color, box-shadow, opacity, transform;
|
|
104
|
-
transition-duration: var(--
|
|
104
|
+
transition-duration: var(--duration-normal, 0.3s);
|
|
105
105
|
transition-timing-function: cubic-bezier(0.36, 0, 0.66, 1);
|
|
106
106
|
/* Only the positioning switch lives here; the leaving slot's box is
|
|
107
107
|
frozen by the before-leave hook (pinLeaveGeometry), same contract
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
.hk-blocking-toast-move {
|
|
129
|
-
transition: transform var(--
|
|
129
|
+
transition: transform var(--duration-normal, 0.3s) ease;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Reduced motion: drop the slide/FLIP choreography, cards snap in and
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h, nextTick } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkBoard from "./HkBoard";
|
|
5
|
+
|
|
6
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
7
|
+
const restores: Array<() => void> = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const restore of restores.splice(0)) restore();
|
|
11
|
+
for (const m of mounts.splice(0)) {
|
|
12
|
+
m.app.unmount();
|
|
13
|
+
m.container.remove();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** The board's camera lives in the board's OWN pixels: the world transform is
|
|
18
|
+
* written in them. */
|
|
19
|
+
function worldTransform(container: HTMLElement): { x: number; y: number; k: number } {
|
|
20
|
+
const world = container.querySelector<HTMLElement>(".hk-board-world")!;
|
|
21
|
+
const m = /translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)\s*scale\(([\d.]+)\)/.exec(
|
|
22
|
+
world.style.transform,
|
|
23
|
+
);
|
|
24
|
+
if (!m) throw new Error(`unexpected world transform: ${world.style.transform}`);
|
|
25
|
+
return { x: parseFloat(m[1]), y: parseFloat(m[2]), k: parseFloat(m[3]) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pointer(type: string, id: number, x: number, y: number): PointerEvent {
|
|
29
|
+
return new PointerEvent(type, { bubbles: true, pointerId: id, pointerType: "touch", clientX: x, clientY: y });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function mountBoard() {
|
|
33
|
+
// The board reads its viewport size at mount (clientWidth/Height, i.e. the
|
|
34
|
+
// layout box): pin it on the prototype for the duration so the camera's
|
|
35
|
+
// clamp has a real viewport to work with, the way a laid-out page would.
|
|
36
|
+
const proto = HTMLElement.prototype as unknown as {
|
|
37
|
+
clientWidth: number;
|
|
38
|
+
clientHeight: number;
|
|
39
|
+
};
|
|
40
|
+
const beforeW = Object.getOwnPropertyDescriptor(proto, "clientWidth");
|
|
41
|
+
const beforeH = Object.getOwnPropertyDescriptor(proto, "clientHeight");
|
|
42
|
+
Object.defineProperty(proto, "clientWidth", {
|
|
43
|
+
configurable: true,
|
|
44
|
+
get(this: HTMLElement) {
|
|
45
|
+
return this.classList?.contains("hk-board") ? 800 : 0;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(proto, "clientHeight", {
|
|
49
|
+
configurable: true,
|
|
50
|
+
get(this: HTMLElement) {
|
|
51
|
+
return this.classList?.contains("hk-board") ? 600 : 0;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
restores.push(() => {
|
|
55
|
+
if (beforeW) Object.defineProperty(proto, "clientWidth", beforeW);
|
|
56
|
+
else delete (proto as { clientWidth?: number }).clientWidth;
|
|
57
|
+
if (beforeH) Object.defineProperty(proto, "clientHeight", beforeH);
|
|
58
|
+
else delete (proto as { clientHeight?: number }).clientHeight;
|
|
59
|
+
});
|
|
60
|
+
const container = document.createElement("div");
|
|
61
|
+
document.body.appendChild(container);
|
|
62
|
+
const app = createApp(
|
|
63
|
+
defineComponent({
|
|
64
|
+
setup: () => () =>
|
|
65
|
+
h(HkBoard, {
|
|
66
|
+
// Content far larger than the viewport, so the camera's clamp lets
|
|
67
|
+
// it pan instead of re-centring a board that already fits.
|
|
68
|
+
nodes: [
|
|
69
|
+
{ id: "a", x: 0, y: 0, w: 200, h: 120, label: "A" },
|
|
70
|
+
{ id: "b", x: 3000, y: 3000, w: 200, h: 120, label: "B" },
|
|
71
|
+
],
|
|
72
|
+
fitOnMount: false,
|
|
73
|
+
// No tween: the assertions read the camera right after the event.
|
|
74
|
+
animated: false,
|
|
75
|
+
grid: false,
|
|
76
|
+
minimap: false,
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
app.mount(container);
|
|
81
|
+
mounts.push({ app, container });
|
|
82
|
+
await nextTick();
|
|
83
|
+
const viewport = container.querySelector<HTMLElement>(".hk-board")!;
|
|
84
|
+
// The board is drawn twice the size it is laid out at, and its own box
|
|
85
|
+
// reports the layout size — the shape a scaled or zoomed root produces.
|
|
86
|
+
Object.defineProperty(viewport, "getBoundingClientRect", {
|
|
87
|
+
configurable: true,
|
|
88
|
+
value: () => ({ left: 0, top: 0, right: 1600, bottom: 1200, width: 1600, height: 1200, x: 0, y: 0 }) as DOMRect,
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(viewport, "offsetWidth", { configurable: true, get: () => 800 });
|
|
91
|
+
Object.defineProperty(viewport, "offsetHeight", { configurable: true, get: () => 600 });
|
|
92
|
+
Object.defineProperty(viewport, "clientWidth", { configurable: true, get: () => 800 });
|
|
93
|
+
Object.defineProperty(viewport, "clientHeight", { configurable: true, get: () => 600 });
|
|
94
|
+
// happy-dom has no pointer capture: the board captures every finger, and a
|
|
95
|
+
// throw there would end the gesture before it starts.
|
|
96
|
+
Object.defineProperty(viewport, "setPointerCapture", { configurable: true, value: () => {} });
|
|
97
|
+
Object.defineProperty(viewport, "releasePointerCapture", { configurable: true, value: () => {} });
|
|
98
|
+
Object.defineProperty(viewport, "hasPointerCapture", { configurable: true, value: () => true });
|
|
99
|
+
return { container, viewport };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("HkBoard scaled-space gestures", () => {
|
|
103
|
+
it("pans by the finger's own distance inside a scaled root", async () => {
|
|
104
|
+
const { container, viewport } = await mountBoard();
|
|
105
|
+
const before = worldTransform(container);
|
|
106
|
+
|
|
107
|
+
viewport.dispatchEvent(pointer("pointerdown", 1, 1600, 1600));
|
|
108
|
+
viewport.dispatchEvent(pointer("pointermove", 1, 1520, 1540));
|
|
109
|
+
await nextTick();
|
|
110
|
+
|
|
111
|
+
const after = worldTransform(container);
|
|
112
|
+
expect(after.x, "-80 drawn px are -40 of the board's own").toBeCloseTo(before.x - 40, 4);
|
|
113
|
+
expect(after.y, "-60 drawn px are -30 of the board's own").toBeCloseTo(before.y - 30, 4);
|
|
114
|
+
viewport.dispatchEvent(pointer("pointerup", 1, 1520, 1540));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("keeps a zoom anchored under the pointer inside a scaled root", async () => {
|
|
118
|
+
const { container, viewport } = await mountBoard();
|
|
119
|
+
const wheel = new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -1 });
|
|
120
|
+
Object.defineProperty(wheel, "clientX", { value: 800 });
|
|
121
|
+
Object.defineProperty(wheel, "clientY", { value: 600 });
|
|
122
|
+
// The pointer sits at the board's own (400, 300) — half the layout box.
|
|
123
|
+
const anchor = { x: 400, y: 300 };
|
|
124
|
+
const before = worldTransform(container);
|
|
125
|
+
const world = {
|
|
126
|
+
x: (anchor.x - before.x) / before.k,
|
|
127
|
+
y: (anchor.y - before.y) / before.k,
|
|
128
|
+
};
|
|
129
|
+
viewport.dispatchEvent(wheel);
|
|
130
|
+
await nextTick();
|
|
131
|
+
const after = worldTransform(container);
|
|
132
|
+
expect(after.k).toBeGreaterThan(before.k);
|
|
133
|
+
expect(after.x + world.x * after.k, "the world point stays under the pointer").toBeCloseTo(anchor.x, 3);
|
|
134
|
+
expect(after.y + world.y * after.k).toBeCloseTo(anchor.y, 3);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
type BoardEdgeStyle,
|
|
60
60
|
} from "../utils/boardEdges";
|
|
61
61
|
import "./HkBoard.scss";
|
|
62
|
+
import { drawnScale } from "../composables/layoutGeometry";
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* A node on the board. `hidden` nodes are junction-only (invisible).
|
|
@@ -211,9 +212,23 @@ export default defineComponent({
|
|
|
211
212
|
// — SCADA scenes, mind maps — still get `nodeClick` on a short tap.
|
|
212
213
|
let pressed: { id: string; pointerId: number; x: number; y: number } | null = null;
|
|
213
214
|
|
|
215
|
+
/** How many of the board's own pixels one DRAWN pixel is worth: the
|
|
216
|
+
* camera and every anchor live in the board's local CSS pixels while the
|
|
217
|
+
* pointer arrives in drawn ones, and a scaled or zoomed root separates
|
|
218
|
+
* the two (see `layoutGeometry`). */
|
|
219
|
+
const pxScale = (): number => {
|
|
220
|
+
const el = viewportRef.value;
|
|
221
|
+
if (!el) return 1;
|
|
222
|
+
const scale = drawnScale(el);
|
|
223
|
+
const factor = el.offsetWidth > 0 ? scale.x : el.offsetHeight > 0 ? scale.y : 1;
|
|
224
|
+
return factor > 0 ? factor : 1;
|
|
225
|
+
};
|
|
226
|
+
|
|
214
227
|
const localPoint = (e: PointerEvent | WheelEvent): BoardPoint => {
|
|
215
|
-
const
|
|
216
|
-
|
|
228
|
+
const el = viewportRef.value;
|
|
229
|
+
const rect = el?.getBoundingClientRect();
|
|
230
|
+
const k = pxScale();
|
|
231
|
+
return { x: (e.clientX - (rect?.left ?? 0)) / k, y: (e.clientY - (rect?.top ?? 0)) / k };
|
|
217
232
|
};
|
|
218
233
|
|
|
219
234
|
function onWheel(e: WheelEvent): void {
|
|
@@ -237,14 +252,10 @@ export default defineComponent({
|
|
|
237
252
|
function rearmPinch(): void {
|
|
238
253
|
const [a, b] = [...pointers.values()];
|
|
239
254
|
if (!a || !b) return;
|
|
240
|
-
const rect = viewportRef.value?.getBoundingClientRect();
|
|
241
255
|
pinchBase = {
|
|
242
256
|
cam: { ...camera.value },
|
|
243
257
|
dist: Math.hypot(a.x - b.x, a.y - b.y) || 1,
|
|
244
|
-
mid: {
|
|
245
|
-
x: (a.x + b.x) / 2 - (rect?.left ?? 0),
|
|
246
|
-
y: (a.y + b.y) / 2 - (rect?.top ?? 0),
|
|
247
|
-
},
|
|
258
|
+
mid: localPoint({ clientX: (a.x + b.x) / 2, clientY: (a.y + b.y) / 2 } as PointerEvent),
|
|
248
259
|
};
|
|
249
260
|
}
|
|
250
261
|
|
|
@@ -273,17 +284,18 @@ export default defineComponent({
|
|
|
273
284
|
if (pinchBase && pointers.size >= 2) {
|
|
274
285
|
const [a, b] = [...pointers.values()];
|
|
275
286
|
const dist = Math.hypot(a.x - b.x, a.y - b.y) || 1;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
};
|
|
287
|
+
const mid = localPoint({
|
|
288
|
+
clientX: (a.x + b.x) / 2,
|
|
289
|
+
clientY: (a.y + b.y) / 2,
|
|
290
|
+
} as PointerEvent);
|
|
281
291
|
pinchBase.mid = mid;
|
|
282
292
|
cam.pinchZoom(pinchBase.cam, dist / pinchBase.dist, mid);
|
|
283
293
|
return;
|
|
284
294
|
}
|
|
285
295
|
if (dragState) {
|
|
286
|
-
|
|
296
|
+
// The node lives in WORLD units: the drawn delta comes down through
|
|
297
|
+
// the ancestor scale and then through the camera's own zoom.
|
|
298
|
+
const k = (camera.value.k || 1) * pxScale();
|
|
287
299
|
const dx = (e.clientX - dragState.sx) / k;
|
|
288
300
|
const dy = (e.clientY - dragState.sy) / k;
|
|
289
301
|
if (Math.abs(e.clientX - dragState.sx) + Math.abs(e.clientY - dragState.sy) > 3) dragState.moved = true;
|
|
@@ -293,7 +305,8 @@ export default defineComponent({
|
|
|
293
305
|
return;
|
|
294
306
|
}
|
|
295
307
|
if (panState) {
|
|
296
|
-
|
|
308
|
+
const k = pxScale();
|
|
309
|
+
cam.panBy((e.clientX - panState.px) / k, (e.clientY - panState.py) / k);
|
|
297
310
|
panState = { px: e.clientX, py: e.clientY };
|
|
298
311
|
}
|
|
299
312
|
}
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
justify-content: center;
|
|
24
24
|
width: 1.125rem;
|
|
25
25
|
height: 1.125rem;
|
|
26
|
-
border-radius: var(--
|
|
27
|
-
border: 1px solid var(--hi-color-border, rgba(
|
|
28
|
-
background: var(--hi-color-surface, rgba(
|
|
26
|
+
border-radius: var(--radius-sm, 4px);
|
|
27
|
+
border: 1px solid var(--hi-color-border, rgba(100,100,100,0.08));
|
|
28
|
+
background: var(--hi-color-surface, rgba(240,244,248,0.95));
|
|
29
29
|
transition:
|
|
30
|
-
border-color var(--
|
|
31
|
-
background var(--
|
|
30
|
+
border-color var(--duration-normal, 0.3s) ease,
|
|
31
|
+
background var(--duration-normal, 0.3s) ease;
|
|
32
32
|
flex-shrink: 0;
|
|
33
33
|
position: relative;
|
|
34
34
|
}
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
.hk-checkbox-box[data-checked] {
|
|
41
|
-
background: var(--hi-color-primary, #
|
|
42
|
-
border-color: var(--hi-color-primary, #
|
|
41
|
+
background: var(--hi-color-primary, #7aa2f7);
|
|
42
|
+
border-color: var(--hi-color-primary, #7aa2f7);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
.hk-checkbox-box[data-indeterminate] {
|
|
46
|
-
background: var(--hi-color-primary, #
|
|
47
|
-
border-color: var(--hi-color-primary, #
|
|
46
|
+
background: var(--hi-color-primary, #7aa2f7);
|
|
47
|
+
border-color: var(--hi-color-primary, #7aa2f7);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/* ---------- input ---------- */
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
:focus-visible keeps mouse toggles ring-free. */
|
|
63
63
|
.hk-checkbox-box:has(.hk-checkbox-input:focus-visible) {
|
|
64
64
|
box-shadow:
|
|
65
|
-
0 0 0 2px var(--hi-color-surface, rgba(
|
|
66
|
-
0 0 0 4px var(--hi-color-primary, #
|
|
65
|
+
0 0 0 2px var(--hi-color-surface, rgba(240,244,248,0.95)),
|
|
66
|
+
0 0 0 4px var(--hi-color-primary, #7aa2f7);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/* ---------- markers ---------- */
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
padding: var(--space-8, 8px) var(--space-12, 12px);
|
|
24
24
|
background: color-mix(in srgb, rgb(var(--color-surface)) 55%, transparent);
|
|
25
25
|
border: 1px solid color-mix(in srgb, rgb(var(--color-text)) 14%, transparent);
|
|
26
|
-
border-radius: var(--
|
|
26
|
+
border-radius: var(--radius-sm, 4px);
|
|
27
27
|
color: rgb(var(--color-text));
|
|
28
28
|
font-family: inherit;
|
|
29
29
|
font-size: var(--text-sm, 0.875rem);
|
|
30
30
|
line-height: 1.5;
|
|
31
31
|
transition:
|
|
32
|
-
background-color var(--
|
|
33
|
-
border-color var(--
|
|
34
|
-
box-shadow var(--
|
|
32
|
+
background-color var(--duration-fast, 0.15s) ease,
|
|
33
|
+
border-color var(--duration-fast, 0.15s) ease,
|
|
34
|
+
box-shadow var(--duration-fast, 0.15s) ease;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.hk-dp-trigger {
|
|
@@ -102,11 +102,11 @@
|
|
|
102
102
|
padding: 0;
|
|
103
103
|
border: 0;
|
|
104
104
|
background: transparent;
|
|
105
|
-
border-radius: var(--
|
|
105
|
+
border-radius: var(--radius-sm, 4px);
|
|
106
106
|
color: var(--hi-color-muted);
|
|
107
107
|
cursor: pointer;
|
|
108
108
|
opacity: 0;
|
|
109
|
-
transition: opacity var(--
|
|
109
|
+
transition: opacity var(--duration-fast, 0.15s) ease, color var(--duration-fast, 0.15s) ease;
|
|
110
110
|
|
|
111
111
|
.hk-dp-trigger:hover &,
|
|
112
112
|
.hk-dp-trigger:focus &,
|
|
@@ -183,13 +183,13 @@
|
|
|
183
183
|
border: 0;
|
|
184
184
|
width: 1.75rem;
|
|
185
185
|
height: 1.75rem;
|
|
186
|
-
border-radius: var(--
|
|
186
|
+
border-radius: var(--radius-sm, 4px);
|
|
187
187
|
color: var(--hi-color-muted);
|
|
188
188
|
cursor: pointer;
|
|
189
189
|
display: inline-flex;
|
|
190
190
|
align-items: center;
|
|
191
191
|
justify-content: center;
|
|
192
|
-
transition: background var(--
|
|
192
|
+
transition: background var(--duration-fast, 0.15s) ease, color var(--duration-fast, 0.15s) ease;
|
|
193
193
|
|
|
194
194
|
&:hover:not(:disabled) {
|
|
195
195
|
background: var(--hi-color-surface);
|
|
@@ -232,8 +232,8 @@
|
|
|
232
232
|
color: inherit;
|
|
233
233
|
cursor: pointer;
|
|
234
234
|
white-space: nowrap;
|
|
235
|
-
border-radius: var(--
|
|
236
|
-
transition: background var(--
|
|
235
|
+
border-radius: var(--radius-sm, 4px);
|
|
236
|
+
transition: background var(--duration-fast, 0.15s) ease, color var(--duration-fast, 0.15s) ease;
|
|
237
237
|
|
|
238
238
|
&:hover {
|
|
239
239
|
background: var(--hi-color-surface);
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
appearance: none;
|
|
280
280
|
background: transparent;
|
|
281
281
|
border: 0;
|
|
282
|
-
border-radius: var(--
|
|
282
|
+
border-radius: var(--radius-sm, 4px);
|
|
283
283
|
/* Fixed height keeps the six-row day grid — and therefore the shared
|
|
284
284
|
* stage height — independent of the panel width, so drilling between
|
|
285
285
|
* views never changes the popup's size (enlarged on touch). */
|
|
@@ -291,7 +291,7 @@
|
|
|
291
291
|
cursor: pointer;
|
|
292
292
|
font-size: var(--text-xs, 0.75rem);
|
|
293
293
|
font-variant-numeric: tabular-nums;
|
|
294
|
-
transition: background var(--
|
|
294
|
+
transition: background var(--duration-fast, 0.15s) ease, color var(--duration-fast, 0.15s) ease;
|
|
295
295
|
|
|
296
296
|
&:hover:not(:disabled):not(.is-selected) {
|
|
297
297
|
background: var(--hi-color-surface);
|