@celestia-island/hikari 0.45.4 → 0.45.6

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.4",
3
+ "version": "0.45.6",
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,17 +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;
37
+ flex-direction: column;
35
38
  align-items: center;
36
- gap: var(--space-12, 0.75rem);
39
+ gap: var(--space-10, 0.625rem);
40
+ text-align: center;
37
41
  }
38
42
 
39
- .s-about-modal-logo-img {
40
- width: 3rem;
41
- height: 3rem;
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;
42
52
  flex-shrink: 0;
43
- border-radius: var(--radius-lg, 12px);
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%);
61
+ }
62
+
63
+ .s-about-modal-logo-img {
64
+ width: 3.75rem;
65
+ height: 3.75rem;
44
66
  object-fit: contain;
45
67
  user-select: none;
46
68
  }
@@ -49,41 +71,89 @@
49
71
  display: flex;
50
72
  align-items: center;
51
73
  justify-content: center;
52
- width: 3rem;
53
- height: 3rem;
54
- flex-shrink: 0;
74
+ width: 3.25rem;
75
+ height: 3.25rem;
55
76
  border-radius: var(--radius-lg, 12px);
56
- 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
+ );
57
82
  color: rgb(var(--color-on-solid-text, 255 255 255));
58
- font-size: var(--text-lg, 1.125rem);
83
+ font-size: var(--text-xl, 1.25rem);
59
84
  font-weight: 700;
60
85
  }
61
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
+
62
94
  .s-about-modal-name {
63
95
  margin: 0;
64
- font-size: var(--text-lg, 1.125rem);
96
+ font-size: var(--text-xl, 1.25rem);
65
97
  font-weight: 700;
98
+ line-height: 1.3;
99
+ letter-spacing: 0.01em;
66
100
  color: rgb(var(--color-text));
67
101
  }
68
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
+
69
117
  .s-about-modal-version {
70
- margin: var(--space-4, 0.25rem) 0 0;
71
- 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 {
72
126
  color: rgb(var(--color-muted));
73
127
  }
74
128
 
75
- .s-about-modal-slogan {
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));
137
+ }
138
+
139
+ .s-about-modal-made-by {
76
140
  margin: 0;
77
- font-size: var(--text-base, 1rem);
78
- font-weight: 600;
141
+ font-size: var(--text-sm, 0.8125rem);
79
142
  text-align: center;
80
143
  color: rgb(var(--color-text));
81
144
  }
82
145
 
83
- .s-about-modal-tagline {
84
- margin: var(--space-2, 0.125rem) 0 0;
85
- font-size: var(--text-xs, 0.75rem);
86
- 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;
87
157
  }
88
158
 
89
159
  .s-about-modal-description {
@@ -91,45 +161,84 @@
91
161
  font-size: var(--text-xs, 0.75rem);
92
162
  line-height: 1.6;
93
163
  text-align: center;
164
+ text-wrap: pretty;
94
165
  color: rgb(var(--color-muted));
95
166
  }
96
167
 
168
+ /* ── Spec card: component version rows ────────────────────────────────── */
169
+
97
170
  .s-about-modal-rows {
98
171
  display: flex;
99
172
  flex-direction: column;
100
- 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%);
101
177
  }
102
178
 
103
179
  .s-about-modal-row {
104
180
  display: flex;
105
181
  align-items: center;
106
182
  justify-content: space-between;
107
- 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%));
108
189
  }
109
190
 
110
191
  .s-about-modal-row-label {
111
- font-size: var(--text-sm, 0.8125rem);
192
+ flex: 0 1 auto;
193
+ font-size: var(--text-xs, 0.75rem);
112
194
  color: rgb(var(--color-muted));
113
195
  }
114
196
 
115
197
  .s-about-modal-row-value {
116
- font-size: var(--text-sm, 0.8125rem);
117
- 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 {
118
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));
119
210
  }
120
211
 
121
- .s-about-modal-row-link {
122
- color: rgb(var(--color-primary));
123
- 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));
124
222
  }
125
223
 
126
- .s-about-modal-row-link:hover {
127
- 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));
128
228
  }
129
229
 
130
- .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 {
131
239
  display: flex;
132
240
  flex-direction: column;
241
+ align-items: center;
133
242
  gap: var(--space-8, 0.5rem);
134
243
  }
135
244
 
