@celestia-island/hikari 0.46.0 → 0.47.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 +1 -1
- package/src/components/HkAuthMethodList.scss +89 -10
- package/src/components/HkAuthMethodList.test.tsx +33 -21
- package/src/components/HkAuthMethodList.tsx +58 -30
- package/src/components/HkIconButtonGroup.tsx +15 -7
- package/src/composables/useConnectionProbe.ts +19 -0
- package/src/composables/useProgressDialog.ts +24 -0
- package/src/errorReporting/state.ts +30 -0
- package/src/i18n/context.ts +32 -0
- package/src/index.ts +16 -0
- package/src/runtime/animationBus.ts +42 -1
- package/src/runtime/cronBus.ts +18 -0
- package/src/runtime/historySafetyNet.ts +39 -0
- package/src/runtime/imageFallback.ts +25 -1
- package/src/runtime/index.ts +18 -0
- package/src/runtime/intervalBus.ts +18 -0
- package/src/runtime/navigationGuard.ts +20 -0
- package/src/runtime/pageLifecycle.ts +21 -1
- package/src/runtime/registry.integration.test.ts +309 -0
- package/src/runtime/registry.test.ts +279 -0
- package/src/runtime/registry.ts +315 -0
- package/src/runtime/tooltipBridge.ts +23 -1
- package/src/runtime/useBlockingToast.ts +16 -0
- package/src/runtime/useOverlay.ts +27 -1
- package/src/runtime/usePopupManager.ts +32 -0
- package/src/runtime/useToast.ts +22 -0
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
/* Alternative sign-in methods as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/* Alternative sign-in methods as CENTERED INDEPENDENT framed icon tiles
|
|
2
|
+
* (login card). The block container `.s-auth-methods` and its spacing are
|
|
3
|
+
* owned by HkAuthCard's slot styles (styles/admin-tokens.scss); this file
|
|
4
|
+
* only styles the divider and the tile row the component renders into that
|
|
5
|
+
* slot.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT a button group (2026-09-14 user direction): no shared
|
|
8
|
+
* track, no shared frame — each provider carries its own border so the row
|
|
9
|
+
* stays as wide as its content, centered in the card. */
|
|
6
10
|
|
|
7
11
|
/* "Other ways to sign in" divider row between rules. */
|
|
8
12
|
.s-auth-methods-divider {
|
|
@@ -24,16 +28,91 @@
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/* The component's own wrapper is `display: contents` — layout-wise the
|
|
27
|
-
divider and the
|
|
31
|
+
divider and the tile row remain direct children of HkAuthCard's
|
|
28
32
|
`.s-auth-methods` slot container. */
|
|
29
33
|
.s-auth-methods-list {
|
|
30
34
|
display: contents;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
/* The
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/* The tile row: fill the card column and center the independent tiles
|
|
38
|
+
inside it. The gap between tiles is the INDEPENDENT-action spacing
|
|
39
|
+
(--space-12) — comfortable air, unlike a group's shoulder-to-shoulder
|
|
40
|
+
track gap. */
|
|
41
|
+
.s-auth-methods .s-auth-methods-tiles {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-wrap: wrap;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
gap: var(--space-12, 0.75rem);
|
|
37
47
|
width: 100%;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* The HkTooltip wrapper span is the flex child between tiles — let it
|
|
51
|
+
shrink to the button so the row gap (not the wrapper) spaces them. */
|
|
52
|
+
.s-auth-methods .s-auth-methods-tiles > .hk-tooltip-wrapper {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
flex: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* One provider tile: a framed 44px square (the login-chooser tile size,
|
|
58
|
+
same as the icon group's md item). Each tile owns its whole frame —
|
|
59
|
+
border, hover wash, focus ring — with no track around the row. */
|
|
60
|
+
.s-auth-methods-tile {
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
width: 2.75rem;
|
|
65
|
+
height: 2.75rem;
|
|
66
|
+
padding: 0;
|
|
67
|
+
font: inherit;
|
|
68
|
+
color: rgb(var(--color-text, 25 30 40));
|
|
69
|
+
background: rgb(var(--color-surface, 255 255 255) / 55%);
|
|
70
|
+
border: 1px solid rgb(var(--color-border, 222 224 228) / 55%);
|
|
71
|
+
border-radius: var(--radius-md, 8px);
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
outline: none;
|
|
74
|
+
transition-property: background-color, border-color, color, box-shadow, opacity, transform;
|
|
75
|
+
transition-duration: var(--duration-short, 0.15s);
|
|
76
|
+
transition-timing-function: ease;
|
|
77
|
+
|
|
78
|
+
/* A real <button disabled> still matches :hover — guard the dead ones. */
|
|
79
|
+
&:hover:not(:disabled) {
|
|
80
|
+
color: rgb(var(--color-text, 25 30 40));
|
|
81
|
+
background: var(--c-primary-faint, rgb(var(--color-primary, 90 140 255) / 5%));
|
|
82
|
+
border-color: var(--c-primary-medium, rgb(var(--color-primary, 90 140 255) / 25%));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Tactile press feedback — the button-family cadence. */
|
|
86
|
+
&:active:not(:disabled) {
|
|
87
|
+
transform: scale(0.96);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Outset double ring, same grammar as .hk-btn. */
|
|
91
|
+
&:focus-visible {
|
|
92
|
+
box-shadow:
|
|
93
|
+
0 0 0 2px rgb(var(--color-surface, 255 255 255)),
|
|
94
|
+
0 0 0 4px rgb(var(--color-primary, 90 140 255));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&:disabled {
|
|
98
|
+
opacity: 0.5;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.s-auth-methods-tile-icon {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
38
106
|
justify-content: center;
|
|
107
|
+
line-height: 0;
|
|
108
|
+
/* Brand marks (svg/img vnodes) at their intrinsic size; lucide comps
|
|
109
|
+
sized by the host. The box never grows past the square. */
|
|
110
|
+
max-width: 100%;
|
|
111
|
+
max-height: 100%;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.s-auth-methods-tile-initial {
|
|
115
|
+
font-size: var(--text-sm, 0.8125rem);
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
line-height: 1;
|
|
39
118
|
}
|
|
@@ -28,27 +28,39 @@ const methods = [
|
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
describe("HkAuthMethodList", () => {
|
|
31
|
-
it("renders
|
|
31
|
+
it("renders centered independent framed tiles with an optional divider", () => {
|
|
32
32
|
// The card's `methods` slot owns the `.s-auth-methods` container —
|
|
33
|
-
// reproduce that context here (the divider and the
|
|
33
|
+
// reproduce that context here (the divider and the tile row are layout
|
|
34
34
|
// children of it through the component's display:contents wrapper).
|
|
35
35
|
const c = mount(
|
|
36
36
|
h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { divider: "其他方式登录", methods })),
|
|
37
37
|
);
|
|
38
38
|
const divider = c.querySelector(".s-auth-methods-divider");
|
|
39
39
|
expect(divider?.textContent).toContain("其他方式登录");
|
|
40
|
-
const
|
|
41
|
-
expect(
|
|
42
|
-
expect(group!.classList.contains("hk-icon-group-buttons")).toBe(true);
|
|
43
|
-
const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .hk-icon-group-item");
|
|
44
|
-
expect(buttons.length).toBe(2);
|
|
40
|
+
const tiles = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .s-auth-methods-tile");
|
|
41
|
+
expect(tiles.length).toBe(2);
|
|
45
42
|
// Icon-only: the label is the accessible name + tooltip, never visible text.
|
|
46
|
-
expect(
|
|
47
|
-
expect(
|
|
48
|
-
expect(
|
|
43
|
+
expect(tiles[0]!.getAttribute("aria-label")).toBe("GitHub");
|
|
44
|
+
expect(tiles[1]!.getAttribute("aria-label")).toBe("LinuxDo");
|
|
45
|
+
expect(tiles[0]!.textContent).not.toContain("GitHub");
|
|
49
46
|
});
|
|
50
47
|
|
|
51
|
-
it("wraps
|
|
48
|
+
it("never wraps the providers in a button-group track", () => {
|
|
49
|
+
// 2026-09-14 user direction: a shared group frame (one wide bordered
|
|
50
|
+
// track with the icons floating inside) is NOT this component's
|
|
51
|
+
// grammar — every provider is an independent framed tile. A regression
|
|
52
|
+
// back to HkIconButtonGroup (or any .hk-icon-group markup) must fail
|
|
53
|
+
// here, because the button group is reserved for selectors and tight
|
|
54
|
+
// action strips.
|
|
55
|
+
const c = mount(
|
|
56
|
+
h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })),
|
|
57
|
+
);
|
|
58
|
+
expect(c.querySelector(".s-auth-methods .hk-icon-group")).toBeNull();
|
|
59
|
+
expect(c.querySelector(".s-auth-methods [role='group']")).toBeNull();
|
|
60
|
+
expect(c.querySelector(".s-auth-methods [role='radiogroup']")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("wraps every tile in a tooltip carrying the provider label", () => {
|
|
52
64
|
const c = mount(
|
|
53
65
|
h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })),
|
|
54
66
|
);
|
|
@@ -71,12 +83,12 @@ describe("HkAuthMethodList", () => {
|
|
|
71
83
|
},
|
|
72
84
|
})),
|
|
73
85
|
);
|
|
74
|
-
const
|
|
75
|
-
|
|
86
|
+
const tiles = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .s-auth-methods-tile");
|
|
87
|
+
tiles[1]!.click();
|
|
76
88
|
await nextTick();
|
|
77
89
|
// Both rows are asserted so a constant-literal emit cannot ride an
|
|
78
90
|
// accidental fixture coincidence.
|
|
79
|
-
|
|
91
|
+
tiles[0]!.click();
|
|
80
92
|
await nextTick();
|
|
81
93
|
expect(picked).toEqual(["linuxdo", "github"]);
|
|
82
94
|
});
|
|
@@ -95,15 +107,15 @@ describe("HkAuthMethodList", () => {
|
|
|
95
107
|
},
|
|
96
108
|
})),
|
|
97
109
|
);
|
|
98
|
-
const
|
|
99
|
-
expect(
|
|
100
|
-
// The prebuilt brand vnode renders inside the icon
|
|
101
|
-
expect(
|
|
110
|
+
const tiles = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .s-auth-methods-tile");
|
|
111
|
+
expect(tiles.length).toBe(3);
|
|
112
|
+
// The prebuilt brand vnode renders inside the icon box.
|
|
113
|
+
expect(tiles[0]!.querySelector(".brand-gh")).toBeTruthy();
|
|
102
114
|
// A missing icon falls back to the label initial (never an empty box).
|
|
103
|
-
expect(
|
|
115
|
+
expect(tiles[2]!.querySelector<HTMLElement>(".s-auth-methods-tile-initial")?.textContent).toBe("F");
|
|
104
116
|
// Disabled entries render dead and swallow clicks.
|
|
105
|
-
expect(
|
|
106
|
-
|
|
117
|
+
expect(tiles[1]!.disabled).toBe(true);
|
|
118
|
+
tiles[1]!.click();
|
|
107
119
|
await nextTick();
|
|
108
120
|
expect(picked).toBe("");
|
|
109
121
|
});
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { defineComponent } from "vue";
|
|
2
|
-
import
|
|
2
|
+
import HkTooltip from "./HkTooltip";
|
|
3
3
|
import "./HkAuthMethodList.scss";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* HkAuthMethodList — the auth card's third-party sign-in block (the
|
|
7
7
|
* "other ways to sign in" row under the credentials form).
|
|
8
8
|
*
|
|
9
|
-
* Since 2026-09-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* (
|
|
16
|
-
* deployment offers several providers. The `select` event carries the
|
|
17
|
-
* provider `key` exactly as before, so consumers swap rendering
|
|
18
|
-
* without touching their OAuth flow.
|
|
9
|
+
* Since 2026-09-14 (user direction) the block renders as CENTERED
|
|
10
|
+
* INDEPENDENT icon tiles: every provider is its own framed 44px icon
|
|
11
|
+
* button with its own tooltip — NOT a button group. A group track (one
|
|
12
|
+
* shared frame stretched across the card with the icons floating in
|
|
13
|
+
* the middle) reads as an oversized empty box; the button-group
|
|
14
|
+
* component is reserved for its two fundamental jobs — a selector
|
|
15
|
+
* (single/multiple) and a tight action strip (see HkIconButtonGroup).
|
|
19
16
|
*
|
|
20
|
-
*
|
|
17
|
+
* Provider identity is revealed on hover: each tile wraps itself in an
|
|
18
|
+
* HkTooltip carrying the provider label, which matches the platform
|
|
19
|
+
* login-chooser pattern (Windows/macOS account tiles) and keeps the
|
|
20
|
+
* card compact when a deployment offers several providers. The
|
|
21
|
+
* `select` event carries the provider `key` exactly as before, so
|
|
22
|
+
* consumers swap rendering without touching their OAuth flow.
|
|
23
|
+
*
|
|
24
|
+
* The wrapper stays `display: contents`: the divider and the tile row
|
|
21
25
|
* remain layout children of HkAuthCard's `.s-auth-methods` slot
|
|
22
26
|
* container (it owns the side padding and vertical rhythm).
|
|
23
27
|
*/
|
|
@@ -35,11 +39,11 @@ export default defineComponent({
|
|
|
35
39
|
/** Accessible name + tooltip text (the hover reveal). */
|
|
36
40
|
label: string;
|
|
37
41
|
/** Prebuilt icon vnode (brand SVG, <img>, …) rendered inside the
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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. */
|
|
43
47
|
icon?: unknown;
|
|
44
48
|
disabled?: boolean;
|
|
45
49
|
}>,
|
|
@@ -47,7 +51,7 @@ export default defineComponent({
|
|
|
47
51
|
},
|
|
48
52
|
},
|
|
49
53
|
emits: {
|
|
50
|
-
/** A provider
|
|
54
|
+
/** A provider tile was clicked. */
|
|
51
55
|
select: (_key: string) => true,
|
|
52
56
|
},
|
|
53
57
|
setup(props, { emit }) {
|
|
@@ -58,18 +62,42 @@ export default defineComponent({
|
|
|
58
62
|
<span>{props.divider}</span>
|
|
59
63
|
</div>
|
|
60
64
|
)}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
{/* A plain row, deliberately NOT role="group": every tile is an
|
|
66
|
+
independent action (its own frame, its own tooltip), and the
|
|
67
|
+
divider above names the section for sighted users. Group
|
|
68
|
+
semantics belong to the selector/tight-strip components. */}
|
|
69
|
+
<div class="s-auth-methods-tiles">
|
|
70
|
+
{props.methods.map((method) => {
|
|
71
|
+
const tile = (
|
|
72
|
+
<button
|
|
73
|
+
key={method.key}
|
|
74
|
+
type="button"
|
|
75
|
+
class="s-auth-methods-tile"
|
|
76
|
+
data-key={method.key}
|
|
77
|
+
aria-label={method.label}
|
|
78
|
+
title={undefined} // the HkTooltip popup owns the hover text
|
|
79
|
+
disabled={method.disabled === true}
|
|
80
|
+
onClick={() => emit("select", method.key)}
|
|
81
|
+
>
|
|
82
|
+
<span class="s-auth-methods-tile-icon" aria-hidden="true">
|
|
83
|
+
{method.icon != null
|
|
84
|
+
? method.icon
|
|
85
|
+
: (
|
|
86
|
+
<span class="s-auth-methods-tile-initial">
|
|
87
|
+
{method.label.charAt(0).toUpperCase()}
|
|
88
|
+
</span>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
</span>
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
return (
|
|
95
|
+
<HkTooltip key={method.key} text={method.label} placement="top" delay={300}>
|
|
96
|
+
{tile}
|
|
97
|
+
</HkTooltip>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
73
101
|
</div>
|
|
74
102
|
);
|
|
75
103
|
},
|
|
@@ -25,13 +25,21 @@ export interface HkIconButtonGroupOption {
|
|
|
25
25
|
* HkIconButtonGroup — the icon-only variant of the button-group family
|
|
26
26
|
* (the same centered strip grammar as HkTabs, minus the text).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - "single"
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* The group has exactly TWO fundamental jobs (2026-09-14 user direction
|
|
29
|
+
* — a group is a semantic component, not a generic box to throw buttons
|
|
30
|
+
* into), carried by three working modes:
|
|
31
|
+
* - a SELECTOR: "single" (radiogroup: one active key via v-model,
|
|
32
|
+
* re-clicking the active key is a no-op — radio semantics) or
|
|
33
|
+
* "multiple" (toggle set: v-model is a string[] of active keys,
|
|
34
|
+
* every item carries aria-pressed)
|
|
35
|
+
* - a TIGHT ACTION STRIP: "buttons" (no selection state) — related
|
|
36
|
+
* actions packed shoulder-to-shoulder in ONE shared track, the
|
|
37
|
+
* toolbar grammar
|
|
38
|
+
*
|
|
39
|
+
* Anything else is NOT a group: independent actions that merely live on
|
|
40
|
+
* the same row (e.g. a login card's third-party provider tiles) render
|
|
41
|
+
* as separate framed buttons with comfortable spacing — see
|
|
42
|
+
* HkAuthMethodList for the reference pattern.
|
|
35
43
|
*
|
|
36
44
|
* Every item is icon-only and rides one size taller than a standard
|
|
37
45
|
* icon button (md = 44px vs the 32px icon button / 40px text button) —
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { onMounted, onUnmounted, ref, type Ref } from "vue";
|
|
2
2
|
|
|
3
3
|
import { scheduleInterval, scheduleIntervalAfter, type IntervalHandle } from "../runtime/intervalBus";
|
|
4
|
+
import { reportHkRuntime, type HkRuntimeHandle } from "../runtime/registry";
|
|
4
5
|
|
|
5
6
|
/** Fetch-based connectivity probe — the hikari-local successor of the
|
|
6
7
|
* plana-ui probe. Polls `/api/health` (or a custom endpoint set via
|
|
@@ -28,6 +29,20 @@ const DEFAULT_RETRY_TOTAL = 3;
|
|
|
28
29
|
|
|
29
30
|
let probeEndpoint: string | (() => string) = "";
|
|
30
31
|
|
|
32
|
+
// Runtime-registry reporting (the "context of contexts"): the probe is a
|
|
33
|
+
// per-component composable, so the MODULE is the hook — one registry
|
|
34
|
+
// entry counting live probe instances, with the configured endpoint in
|
|
35
|
+
// the read facet. Registration API unchanged.
|
|
36
|
+
let liveProbes = 0;
|
|
37
|
+
let probeRuntime: HkRuntimeHandle | null = null;
|
|
38
|
+
function ensureProbeRuntime(): HkRuntimeHandle {
|
|
39
|
+
return (probeRuntime ??= reportHkRuntime("connectionProbe", {
|
|
40
|
+
kind: "hook",
|
|
41
|
+
description: "The fetch-based connectivity probe: per-component /api/health polling with retry and countdown.",
|
|
42
|
+
read: () => ({ liveProbes, endpoint: endpointUrl() }),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/** Override the probed endpoint (defaults to same-origin `/api/health`). */
|
|
32
47
|
export function setProbeEndpoint(endpoint: string | (() => string)): void {
|
|
33
48
|
probeEndpoint = endpoint;
|
|
@@ -127,11 +142,15 @@ export function useConnectionProbe(): {
|
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
onMounted(() => {
|
|
145
|
+
liveProbes += 1;
|
|
146
|
+
ensureProbeRuntime().pulse({ liveProbes });
|
|
130
147
|
void probeOnce();
|
|
131
148
|
tickHandle = scheduleInterval(applyCountdown, 1000);
|
|
132
149
|
});
|
|
133
150
|
|
|
134
151
|
onUnmounted(() => {
|
|
152
|
+
liveProbes = Math.max(0, liveProbes - 1);
|
|
153
|
+
probeRuntime?.pulse({ liveProbes });
|
|
135
154
|
pollHandle?.disconnect();
|
|
136
155
|
pollHandle = null;
|
|
137
156
|
tickHandle?.disconnect();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { reactive } from "vue";
|
|
2
2
|
|
|
3
|
+
import { reportHkRuntime, type HkRuntimeHandle } from "../runtime/registry";
|
|
4
|
+
|
|
3
5
|
export interface ProgressDialogState {
|
|
4
6
|
open: boolean;
|
|
5
7
|
title: string;
|
|
@@ -24,7 +26,27 @@ const state = reactive<ProgressDialogState>({
|
|
|
24
26
|
max: 100,
|
|
25
27
|
});
|
|
26
28
|
|
|
29
|
+
// Runtime-registry reporting (the "context of contexts"). Lazy: the
|
|
30
|
+
// progress-dialog context reports itself on first use and pulses on
|
|
31
|
+
// open/close, so the live dialog tree is answerable from
|
|
32
|
+
// readHkRuntime("progressDialog") without importing this module.
|
|
33
|
+
let runtimeReport: HkRuntimeHandle | null = null;
|
|
34
|
+
function ensureRuntimeReport(): HkRuntimeHandle {
|
|
35
|
+
return (runtimeReport ??= reportHkRuntime("progressDialog", {
|
|
36
|
+
kind: "context",
|
|
37
|
+
description: "The progress dialog context: the single global task-progress surface (title, log tail, value/max).",
|
|
38
|
+
read: () => ({
|
|
39
|
+
open: state.open,
|
|
40
|
+
title: state.title,
|
|
41
|
+
logLines: state.logs.length,
|
|
42
|
+
value: state.value,
|
|
43
|
+
max: state.max,
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
export function showProgressDialog(opts: { title: string }): ProgressDialogHandle {
|
|
49
|
+
ensureRuntimeReport().pulse({ open: true, title: opts.title });
|
|
28
50
|
state.title = opts.title;
|
|
29
51
|
state.logs = [];
|
|
30
52
|
state.value = null;
|
|
@@ -43,10 +65,12 @@ export function showProgressDialog(opts: { title: string }): ProgressDialogHandl
|
|
|
43
65
|
},
|
|
44
66
|
close() {
|
|
45
67
|
state.open = false;
|
|
68
|
+
runtimeReport?.pulse({ open: false });
|
|
46
69
|
},
|
|
47
70
|
};
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
export function useProgressDialog(): ProgressDialogState {
|
|
74
|
+
ensureRuntimeReport();
|
|
51
75
|
return state;
|
|
52
76
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ref, type Ref } from "vue";
|
|
2
2
|
|
|
3
|
+
import { reportHkRuntime, type HkRuntimeHandle } from "../runtime/registry";
|
|
4
|
+
|
|
3
5
|
/** Where a reported error came from. */
|
|
4
6
|
export type HkErrorSource = "vue" | "window" | "rejection" | "manual";
|
|
5
7
|
|
|
@@ -66,6 +68,10 @@ export function getErrorReportingOptions(): HkErrorReportingOptions {
|
|
|
66
68
|
|
|
67
69
|
export function setErrorReportingOptions(options: HkErrorReportingOptions): void {
|
|
68
70
|
activeOptions = options;
|
|
71
|
+
runtimeReport?.setMeta({
|
|
72
|
+
captureWindow: options.captureWindow !== false,
|
|
73
|
+
captureRejection: options.captureRejection !== false,
|
|
74
|
+
});
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export function useErrorReportingState(): Ref<HkReportedError | null> {
|
|
@@ -86,6 +92,11 @@ export function reportError(err: unknown, source: HkErrorSource, info?: string):
|
|
|
86
92
|
|
|
87
93
|
if (currentError.value !== null) return null;
|
|
88
94
|
|
|
95
|
+
// Accepted onto the landing (first-error-wins): later errors while the
|
|
96
|
+
// landing is up still log/fire onError above but do not count.
|
|
97
|
+
acceptedReports += 1;
|
|
98
|
+
runtimeReport?.pulse({ reports: acceptedReports });
|
|
99
|
+
|
|
89
100
|
const normalized = normalizeError(err);
|
|
90
101
|
const record: HkReportedError = {
|
|
91
102
|
source,
|
|
@@ -102,3 +113,22 @@ export function reportError(err: unknown, source: HkErrorSource, info?: string):
|
|
|
102
113
|
export function clearErrorReportingState(): void {
|
|
103
114
|
currentError.value = null;
|
|
104
115
|
}
|
|
116
|
+
|
|
117
|
+
// Runtime-registry reporting (the "context of contexts"): the error
|
|
118
|
+
// reporting module is a global singleton, so it reports itself at module
|
|
119
|
+
// init and pulses on every accepted error. The read facet keeps the
|
|
120
|
+
// current landing card observable (name/message/source only — never the
|
|
121
|
+
// whole error object) from readHkRuntime("errorReporting").
|
|
122
|
+
let acceptedReports = 0;
|
|
123
|
+
const runtimeReport: HkRuntimeHandle = reportHkRuntime("errorReporting", {
|
|
124
|
+
kind: "plugin",
|
|
125
|
+
description: "The global error reporting landing: app.config.errorHandler + window/rejection hooks with the full-screen error card.",
|
|
126
|
+
meta: { captureWindow: activeOptions.captureWindow !== false, captureRejection: activeOptions.captureRejection !== false },
|
|
127
|
+
read: () => ({
|
|
128
|
+
reports: acceptedReports,
|
|
129
|
+
landingUp: currentError.value !== null,
|
|
130
|
+
current: currentError.value
|
|
131
|
+
? { name: currentError.value.name, message: currentError.value.message, source: currentError.value.source }
|
|
132
|
+
: null,
|
|
133
|
+
}),
|
|
134
|
+
});
|
package/src/i18n/context.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { reactive } from "vue";
|
|
2
2
|
|
|
3
|
+
import { reportHkRuntime } from "../runtime/registry";
|
|
4
|
+
|
|
3
5
|
type Messages = Record<string, string>;
|
|
4
6
|
|
|
5
7
|
interface LocaleModule {
|
|
@@ -63,6 +65,7 @@ export async function setLocale(locale: string): Promise<void> {
|
|
|
63
65
|
...(mergedMessages["en"] ?? {}),
|
|
64
66
|
...(mergedMessages[locale] ?? {}),
|
|
65
67
|
};
|
|
68
|
+
runtime.pulse({ locale });
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export function mergeMessages(userMessages: Messages, locale?: string): void {
|
|
@@ -82,6 +85,7 @@ export function mergeMessages(userMessages: Messages, locale?: string): void {
|
|
|
82
85
|
...(mergedMessages["en"] ?? {}),
|
|
83
86
|
...(mergedMessages[state.locale] ?? {}),
|
|
84
87
|
};
|
|
88
|
+
runtime.pulse({ locale: state.locale });
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
export function useI18n() {
|
|
@@ -96,3 +100,31 @@ export function useI18n() {
|
|
|
96
100
|
|
|
97
101
|
return { t, locale: state.locale };
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
// Runtime-registry reporting (the "context of contexts"): the i18n
|
|
105
|
+
// context is a module singleton, so it reports itself at module init and
|
|
106
|
+
// pulses on every locale switch / message merge. The facets make the
|
|
107
|
+
// locale tree reachable and steerable from anywhere via
|
|
108
|
+
// readHkRuntime("i18n") / writeHkRuntime("i18n", …) without importing
|
|
109
|
+
// this module.
|
|
110
|
+
const runtime = reportHkRuntime("i18n", {
|
|
111
|
+
kind: "context",
|
|
112
|
+
description: "The i18n context: active locale, message tables and per-locale merged overrides.",
|
|
113
|
+
meta: { locale: state.locale },
|
|
114
|
+
read: () => ({
|
|
115
|
+
locale: state.locale,
|
|
116
|
+
keys: Object.keys(state.messages).length,
|
|
117
|
+
mergedLocales: Object.keys(mergedMessages),
|
|
118
|
+
}),
|
|
119
|
+
write: (op) => {
|
|
120
|
+
if (op.type === "setLocale" && typeof op.locale === "string") {
|
|
121
|
+
void setLocale(op.locale);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (op.type === "mergeMessages" && typeof op.messages === "object" && op.messages !== null) {
|
|
125
|
+
mergeMessages(op.messages as Messages, typeof op.locale === "string" ? op.locale : undefined);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`unknown i18n write op "${op.type}"`);
|
|
129
|
+
},
|
|
130
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -244,6 +244,22 @@ export {
|
|
|
244
244
|
type HkTooltipBridgeOptions,
|
|
245
245
|
installHkImageFallback,
|
|
246
246
|
type HkImageFallbackOptions,
|
|
247
|
+
reportHkRuntime,
|
|
248
|
+
getHkRuntimeEntry,
|
|
249
|
+
listHkRuntime,
|
|
250
|
+
readHkRuntime,
|
|
251
|
+
writeHkRuntime,
|
|
252
|
+
hkRuntimeSnapshot,
|
|
253
|
+
useHkRegistry,
|
|
254
|
+
type HkRuntimeKind,
|
|
255
|
+
type HkRuntimeStatus,
|
|
256
|
+
type HkRuntimeMeta,
|
|
257
|
+
type HkRuntimeEntryState,
|
|
258
|
+
type HkRuntimeReport,
|
|
259
|
+
type HkRuntimeHandle,
|
|
260
|
+
type HkRuntimeWriteOp,
|
|
261
|
+
type HkRuntimeSnapshot,
|
|
262
|
+
type HkRuntimeSnapshotEntry,
|
|
247
263
|
} from "./runtime";
|
|
248
264
|
|
|
249
265
|
// i18n
|