@celestia-island/hikari 0.55.249 → 0.55.251
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/HkAdminHeader.test.tsx +17 -4
- package/src/components/HkAffixPicker.scss +15 -43
- package/src/components/HkAltSignIn.tsx +14 -10
- package/src/components/HkAuthCard.test.tsx +6 -4
- package/src/components/HkAuthMethodList.tsx +17 -13
- package/src/components/HkBarChart.scss +54 -0
- package/src/components/HkBarChart.test.tsx +62 -0
- package/src/components/HkBarChart.tsx +114 -0
- package/src/components/HkBoard.scale.test.ts +1 -4
- package/src/components/HkBoard3D.test.tsx +12 -15
- package/src/components/HkBoard3D.tsx +3 -2
- package/src/components/HkCalendarHeatmap.barrel.test.ts +2 -1
- package/src/components/HkCaptchaWidget.tsx +2 -1
- package/src/components/HkCheckbox.test.tsx +6 -2
- package/src/components/HkColorPicker.tsx +3 -2
- package/src/components/HkConnectionStatus.shell.test.tsx +1 -1
- package/src/components/HkContextMenuProvider.tsx +19 -14
- package/src/components/HkCookieConsent.test.tsx +5 -5
- package/src/components/HkCrossfade.test.tsx +1 -1
- package/src/components/HkDatePicker.test.ts +3 -2
- package/src/components/HkDateTag.test.tsx +1 -1
- package/src/components/HkDateTimePicker.test.ts +3 -2
- package/src/components/HkDateTimePicker.tsx +4 -3
- package/src/components/HkDonutChart.scss +95 -0
- package/src/components/HkDonutChart.test.tsx +87 -0
- package/src/components/HkDonutChart.tsx +144 -0
- package/src/components/HkFab.test.tsx +4 -2
- package/src/components/HkFab.tsx +8 -5
- package/src/components/HkFaceShell.barrel.test.ts +3 -2
- package/src/components/HkFaceShell.rail.test.tsx +1 -1
- package/src/components/HkFileBrowserDialog.test.tsx +1 -4
- package/src/components/HkFilePickerField.tsx +3 -2
- package/src/components/HkIcon.tsx +8 -5
- package/src/components/HkIconButtonGroup.tsx +2 -1
- package/src/components/HkInfiniteTail.test.tsx +28 -23
- package/src/components/HkInput.affix.test.ts +3 -3
- package/src/components/HkInput.password.test.tsx +116 -119
- package/src/components/HkInput.tsx +5 -2
- package/src/components/HkKeywordSearchModal.tsx +4 -3
- package/src/components/HkLocalizedInput.test.tsx +2 -5
- package/src/components/HkMarkdownRenderer.tsx +36 -19
- package/src/components/HkMediaVisualizer.tsx +9 -2
- package/src/components/HkMenu.tsx +4 -16
- package/src/components/HkMenuIdentityItem.scss +4 -0
- package/src/components/HkMfaVerifyCard.test.ts +3 -4
- package/src/components/HkMinimap3D.test.tsx +6 -5
- package/src/components/HkMinimap3D.tsx +1 -1
- package/src/components/HkModal.contenthold.test.tsx +1 -1
- package/src/components/HkModal.enter-watchdog.test.tsx +1 -1
- package/src/components/HkModal.leavepin.test.tsx +1 -1
- package/src/components/HkModal.stepflowswap.test.tsx +1 -1
- package/src/components/HkModal.transition-completion.test.tsx +1 -1
- package/src/components/HkModal.width.test.tsx +2 -1
- package/src/components/HkPasswordSurface.tsx +1 -1
- package/src/components/HkPhoneInput.tsx +1 -1
- package/src/components/HkPlaceholderMarquee.test.ts +3 -11
- package/src/components/HkScrollContainer.dock.test.tsx +5 -2
- package/src/components/HkScrollContainer.selectionHint.test.ts +5 -1
- package/src/components/HkScrollContainer.test.ts +2 -1
- package/src/components/HkScrollContainer.tsx +11 -4
- package/src/components/HkSelectPanel.test.tsx +2 -2
- package/src/components/HkSelectPanel.tsx +19 -2
- package/src/components/HkSidebar.rail.test.tsx +1 -1
- package/src/components/HkSidebarRailSwap.test.tsx +1 -1
- package/src/components/HkSolarLocationSetting.test.tsx +31 -21
- package/src/components/HkSolarLocationSetting.tsx +13 -14
- package/src/components/HkTabs.test.ts +2 -2
- package/src/components/HkTabs.tsx +9 -4
- package/src/components/HkTagInput.scss +39 -49
- package/src/components/HkTagInput.tsx +6 -0
- package/src/components/HkTemplateField.scss +24 -10
- package/src/components/HkTemplateField.test.tsx +2 -2
- package/src/components/HkThirdPartySignInCard.tsx +5 -10
- package/src/components/HkTimeZoneGlobe.scss +125 -0
- package/src/components/HkTimeZoneGlobe.test.tsx +128 -0
- package/src/components/HkTimeZoneGlobe.tsx +385 -0
- package/src/components/HkTimeline.test.tsx +1 -1
- package/src/components/HkTreemap.test.tsx +4 -3
- package/src/components/HkTreemapStage.test.tsx +4 -3
- package/src/components/HkUserPicker.scss +11 -18
- package/src/components/HkUserPicker.tsx +5 -0
- package/src/components/HkWallpaperBackdrop.test.tsx +10 -9
- package/src/components/HkWaterfall.styles.test.ts +97 -37
- package/src/components/HkWaterfall.test.tsx +2 -2
- package/src/components/HkWindowedItem.scale.test.ts +8 -4
- package/src/components/StatusTrayDecor.test.tsx +2 -1
- package/src/components/__tests__/hkMarkdownTableWrap.test.ts +6 -1
- package/src/components/_menu-item.contract.test.ts +66 -0
- package/src/components/_menu-item.scss +19 -2
- package/src/components/barChart.test.ts +57 -0
- package/src/components/barChart.ts +104 -0
- package/src/components/board3dHoverBox.test.ts +3 -1
- package/src/components/board3dHoverBox.ts +11 -5
- package/src/components/donutGeometry.test.ts +74 -0
- package/src/components/donutGeometry.ts +148 -0
- package/src/composables/messaging/index.ts +4 -2
- package/src/composables/messaging/messaging.test.ts +7 -7
- package/src/composables/useApproachEnd.test.ts +3 -3
- package/src/composables/useOptionStrip.test.ts +13 -1
- package/src/composables/useOverlayScrollbar.ts +4 -2
- package/src/composables/usePointerReorder.test.ts +3 -1
- package/src/composables/useResourceListModal.tsx +4 -4
- package/src/composables/useSidebarRail.test.ts +5 -5
- package/src/composables/useSizeMorph.test.ts +3 -3
- package/src/composables/useSurfaceMachine.test.ts +4 -4
- package/src/demo.ts +15 -5
- package/src/env.d.ts +8 -1
- package/src/i18n/locales/ar/platform.json +90 -1
- package/src/i18n/locales/de/platform.json +90 -1
- package/src/i18n/locales/en/platform.json +90 -1
- package/src/i18n/locales/es/platform.json +90 -1
- package/src/i18n/locales/fr/platform.json +90 -1
- package/src/i18n/locales/ja/platform.json +90 -1
- package/src/i18n/locales/ko/platform.json +90 -1
- package/src/i18n/locales/pt/platform.json +90 -1
- package/src/i18n/locales/ru/platform.json +90 -1
- package/src/i18n/locales/zh-Hans/platform.json +90 -1
- package/src/i18n/locales/zh-Hant/platform.json +90 -1
- package/src/index.ts +25 -3
- package/src/overlay/overlayRoot.styles.test.ts +3 -0
- package/src/overlay/overlayRoot.ts +6 -2
- package/src/plugins/boot-chrome.test.ts +78 -32
- package/src/plugins/boot-recovery.test.ts +74 -33
- package/src/plugins/bootScriptsPlugin.ts +8 -3
- package/src/plugins/buildHashPlugin.ts +14 -5
- package/src/render.ts +14 -5
- package/src/res/bootChrome.test.ts +2 -1
- package/src/runtime/closeGuard.ts +5 -1
- package/src/runtime/credentialAutofill.test.ts +4 -2
- package/src/runtime/historySafetyNet.ts +37 -13
- package/src/runtime/mobileViewport.test.ts +3 -0
- package/src/runtime/mobileViewport.ts +5 -3
- package/src/runtime/navigationGuard.test.ts +11 -1
- package/src/runtime/tooltipBridge.ts +6 -1
- package/src/runtime/useClipboard.test.ts +1 -1
- package/src/runtime/viewportGutter.test.ts +11 -8
- package/src/runtime/windowChrome.ts +15 -12
- package/src/scene3d/lighting.test.ts +25 -12
- package/src/scene3d/modelLayer.test.ts +16 -8
- package/src/styles/admin-tokens.scss +9 -0
- package/src/test-utils/fakes.ts +49 -0
- package/src/test-utils/invalid.ts +20 -0
- package/src/theme/solarGlobeAlignment.test.ts +88 -0
- package/src/theme/solarGlobeMap.test.ts +66 -0
- package/src/theme/solarGlobeMap.ts +193 -0
- package/src/theme/solarTimeZoneLocales.test.ts +92 -0
- package/src/theme/solarTimeZones.test.ts +142 -0
- package/src/theme/solarTimeZones.ts +190 -0
- package/src/theme/themeDecor.test.ts +2 -1
- package/src/theme/useBackgroundLuminance.test.ts +42 -20
- package/src/theme/useSolarTime.test.ts +8 -9
- package/src/theme/useSolarTime.ts +32 -18
- package/src/theme/wallpaper.ts +31 -8
- package/src/theme/wallpaperCustom.test.ts +42 -0
- package/src/theme/wallpaperShaderRenderer.test.ts +66 -34
- package/src/theme/wallpaperSsr.test.ts +4 -2
- package/src/utils/dom.test.ts +31 -10
- package/src/utils/typing.ts +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createApp, h } from "vue";
|
|
3
3
|
|
|
4
|
+
/** The slice of HkMenu's item shape this stub renders. */
|
|
5
|
+
interface MenuItemStub {
|
|
6
|
+
key: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
checked?: boolean;
|
|
9
|
+
danger?: boolean;
|
|
10
|
+
children?: MenuItemStub[];
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
// HkMenu renders its levels through HkSelectPanel, which teleports to
|
|
5
14
|
// body — replace just HkMenu (imported by the component from
|
|
6
15
|
// ./HkMenu) with an inline passthrough that renders the header slot
|
|
@@ -10,18 +19,19 @@ import { createApp, h } from "vue";
|
|
|
10
19
|
// re-declared as an emit, which would strip it from props).
|
|
11
20
|
vi.mock("./HkMenu", async () => {
|
|
12
21
|
const { defineComponent, h } = await import("vue");
|
|
22
|
+
const { asPropType } = await import("../utils/typing");
|
|
13
23
|
const HMenuStub = defineComponent({
|
|
14
24
|
name: "HMenu",
|
|
15
25
|
props: {
|
|
16
26
|
open: { type: Boolean, default: false },
|
|
17
|
-
items: { type: Array, default: () => [] },
|
|
27
|
+
items: { type: asPropType<MenuItemStub[]>(Array), default: () => [] },
|
|
18
28
|
title: { type: String, default: "" },
|
|
19
29
|
},
|
|
20
30
|
setup(props, { slots, attrs }) {
|
|
21
31
|
return () => {
|
|
22
32
|
if (!props.open) return null;
|
|
23
33
|
const rows: ReturnType<typeof h>[] = [];
|
|
24
|
-
const flatten = (items:
|
|
34
|
+
const flatten = (items: MenuItemStub[]) => {
|
|
25
35
|
for (const item of items ?? []) {
|
|
26
36
|
rows.push(
|
|
27
37
|
h(
|
|
@@ -31,7 +41,10 @@ vi.mock("./HkMenu", async () => {
|
|
|
31
41
|
"data-key": item.key,
|
|
32
42
|
"data-checked": item.checked || undefined,
|
|
33
43
|
"data-danger": item.danger || undefined,
|
|
34
|
-
onClick: () =>
|
|
44
|
+
onClick: () => {
|
|
45
|
+
const onSelect = attrs.onSelect;
|
|
46
|
+
if (typeof onSelect === "function") onSelect(item.key);
|
|
47
|
+
},
|
|
35
48
|
},
|
|
36
49
|
item.label,
|
|
37
50
|
),
|
|
@@ -39,7 +52,7 @@ vi.mock("./HkMenu", async () => {
|
|
|
39
52
|
if (item.children?.length) flatten(item.children);
|
|
40
53
|
}
|
|
41
54
|
};
|
|
42
|
-
flatten(props.items
|
|
55
|
+
flatten(props.items);
|
|
43
56
|
return h("div", { class: "menu-stub" }, [...(slots.header?.() ?? []), ...rows]);
|
|
44
57
|
};
|
|
45
58
|
},
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* their own class on top via chipClass), while the popup — search,
|
|
5
5
|
* selected tags, option rows, custom row — is fully standardized here. */
|
|
6
6
|
|
|
7
|
+
@use "./menu-item" as mi;
|
|
8
|
+
|
|
7
9
|
/* ── chip base ────────────────────────────────────────────────────────
|
|
8
10
|
* Neutral clickable chip; hosts override visually via their own class
|
|
9
11
|
* (HkPhoneInput's dial chip, HkLocalizedInput's badge chip, …). */
|
|
@@ -232,55 +234,25 @@
|
|
|
232
234
|
display: flex;
|
|
233
235
|
flex-direction: column;
|
|
234
236
|
gap: 1px;
|
|
235
|
-
|
|
237
|
+
/* GEOMETRY-NEUTRAL band (menu-item WIDTH invariant): block-axis rhythm
|
|
238
|
+
* only, NO inline term — the hosting panel's inset (popout 8px / sheet
|
|
239
|
+
* 16px) is the one horizontal inset these rows get. The old 6px inline
|
|
240
|
+
* term stacked a second inset and pulled the rows' wash 6px inside
|
|
241
|
+
* every sibling family's in the same sheet. */
|
|
242
|
+
padding-block: var(--space-4, 4px) var(--space-8, 8px);
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
.hk-affix-row {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
border-radius: var(--radius-sm, 6px);
|
|
247
|
-
background: transparent;
|
|
248
|
-
color: rgb(var(--color-text));
|
|
249
|
-
font-size: var(--text-sm, 0.8125rem);
|
|
250
|
-
text-align: start;
|
|
251
|
-
cursor: pointer;
|
|
252
|
-
appearance: none;
|
|
253
|
-
transition: background 0.1s ease;
|
|
254
|
-
|
|
255
|
-
&:hover {
|
|
256
|
-
background: rgb(var(--color-primary) / 10%);
|
|
257
|
-
outline: none;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/* Keyboard position is drawn INSIDE the row (HkSelectionGrid branch-fix
|
|
261
|
-
* pattern) and distinct from the hover tint, so overlapping popup/sheet
|
|
262
|
-
* chrome cannot shave a per-side outer ring and focus reads as focus. */
|
|
263
|
-
&:focus-visible {
|
|
264
|
-
background: rgb(var(--color-primary) / 10%);
|
|
265
|
-
outline: 2px solid rgb(var(--color-primary));
|
|
266
|
-
outline-offset: -2px;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
&[data-active] {
|
|
270
|
-
background: rgb(var(--color-primary) / 16%);
|
|
271
|
-
font-weight: 600;
|
|
272
|
-
}
|
|
246
|
+
/* Unified menu-item spec (_menu-item.scss): one row grammar with the
|
|
247
|
+
* select options and menu rows around it — metrics (34px / 6×12,
|
|
248
|
+
* 44px / 10×16 inside sheets via the shared registration list),
|
|
249
|
+
* reserved 1px border, guarded hover, primary wash + strong border on
|
|
250
|
+
* [data-active], disabled dim, 2px inset focus ring. The sheet reach
|
|
251
|
+
* comes from the registration list, not from here. */
|
|
252
|
+
@include mi.item;
|
|
273
253
|
|
|
274
254
|
&[data-disabled] {
|
|
275
|
-
opacity: 0.5;
|
|
276
255
|
cursor: default;
|
|
277
|
-
|
|
278
|
-
/* The button stays keyboard-focusable (data-disabled only), so the
|
|
279
|
-
* unpickable row must not paint the pickable tint on focus either. */
|
|
280
|
-
&:hover,
|
|
281
|
-
&:focus-visible {
|
|
282
|
-
background: transparent;
|
|
283
|
-
}
|
|
284
256
|
}
|
|
285
257
|
|
|
286
258
|
/* The "Use <query>" free-text row reads as an add affordance. Truncation
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { defineComponent, ref } from "vue";
|
|
2
|
-
import HkPopover from "./HkPopover";
|
|
1
|
+
import { defineComponent, ref, type PropType } from "vue";
|
|
2
|
+
import HkPopover, { type PopupPlacement } from "./HkPopover";
|
|
3
3
|
import HkMenuActionItem from "./HkMenuActionItem";
|
|
4
|
+
import { asPropType } from "../utils/typing";
|
|
4
5
|
import "./HkAltSignIn.scss";
|
|
5
6
|
|
|
7
|
+
/** One alternative sign-in entry offered in the popover. */
|
|
8
|
+
export interface HkAltSignInEntry {
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: Record<string, unknown>;
|
|
12
|
+
danger?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* Shared "alternative sign-in" entry for auth cards (P77).
|
|
8
17
|
*
|
|
@@ -23,18 +32,13 @@ export default defineComponent({
|
|
|
23
32
|
label: { type: String, required: true },
|
|
24
33
|
/** Alternative sign-in entries offered in the dropdown. */
|
|
25
34
|
entries: {
|
|
26
|
-
type: Array
|
|
27
|
-
key: string;
|
|
28
|
-
label: string;
|
|
29
|
-
icon?: Record<string, unknown>;
|
|
30
|
-
danger?: boolean;
|
|
31
|
-
}>,
|
|
35
|
+
type: asPropType<HkAltSignInEntry[]>(Array),
|
|
32
36
|
required: true,
|
|
33
37
|
},
|
|
34
38
|
disabled: { type: Boolean, default: false },
|
|
35
39
|
/** Popover placement around the trigger row. */
|
|
36
40
|
placement: {
|
|
37
|
-
type: String
|
|
41
|
+
type: String as PropType<PopupPlacement>,
|
|
38
42
|
default: "top",
|
|
39
43
|
} as const,
|
|
40
44
|
},
|
|
@@ -71,7 +75,7 @@ export default defineComponent({
|
|
|
71
75
|
<HkPopover
|
|
72
76
|
modelValue={open.value}
|
|
73
77
|
onUpdate:modelValue={(v: boolean) => (open.value = v)}
|
|
74
|
-
placement={props.placement
|
|
78
|
+
placement={props.placement}
|
|
75
79
|
anchorRef={anchor.value}
|
|
76
80
|
>
|
|
77
81
|
<div class="hk-alt-signin__panel" role="menu">
|
|
@@ -6,23 +6,25 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
import { createApp, h } from "vue";
|
|
7
7
|
|
|
8
8
|
import { HkAuthCard } from "./HkAuthCard";
|
|
9
|
+
import { fakeAs } from "../test-utils/fakes";
|
|
9
10
|
|
|
10
11
|
/** Injectable ResizeObserver: captures the callback so tests can fire
|
|
11
12
|
* content changes deterministically (happy-dom's own RO never fires —
|
|
12
13
|
* there is no layout engine). Same harness as useSizeMorph.test.ts. */
|
|
13
|
-
class FakeResizeObserver {
|
|
14
|
+
class FakeResizeObserver implements ResizeObserver {
|
|
14
15
|
static instances: FakeResizeObserver[] = [];
|
|
15
16
|
callback: () => void;
|
|
16
17
|
observed: Element[] = [];
|
|
17
18
|
disconnected = false;
|
|
18
19
|
|
|
19
|
-
constructor(callback: () => void) {
|
|
20
|
+
constructor(callback: (...args: never[]) => void) {
|
|
20
21
|
this.callback = callback;
|
|
21
22
|
FakeResizeObserver.instances.push(this);
|
|
22
23
|
}
|
|
23
24
|
observe(el: Element): void {
|
|
24
25
|
this.observed.push(el);
|
|
25
26
|
}
|
|
27
|
+
unobserve(): void {}
|
|
26
28
|
disconnect(): void {
|
|
27
29
|
this.disconnected = true;
|
|
28
30
|
}
|
|
@@ -77,7 +79,7 @@ function fireTransitionEnd(el: HTMLElement, propertyName: string) {
|
|
|
77
79
|
beforeEach(() => {
|
|
78
80
|
FakeResizeObserver.instances = [];
|
|
79
81
|
naturalHeight = 140;
|
|
80
|
-
globalThis.ResizeObserver = FakeResizeObserver
|
|
82
|
+
globalThis.ResizeObserver = FakeResizeObserver;
|
|
81
83
|
Object.defineProperty(offsetHeightOwner(), "offsetHeight", {
|
|
82
84
|
configurable: true,
|
|
83
85
|
get: () => naturalHeight,
|
|
@@ -194,7 +196,7 @@ describe("HkAuthCard", () => {
|
|
|
194
196
|
|
|
195
197
|
it("snaps unclipped under prefers-reduced-motion without arming the backstop", () => {
|
|
196
198
|
vi.useFakeTimers();
|
|
197
|
-
vi.spyOn(window, "matchMedia").mockReturnValue({ matches: true }
|
|
199
|
+
vi.spyOn(window, "matchMedia").mockReturnValue(fakeAs<MediaQueryList>({ matches: true }));
|
|
198
200
|
naturalHeight = 140;
|
|
199
201
|
const c = mount(h(HkAuthCard, { title: "T" }));
|
|
200
202
|
const wrapper = wrapperOf(c);
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { defineComponent } from "vue";
|
|
2
2
|
import HkTooltip from "./HkTooltip";
|
|
3
|
+
import { asPropType } from "../utils/typing";
|
|
3
4
|
import "./HkAuthMethodList.scss";
|
|
4
5
|
|
|
6
|
+
/** One third-party sign-in provider tile. */
|
|
7
|
+
export interface HkAuthMethodListEntry {
|
|
8
|
+
key: string;
|
|
9
|
+
/** Accessible name + tooltip text (the hover reveal). */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Prebuilt icon vnode (brand SVG, <img>, …) rendered inside the
|
|
12
|
+
* framed tile. Typed loose on purpose (same as HkIconButtonGroup
|
|
13
|
+
* options): hosts materialize hikari against their own vue store,
|
|
14
|
+
* and a hard VNode type breaks typecheck whenever the host's vue
|
|
15
|
+
* minor differs. A missing icon falls back to the label's
|
|
16
|
+
* initial. */
|
|
17
|
+
icon?: unknown;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* HkAuthMethodList — the auth card's third-party sign-in block (the
|
|
7
23
|
* "other ways to sign in" row under the credentials form).
|
|
@@ -34,19 +50,7 @@ export default defineComponent({
|
|
|
34
50
|
divider: { type: String, default: "" },
|
|
35
51
|
/** Provider entries, in render order. */
|
|
36
52
|
methods: {
|
|
37
|
-
type: Array
|
|
38
|
-
key: string;
|
|
39
|
-
/** Accessible name + tooltip text (the hover reveal). */
|
|
40
|
-
label: string;
|
|
41
|
-
/** Prebuilt icon vnode (brand SVG, <img>, …) rendered inside the
|
|
42
|
-
* framed tile. Typed loose on purpose (same as HkIconButtonGroup
|
|
43
|
-
* options): hosts materialize hikari against their own vue store,
|
|
44
|
-
* and a hard VNode type breaks typecheck whenever the host's vue
|
|
45
|
-
* minor differs. A missing icon falls back to the label's
|
|
46
|
-
* initial. */
|
|
47
|
-
icon?: unknown;
|
|
48
|
-
disabled?: boolean;
|
|
49
|
-
}>,
|
|
53
|
+
type: asPropType<HkAuthMethodListEntry[]>(Array),
|
|
50
54
|
required: true,
|
|
51
55
|
},
|
|
52
56
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// HkBarChart anatomy — self-sufficient styling (the component
|
|
2
|
+
// self-sufficiency guard): every class lives here, no host atomic or
|
|
3
|
+
// utility layer. Ink comes from theme tokens so scheme flips repaint
|
|
4
|
+
// with no re-render.
|
|
5
|
+
//
|
|
6
|
+
// Upstreamed from wowsp's PlaytimeTrendChart.scss.
|
|
7
|
+
//
|
|
8
|
+
// AI-generated (GLM-5.3 via ZCode), 2026-10-31 — SySL-1.0 §2.3.
|
|
9
|
+
|
|
10
|
+
.hk-bar-chart {
|
|
11
|
+
display: block;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: auto;
|
|
14
|
+
|
|
15
|
+
&--empty {
|
|
16
|
+
height: 120px;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
font-size: var(--text-sm, 13px);
|
|
21
|
+
color: var(--hi-color-muted);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__grid {
|
|
25
|
+
stroke: rgb(var(--color-text) / 0.12);
|
|
26
|
+
stroke-width: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__axis {
|
|
30
|
+
fill: rgb(var(--color-text) / 0.55);
|
|
31
|
+
font-size: 10px;
|
|
32
|
+
text-anchor: end;
|
|
33
|
+
font-variant-numeric: tabular-nums;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&__x {
|
|
37
|
+
fill: rgb(var(--color-text) / 0.55);
|
|
38
|
+
font-size: 10px;
|
|
39
|
+
text-anchor: middle;
|
|
40
|
+
font-variant-numeric: tabular-nums;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&__bar {
|
|
44
|
+
fill: rgb(var(--color-primary));
|
|
45
|
+
outline: none;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
transition: filter 0.16s ease;
|
|
48
|
+
|
|
49
|
+
&:hover,
|
|
50
|
+
&:focus-visible {
|
|
51
|
+
filter: brightness(1.08) saturate(1.15);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkBarChart from "./HkBarChart";
|
|
5
|
+
|
|
6
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
7
|
+
|
|
8
|
+
function mount(node: ReturnType<typeof h>) {
|
|
9
|
+
const container = document.createElement("div");
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
const app = createApp({ render: () => node });
|
|
12
|
+
app.mount(container);
|
|
13
|
+
mounts.push({ app, container });
|
|
14
|
+
return container;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
19
|
+
app.unmount();
|
|
20
|
+
container.remove();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const BARS = [
|
|
25
|
+
{ label: "Mon", value: 3600, hint: "Mon: 1h" },
|
|
26
|
+
{ label: "Tue", value: 7200, hint: "Tue: 2h" },
|
|
27
|
+
{ label: "Wed", value: 0 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
describe("HkBarChart", () => {
|
|
31
|
+
it("renders one labeled bar per bucket and skips zero buckets' paths", () => {
|
|
32
|
+
const el = mount(<HkBarChart bars={BARS} />);
|
|
33
|
+
const bars = el.querySelectorAll(".hk-bar-chart__bar");
|
|
34
|
+
expect(bars).toHaveLength(2); // the zero bucket has no path
|
|
35
|
+
expect([...bars].map((b) => b.getAttribute("data-hint"))).toEqual(["Mon: 1h", "Tue: 2h"]);
|
|
36
|
+
const xs = [...el.querySelectorAll(".hk-bar-chart__x")].map((t) => t.textContent);
|
|
37
|
+
expect(xs).toEqual(["Mon", "Tue", "Wed"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("formats axis guides through the value formatter", () => {
|
|
41
|
+
const el = mount(
|
|
42
|
+
<HkBarChart bars={BARS} formatValue={(v) => `${Math.round(v / 3600)}h`} />,
|
|
43
|
+
);
|
|
44
|
+
const axis = [...el.querySelectorAll(".hk-bar-chart__axis")].map((t) => t.textContent);
|
|
45
|
+
// niceMax(7200s) = 10000s → guides 0 / 5000 / 10000.
|
|
46
|
+
expect(axis).toEqual(["0h", "1h", "3h"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows the empty state when every bucket is zero", () => {
|
|
50
|
+
const el = mount(
|
|
51
|
+
<HkBarChart bars={[{ label: "a", value: 0 }]} emptyText="nothing here" />,
|
|
52
|
+
);
|
|
53
|
+
expect(el.querySelector(".hk-bar-chart--empty")?.textContent).toBe("nothing here");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("carries the chart-level aria label", () => {
|
|
57
|
+
const el = mount(<HkBarChart bars={BARS} ariaLabel="weekly playtime" />);
|
|
58
|
+
expect(el.querySelector(".hk-bar-chart")?.getAttribute("aria-label")).toBe(
|
|
59
|
+
"weekly playtime",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { computed, defineComponent, type PropType } from "vue";
|
|
2
|
+
|
|
3
|
+
import { barLayout, type BarSlot } from "./barChart";
|
|
4
|
+
import "./HkBarChart.scss";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HkBarChart — one bar per bucket over a hand-drawn declarative SVG (no
|
|
8
|
+
* chart library; the HkTreemap/HkCalendarHeatmap dependency-free line).
|
|
9
|
+
*
|
|
10
|
+
* Upstreamed from wowsp's playtime trend chart (2026-10 user direction:
|
|
11
|
+
* the apps' best charts become standard hikari business components).
|
|
12
|
+
* Gridlines at 0 / half / a nice max, x labels under every bar, ink
|
|
13
|
+
* styled in SCSS from theme tokens so scheme flips repaint without a
|
|
14
|
+
* re-render, and hover hints through the consumer's own tooltip channel
|
|
15
|
+
* (`hint` lands in both `aria-label` and the `data-hint` attribute the
|
|
16
|
+
* wowsp delegated-tooltip hook reads — hikari itself renders no native
|
|
17
|
+
* title bubble).
|
|
18
|
+
*
|
|
19
|
+
* Dumb by design: the caller owns aggregation and passes the settled
|
|
20
|
+
* buckets in. All geometry lives in ./barChart (pure, pinned by its own
|
|
21
|
+
* tests).
|
|
22
|
+
*
|
|
23
|
+
* AI-generated (GLM-5.3 via ZCode), 2026-10-31 — SySL-1.0 §2.3.
|
|
24
|
+
*/
|
|
25
|
+
export default defineComponent({
|
|
26
|
+
name: "HkBarChart",
|
|
27
|
+
props: {
|
|
28
|
+
/** The bars, in axis order (oldest → newest for trend charts). */
|
|
29
|
+
bars: {
|
|
30
|
+
type: Array as PropType<{ label: string; value: number; hint?: string }[]>,
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
/** Frame size (the SVG viewBox; CSS scales it). */
|
|
34
|
+
width: { type: Number, default: 640 },
|
|
35
|
+
height: { type: Number, default: 220 },
|
|
36
|
+
/** Value formatter for the axis guides and the aria label. */
|
|
37
|
+
formatValue: {
|
|
38
|
+
type: Function as PropType<(v: number) => string>,
|
|
39
|
+
default: (v: number) => String(Math.round(v)),
|
|
40
|
+
},
|
|
41
|
+
/** Accessible name for the whole chart; defaults to a value summary. */
|
|
42
|
+
ariaLabel: { type: String, default: null },
|
|
43
|
+
/** Rendered when every bucket is zero / the array is empty. */
|
|
44
|
+
emptyText: { type: String, default: "No data" },
|
|
45
|
+
},
|
|
46
|
+
emits: {
|
|
47
|
+
/** A bar was clicked (its bucket index). */
|
|
48
|
+
select: (index: number) => typeof index === "number",
|
|
49
|
+
},
|
|
50
|
+
setup(props, { emit }) {
|
|
51
|
+
const layout = computed(() =>
|
|
52
|
+
barLayout(props.bars.map((b) => b.value), {
|
|
53
|
+
width: props.width,
|
|
54
|
+
height: props.height,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
const empty = computed(() => props.bars.every((b) => !(b.value > 0)));
|
|
58
|
+
const aria = computed(
|
|
59
|
+
() =>
|
|
60
|
+
props.ariaLabel ??
|
|
61
|
+
props.bars.map((b) => `${b.label} ${props.formatValue(b.value)}`).join(", "),
|
|
62
|
+
);
|
|
63
|
+
const hintOf = (b: { label: string; value: number; hint?: string }, i: number): string =>
|
|
64
|
+
b.hint ?? `${b.label}: ${props.formatValue(b.value)}`;
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
if (empty.value) {
|
|
68
|
+
return <div class="hk-bar-chart hk-bar-chart--empty">{props.emptyText}</div>;
|
|
69
|
+
}
|
|
70
|
+
const l = layout.value;
|
|
71
|
+
return (
|
|
72
|
+
<svg
|
|
73
|
+
class="hk-bar-chart"
|
|
74
|
+
viewBox={`0 0 ${l.width} ${l.height}`}
|
|
75
|
+
preserveAspectRatio="xMidYMid meet"
|
|
76
|
+
role="img"
|
|
77
|
+
aria-label={aria.value}
|
|
78
|
+
>
|
|
79
|
+
{l.gridlines.map((g) => (
|
|
80
|
+
<g key={g.value}>
|
|
81
|
+
<line
|
|
82
|
+
class="hk-bar-chart__grid"
|
|
83
|
+
x1={l.sidePad}
|
|
84
|
+
y1={g.y}
|
|
85
|
+
x2={l.width - 8}
|
|
86
|
+
y2={g.y}
|
|
87
|
+
/>
|
|
88
|
+
<text class="hk-bar-chart__axis" x={l.sidePad - 6} y={g.y + 3}>
|
|
89
|
+
{props.formatValue(g.value)}
|
|
90
|
+
</text>
|
|
91
|
+
</g>
|
|
92
|
+
))}
|
|
93
|
+
{l.bars.map((s: BarSlot, i: number) => (
|
|
94
|
+
<g key={i}>
|
|
95
|
+
{s.path ? (
|
|
96
|
+
<path
|
|
97
|
+
class="hk-bar-chart__bar"
|
|
98
|
+
d={s.path}
|
|
99
|
+
data-hint={hintOf(props.bars[i], i)}
|
|
100
|
+
aria-label={hintOf(props.bars[i], i)}
|
|
101
|
+
tabindex={0}
|
|
102
|
+
onClick={() => emit("select", i)}
|
|
103
|
+
/>
|
|
104
|
+
) : null}
|
|
105
|
+
<text class="hk-bar-chart__x" x={s.centerX} y={l.baselineY + 14}>
|
|
106
|
+
{props.bars[i].label}
|
|
107
|
+
</text>
|
|
108
|
+
</g>
|
|
109
|
+
))}
|
|
110
|
+
</svg>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -33,10 +33,7 @@ async function mountBoard() {
|
|
|
33
33
|
// The board reads its viewport size at mount (clientWidth/Height, i.e. the
|
|
34
34
|
// layout box): pin it on the prototype for the duration so the camera's
|
|
35
35
|
// clamp has a real viewport to work with, the way a laid-out page would.
|
|
36
|
-
const proto = HTMLElement.prototype
|
|
37
|
-
clientWidth: number;
|
|
38
|
-
clientHeight: number;
|
|
39
|
-
};
|
|
36
|
+
const proto = HTMLElement.prototype;
|
|
40
37
|
const beforeW = Object.getOwnPropertyDescriptor(proto, "clientWidth");
|
|
41
38
|
const beforeH = Object.getOwnPropertyDescriptor(proto, "clientHeight");
|
|
42
39
|
Object.defineProperty(proto, "clientWidth", {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
|
8
8
|
import * as THREE from "three";
|
|
9
|
+
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
|
9
10
|
|
|
10
11
|
const H = vi.hoisted(() => {
|
|
11
12
|
class FakeWebGLRenderer {
|
|
@@ -186,7 +187,7 @@ describe("HkBoard3D", () => {
|
|
|
186
187
|
expect(body.userData.board3dId).toBe("node-1");
|
|
187
188
|
expect(label.classList.contains("hk-board3d-label")).toBe(true);
|
|
188
189
|
// Label rides the object as a CSS2D child at the requested offset.
|
|
189
|
-
const tag = body.children.find((c) =>
|
|
190
|
+
const tag = body.children.find((c) => c instanceof CSS2DObject && c.element === label);
|
|
190
191
|
expect(tag).toBeDefined();
|
|
191
192
|
expect(tag!.position.y).toBe(2);
|
|
192
193
|
|
|
@@ -329,7 +330,7 @@ describe("HkBoard3D", () => {
|
|
|
329
330
|
};
|
|
330
331
|
engine!.setObject("node", { object: body, label: chip() });
|
|
331
332
|
const labelChildren = () =>
|
|
332
|
-
body.children.filter((c) =>
|
|
333
|
+
body.children.filter((c) => c instanceof CSS2DObject).length;
|
|
333
334
|
expect(labelChildren()).toBe(1);
|
|
334
335
|
|
|
335
336
|
// Re-registering the same id must REPLACE its chip, not stack a second.
|
|
@@ -414,9 +415,9 @@ describe("HkBoard3D", () => {
|
|
|
414
415
|
const body = new THREE.Mesh(new THREE.SphereGeometry(1, 8, 6), new THREE.MeshBasicMaterial());
|
|
415
416
|
engine!.setObject("node", { object: body });
|
|
416
417
|
|
|
417
|
-
//
|
|
418
|
-
// desynchronise the board from its own scene.
|
|
419
|
-
(engine!.objects()
|
|
418
|
+
// An untyped consumer reaching for the mutable Map API must not be able
|
|
419
|
+
// to desynchronise the board from its own scene.
|
|
420
|
+
Map.prototype.clear.call(engine!.objects());
|
|
420
421
|
expect(engine!.objects().has("node")).toBe(true);
|
|
421
422
|
engine!.setObject("node", null);
|
|
422
423
|
expect(body.parent).toBeNull();
|
|
@@ -448,7 +449,7 @@ describe("HkBoard3D", () => {
|
|
|
448
449
|
// radius to `max(min, min(max, r))`, so a reversed window pins the
|
|
449
450
|
// eye at 600 and defeats every fit (the mocked controls in this
|
|
450
451
|
// suite do not clamp, hence the direct assertion).
|
|
451
|
-
const controls = engine!.controls
|
|
452
|
+
const controls = engine!.controls;
|
|
452
453
|
expect(controls.minDistance).toBe(2);
|
|
453
454
|
expect(controls.maxDistance).toBe(600);
|
|
454
455
|
|
|
@@ -615,10 +616,8 @@ describe("HkBoard3D", () => {
|
|
|
615
616
|
engine!.applyLighting({ ambientIntensity: 0.9 });
|
|
616
617
|
engine!.fitGround({ min: [0, 0, 0], max: [10, 0, 10] });
|
|
617
618
|
engine!.setAmbientIntensity(0.3);
|
|
618
|
-
const ambient = engine!.scene.children.find(
|
|
619
|
-
|
|
620
|
-
) as unknown as { intensity: number };
|
|
621
|
-
expect(ambient.intensity).toBeCloseTo(0.3, 5);
|
|
619
|
+
const ambient = engine!.scene.children.find((c) => c instanceof THREE.AmbientLight);
|
|
620
|
+
expect(ambient!.intensity).toBeCloseTo(0.3, 5);
|
|
622
621
|
});
|
|
623
622
|
|
|
624
623
|
it("disposes the lighting rig (and its shadow map) on unmount", async () => {
|
|
@@ -632,11 +631,9 @@ describe("HkBoard3D", () => {
|
|
|
632
631
|
ambientIntensity: 0.9,
|
|
633
632
|
points: [{ color: [1, 0, 0], intensity: 2, range: 10, position: [0, 3, 0] }],
|
|
634
633
|
});
|
|
635
|
-
const sun = engine!.scene.children.find(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const sunSpy = vi.spyOn(sun, "dispose");
|
|
639
|
-
const shadowSpy = vi.spyOn(sun.shadow, "dispose");
|
|
634
|
+
const sun = engine!.scene.children.find((c) => c instanceof THREE.DirectionalLight);
|
|
635
|
+
const sunSpy = vi.spyOn(sun!, "dispose");
|
|
636
|
+
const shadowSpy = vi.spyOn(sun!.shadow, "dispose");
|
|
640
637
|
const scene = engine!.scene;
|
|
641
638
|
|
|
642
639
|
const m = mounts.splice(0)[0];
|
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
type LightingRigOptions,
|
|
87
87
|
} from "../scene3d/lighting";
|
|
88
88
|
import { BOARD3D_HELPERS_LAYER, BOARD3D_MAIN_LAYER, fitDistance } from "../utils/scene3d";
|
|
89
|
+
import { asPropType } from "../utils/typing";
|
|
89
90
|
import {
|
|
90
91
|
createHoverBox,
|
|
91
92
|
isVisibleInHierarchy,
|
|
@@ -336,11 +337,11 @@ export default defineComponent({
|
|
|
336
337
|
/** Starting eye / orbit target. STATIC after mount (a non-finite
|
|
337
338
|
* or short array falls back to the defaults). */
|
|
338
339
|
initialPosition: {
|
|
339
|
-
type:
|
|
340
|
+
type: asPropType<[number, number, number]>(Array),
|
|
340
341
|
default: () => [22, 16, 26],
|
|
341
342
|
},
|
|
342
343
|
initialTarget: {
|
|
343
|
-
type:
|
|
344
|
+
type: asPropType<[number, number, number]>(Array),
|
|
344
345
|
default: () => [0, 0, 0],
|
|
345
346
|
},
|
|
346
347
|
/** Orbit distance window. STATIC after mount (applied to the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import * as barrel from "../index";
|
|
3
|
+
import { asIndexable } from "../utils/typing";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The public barrel guard for the calendar heatmap's exports (R3 on the
|
|
@@ -15,7 +16,7 @@ import * as barrel from "../index";
|
|
|
15
16
|
*/
|
|
16
17
|
describe("the public barrel", () => {
|
|
17
18
|
it("exports the heatmap component under both family names", () => {
|
|
18
|
-
const hk = barrel.HkCalendarHeatmap
|
|
19
|
+
const hk = asIndexable(barrel.HkCalendarHeatmap);
|
|
19
20
|
expect(hk, "HkCalendarHeatmap must be re-exported").toBeTruthy();
|
|
20
21
|
expect(hk.name).toBe("HkCalendarHeatmap");
|
|
21
22
|
// The H- alias rides the HCard/HkCard family convention.
|
|
@@ -2,6 +2,7 @@ import { computed, defineComponent, onBeforeUnmount, onMounted, ref, watch, type
|
|
|
2
2
|
import { ShieldAlert } from "lucide-vue-next";
|
|
3
3
|
|
|
4
4
|
import { useI18n } from "../i18n/context";
|
|
5
|
+
import { asIndexable } from "../utils/typing";
|
|
5
6
|
|
|
6
7
|
import "./HkCaptchaWidget.scss";
|
|
7
8
|
|
|
@@ -65,7 +66,7 @@ type GrecaptchaApi = {
|
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
function getGlobal(name: string): unknown {
|
|
68
|
-
return (window
|
|
69
|
+
return asIndexable(window)[name];
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export const HkCaptchaWidget = defineComponent({
|