@@ -137,38 +246,50 @@
137
246
  display: flex;
138
247
  flex-wrap: wrap;
139
248
  justify-content: center;
140
- gap: var(--space-8, 0.5rem);
249
+ gap: var(--space-6, 0.375rem);
141
250
  }
142
251
 
252
+ // Ghost pills: the chips are secondary navigation, so they carry a hairline
253
+ // instead of a filled brand block.
143
254
  .s-about-modal-link {
144
- padding: var(--space-4, 0.25rem) var(--space-8, 0.5rem);
145
- border-radius: var(--radius-sm, 4px);
146
- background: rgb(var(--color-primary) / 12%);
147
- color: rgb(var(--color-primary));
255
+ padding: var(--space-2, 0.125rem) var(--space-10, 0.625rem);
256
+ border: 1px solid var(--border-subtle, rgb(100 100 100 / 12%));
257
+ border-radius: var(--radius-full, 9999px);
258
+ color: rgb(var(--color-muted));
148
259
  font-size: var(--text-xs, 0.75rem);
260
+ line-height: 1.7;
149
261
  text-decoration: none;
150
- transition: background 200ms ease;
262
+ transition:
263
+ color var(--duration-short, 0.15s) ease,
264
+ border-color var(--duration-short, 0.15s) ease,
265
+ background var(--duration-short, 0.15s) ease;
151
266
  }
152
267
 
153
268
  .s-about-modal-link:hover {
154
- background: rgb(var(--color-primary) / 20%);
269
+ border-color: var(--c-primary-strong, rgb(var(--color-primary) / 40%));
270
+ background: var(--c-primary-faint, rgb(var(--color-primary) / 5%));
271
+ color: rgb(var(--color-primary));
155
272
  }
156
273
 
157
274
  .s-about-modal-link:active {
158
- background: rgb(var(--color-primary) / 26%);
275
+ background: var(--c-primary-subtle, rgb(var(--color-primary) / 8%));
159
276
  }
160
277
 
161
278
  .s-about-modal-link:focus-visible {
279
+ outline: none;
162
280
  box-shadow:
163
281
  0 0 0 2px rgb(var(--color-surface)),
164
282
  0 0 0 4px rgb(var(--color-primary));
165
283
  }
166
284
 
285
+ /* ── Legal footer: filing links + copyright ───────────────────────────── */
286
+
167
287
  .s-about-modal-footer-links {
168
288
  display: flex;
169
289
  flex-wrap: wrap;
170
290
  justify-content: center;
171
- gap: var(--space-12, 0.75rem);
291
+ gap: var(--space-16, 1rem);
292
+ margin-top: calc(-1 * var(--space-4, 0.25rem));
172
293
  }
173
294
 
174
295
  .s-about-modal-footer-link {
@@ -185,4 +306,5 @@
185
306
  .s-about-modal-footer {
186
307
  display: flex;
187
308
  justify-content: center;
309
+ margin-top: calc(-1 * var(--space-6, 0.375rem));
188
310
  }
@@ -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.)
@@ -94,40 +96,79 @@ describe("HkAboutModal branding", () => {
94
96
  expect(query<HTMLElement>(".s-about-modal-description").textContent).toBe("Save the world.");
95
97
  });
96
98
 
