@celestia-island/hikari 0.45.5 → 0.45.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -30,25 +30,39 @@
|
|
|
30
30
|
gap: var(--space-16, 1rem);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* ── Identity hero: haloed logo → name → version · tagline ─────────────── */
|
|
34
|
+
|
|
33
35
|
.s-about-modal-header {
|
|
34
36
|
display: flex;
|
|
35
37
|
flex-direction: column;
|
|
36
38
|
align-items: center;
|
|
37
|
-
gap: var(--space-
|
|
39
|
+
gap: var(--space-10, 0.625rem);
|
|
38
40
|
text-align: center;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Framed mark. The app logo is a light-on-light illustration (a white tablet
|
|
44
|
+
// render with its own baked soft shadow), so it needs a tinted, bounded bed:
|
|
45
|
+
// floating on the modal surface it read as a stray smudge, and the same bed
|
|
46
|
+
// gives it contrast on dark themes without hard-coding a mode.
|
|
47
|
+
.s-about-modal-logo-frame {
|
|
48
|
+
display: grid;
|
|
49
|
+
place-items: center;
|
|
50
|
+
width: 5rem;
|
|
51
|
+
height: 5rem;
|
|
52
|
+
flex-shrink: 0;
|
|
53
|
+
border: 1px solid rgb(var(--color-primary) / 14%);
|
|
54
|
+
border-radius: var(--radius-xl, 16px);
|
|
55
|
+
background: linear-gradient(
|
|
56
|
+
155deg,
|
|
57
|
+
rgb(var(--color-primary) / 13%),
|
|
58
|
+
rgb(var(--color-primary) / 4%)
|
|
59
|
+
);
|
|
60
|
+
box-shadow: 0 6px 18px rgb(var(--color-primary) / 12%);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
.s-about-modal-logo-img {
|
|
48
|
-
width:
|
|
49
|
-
height:
|
|
50
|
-
flex-shrink: 0;
|
|
51
|
-
border-radius: var(--radius-lg, 12px);
|
|
64
|
+
width: 3.75rem;
|
|
65
|
+
height: 3.75rem;
|
|
52
66
|
object-fit: contain;
|
|
53
67
|
user-select: none;
|
|
54
68
|
}
|
|
@@ -57,35 +71,69 @@
|
|
|
57
71
|
display: flex;
|
|
58
72
|
align-items: center;
|
|
59
73
|
justify-content: center;
|
|
60
|
-
width:
|
|
61
|
-
height:
|
|
62
|
-
flex-shrink: 0;
|
|
74
|
+
width: 3.25rem;
|
|
75
|
+
height: 3.25rem;
|
|
63
76
|
border-radius: var(--radius-lg, 12px);
|
|
64
|
-
background:
|
|
77
|
+
background: linear-gradient(
|
|
78
|
+
150deg,
|
|
79
|
+
rgb(var(--color-primary)),
|
|
80
|
+
rgb(var(--color-secondary, var(--color-primary)))
|
|
81
|
+
);
|
|
65
82
|
color: rgb(var(--color-on-solid-text, 255 255 255));
|
|
66
|
-
font-size: var(--text-
|
|
83
|
+
font-size: var(--text-xl, 1.25rem);
|
|
67
84
|
font-weight: 700;
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
.s-about-modal-identity {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: var(--space-2, 0.125rem);
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
.s-about-modal-name {
|
|
71
95
|
margin: 0;
|
|
72
|
-
font-size: var(--text-
|
|
96
|
+
font-size: var(--text-xl, 1.25rem);
|
|
73
97
|
font-weight: 700;
|
|
98
|
+
line-height: 1.3;
|
|
99
|
+
letter-spacing: 0.01em;
|
|
74
100
|
color: rgb(var(--color-text));
|
|
75
101
|
}
|
|
76
102
|
|
|
103
|
+
// Version and tagline share one compact muted line — two stacked lines of
|
|
104
|
+
// equal weight made the hero read as a form.
|
|
105
|
+
.s-about-modal-subtitle {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
gap: var(--space-6, 0.375rem);
|
|
111
|
+
margin: 0;
|
|
112
|
+
font-size: var(--text-xs, 0.75rem);
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
color: rgb(var(--color-muted));
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
.s-about-modal-version {
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
font-variant-numeric: tabular-nums;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.s-about-modal-subtitle-sep {
|
|
122
|
+
color: rgb(var(--color-border));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.s-about-modal-tagline {
|
|
80
126
|
color: rgb(var(--color-muted));
|
|
81
127
|
}
|
|
82
128
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
129
|
+
/* ── Credits: made-by sentence + organization blurb ────────────────────── */
|
|
130
|
+
|
|
131
|
+
.s-about-modal-credits {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--space-4, 0.25rem);
|
|
136
|
+
margin-top: calc(-1 * var(--space-4, 0.25rem));
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
.s-about-modal-made-by {
|
|
@@ -95,10 +143,17 @@
|
|
|
95
143
|
color: rgb(var(--color-text));
|
|
96
144
|
}
|
|
97
145
|
|
|
98
|
-
.s-about-modal-
|
|
99
|
-
margin:
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
.s-about-modal-made-by .s-about-modal-row-link {
|
|
147
|
+
margin: 0 0.15em;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.s-about-modal-row-link {
|
|
151
|
+
color: rgb(var(--color-primary));
|
|
152
|
+
text-decoration: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.s-about-modal-row-link:hover {
|
|
156
|
+
text-decoration: underline;
|
|
102
157
|
}
|
|
103
158
|
|
|
104
159
|
.s-about-modal-description {
|
|
@@ -106,84 +161,143 @@
|
|
|
106
161
|
font-size: var(--text-xs, 0.75rem);
|
|
107
162
|
line-height: 1.6;
|
|
108
163
|
text-align: center;
|
|
164
|
+
text-wrap: pretty;
|
|
109
165
|
color: rgb(var(--color-muted));
|
|
110
166
|
}
|
|
111
167
|
|
|
168
|
+
/* ── Spec card: component version rows ────────────────────────────────── */
|
|
169
|
+
|
|
112
170
|
.s-about-modal-rows {
|
|
113
171
|
display: flex;
|
|
114
172
|
flex-direction: column;
|
|
115
|
-
|
|
173
|
+
padding: var(--space-2, 0.125rem) var(--space-14, 0.875rem);
|
|
174
|
+
border: 1px solid var(--border-subtle, rgb(100 100 100 / 12%));
|
|
175
|
+
border-radius: var(--radius-lg, 12px);
|
|
176
|
+
background: rgb(var(--color-primary) / 3%);
|
|
116
177
|
}
|
|
117
178
|
|
|
118
179
|
.s-about-modal-row {
|
|
119
180
|
display: flex;
|
|
120
181
|
align-items: center;
|
|
121
182
|
justify-content: space-between;
|
|
122
|
-
gap: var(--space-
|
|
183
|
+
gap: var(--space-12, 0.75rem);
|
|
184
|
+
min-height: 2.125rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.s-about-modal-row + .s-about-modal-row {
|
|
188
|
+
border-top: 1px solid var(--border-faint, rgb(100 100 100 / 10%));
|
|
123
189
|
}
|
|
124
190
|
|
|
125
191
|
.s-about-modal-row-label {
|
|
126
|
-
|
|
192
|
+
flex: 0 1 auto;
|
|
193
|
+
font-size: var(--text-xs, 0.75rem);
|
|
127
194
|
color: rgb(var(--color-muted));
|
|
128
195
|
}
|
|
129
196
|
|
|
130
197
|
.s-about-modal-row-value {
|
|
131
|
-
|
|
132
|
-
|
|
198
|
+
display: inline-flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: var(--space-8, 0.5rem);
|
|
201
|
+
flex: 0 0 auto;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.s-about-modal-row-number {
|
|
133
205
|
font-family: var(--font-mono, vars.$hikari-font-family-mono);
|
|
206
|
+
font-size: var(--text-xs, 0.75rem);
|
|
207
|
+
font-variant-numeric: tabular-nums;
|
|
208
|
+
letter-spacing: -0.01em;
|
|
209
|
+
color: rgb(var(--color-text));
|
|
134
210
|
}
|
|
135
211
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
212
|
+
// Environment word (生产 / 测试) lives in its own pill so it never melts
|
|
213
|
+
// into the version string.
|
|
214
|
+
.s-about-modal-row-meta {
|
|
215
|
+
padding: 0 var(--space-6, 0.375rem);
|
|
216
|
+
border: 1px solid var(--border-subtle, rgb(100 100 100 / 12%));
|
|
217
|
+
border-radius: var(--radius-full, 9999px);
|
|
218
|
+
font-size: var(--text-2xs, 0.625rem);
|
|
219
|
+
line-height: 1.6;
|
|
220
|
+
white-space: nowrap;
|
|
221
|
+
color: rgb(var(--color-muted));
|
|
139
222
|
}
|
|
140
223
|
|
|
141
|
-
.s-about-modal-row-
|
|
142
|
-
|
|
224
|
+
.s-about-modal-row-meta[data-tone="positive"] {
|
|
225
|
+
border-color: rgb(var(--color-success) / 32%);
|
|
226
|
+
background: rgb(var(--color-success) / 10%);
|
|
227
|
+
color: rgb(var(--color-success));
|
|
143
228
|
}
|
|
144
229
|
|
|
145
|
-
.s-about-modal-
|
|
230
|
+
.s-about-modal-row-meta[data-tone="caution"] {
|
|
231
|
+
border-color: rgb(var(--color-warning) / 32%);
|
|
232
|
+
background: rgb(var(--color-warning) / 10%);
|
|
233
|
+
color: rgb(var(--color-warning));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── Chips: licenses and external links ───────────────────────────────── */
|
|
237
|
+
|
|
238
|
+
.s-about-modal-chips {
|
|
146
239
|
display: flex;
|
|
147
240
|
flex-direction: column;
|
|
241
|
+
align-items: center;
|
|
148
242
|
gap: var(--space-8, 0.5rem);
|
|
149
243
|
}
|
|
150
244
|
|
|
245
|
+
// One chip group (licenses / links). Must span the chips block — inside an
|
|
246
|
+
// `align-items: center` column a plain block would shrink to fit-content and
|
|
247
|
+
// a long chip list would then wrap against that collapsed width instead of
|
|
248
|
+
// the modal's.
|
|
249
|
+
.s-about-modal-links {
|
|
250
|
+
width: 100%;
|
|
251
|
+
}
|
|
252
|
+
|
|
151
253
|
.s-about-modal-links-list {
|
|
152
254
|
display: flex;
|
|
153
255
|
flex-wrap: wrap;
|
|
154
256
|
justify-content: center;
|
|
155
|
-
gap: var(--space-
|
|
257
|
+
gap: var(--space-6, 0.375rem);
|
|
156
258
|
}
|
|
157
259
|
|
|
260
|
+
// Ghost pills: the chips are secondary navigation, so they carry a hairline
|
|
261
|
+
// instead of a filled brand block.
|
|
158
262
|
.s-about-modal-link {
|
|
159
|
-
padding: var(--space-
|
|
160
|
-
border
|
|
161
|
-
|
|
162
|
-
color: rgb(var(--color-
|
|
263
|
+
padding: var(--space-2, 0.125rem) var(--space-10, 0.625rem);
|
|
264
|
+
border: 1px solid var(--border-subtle, rgb(100 100 100 / 12%));
|
|
265
|
+
border-radius: var(--radius-full, 9999px);
|
|
266
|
+
color: rgb(var(--color-muted));
|
|
163
267
|
font-size: var(--text-xs, 0.75rem);
|
|
268
|
+
line-height: 1.7;
|
|
164
269
|
text-decoration: none;
|
|
165
|
-
transition:
|
|
270
|
+
transition:
|
|
271
|
+
color var(--duration-short, 0.15s) ease,
|
|
272
|
+
border-color var(--duration-short, 0.15s) ease,
|
|
273
|
+
background var(--duration-short, 0.15s) ease;
|
|
166
274
|
}
|
|
167
275
|
|
|
168
276
|
.s-about-modal-link:hover {
|
|
169
|
-
|
|
277
|
+
border-color: var(--c-primary-strong, rgb(var(--color-primary) / 40%));
|
|
278
|
+
background: var(--c-primary-faint, rgb(var(--color-primary) / 5%));
|
|
279
|
+
color: rgb(var(--color-primary));
|
|
170
280
|
}
|
|
171
281
|
|
|
172
282
|
.s-about-modal-link:active {
|
|
173
|
-
background: rgb(var(--color-primary) /
|
|
283
|
+
background: var(--c-primary-subtle, rgb(var(--color-primary) / 8%));
|
|
174
284
|
}
|
|
175
285
|
|
|
176
286
|
.s-about-modal-link:focus-visible {
|
|
287
|
+
outline: none;
|
|
177
288
|
box-shadow:
|
|
178
289
|
0 0 0 2px rgb(var(--color-surface)),
|
|
179
290
|
0 0 0 4px rgb(var(--color-primary));
|
|
180
291
|
}
|
|
181
292
|
|
|
293
|
+
/* ── Legal footer: filing links + copyright ───────────────────────────── */
|
|
294
|
+
|
|
182
295
|
.s-about-modal-footer-links {
|
|
183
296
|
display: flex;
|
|
184
297
|
flex-wrap: wrap;
|
|
185
298
|
justify-content: center;
|
|
186
|
-
gap: var(--space-
|
|
299
|
+
gap: var(--space-16, 1rem);
|
|
300
|
+
margin-top: calc(-1 * var(--space-4, 0.25rem));
|
|
187
301
|
}
|
|
188
302
|
|
|
189
303
|
.s-about-modal-footer-link {
|
|
@@ -200,4 +314,5 @@
|
|
|
200
314
|
.s-about-modal-footer {
|
|
201
315
|
display: flex;
|
|
202
316
|
justify-content: center;
|
|
317
|
+
margin-top: calc(-1 * var(--space-6, 0.375rem));
|
|
203
318
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { compile } from "sass";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compiled-stylesheet guard for HkAboutModal.
|
|
10
|
+
*
|
|
11
|
+
* Vitest stubs CSS (`test.css` is off), so the runtime suite can never catch
|
|
12
|
+
* a class the component emits that lost its rule — the #493 redesign dropped
|
|
13
|
+
* the `.s-about-modal-links` rule while keeping the element and every test
|
|
14
|
+
* stayed green. This test compiles the component sheet directly and asserts
|
|
15
|
+
* both directions of the class contract:
|
|
16
|
+
*
|
|
17
|
+
* 1. every `s-about-modal-*` class the TSX renders has a rule, and
|
|
18
|
+
* 2. every `s-about-modal-*` rule in the sheet is rendered by the TSX
|
|
19
|
+
* (no dead selectors).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const componentDir = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
23
|
+
|
|
24
|
+
function classesFrom(source: string): Set<string> {
|
|
25
|
+
// The component only uses static class strings; collect them from the
|
|
26
|
+
// `class="…"` attributes plus any bare `s-about-modal-*` token.
|
|
27
|
+
const found = new Set<string>();
|
|
28
|
+
for (const match of source.matchAll(/s-about-modal[\w-]*/g)) found.add(match[0]);
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("HkAboutModal stylesheet contract", () => {
|
|
33
|
+
const tsx = readFileSync(resolve(componentDir, "HkAboutModal.tsx"), "utf8");
|
|
34
|
+
const scss = readFileSync(resolve(componentDir, "HkAboutModal.scss"), "utf8");
|
|
35
|
+
const css = compile(resolve(componentDir, "HkAboutModal.scss"), {
|
|
36
|
+
style: "expanded",
|
|
37
|
+
}).css;
|
|
38
|
+
|
|
39
|
+
it("compiles without warnings", () => {
|
|
40
|
+
expect(css).toContain(".s-about-modal");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("gives every emitted class a rule", () => {
|
|
44
|
+
// Selector-side scan of the compiled sheet (not the SCSS source): a rule
|
|
45
|
+
// that failed to compile would otherwise still count as "present".
|
|
46
|
+
const declared = new Set([...css.matchAll(/\.(s-about-modal[\w-]*)/g)].map((m) => m[1]!));
|
|
47
|
+
const missing = [...classesFrom(tsx)].filter((name) => !declared.has(name)).sort();
|
|
48
|
+
expect(missing, "classes rendered without a stylesheet rule").toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("declares no rule the component never renders", () => {
|
|
52
|
+
const rendered = classesFrom(tsx);
|
|
53
|
+
const dead = [...new Set([...scss.matchAll(/\.(s-about-modal[\w-]*)/g)].map((m) => m[1]!))]
|
|
54
|
+
.filter((name) => !rendered.has(name))
|
|
55
|
+
.sort();
|
|
56
|
+
expect(dead, "stylesheet selectors with no matching element").toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -5,14 +5,16 @@ import { HkAboutModal } from "./HkAboutModal";
|
|
|
5
5
|
import { setLocale } from "../i18n/context";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* HkAboutModal contract tests for the branding
|
|
8
|
+
* HkAboutModal contract tests for the branding + redesign waves:
|
|
9
9
|
* - the first-letter tile is the default identity and `logoSrc` swaps it
|
|
10
|
-
* for an image
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
10
|
+
* for an image inside the identity frame
|
|
11
|
+
* - version and tagline share one subtitle line
|
|
12
|
+
* - description / made-by / license / copyright render the given strings,
|
|
13
|
+
* madeByNameHref turns the author name into an external link
|
|
14
|
+
* - component rows split the machine value from an optional meta pill
|
|
13
15
|
* - the backdrop factory renders inside the clipped backdrop layer
|
|
14
|
-
* - links render as
|
|
15
|
-
* -
|
|
16
|
+
* - licenses and links render as chips under one centered block
|
|
17
|
+
* - footer (filing) links render as external links
|
|
16
18
|
*
|
|
17
19
|
* (Repo test convention: raw createApp + document queries, no
|
|
18
20
|
* @vue/test-utils dependency.)
|
|
@@ -117,10 +119,10 @@ describe("HkAboutModal branding", () => {
|
|
|
117
119
|
expect(document.body.querySelector(".s-about-modal-row-link")).toBeNull();
|
|
118
120
|
});
|
|
119
121
|
|
|
120
|
-
it("renders software-component version rows", async () => {
|
|
122
|
+
it("renders software-component version rows with a meta pill and tone", async () => {
|
|
121
123
|
mountAbout({
|
|
122
124
|
componentVersions: [
|
|
123
|
-
{ label: "WebUI 版本", value: "0.1.138 REW2HF 生产" },
|
|
125
|
+
{ label: "WebUI 版本", value: "0.1.138 REW2HF", meta: "生产", metaTone: "positive" },
|
|
124
126
|
{ label: "计算引擎版本", value: "0.1.0" },
|
|
125
127
|
],
|
|
126
128
|
});
|
|
@@ -128,8 +130,37 @@ describe("HkAboutModal branding", () => {
|
|
|
128
130
|
const rows = [...document.body.querySelectorAll<HTMLElement>(".s-about-modal-row")];
|
|
129
131
|
expect(rows.length).toBe(2);
|
|
130
132
|
expect(rows[0]!.textContent).toContain("WebUI 版本");
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
// The number and the environment word are separate nodes: the value
|
|
134
|
+
// stays machine-readable, the tag carries the typography.
|
|
135
|
+
const number = query<HTMLElement>(".s-about-modal-row-number");
|
|
136
|
+
expect(number.textContent).toBe("0.1.138 REW2HF");
|
|
137
|
+
const meta = query<HTMLElement>(".s-about-modal-row-meta");
|
|
138
|
+
expect(meta.textContent).toBe("生产");
|
|
139
|
+
expect(meta.getAttribute("data-tone")).toBe("positive");
|
|
140
|
+
// The second row has no meta, so it renders no pill.
|
|
141
|
+
expect(rows[1]!.querySelector(".s-about-modal-row-meta")).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("defaults the meta pill to the neutral tone", async () => {
|
|
145
|
+
mountAbout({
|
|
146
|
+
componentVersions: [{ label: "计算引擎版本", value: "0.1.0", meta: "测试" }],
|
|
147
|
+
});
|
|
148
|
+
await flushModal();
|
|
149
|
+
expect(query<HTMLElement>(".s-about-modal-row-meta").getAttribute("data-tone")).toBe(
|
|
150
|
+
"neutral",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("carries the caution tone through to the pill", async () => {
|
|
155
|
+
mountAbout({
|
|
156
|
+
componentVersions: [
|
|
157
|
+
{ label: "云模型调度引擎版本", value: "0.1.26", meta: "预发", metaTone: "caution" },
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
await flushModal();
|
|
161
|
+
expect(query<HTMLElement>(".s-about-modal-row-meta").getAttribute("data-tone")).toBe(
|
|
162
|
+
"caution",
|
|
163
|
+
);
|
|
133
164
|
});
|
|
134
165
|
|
|
135
166
|
it("renders centered license chips", async () => {
|
|
@@ -205,5 +236,43 @@ describe("HkAboutModal branding", () => {
|
|
|
205
236
|
expect(document.body.querySelectorAll(".s-about-modal-row").length).toBe(0);
|
|
206
237
|
expect(document.body.querySelector(".s-about-modal-made-by")).toBeNull();
|
|
207
238
|
expect(document.body.querySelector(".s-about-modal-links")).toBeNull();
|
|
239
|
+
expect(document.body.querySelector(".s-about-modal-chips")).toBeNull();
|
|
240
|
+
expect(document.body.querySelector(".s-about-modal-credits")).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("groups license and link chips under one centered chips block", async () => {
|
|
244
|
+
mountAbout({
|
|
245
|
+
licenses: [{ label: "SySL-1.0", href: "https://example.test/sysl" }],
|
|
246
|
+
links: [{ label: "celestia.world", href: "https://celestia.world" }],
|
|
247
|
+
});
|
|
248
|
+
await flushModal();
|
|
249
|
+
const chips = query<HTMLElement>(".s-about-modal-chips");
|
|
250
|
+
const slots = [...chips.querySelectorAll<HTMLElement>(".s-about-modal-links")].map((el) =>
|
|
251
|
+
el.getAttribute("data-slot"),
|
|
252
|
+
);
|
|
253
|
+
expect(slots).toEqual(["licenses", "links"]);
|
|
254
|
+
expect(
|
|
255
|
+
[...chips.querySelectorAll<HTMLAnchorElement>(".s-about-modal-link")].map(
|
|
256
|
+
(chip) => chip.textContent,
|
|
257
|
+
),
|
|
258
|
+
).toEqual(["SySL-1.0", "celestia.world"]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("frames the logo image inside the identity frame", async () => {
|
|
262
|
+
mountAbout({ logoSrc: "data:image/webp;base64,AAA" });
|
|
263
|
+
await flushModal();
|
|
264
|
+
const frame = query<HTMLElement>(".s-about-modal-logo-frame");
|
|
265
|
+
expect(frame.querySelector("img.s-about-modal-logo-img")).toBeTruthy();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps version and tagline on one subtitle line", async () => {
|
|
269
|
+
mountAbout({ version: "9.9.9", tagline: "Chat with your agents" });
|
|
270
|
+
await flushModal();
|
|
271
|
+
const subtitle = query<HTMLElement>(".s-about-modal-subtitle");
|
|
272
|
+
expect(subtitle.querySelector(".s-about-modal-version")?.textContent).toContain("9.9.9");
|
|
273
|
+
expect(subtitle.querySelector(".s-about-modal-tagline")?.textContent).toBe(
|
|
274
|
+
"Chat with your agents",
|
|
275
|
+
);
|
|
276
|
+
expect(subtitle.querySelector(".s-about-modal-subtitle-sep")).toBeTruthy();
|
|
208
277
|
});
|
|
209
278
|
});
|
|
@@ -10,23 +10,34 @@ export interface HAboutLink {
|
|
|
10
10
|
href: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* One software-component version row.
|
|
15
|
+
*
|
|
16
|
+
* `value` is the machine-readable half (version, optionally plus a build
|
|
17
|
+
* hash) and renders in the mono/tabular face; `meta` is the optional
|
|
18
|
+
* human-facing trailing tag (e.g. the environment word 生产 / 测试),
|
|
19
|
+
* rendered as a separate muted pill so it never melts into the version
|
|
20
|
+
* string. `metaTone` tints that pill when the tag carries a status
|
|
21
|
+
* meaning (e.g. a production environment).
|
|
22
|
+
*/
|
|
14
23
|
export interface HAboutComponentVersion {
|
|
15
24
|
label: string;
|
|
16
25
|
value: string;
|
|
26
|
+
meta?: string;
|
|
27
|
+
metaTone?: "neutral" | "positive" | "caution";
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
31
|
* HkAboutModal — version / about dialog.
|
|
21
32
|
* (Upstreamed from shittim-chest's plana-legacy layer.)
|
|
22
33
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* Layout (2026-09 redesign): a centered identity hero (haloed logo, name,
|
|
35
|
+
* then version + tagline on one compact line), an optional credits block
|
|
36
|
+
* (made-by sentence with a linked author name + organization blurb), a
|
|
37
|
+
* bordered spec card holding the software-component versions, ghost chip
|
|
38
|
+
* rows for licenses / external links, and a muted legal footer (filing
|
|
39
|
+
* links + copyright). Every branding prop is optional — the modal degrades
|
|
40
|
+
* to the plain identity card when none are given.
|
|
30
41
|
*/
|
|
31
42
|
export const HkAboutModal = defineComponent({
|
|
32
43
|
name: "HkAboutModal",
|
|
@@ -38,9 +49,9 @@ export const HkAboutModal = defineComponent({
|
|
|
38
49
|
version: { type: String, required: true },
|
|
39
50
|
/** Optional logo image URL — replaces the first-letter tile. */
|
|
40
51
|
logoSrc: { type: String, default: undefined },
|
|
41
|
-
/** Optional one-liner
|
|
52
|
+
/** Optional one-liner shown beside the version (e.g. the app tagline). */
|
|
42
53
|
tagline: { type: String, default: undefined },
|
|
43
|
-
/** Optional centered small line under the
|
|
54
|
+
/** Optional centered small line under the credits (organization blurb). */
|
|
44
55
|
description: { type: String, default: undefined },
|
|
45
56
|
/** Made-by sentence: text before the linked author name (e.g. "由"). */
|
|
46
57
|
madeByPrefix: { type: String, default: undefined },
|
|
@@ -100,6 +111,27 @@ export const HkAboutModal = defineComponent({
|
|
|
100
111
|
);
|
|
101
112
|
};
|
|
102
113
|
|
|
114
|
+
const renderChips = (items: HAboutLink[], slot: string) => {
|
|
115
|
+
if (items.length === 0) return null;
|
|
116
|
+
return (
|
|
117
|
+
<div class="s-about-modal-links" data-slot={slot}>
|
|
118
|
+
<div class="s-about-modal-links-list">
|
|
119
|
+
{items.map((item) => (
|
|
120
|
+
<a
|
|
121
|
+
key={item.href}
|
|
122
|
+
href={item.href}
|
|
123
|
+
target="_blank"
|
|
124
|
+
rel="noopener noreferrer"
|
|
125
|
+
class="s-about-modal-link"
|
|
126
|
+
>
|
|
127
|
+
{item.label}
|
|
128
|
+
</a>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
103
135
|
return () => (
|
|
104
136
|
<HModal
|
|
105
137
|
modelValue={props.modelValue}
|
|
@@ -115,67 +147,70 @@ export const HkAboutModal = defineComponent({
|
|
|
115
147
|
)}
|
|
116
148
|
<div class="s-about-modal-body">
|
|
117
149
|
<header class="s-about-modal-header">
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
<div class="s-about-modal-logo-frame">
|
|
151
|
+
{props.logoSrc ? (
|
|
152
|
+
<img
|
|
153
|
+
class="s-about-modal-logo-img"
|
|
154
|
+
src={props.logoSrc}
|
|
155
|
+
alt=""
|
|
156
|
+
draggable={false}
|
|
157
|
+
/>
|
|
158
|
+
) : (
|
|
159
|
+
<div class="s-about-modal-logo">{props.appName.slice(0, 1).toUpperCase()}</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
<div class="s-about-modal-identity">
|
|
124
163
|
<h2 class="s-about-modal-name">{props.appName}</h2>
|
|
125
|
-
<p class="s-about-modal-
|
|
126
|
-
|
|
164
|
+
<p class="s-about-modal-subtitle">
|
|
165
|
+
<span class="s-about-modal-version">
|
|
166
|
+
{t("hikari::about.version", "Version")} {props.version}
|
|
167
|
+
</span>
|
|
168
|
+
{props.tagline && (
|
|
169
|
+
<>
|
|
170
|
+
<span class="s-about-modal-subtitle-sep" aria-hidden="true">
|
|
171
|
+
·
|
|
172
|
+
</span>
|
|
173
|
+
<span class="s-about-modal-tagline">{props.tagline}</span>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
127
176
|
</p>
|
|
128
|
-
{props.tagline && <p class="s-about-modal-tagline">{props.tagline}</p>}
|
|
129
177
|
</div>
|
|
130
178
|
</header>
|
|
131
179
|
|
|
132
|
-
{
|
|
133
|
-
|
|
180
|
+
{(props.madeByName || props.description) && (
|
|
181
|
+
<div class="s-about-modal-credits">
|
|
182
|
+
{renderMadeBy()}
|
|
183
|
+
{props.description && (
|
|
184
|
+
<p class="s-about-modal-description">{props.description}</p>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
134
188
|
|
|
135
189
|
{props.componentVersions.length > 0 && (
|
|
136
190
|
<div class="s-about-modal-rows">
|
|
137
191
|
{props.componentVersions.map((component) => (
|
|
138
192
|
<div class="s-about-modal-row" key={component.label}>
|
|
139
193
|
<span class="s-about-modal-row-label">{component.label}</span>
|
|
140
|
-
<span class="s-about-modal-row-value">
|
|
194
|
+
<span class="s-about-modal-row-value">
|
|
195
|
+
<span class="s-about-modal-row-number">{component.value}</span>
|
|
196
|
+
{component.meta && (
|
|
197
|
+
<span
|
|
198
|
+
class="s-about-modal-row-meta"
|
|
199
|
+
data-tone={component.metaTone ?? "neutral"}
|
|
200
|
+
>
|
|
201
|
+
{component.meta}
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
</span>
|
|
141
205
|
</div>
|
|
142
206
|
))}
|
|
143
207
|
</div>
|
|
144
208
|
)}
|
|
145
209
|
|
|
146
|
-
{props.licenses.length > 0 && (
|
|
147
|
-
<div class="s-about-modal-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<a
|
|
151
|
-
key={license.href}
|
|
152
|
-
href={license.href}
|
|
153
|
-
target="_blank"
|
|
154
|
-
rel="noopener noreferrer"
|
|
155
|
-
class="s-about-modal-link"
|
|
156
|
-
>
|
|
157
|
-
{license.label}
|
|
158
|
-
</a>
|
|
159
|
-
))}
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
)}
|
|
163
|
-
|
|
164
|
-
{props.links.length > 0 && (
|
|
165
|
-
<div class="s-about-modal-links">
|
|
166
|
-
<div class="s-about-modal-links-list">
|
|
167
|
-
{props.links.map((link) => (
|
|
168
|
-
<a
|
|
169
|
-
key={link.href}
|
|
170
|
-
href={link.href}
|
|
171
|
-
target="_blank"
|
|
172
|
-
rel="noopener noreferrer"
|
|
173
|
-
class="s-about-modal-link"
|
|
174
|
-
>
|
|
175
|
-
{link.label}
|
|
176
|
-
</a>
|
|
177
|
-
))}
|
|
178
|
-
</div>
|
|
210
|
+
{(props.licenses.length > 0 || props.links.length > 0) && (
|
|
211
|
+
<div class="s-about-modal-chips">
|
|
212
|
+
{renderChips(props.licenses, "licenses")}
|
|
213
|
+
{renderChips(props.links, "links")}
|
|
179
214
|
</div>
|
|
180
215
|
)}
|
|
181
216
|
|
package/src/index.ts
CHANGED
|
@@ -353,7 +353,11 @@ export { HkSecretRevealModal as HSecretRevealModal } from "./components/HkSecret
|
|
|
353
353
|
export { HkCaptchaWidget as HCaptchaWidget, type HkCaptchaProvider } from "./components/HkCaptchaWidget";
|
|
354
354
|
export { HkCaptchaModal as HCaptchaModal } from "./components/HkCaptchaModal";
|
|
355
355
|
export { HkProtocolModal as HProtocolModal } from "./components/HkProtocolModal";
|
|
356
|
-
export {
|
|
356
|
+
export {
|
|
357
|
+
HkAboutModal as HAboutModal,
|
|
358
|
+
type HAboutComponentVersion,
|
|
359
|
+
type HAboutLink,
|
|
360
|
+
} from "./components/HkAboutModal";
|
|
357
361
|
export { HkLogWindow as HLogWindow, type HLogTab } from "./components/HkLogWindow";
|
|
358
362
|
export { HkCookieConsent as HCookieConsent } from "./components/HkCookieConsent";
|
|
359
363
|
export {
|