@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.45.5",
3
+ "version": "0.45.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -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-8, 0.5rem);
39
+ gap: var(--space-10, 0.625rem);
38
40
  text-align: center;
39
41
  }
40
42
 
41
- .s-about-modal-header > div {
42
- display: flex;
43
- flex-direction: column;
44
- align-items: center;
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: 3rem;
49
- height: 3rem;
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: 3rem;
61
- height: 3rem;
62
- flex-shrink: 0;
74
+ width: 3.25rem;
75
+ height: 3.25rem;
63
76
  border-radius: var(--radius-lg, 12px);
64
- background: rgb(var(--color-primary));
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-lg, 1.125rem);
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-lg, 1.125rem);
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
- margin: var(--space-4, 0.25rem) 0 0;
79
- font-size: var(--text-sm, 0.8125rem);
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
- .s-about-modal-slogan {
84
- margin: 0;
85
- font-size: var(--text-base, 1rem);
86
- font-weight: 600;
87
- text-align: center;
88
- color: rgb(var(--color-text));
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-tagline {
99
- margin: var(--space-2, 0.125rem) 0 0;
100
- font-size: var(--text-xs, 0.75rem);
101
- color: rgb(var(--color-muted));
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
- gap: var(--space-8, 0.5rem);
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-8, 0.5rem);
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
- font-size: var(--text-sm, 0.8125rem);
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
- font-size: var(--text-sm, 0.8125rem);
132
- color: rgb(var(--color-text));
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
- .s-about-modal-row-link {
137
- color: rgb(var(--color-primary));
138
- text-decoration: none;
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-link:hover {
142
- text-decoration: underline;
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-links {
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-8, 0.5rem);
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-4, 0.25rem) var(--space-8, 0.5rem);
160
- border-radius: var(--radius-sm, 4px);
161
- background: rgb(var(--color-primary) / 12%);
162
- color: rgb(var(--color-primary));
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: background 200ms ease;
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
- background: rgb(var(--color-primary) / 20%);
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) / 26%);
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-12, 0.75rem);
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 wave:
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
- * - tagline / description / author / license / copyright render the given
12
- * strings, authorHref turns the author value into an external link
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 external chips
15
- * - author / license labels resolve through the i18n bundles
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
- expect(rows[0]!.textContent).toContain("0.1.138 REW2HF 生产");
132
- expect(rows[1]!.textContent).toContain("计算引擎版本");
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
- /** One software-component version row (label left, composed value right). */
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
- * Shows a centered identity block (logo above the name above the tagline)
24
- * plus optional branding: a made-by sentence with a linked author name, a
25
- * centered organization line, software-component version rows, license and
26
- * external link chips, a decorative backdrop layer (canvas or anything
27
- * else, rendered behind the content and pointer-inert) and centered legal
28
- * footer links (e.g. ICP filings). Every branding prop is optional — the
29
- * modal degrades to the plain identity card when none are given.
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 under the version (e.g. the app tagline). */
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 header (organization blurb). */
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
- {props.logoSrc ? (
119
- <img class="s-about-modal-logo-img" src={props.logoSrc} alt="" draggable={false} />
120
- ) : (
121
- <div class="s-about-modal-logo">{props.appName.slice(0, 1).toUpperCase()}</div>
122
- )}
123
- <div>
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-version">
126
- {t("hikari::about.version", "Version")} {props.version}
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
- {renderMadeBy()}
133
- {props.description && <p class="s-about-modal-description">{props.description}</p>}
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">{component.value}</span>
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-links">
148
- <div class="s-about-modal-links-list">
149
- {props.licenses.map((license) => (
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 { HkAboutModal as HAboutModal, type HAboutLink } from "./components/HkAboutModal";
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 {