97
- it("renders the made-by row with a linked origin", async () => {
99
+ it("renders the made-by sentence with a linked author name", async () => {
98
100
  mountAbout({
99
- author: "由 伊欧 主创",
100
- origin: "来自 Celestia Island",
101
- originHref: "https://github.com/celestia-island",
102
- license: "BUSL-1.1",
101
+ madeByPrefix: "由",
102
+ madeByName: "伊欧",
103
+ madeByNameHref: "https://github.com/langyo",
104
+ madeBySuffix: " 主创,来自 Celestia Island",
103
105
  });
104
106
  await flushModal();
105
- const row = query<HTMLElement>(".s-about-modal-row");
106
- expect(row.querySelector(".s-about-modal-row-label")?.textContent).toBe("由 伊欧 主创");
107
+ const line = query<HTMLElement>(".s-about-modal-made-by");
108
+ expect(line.textContent).toBe("由伊欧 主创,来自 Celestia Island");
107
109
  const link = query<HTMLAnchorElement>(".s-about-modal-row-link");
108
- expect(link.textContent).toBe("来自 Celestia Island");
109
- expect(link.getAttribute("href")).toBe("https://github.com/celestia-island");
110
+ expect(link.textContent).toBe("伊欧");
111
+ expect(link.getAttribute("href")).toBe("https://github.com/langyo");
110
112
  expect(link.getAttribute("target")).toBe("_blank");
111
- const rows = [...document.body.querySelectorAll<HTMLElement>(".s-about-modal-row")];
112
- const licenseRow = rows.find((row) => row.textContent?.includes("BUSL-1.1"));
113
- expect(licenseRow, "license row renders").toBeTruthy();
114
- expect(licenseRow!.textContent).toContain("License");
115
113
  });
116
114
 
117
- it("renders the made-by row as plain text without originHref", async () => {
118
- mountAbout({ author: "由 伊欧 主创", origin: "来自 Celestia Island" });
115
+ it("renders the made-by sentence without a link when no href", async () => {
116
+ mountAbout({ madeByPrefix: "由", madeByName: "伊欧", madeBySuffix: " 主创" });
119
117
  await flushModal();
120
- expect(query<HTMLElement>(".s-about-modal-row").textContent).toContain("来自 Celestia Island");
118
+ expect(query<HTMLElement>(".s-about-modal-made-by").textContent).toBe("由伊欧 主创");
121
119
  expect(document.body.querySelector(".s-about-modal-row-link")).toBeNull();
122
120
  });
123
121
 
124
- it("renders the centered slogan and centered legal footer links", async () => {
122
+ it("renders software-component version rows with a meta pill and tone", async () => {
123
+ mountAbout({
124
+ componentVersions: [
125
+ { label: "WebUI 版本", value: "0.1.138 REW2HF", meta: "生产", metaTone: "positive" },
126
+ { label: "计算引擎版本", value: "0.1.0" },
127
+ ],
128
+ });
129
+ await flushModal();
130
+ const rows = [...document.body.querySelectorAll<HTMLElement>(".s-about-modal-row")];
131
+ expect(rows.length).toBe(2);
132
+ expect(rows[0]!.textContent).toContain("WebUI 版本");
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("renders centered license chips", async () => {
155
+ mountAbout({
156
+ licenses: [
157
+ { label: "SySL-1.0", href: "https://github.com/celestia-island/hikari/blob/master/LICENSE" },
158
+ { label: "BUSL-1.1", href: "https://github.com/celestia-island/shittim-chest/blob/master/LICENSE" },
159
+ ],
160
+ });
161
+ await flushModal();
162
+ const chips = [...document.body.querySelectorAll<HTMLAnchorElement>(".s-about-modal-link")];
163
+ expect(chips.map((chip) => chip.textContent)).toEqual(["SySL-1.0", "BUSL-1.1"]);
164
+ for (const chip of chips) expect(chip.getAttribute("target")).toBe("_blank");
165
+ });
166
+
167
+ it("renders centered legal footer links", async () => {
125
168
  mountAbout({
126
- slogan: "技术宅拯救世界。",
127
169
  footerLinks: [{ label: "京ICP备2026xxxx号", href: "https://beian.miit.gov.cn/" }],
128
170
  });
129
171
  await flushModal();
130
- expect(query<HTMLElement>(".s-about-modal-slogan").textContent).toBe("技术宅拯救世界。");
131
172
  const links = [
132
173
  ...document.body.querySelectorAll<HTMLAnchorElement>(".s-about-modal-footer-link"),
133
174
  ];
@@ -177,19 +218,49 @@ describe("HkAboutModal branding", () => {
177
218
  expect(badge.textContent).toContain(`© ${year} Test App`);
178
219
  });
179
220
 
180
- it("localizes the license label", async () => {
181
- mountAbout({ license: "BUSL-1.1" });
221
+ it("renders no branding rows when no branding props are given", async () => {
222
+ mountAbout();
182
223
  await flushModal();
183
- const labels = () => [
184
- ...document.body.querySelectorAll<HTMLElement>(".s-about-modal-row-label"),
185
- ];
224
+ expect(document.body.querySelectorAll(".s-about-modal-row").length).toBe(0);
225
+ expect(document.body.querySelector(".s-about-modal-made-by")).toBeNull();
226
+ expect(document.body.querySelector(".s-about-modal-links")).toBeNull();
227
+ expect(document.body.querySelector(".s-about-modal-chips")).toBeNull();
228
+ expect(document.body.querySelector(".s-about-modal-credits")).toBeNull();
229
+ });
186
230
 
187
- setLocale("zh-Hans");
231
+ it("groups license and link chips under one centered chips block", async () => {
232
+ mountAbout({
233
+ licenses: [{ label: "SySL-1.0", href: "https://example.test/sysl" }],
234
+ links: [{ label: "celestia.world", href: "https://celestia.world" }],
235
+ });
188
236
  await flushModal();
189
- expect(labels().some((el) => el.textContent === "许可证")).toBe(true);
237
+ const chips = query<HTMLElement>(".s-about-modal-chips");
238
+ const slots = [...chips.querySelectorAll<HTMLElement>(".s-about-modal-links")].map((el) =>
239
+ el.getAttribute("data-slot"),
240
+ );
241
+ expect(slots).toEqual(["licenses", "links"]);
242
+ expect(
243
+ [...chips.querySelectorAll<HTMLAnchorElement>(".s-about-modal-link")].map(
244
+ (chip) => chip.textContent,
245
+ ),
246
+ ).toEqual(["SySL-1.0", "celestia.world"]);
247
+ });
248
+
249
+ it("frames the logo image inside the identity frame", async () => {
250
+ mountAbout({ logoSrc: "data:image/webp;base64,AAA" });
251
+ await flushModal();
252
+ const frame = query<HTMLElement>(".s-about-modal-logo-frame");
253
+ expect(frame.querySelector("img.s-about-modal-logo-img")).toBeTruthy();
254
+ });
190
255
 
191
- setLocale("en");
256
+ it("keeps version and tagline on one subtitle line", async () => {
257
+ mountAbout({ version: "9.9.9", tagline: "Chat with your agents" });
192
258
  await flushModal();
193
- expect(labels().some((el) => el.textContent === "License")).toBe(true);
259
+ const subtitle = query<HTMLElement>(".s-about-modal-subtitle");
260
+ expect(subtitle.querySelector(".s-about-modal-version")?.textContent).toContain("9.9.9");
261
+ expect(subtitle.querySelector(".s-about-modal-tagline")?.textContent).toBe(
262
+ "Chat with your agents",
263
+ );
264
+ expect(subtitle.querySelector(".s-about-modal-subtitle-sep")).toBeTruthy();
194
265
  });
195
266
  });
@@ -10,18 +10,34 @@ export interface HAboutLink {
10
10
  href: string;
11
11
  }
12
12
 
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
+ */
23
+ export interface HAboutComponentVersion {
24
+ label: string;
25
+ value: string;
26
+ meta?: string;
27
+ metaTone?: "neutral" | "positive" | "caution";
28
+ }
29
+
13
30
  /**
14
31
  * HkAboutModal — version / about dialog.
15
32
  * (Upstreamed from shittim-chest's plana-legacy layer.)
16
33
  *
17
- * Shows the app identity and version metadata plus optional branding: a
18
- * logo image, tagline, centered slogan + organization line, a made-by /
19
- * origin row, license row, a decorative backdrop layer (canvas or anything
20
- * else, rendered behind the content and pointer-inert), external link
21
- * chips and centered legal footer links (e.g. ICP filings). Version/build
22
- * hashes render as short hashes when longer than 12 chars (full value in
23
- * `title` tooltip). Every branding prop is optional — the modal degrades
24
- * to the plain identity + version 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.
25
41
  */
26
42
  export const HkAboutModal = defineComponent({
27
43
  name: "HkAboutModal",
@@ -31,28 +47,27 @@ export const HkAboutModal = defineComponent({
31
47
  appName: { type: String, required: true },
32
48
  /** Application version (e.g. "0.1.4"). */
33
49
  version: { type: String, required: true },
34
- /** Optional app build hash / commit. */
35
- buildHash: { type: String, default: undefined },
36
- /** Optional engine version (backend), e.g. "0.2.1". */
37
- engineVersion: { type: String, default: undefined },
38
- /** Optional engine build hash / commit. */
39
- engineBuildHash: { type: String, default: undefined },
40
50
  /** Optional logo image URL — replaces the first-letter tile. */
41
51
  logoSrc: { type: String, default: undefined },
42
- /** Optional one-liner under the version (e.g. the app tagline). */
52
+ /** Optional one-liner shown beside the version (e.g. the app tagline). */
43
53
  tagline: { type: String, default: undefined },
44
- /** Optional centered slogan below the header (the app's one-liner). */
45
- slogan: { type: String, default: undefined },
46
- /** Optional centered small line under the slogan (organization blurb). */
54
+ /** Optional centered small line under the credits (organization blurb). */
47
55
  description: { type: String, default: undefined },
48
- /** Optional made-by row: left label (e.g. "由 伊欧 主创"). */
49
- author: { type: String, default: undefined },
50
- /** Optional right value of the made-by row (e.g. "来自 Celestia Island"). */
51
- origin: { type: String, default: undefined },
52
- /** Optional link applied to the origin value. */
53
- originHref: { type: String, default: undefined },
54
- /** Optional license identifier row (e.g. "BUSL-1.1"). */
55
- license: { type: String, default: undefined },
56
+ /** Made-by sentence: text before the linked author name (e.g. "由"). */
57
+ madeByPrefix: { type: String, default: undefined },
58
+ /** Made-by sentence: the linked author name (e.g. "伊欧"). */
59
+ madeByName: { type: String, default: undefined },
60
+ /** Link applied to the author name (e.g. the GitHub profile). */
61
+ madeByNameHref: { type: String, default: undefined },
62
+ /** Made-by sentence: text after the name (e.g. " 主创,来自 …"). */
63
+ madeBySuffix: { type: String, default: undefined },
64
+ /** License chips (e.g. SySL-1.0 / BUSL-1.1), rendered centered. */
65
+ licenses: { type: Array as PropType<HAboutLink[]>, default: () => [] },
66
+ /** Software-component version rows (WebUI / engines), label + value. */
67
+ componentVersions: {
68
+ type: Array as PropType<HAboutComponentVersion[]>,
69
+ default: () => [],
70
+ },
56
71
  /** Optional copyright holder in the footer (defaults to the app name). */
57
72
  copyright: { type: String, default: undefined },
58
73
  /** Optional external links (e.g. GitHub, docs). */
@@ -73,21 +88,47 @@ export const HkAboutModal = defineComponent({
73
88
  setup(props, { emit }) {
74
89
  const { t } = useI18n();
75
90
 
76
- function shortHash(hash: string): string {
77
- return hash.length > 12 ? `${hash.slice(0, 12)}…` : hash;
78
- }
79
-
80
- const renderOriginValue = () => {
81
- if (!props.originHref) return props.origin;
82
- return (
91
+ const renderMadeBy = () => {
92
+ if (!props.madeByName) return null;
93
+ const name = props.madeByNameHref ? (
83
94
  <a
84
95
  class="s-about-modal-row-link"
85
- href={props.originHref}
96
+ href={props.madeByNameHref}
86
97
  target="_blank"
87
98
  rel="noopener noreferrer"
88
99
  >
89
- {props.origin}
100
+ {props.madeByName}
90
101
  </a>
102
+ ) : (
103
+ <span>{props.madeByName}</span>
104
+ );
105
+ return (
106
+ <p class="s-about-modal-made-by">
107
+ {props.madeByPrefix}
108
+ {name}
109
+ {props.madeBySuffix}
110
+ </p>
111
+ );
112
+ };
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>
91
132
  );
92
133
  };
93
134
 
@@ -106,81 +147,70 @@ export const HkAboutModal = defineComponent({
106
147
  )}
107
148
  <div class="s-about-modal-body">
108
149
  <header class="s-about-modal-header">
109
- {props.logoSrc ? (
110
- <img class="s-about-modal-logo-img" src={props.logoSrc} alt="" draggable={false} />
111
- ) : (
112
- <div class="s-about-modal-logo">{props.appName.slice(0, 1).toUpperCase()}</div>
113
- )}
114
- <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">
115
163
  <h2 class="s-about-modal-name">{props.appName}</h2>
116
- <p class="s-about-modal-version">
117
- {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
+ )}
118
176
  </p>
119
- {props.tagline && <p class="s-about-modal-tagline">{props.tagline}</p>}
120
177
  </div>
121
178
  </header>
122
179
 
123
- {props.slogan && <p class="s-about-modal-slogan">{props.slogan}</p>}
124
- {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
+ )}
125
188
 
126
- <div class="s-about-modal-rows">
127
- {(props.author || props.origin) && (
128
- <div class="s-about-modal-row">
129
- <span class="s-about-modal-row-label">{props.author}</span>
130
- <span class="s-about-modal-row-value">{renderOriginValue()}</span>
131
- </div>
132
- )}
133
- {props.license && (
134
- <div class="s-about-modal-row">
135
- <span class="s-about-modal-row-label">
136
- {t("hikari::about.license", "License")}
137
- </span>
138
- <span class="s-about-modal-row-value">{props.license}</span>
139
- </div>
140
- )}
141
- {props.buildHash && (
142
- <div class="s-about-modal-row">
143
- <span class="s-about-modal-row-label">{t("hikari::about.buildHash", "Build")}</span>
144
- <span class="s-about-modal-row-value" title={props.buildHash}>
145
- {shortHash(props.buildHash)}
146
- </span>
147
- </div>
148
- )}
149
- {props.engineVersion && (
150
- <div class="s-about-modal-row">
151
- <span class="s-about-modal-row-label">
152
- {t("hikari::about.engineVersion", "Engine version")}
153
- </span>
154
- <span class="s-about-modal-row-value">{props.engineVersion}</span>
155
- </div>
156
- )}
157
- {props.engineBuildHash && (
158
- <div class="s-about-modal-row">
159
- <span class="s-about-modal-row-label">
160
- {t("hikari::about.engineBuildHash", "Engine build")}
161
- </span>
162
- <span class="s-about-modal-row-value" title={props.engineBuildHash}>
163
- {shortHash(props.engineBuildHash)}
164
- </span>
165
- </div>
166
- )}
167
- </div>
189
+ {props.componentVersions.length > 0 && (
190
+ <div class="s-about-modal-rows">
191
+ {props.componentVersions.map((component) => (
192
+ <div class="s-about-modal-row" key={component.label}>
193
+ <span class="s-about-modal-row-label">{component.label}</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>
205
+ </div>
206
+ ))}
207
+ </div>
208
+ )}
168
209
 
169
- {props.links.length > 0 && (
170
- <div class="s-about-modal-links">
171
- <div class="s-about-modal-links-list">
172
- {props.links.map((link) => (
173
- <a
174
- key={link.href}
175
- href={link.href}
176
- target="_blank"
177
- rel="noopener noreferrer"
178
- class="s-about-modal-link"
179
- >
180
- {link.label}
181
- </a>
182
- ))}
183
- </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")}
184
214
  </div>
185
215
  )}
186
216
 
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "فشل النسخ",
15
15
  "hikari::about.title": "حول",
16
16
  "hikari::about.version": "الإصدار",
17
- "hikari::about.buildHash": "البناء",
18
- "hikari::about.engineVersion": "إصدار المحرك",
19
- "hikari::about.engineBuildHash": "بناء المحرك",
20
- "hikari::about.license": "الترخيص",
21
17
  "hikari::theme.mode": "وضع السمة",
22
18
  "hikari::theme.modeLight": "فاتح",
23
19
  "hikari::theme.modeDark": "داكن",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "Kopieren fehlgeschlagen",
15
15
  "hikari::about.title": "Über",
16
16
  "hikari::about.version": "Version",
17
- "hikari::about.buildHash": "Build",
18
- "hikari::about.engineVersion": "Engine-Version",
19
- "hikari::about.engineBuildHash": "Engine-Build",
20
- "hikari::about.license": "Lizenz",
21
17
  "hikari::theme.mode": "Design-Modus",
22
18
  "hikari::theme.modeLight": "Hell",
23
19
  "hikari::theme.modeDark": "Dunkel",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "Copy failed",
15
15
  "hikari::about.title": "About",
16
16
  "hikari::about.version": "Version",
17
- "hikari::about.buildHash": "Build",
18
- "hikari::about.engineVersion": "Engine version",
19
- "hikari::about.engineBuildHash": "Engine build",
20
- "hikari::about.license": "License",
21
17
  "hikari::theme.mode": "Theme mode",
22
18
  "hikari::theme.modeLight": "Light",
23
19
  "hikari::theme.modeDark": "Dark",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "Error al copiar",
15
15
  "hikari::about.title": "Acerca de",
16
16
  "hikari::about.version": "Versión",
17
- "hikari::about.buildHash": "Compilación",
18
- "hikari::about.engineVersion": "Versión del motor",
19
- "hikari::about.engineBuildHash": "Compilación del motor",
20
- "hikari::about.license": "Licencia",
21
17
  "hikari::theme.mode": "Modo de tema",
22
18
  "hikari::theme.modeLight": "Claro",
23
19
  "hikari::theme.modeDark": "Oscuro",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "Échec de la copie",
15
15
  "hikari::about.title": "À propos",
16
16
  "hikari::about.version": "Version",
17
- "hikari::about.buildHash": "Build",
18
- "hikari::about.engineVersion": "Version du moteur",
19
- "hikari::about.engineBuildHash": "Build du moteur",
20
- "hikari::about.license": "Licence",
21
17
  "hikari::theme.mode": "Mode du thème",
22
18
  "hikari::theme.modeLight": "Clair",
23
19
  "hikari::theme.modeDark": "Sombre",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "コピーに失敗しました",
15
15
  "hikari::about.title": "このアプリについて",
16
16
  "hikari::about.version": "バージョン",
17
- "hikari::about.buildHash": "ビルド",
18
- "hikari::about.engineVersion": "エンジンバージョン",
19
- "hikari::about.engineBuildHash": "エンジンビルド",
20
- "hikari::about.license": "ライセンス",
21
17
  "hikari::theme.mode": "テーマモード",
22
18
  "hikari::theme.modeLight": "ライト",
23
19
  "hikari::theme.modeDark": "ダーク",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "복사 실패",
15
15
  "hikari::about.title": "정보",
16
16
  "hikari::about.version": "버전",
17
- "hikari::about.buildHash": "빌드",
18
- "hikari::about.engineVersion": "엔진 버전",
19
- "hikari::about.engineBuildHash": "엔진 빌드",
20
- "hikari::about.license": "라이선스",
21
17
  "hikari::theme.mode": "테마 모드",
22
18
  "hikari::theme.modeLight": "라이트",
23
19
  "hikari::theme.modeDark": "다크",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "Falha ao copiar",
15
15
  "hikari::about.title": "Sobre",
16
16
  "hikari::about.version": "Versão",
17
- "hikari::about.buildHash": "Build",
18
- "hikari::about.engineVersion": "Versão do motor",
19
- "hikari::about.engineBuildHash": "Build do motor",
20
- "hikari::about.license": "Licença",
21
17
  "hikari::theme.mode": "Modo do tema",
22
18
  "hikari::theme.modeLight": "Claro",
23
19
  "hikari::theme.modeDark": "Escuro",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "Не удалось скопировать",
15
15
  "hikari::about.title": "О программе",
16
16
  "hikari::about.version": "Версия",
17
- "hikari::about.buildHash": "Сборка",
18
- "hikari::about.engineVersion": "Версия движка",
19
- "hikari::about.engineBuildHash": "Сборка движка",
20
- "hikari::about.license": "Лицензия",
21
17
  "hikari::theme.mode": "Режим темы",
22
18
  "hikari::theme.modeLight": "Светлая",
23
19
  "hikari::theme.modeDark": "Тёмная",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "复制失败",
15
15
  "hikari::about.title": "关于",
16
16
  "hikari::about.version": "版本",
17
- "hikari::about.buildHash": "构建",
18
- "hikari::about.engineVersion": "引擎版本",
19
- "hikari::about.engineBuildHash": "引擎构建",
20
- "hikari::about.license": "许可证",
21
17
  "hikari::theme.mode": "主题模式",
22
18
  "hikari::theme.modeLight": "浅色",
23
19
  "hikari::theme.modeDark": "深色",
@@ -14,10 +14,6 @@
14
14
  "hikari::clipboard.copyFailed": "複製失敗",
15
15
  "hikari::about.title": "關於",
16
16
  "hikari::about.version": "版本",
17
- "hikari::about.buildHash": "建置",
18
- "hikari::about.engineVersion": "引擎版本",
19
- "hikari::about.engineBuildHash": "引擎建置",
20
- "hikari::about.license": "授權條款",
21
17
  "hikari::theme.mode": "主題模式",
22
18
  "hikari::theme.modeLight": "淺色",
23
19
  "hikari::theme.modeDark": "深色",
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 {