@celestia-island/hikari 0.55.280 → 0.55.282
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/HkModelCard.scss +97 -0
- package/src/components/HkModelCard.test.tsx +142 -0
- package/src/components/HkModelCard.tsx +80 -0
- package/src/components/HkSolarLocationSetting.scss +63 -2
- package/src/components/HkSolarLocationSetting.test.tsx +260 -21
- package/src/components/HkSolarLocationSetting.tsx +263 -31
- package/src/components/HkTimeZoneGlobe.tsx +8 -10
- package/src/i18n/locales/ar/platform.json +18 -2
- package/src/i18n/locales/de/platform.json +18 -2
- package/src/i18n/locales/en/platform.json +19 -3
- package/src/i18n/locales/es/platform.json +18 -2
- package/src/i18n/locales/fr/platform.json +19 -3
- package/src/i18n/locales/ja/platform.json +18 -2
- package/src/i18n/locales/ko/platform.json +18 -2
- package/src/i18n/locales/pt/platform.json +18 -2
- package/src/i18n/locales/ru/platform.json +18 -2
- package/src/i18n/locales/zh-Hans/platform.json +19 -3
- package/src/i18n/locales/zh-Hant/platform.json +19 -3
- package/src/index.ts +1 -0
- package/src/theme/solarTimeZones.ts +16 -0
- package/src/theme/useSolarTime.test.ts +153 -0
- package/src/theme/useSolarTime.ts +135 -8
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
@use "../styles/polyfill" as *;
|
|
2
|
+
|
|
3
|
+
/* HkModelCard — the dense model card for tier-grouped model grids: a
|
|
4
|
+
leading identity lane (the consumer's model tag), an optional actions
|
|
5
|
+
lane (the serving switch), an optional caption (which provider a
|
|
6
|
+
merged card came from) and an optional footer (usage + capability
|
|
7
|
+
hints). The tier rides the card as an inset leading stripe; disabled
|
|
8
|
+
dims without killing the controls inside.
|
|
9
|
+
|
|
10
|
+
Motion contract: hover feedback is BORDER COLOR ONLY. The card never
|
|
11
|
+
transforms — a hover lift displaces nothing, reflows nothing, and
|
|
12
|
+
(2026-11-04 user direction) never animates the card upward. */
|
|
13
|
+
|
|
14
|
+
.hk-model-card {
|
|
15
|
+
--hk-model-card-accent: transparent;
|
|
16
|
+
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: var(--space-4);
|
|
20
|
+
padding: var(--space-6) var(--space-8);
|
|
21
|
+
border: 1px solid var(--border-subtle);
|
|
22
|
+
border-radius: var(--radius-md);
|
|
23
|
+
background: rgb(var(--color-muted) / 4%);
|
|
24
|
+
min-width: 0;
|
|
25
|
+
box-shadow: inset 2px 0 0 0 var(--hk-model-card-accent);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.hk-model-card[data-tier="deep"] {
|
|
29
|
+
--hk-model-card-accent: rgb(var(--color-primary));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hk-model-card[data-tier="normal"] {
|
|
33
|
+
--hk-model-card-accent: rgb(var(--color-success));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.hk-model-card[data-tier="basic"] {
|
|
37
|
+
--hk-model-card-accent: rgb(var(--color-muted) / 45%);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hk-model-card[data-hoverable] {
|
|
41
|
+
transition: border-color var(--duration-normal) ease;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hk-model-card[data-hoverable]:hover {
|
|
45
|
+
border-color: rgb(var(--color-muted) / 30%);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.hk-model-card[data-interactive] {
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
@include user-select(none);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.hk-model-card[data-interactive]:focus-visible {
|
|
54
|
+
outline: 2px solid rgb(var(--color-primary));
|
|
55
|
+
outline-offset: 2px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.hk-model-card[data-disabled] {
|
|
59
|
+
opacity: 0.5;
|
|
60
|
+
--hk-model-card-accent: transparent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.hk-model-card-top {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: space-between;
|
|
67
|
+
gap: var(--space-6);
|
|
68
|
+
min-width: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.hk-model-card-leading {
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.hk-model-card-actions {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: var(--space-2);
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.hk-model-card-caption {
|
|
83
|
+
font-size: var(--text-2xs);
|
|
84
|
+
color: rgb(var(--color-muted));
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
text-overflow: ellipsis;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.hk-model-card-footer {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
gap: var(--space-6);
|
|
95
|
+
min-width: 0;
|
|
96
|
+
color: rgb(var(--color-muted));
|
|
97
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkModelCard from "./HkModelCard";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HkModelCard anatomy + state guards.
|
|
8
|
+
*
|
|
9
|
+
* (Repo test convention: raw createApp + document queries, no
|
|
10
|
+
* @vue/test-utils dependency.)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
14
|
+
|
|
15
|
+
function mountCard(
|
|
16
|
+
props: Record<string, unknown> = {},
|
|
17
|
+
slots: Record<string, () => unknown> = {},
|
|
18
|
+
): HTMLElement {
|
|
19
|
+
const container = document.createElement("div");
|
|
20
|
+
document.body.appendChild(container);
|
|
21
|
+
const Wrapper = defineComponent({
|
|
22
|
+
setup() {
|
|
23
|
+
return () => h(HkModelCard, props, slots);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const app = createApp(Wrapper);
|
|
27
|
+
app.mount(container);
|
|
28
|
+
mounts.push({ app, container });
|
|
29
|
+
const root = container.querySelector<HTMLElement>(".hk-model-card");
|
|
30
|
+
expect(root, ".hk-model-card renders").toBeTruthy();
|
|
31
|
+
return root!;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
for (const { app, container } of mounts) {
|
|
36
|
+
app.unmount();
|
|
37
|
+
container.remove();
|
|
38
|
+
}
|
|
39
|
+
mounts.length = 0;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("HkModelCard anatomy", () => {
|
|
43
|
+
it("renders the top lane with title and actions content", () => {
|
|
44
|
+
const root = mountCard(
|
|
45
|
+
{},
|
|
46
|
+
{
|
|
47
|
+
title: () => h("span", { class: "t" }, "GLM 5.3"),
|
|
48
|
+
actions: () => h("button", { class: "a" }, "on"),
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
expect(root.querySelector(".hk-model-card-top")).toBeTruthy();
|
|
52
|
+
expect(root.querySelector(".hk-model-card-leading .t")?.textContent).toBe("GLM 5.3");
|
|
53
|
+
expect(root.querySelector(".hk-model-card-actions .a")).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("collapses caption and footer when their slots are empty", () => {
|
|
57
|
+
const root = mountCard({}, { title: () => h("span", "m") });
|
|
58
|
+
expect(root.querySelector(".hk-model-card-caption")).toBeNull();
|
|
59
|
+
expect(root.querySelector(".hk-model-card-footer")).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders caption and footer lanes when they carry content", () => {
|
|
63
|
+
const root = mountCard(
|
|
64
|
+
{},
|
|
65
|
+
{
|
|
66
|
+
title: () => h("span", "m"),
|
|
67
|
+
caption: () => "zai",
|
|
68
|
+
footer: () => h("span", { class: "f" }, "200.0k"),
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
expect(root.querySelector(".hk-model-card-caption")?.textContent).toBe("zai");
|
|
72
|
+
expect(root.querySelector(".hk-model-card-footer .f")?.textContent).toBe("200.0k");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("treats a comment-placeholder slot (consumer v-if) as empty", () => {
|
|
76
|
+
const show = ref(false);
|
|
77
|
+
const root = mountCard(
|
|
78
|
+
{},
|
|
79
|
+
{
|
|
80
|
+
title: () => h("span", "m"),
|
|
81
|
+
caption: () => (show.value ? h("span", "shown") : null),
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
// A null-render slot produces only the comment placeholder — the
|
|
85
|
+
// caption lane must collapse, not render an empty row.
|
|
86
|
+
expect(root.querySelector(".hk-model-card-caption")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("treats a fragment of dead v-if comments as empty (the JSX <>{cond && …}</> shape)", () => {
|
|
90
|
+
const show = ref(false);
|
|
91
|
+
const slotsOf = () => ({
|
|
92
|
+
title: () => h("span", "m"),
|
|
93
|
+
// The compiled shape of a consumer's conditional footer is a
|
|
94
|
+
// Fragment vnode whose children are dead comment placeholders —
|
|
95
|
+
// the guard must recurse into it, not stop at the Fragment.
|
|
96
|
+
footer: () => (<>{show.value && <span class="live" />}</>),
|
|
97
|
+
});
|
|
98
|
+
// All-dead fragment → the footer lane collapses.
|
|
99
|
+
expect(mountCard({}, slotsOf()).querySelector(".hk-model-card-footer")).toBeNull();
|
|
100
|
+
// A fragment with live content still renders its lane.
|
|
101
|
+
show.value = true;
|
|
102
|
+
const live = mountCard({}, slotsOf());
|
|
103
|
+
expect(live.querySelector(".hk-model-card-footer .live")?.textContent).toBe("");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("HkModelCard state attributes", () => {
|
|
108
|
+
it("lands the tier as data-tier", () => {
|
|
109
|
+
expect(mountCard({ tier: "deep" }, {}).dataset.tier).toBe("deep");
|
|
110
|
+
expect(mountCard({ tier: "normal" }, {}).dataset.tier).toBe("normal");
|
|
111
|
+
expect(mountCard({ tier: "basic" }, {}).dataset.tier).toBe("basic");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("an absent tier lands no data-tier at all", () => {
|
|
115
|
+
expect(mountCard({}, {}).hasAttribute("data-tier")).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("hoverable is the default and drops out when disabled", () => {
|
|
119
|
+
expect(mountCard({}, {}).hasAttribute("data-hoverable")).toBe(true);
|
|
120
|
+
expect(mountCard({ disabled: true }, {}).hasAttribute("data-hoverable")).toBe(false);
|
|
121
|
+
expect(mountCard({ hoverable: false }, {}).hasAttribute("data-hoverable")).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("disabled lands the dim marker, interactive lands its marker", () => {
|
|
125
|
+
expect(mountCard({ disabled: true }, {}).hasAttribute("data-disabled")).toBe(true);
|
|
126
|
+
expect(mountCard({ interactive: true }, {}).hasAttribute("data-interactive")).toBe(true);
|
|
127
|
+
// interactive + disabled: the dim wins, the clickability marker goes.
|
|
128
|
+
expect(mountCard({ interactive: true, disabled: true }, {}).hasAttribute("data-interactive")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("HkModelCard interaction", () => {
|
|
133
|
+
it("emits click with the native event", () => {
|
|
134
|
+
let seen: MouseEvent | null = null;
|
|
135
|
+
const root = mountCard(
|
|
136
|
+
{ onClick: (e: MouseEvent) => (seen = e) },
|
|
137
|
+
{ title: () => h("span", "m") },
|
|
138
|
+
);
|
|
139
|
+
root.querySelector<HTMLElement>(".hk-model-card-leading")!.click();
|
|
140
|
+
expect(seen).toBeInstanceOf(MouseEvent);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { defineComponent, type PropType } from "vue";
|
|
2
|
+
import { Comment, Fragment, type VNode } from "vue";
|
|
3
|
+
|
|
4
|
+
import "./HkModelCard.scss";
|
|
5
|
+
|
|
6
|
+
/** The capability tier a model card belongs to — the family's shared
|
|
7
|
+
* vocabulary (arona serves it on every roster row, the consumers group
|
|
8
|
+
* by it). The tier only drives the inset accent stripe; the card never
|
|
9
|
+
* interprets it beyond color. */
|
|
10
|
+
export type ModelCardTier = "deep" | "normal" | "basic";
|
|
11
|
+
|
|
12
|
+
/** Whether a slot actually rendered anything. Testing the slot function
|
|
13
|
+
* for truthiness is not enough: a consumer's `v-if` slot renders a
|
|
14
|
+
* comment placeholder vnode, and a consumer's `<>{cond && …}</>`
|
|
15
|
+
* fragment compiles to a Fragment vnode carrying those same dead
|
|
16
|
+
* comments — both must read as empty (same guard as HkCardList's
|
|
17
|
+
* renderedRows, recursed into fragments). */
|
|
18
|
+
function hasRenderedContent(nodes: unknown): boolean {
|
|
19
|
+
if (nodes == null || nodes === false || nodes === "") return false;
|
|
20
|
+
if (Array.isArray(nodes)) return nodes.some((n) => hasRenderedContent(n));
|
|
21
|
+
if (typeof nodes === "object") {
|
|
22
|
+
const node = nodes as VNode;
|
|
23
|
+
if (node.type === Fragment) return hasRenderedContent(node.children);
|
|
24
|
+
return node.type !== Comment;
|
|
25
|
+
}
|
|
26
|
+
return true; // non-empty text content
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default defineComponent({
|
|
30
|
+
name: "HkModelCard",
|
|
31
|
+
props: {
|
|
32
|
+
/** Capability tier accent (inset leading stripe). Absent = no stripe. */
|
|
33
|
+
tier: { type: String as PropType<ModelCardTier>, default: undefined },
|
|
34
|
+
/** Dimmed, accent-free, hover-inert presentation for a switched-off
|
|
35
|
+
* model. Deliberately NOT pointer-events: none — the row's controls
|
|
36
|
+
* (the serving switch) stay operable exactly to un-disable it. */
|
|
37
|
+
disabled: { type: Boolean, default: false },
|
|
38
|
+
/** Border-emphasis hover feedback. The house standard is color only —
|
|
39
|
+
* the card never transforms, so hovering never displaces neighbors
|
|
40
|
+
* or reflows the grid (2026-11-04 user direction). */
|
|
41
|
+
hoverable: { type: Boolean, default: true },
|
|
42
|
+
/** Clickable-card presentation (cursor + focus ring) for surfaces
|
|
43
|
+
* that cycle state on click. Pure presentation — the consumer owns
|
|
44
|
+
* the actual click semantics and a11y wiring. */
|
|
45
|
+
interactive: { type: Boolean, default: false },
|
|
46
|
+
},
|
|
47
|
+
emits: {
|
|
48
|
+
click: (_e: MouseEvent) => true,
|
|
49
|
+
},
|
|
50
|
+
setup(props, { emit, slots }) {
|
|
51
|
+
return () => {
|
|
52
|
+
const caption = slots.caption?.();
|
|
53
|
+
const footer = slots.footer?.();
|
|
54
|
+
const actions = slots.actions?.();
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
class="hk-model-card"
|
|
58
|
+
data-tier={props.tier}
|
|
59
|
+
data-disabled={props.disabled || undefined}
|
|
60
|
+
data-hoverable={props.hoverable && !props.disabled ? "" : undefined}
|
|
61
|
+
data-interactive={props.interactive && !props.disabled ? "" : undefined}
|
|
62
|
+
onClick={(e: MouseEvent) => emit("click", e)}
|
|
63
|
+
>
|
|
64
|
+
<div class="hk-model-card-top">
|
|
65
|
+
<div class="hk-model-card-leading">{slots.title?.()}</div>
|
|
66
|
+
{hasRenderedContent(actions) && (
|
|
67
|
+
<div class="hk-model-card-actions">{actions}</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
{hasRenderedContent(caption) && (
|
|
71
|
+
<div class="hk-model-card-caption">{caption}</div>
|
|
72
|
+
)}
|
|
73
|
+
{hasRenderedContent(footer) && (
|
|
74
|
+
<div class="hk-model-card-footer">{footer}</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
/* HkSolarLocationSetting — the solar-clock location strategy
|
|
1
|
+
/* HkSolarLocationSetting — the solar-clock location strategy card and
|
|
2
2
|
* its editor modal body (see the component docblock for the strategy
|
|
3
3
|
* ladder). AI-generated: developed with the ZCode agent (model
|
|
4
4
|
* GLM-5.3); human-directed. */
|
|
5
5
|
|
|
6
|
-
/* ── The settings-page
|
|
6
|
+
/* ── The settings-page card (HkCard root; header/body classes are the
|
|
7
|
+
* card family's own) ─────────────────────────────────────────────── */
|
|
7
8
|
.hk-solar-setting {
|
|
9
|
+
min-width: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ── The card body's live summary + Edit button line ──────────────────── */
|
|
13
|
+
.hk-solar-row {
|
|
8
14
|
display: flex;
|
|
9
15
|
align-items: center;
|
|
10
16
|
gap: var(--space-8, 0.5rem);
|
|
@@ -59,6 +65,61 @@
|
|
|
59
65
|
line-height: 1.5;
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
/* ── Detected-value readout (timezone / ip panes) ───────────────────────
|
|
69
|
+
* One info item per line: a fixed label column and a value column of
|
|
70
|
+
* copyable tags, so long values keep their own row and the two columns
|
|
71
|
+
* stay aligned across rows. */
|
|
72
|
+
.hk-solar-detected {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: var(--space-8, 0.5rem);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.hk-solar-detected-row {
|
|
79
|
+
display: grid;
|
|
80
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
81
|
+
gap: var(--space-8, 0.5rem);
|
|
82
|
+
align-items: start;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.hk-solar-detected-label {
|
|
86
|
+
padding-top: var(--space-1, 0.125rem);
|
|
87
|
+
font-size: var(--text-xs);
|
|
88
|
+
color: rgb(var(--color-muted));
|
|
89
|
+
/* Long localized labels wrap instead of pushing the value column out. */
|
|
90
|
+
overflow-wrap: anywhere;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hk-solar-detected-value {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-wrap: wrap;
|
|
96
|
+
gap: var(--space-6, 0.375rem);
|
|
97
|
+
min-width: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Values are labels, not prose — but a readout value that outruns the
|
|
101
|
+
* modal soft-wraps inside its chip instead of clipping or overflowing. */
|
|
102
|
+
.hk-solar-detected-value .hk-badge {
|
|
103
|
+
white-space: normal;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
text-align: left;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Probe status line (IP pane): muted progress / failure note, with the
|
|
109
|
+
* retry affordance inline after the text. */
|
|
110
|
+
.hk-solar-detected-status {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
gap: var(--space-6, 0.375rem);
|
|
115
|
+
color: rgb(var(--color-muted));
|
|
116
|
+
font-size: var(--text-xs);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hk-solar-detected-retry {
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
62
123
|
.hk-solar-manual {
|
|
63
124
|
display: flex;
|
|
64
125
|
flex-direction: column;
|