@celestia-island/hikari 0.55.258 → 0.55.259

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.55.258",
3
+ "version": "0.55.259",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -1,33 +1,43 @@
1
1
  // AI-generated: developed with the DeepSeek Harness agent (model DeepSeek V4.1 Flash); human-directed.
2
2
 
3
- /* HkDateTag — the locale-aware stacked date capsule (see the component
4
- * docblock for the contract). Rows are calendar segments; the year and
5
- * the optional time row read as context (muted, a touch smaller), the
6
- * month/day figures carry the full text color. */
3
+ /* HkDateTag — the locale-aware one-line date pill (see the component
4
+ * docblock for the contract). Segments are the calendar figures laid
5
+ * out on ONE horizontal line in the active locale's order; the year
6
+ * and the optional time segment read as context (muted ink). */
7
7
 
8
8
  .hk-date-tag {
9
9
  display: inline-flex;
10
- flex-direction: column;
11
- align-items: center;
10
+ flex-direction: row;
11
+ align-items: baseline;
12
12
  padding: var(--space-2, 2px) var(--space-10, 0.625rem);
13
13
  border-radius: var(--radius-full, 999px);
14
14
  background: rgb(var(--color-text) / 6%);
15
15
  white-space: nowrap;
16
+ /* One size for the whole pill: segments AND separators inherit it,
17
+ * so a large ambient host font cannot inflate the separators. */
18
+ font-size: var(--text-2xs, 0.625rem);
19
+ line-height: 1.4;
16
20
  }
17
21
 
18
- .hk-date-tag__row {
19
- display: block;
20
- text-align: center;
21
- line-height: 1.25;
22
- font-size: var(--text-2xs, 0.625rem);
22
+ .hk-date-tag__seg {
23
23
  font-variant-numeric: tabular-nums;
24
24
  color: rgb(var(--color-text));
25
25
  }
26
26
 
27
- /* Context rows: the year (wherever the locale puts it) and the optional
28
- * time row — muted and a touch smaller than the month/day figures. */
29
- .hk-date-tag__row--year,
30
- .hk-date-tag__row--time {
27
+ /* Context segments: the year (wherever the locale puts it) and the
28
+ * optional time segment — muted, same size so the one-line baseline
29
+ * stays even. */
30
+ .hk-date-tag__seg--year,
31
+ .hk-date-tag__seg--time {
32
+ color: rgb(var(--color-text) / 55%);
33
+ }
34
+
35
+ /* The locale's own separator literals (and the blank one ahead of the
36
+ * time segment) — muted like the context ink. `pre` keeps that blank
37
+ * separator's single space alive: a space-only flex item would
38
+ * otherwise trim it at line end under the pill's nowrap and glue the
39
+ * time segment to the date. */
40
+ .hk-date-tag__sep {
31
41
  color: rgb(var(--color-text) / 55%);
32
- font-size: calc(var(--text-2xs, 0.625rem) - 1px);
42
+ white-space: pre;
33
43
  }
@@ -6,17 +6,18 @@ import HkDateTagDefault, { HkDateTag } from "./HkDateTag";
6
6
  import { setLocale } from "../i18n/context";
7
7
 
8
8
  /**
9
- * HkDateTag — the locale-aware date capsule. These cases pin the
10
- * COMPONENT contract: the row order follows the ACTIVE hikari locale
11
- * (year on top for zh-Hans, year LAST for en), the order re-derives
12
- * live when the host flips the locale, `time` appends the muted
13
- * fourth row, and unparseable input renders the empty capsule shell.
9
+ * HkDateTag — the locale-aware one-line date pill. These cases pin the
10
+ * COMPONENT contract: the segment order follows the ACTIVE hikari
11
+ * locale (year first for zh-Hans, year LAST for en) with that locale's
12
+ * own separators, the order re-derives live when the host flips the
13
+ * locale, `time` appends the muted trailing segment, and unparseable
14
+ * input renders the empty pill shell.
14
15
  */
15
16
 
16
17
  const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
17
18
 
18
19
  // Built in LOCAL time so the calendar fields (2024-10-06 14:30) survive
19
- // every CI time zone; the capsule renders local calendar segments.
20
+ // every CI time zone; the pill renders local calendar segments.
20
21
  const SHIP_DATE = new Date(2024, 9, 6, 14, 30);
21
22
 
22
23
  function mountTag(props: { value: string | number | Date; time?: boolean }): HTMLElement {
@@ -28,16 +29,20 @@ function mountTag(props: { value: string | number | Date; time?: boolean }): HTM
28
29
  return container;
29
30
  }
30
31
 
31
- function rowsOf(container: HTMLElement): HTMLElement[] {
32
- return [...container.querySelectorAll<HTMLElement>(".hk-date-tag__row")];
32
+ function segsOf(container: HTMLElement): HTMLElement[] {
33
+ return [...container.querySelectorAll<HTMLElement>(".hk-date-tag__seg")];
33
34
  }
34
35
 
35
- function capsuleOf(container: HTMLElement): HTMLElement {
36
+ function pillOf(container: HTMLElement): HTMLElement {
36
37
  const el = container.querySelector<HTMLElement>(".hk-date-tag");
37
- expect(el, "capsule renders").toBeTruthy();
38
+ expect(el, "pill renders").toBeTruthy();
38
39
  return el!;
39
40
  }
40
41
 
42
+ function textOf(container: HTMLElement): string {
43
+ return pillOf(container).textContent ?? "";
44
+ }
45
+
41
46
  // setLocale mutates the module-global i18n state — restore the default
42
47
  // after every case (the HkSolarLocationSetting suite's idiom).
43
48
  afterEach(async () => {
@@ -49,81 +54,91 @@ afterEach(async () => {
49
54
  });
50
55
 
51
56
  describe("HkDateTag locale ordering", () => {
52
- it("renders year/month/day rows top to bottom for zh-Hans", async () => {
57
+ it("lays year/month/day segments out on one line for zh-Hans", async () => {
53
58
  await setLocale("zh-Hans");
54
59
  const container = mountTag({ value: SHIP_DATE });
55
60
 
56
- const capsule = capsuleOf(container);
57
- expect(capsule.classList.contains("hk-date-tag")).toBe(true);
58
- const rows = rowsOf(container);
59
- expect(rows.map((r) => r.textContent)).toEqual(["2024", "10", "06"]);
60
- // The year row carries its modifier wherever it lands.
61
- expect(rows[0].classList.contains("hk-date-tag__row--year")).toBe(true);
62
- expect(rows[1].classList.contains("hk-date-tag__row--year")).toBe(false);
61
+ const pill = pillOf(container);
62
+ expect(pill.classList.contains("hk-date-tag")).toBe(true);
63
+ // One horizontal run of segments, in the locale's order, joined by
64
+ // the locale's own separators.
65
+ const segs = segsOf(container);
66
+ expect(segs.map((s) => s.textContent)).toEqual(["2024", "10", "06"]);
67
+ expect(textOf(container)).toBe("2024/10/06");
68
+ // The year segment carries its modifier wherever it lands.
69
+ expect(segs[0].classList.contains("hk-date-tag__seg--year")).toBe(true);
70
+ expect(segs[1].classList.contains("hk-date-tag__seg--year")).toBe(false);
63
71
  });
64
72
 
65
73
  it("orders month/day/year for en — the year goes LAST", async () => {
66
74
  await setLocale("en");
67
75
  const container = mountTag({ value: SHIP_DATE });
68
76
 
69
- const rows = rowsOf(container);
70
- expect(rows.map((r) => r.textContent)).toEqual(["10", "06", "2024"]);
71
- expect(rows[2].classList.contains("hk-date-tag__row--year")).toBe(true);
77
+ const segs = segsOf(container);
78
+ expect(segs.map((s) => s.textContent)).toEqual(["10", "06", "2024"]);
79
+ expect(textOf(container)).toBe("10/06/2024");
80
+ expect(segs[2].classList.contains("hk-date-tag__seg--year")).toBe(true);
72
81
  });
73
82
 
74
- it("re-orders the rows live when the locale flips under a mounted capsule", async () => {
83
+ it("re-orders the line live when the locale flips under a mounted pill", async () => {
75
84
  await setLocale("zh-Hans");
76
85
  const container = mountTag({ value: SHIP_DATE });
77
- expect(rowsOf(container).map((r) => r.textContent)).toEqual(["2024", "10", "06"]);
86
+ expect(segsOf(container).map((s) => s.textContent)).toEqual(["2024", "10", "06"]);
78
87
 
79
88
  await setLocale("en");
80
89
  await nextTick();
81
- expect(rowsOf(container).map((r) => r.textContent)).toEqual(["10", "06", "2024"]);
90
+ expect(segsOf(container).map((s) => s.textContent)).toEqual(["10", "06", "2024"]);
82
91
 
83
92
  await setLocale("zh-Hans");
84
93
  await nextTick();
85
- expect(rowsOf(container).map((r) => r.textContent)).toEqual(["2024", "10", "06"]);
94
+ expect(segsOf(container).map((s) => s.textContent)).toEqual(["2024", "10", "06"]);
86
95
  });
87
96
  });
88
97
 
89
- describe("HkDateTag time row", () => {
90
- it("appends the muted time row only when asked", async () => {
98
+ describe("HkDateTag time segment", () => {
99
+ it("appends the muted time segment only when asked", async () => {
91
100
  await setLocale("zh-Hans");
92
- const withTime = rowsOf(mountTag({ value: SHIP_DATE, time: true }));
101
+ const withTimeContainer = mountTag({ value: SHIP_DATE, time: true });
102
+ const withTime = segsOf(withTimeContainer);
93
103
  expect(withTime).toHaveLength(4);
94
- expect(withTime[3].classList.contains("hk-date-tag__row--time")).toBe(true);
104
+ expect(withTime[3].classList.contains("hk-date-tag__seg--time")).toBe(true);
95
105
  expect(withTime[3].textContent).toBe("14:30");
106
+ // The blank separator between the date and the time reads as one
107
+ // space in the pill's text.
108
+ expect(textOf(withTimeContainer)).toBe("2024/10/06 14:30");
96
109
 
97
- const withoutTime = rowsOf(mountTag({ value: SHIP_DATE }));
110
+ const withoutTimeContainer = mountTag({ value: SHIP_DATE });
111
+ const withoutTime = segsOf(withoutTimeContainer);
98
112
  expect(withoutTime).toHaveLength(3);
99
- expect(withoutTime.some((r) => r.classList.contains("hk-date-tag__row--time"))).toBe(false);
113
+ expect(withoutTime.some((s) => s.classList.contains("hk-date-tag__seg--time"))).toBe(false);
100
114
  });
101
115
 
102
- it("formats the time row for the active locale", async () => {
116
+ it("formats the time segment for the active locale", async () => {
103
117
  await setLocale("en");
104
- const rows = rowsOf(mountTag({ value: SHIP_DATE, time: true }));
105
- expect(rows).toHaveLength(4);
106
- expect(rows[3].textContent).toBe("02:30 PM");
118
+ const container = mountTag({ value: SHIP_DATE, time: true });
119
+ const segs = segsOf(container);
120
+ expect(segs).toHaveLength(4);
121
+ expect(segs[3].textContent).toBe("02:30 PM");
122
+ expect(textOf(container)).toBe("10/06/2024 02:30 PM");
107
123
  });
108
124
  });
109
125
 
110
126
  describe("HkDateTag value normalization", () => {
111
127
  it("accepts epoch milliseconds and date strings like the Date form", async () => {
112
128
  await setLocale("en");
113
- const expected = ["10", "06", "2024"];
114
- expect(rowsOf(mountTag({ value: SHIP_DATE })).map((r) => r.textContent)).toEqual(expected);
129
+ expect(textOf(mountTag({ value: SHIP_DATE }))).toBe("10/06/2024");
115
130
  // A number is epoch MILLISECONDS.
116
- expect(rowsOf(mountTag({ value: SHIP_DATE.getTime() })).map((r) => r.textContent)).toEqual(expected);
117
- expect(rowsOf(mountTag({ value: "2024-10-06T14:30:00" })).map((r) => r.textContent)).toEqual(expected);
131
+ expect(textOf(mountTag({ value: SHIP_DATE.getTime() }))).toBe("10/06/2024");
132
+ expect(textOf(mountTag({ value: "2024-10-06T14:30:00" }))).toBe("10/06/2024");
118
133
  });
119
134
 
120
- it("renders the empty capsule shell for unparseable input", async () => {
135
+ it("renders the empty pill shell for unparseable input", async () => {
121
136
  await setLocale("zh-Hans");
122
137
  const container = mountTag({ value: "not-a-date" });
123
138
 
124
- const capsule = capsuleOf(container);
125
- expect(capsule.children).toHaveLength(0);
126
- expect(rowsOf(container)).toHaveLength(0);
139
+ const pill = pillOf(container);
140
+ expect(pill.children).toHaveLength(0);
141
+ expect(segsOf(container)).toHaveLength(0);
127
142
  });
128
143
  });
129
144
 
@@ -5,43 +5,44 @@ import { activeLocale } from "../i18n/context";
5
5
  import "./HkDateTag.scss";
6
6
 
7
7
  /**
8
- * HkDateTag — a locale-aware date capsule: a small pill whose calendar
9
- * segments are stacked vertically, ONE PER ROW, each centered, ordered
10
- * the way the ACTIVE hikari locale orders dates (zh: year → month → day
11
- * top to bottom; en: month → day → year, the year LAST; de: day first …).
12
- * With the `time` flag a fourth, muted row carries the locale time
13
- * (hour + minute, 2-digit each).
8
+ * HkDateTag — a locale-aware date pill: ONE horizontal line whose
9
+ * calendar segments read in the order the ACTIVE hikari locale writes
10
+ * dates (zh: 2024/10/06 — year first; en: 10/06/2024 — the year LAST;
11
+ * de: 06.10.2024 — day first), joined by that locale's own literal
12
+ * separators. With the `time` flag a fourth, muted segment carries the
13
+ * locale time (hour + minute, 2-digit) after a blank separator.
14
14
  *
15
15
  * Generic business part (peer of HkTemplateText / HkMarkdownRenderer,
16
- * NOT of the HkInput family): hosts render compact per-row dates in data
17
- * tables and cards — wowsp's water-meter ship tables are the first
18
- * consumer.
16
+ * NOT of the HkInput family): hosts render compact per-row dates in
17
+ * data tables and cards — wowsp's water-meter ship tables are the
18
+ * first consumer.
19
19
  *
20
20
  * Contract:
21
21
  * - `value` accepts a Date, an epoch-MILLISECONDS number or a
22
22
  * date-ish string, normalized with `new Date(value)` (so a number is
23
23
  * NEVER seconds — pass what `Date.now()` returns). Unparseable input
24
- * renders NOTHING but the empty capsule shell; the caller owns the
24
+ * renders NOTHING but the empty pill shell; the caller owns the
25
25
  * empty state.
26
- * - The row order is derived from the locale itself, never a hardcoded
27
- * locale→order map: `Intl.DateTimeFormat(activeLocale(), { year,
28
- * month, day }).formatToParts(date)` supplies the year/month/day
29
- * segments in the order that locale writes them. The component is a
30
- * computed over the reactive i18n state, so a host `setLocale`
31
- * re-orders the rows live.
32
- * - `time` (default false) appends the muted time row
33
- * (`hk-date-tag__row--time`) under the date rows.
26
+ * - The segment order AND the separators come from the locale itself,
27
+ * never a hardcoded locale→layout map:
28
+ * `Intl.DateTimeFormat(activeLocale(), { year, month, day })
29
+ * .formatToParts(date)` supplies the year/month/day segments and
30
+ * the literal separators in the order that locale writes them. The
31
+ * component is a computed over the reactive i18n state, so a host
32
+ * `setLocale` re-orders the line live.
33
+ * - `time` (default false) appends the muted time segment
34
+ * (`hk-date-tag__seg--time`) after a blank separator.
34
35
  */
35
36
  export const HkDateTag = defineComponent({
36
37
  name: "HkDateTag",
37
38
  props: {
38
39
  /** The date to render — Date, epoch milliseconds or a parseable
39
- * string. Invalid input renders the empty capsule. */
40
+ * string. Invalid input renders the empty pill. */
40
41
  value: {
41
42
  type: [String, Number, Date] as PropType<string | number | Date>,
42
43
  required: true,
43
44
  },
44
- /** Append the muted locale time row (hour + minute, 2-digit). */
45
+ /** Append the muted locale time segment (hour + minute, 2-digit). */
45
46
  time: { type: Boolean, default: false },
46
47
  },
47
48
  setup(props) {
@@ -52,25 +53,24 @@ export const HkDateTag = defineComponent({
52
53
 
53
54
  // The locale is read through the reactive i18n state (activeLocale
54
55
  // reads it on every evaluation), so this computed re-runs — and the
55
- // rows re-order — whenever the host calls setLocale. One formatter
56
+ // line re-orders — whenever the host calls setLocale. One formatter
56
57
  // per shape per locale, rebuilt only on a locale/value change (the
57
58
  // HkCalendarHeatmap idiom).
58
59
  const locale = computed(() => activeLocale());
59
60
 
60
- /** The year/month/day segments in the locale's own order. */
61
- const rows = computed<Intl.DateTimeFormatPart[]>(() => {
61
+ /** Every format part — the year/month/day segments interleaved
62
+ * with the locale's own literal separators. */
63
+ const parts = computed<Intl.DateTimeFormatPart[]>(() => {
62
64
  const d = date.value;
63
65
  if (d === null) return [];
64
66
  return new Intl.DateTimeFormat(locale.value, {
65
67
  year: "numeric",
66
68
  month: "2-digit",
67
69
  day: "2-digit",
68
- })
69
- .formatToParts(d)
70
- .filter((part) => part.type === "year" || part.type === "month" || part.type === "day");
70
+ }).formatToParts(d);
71
71
  });
72
72
 
73
- /** The locale time (hour + minute, 2-digit) when the capsule opts
73
+ /** The locale time (hour + minute, 2-digit) when the pill opts
74
74
  * in — null without `time` or for an invalid date. */
75
75
  const timeText = computed<string | null>(() => {
76
76
  const d = date.value;
@@ -80,16 +80,31 @@ export const HkDateTag = defineComponent({
80
80
 
81
81
  return () => (
82
82
  <span class="hk-date-tag">
83
- {rows.value.map((part) => (
84
- <span
85
- key={part.type}
86
- class={part.type === "year" ? "hk-date-tag__row hk-date-tag__row--year" : "hk-date-tag__row"}
87
- >
88
- {part.value}
83
+ {parts.value.map((part, index) =>
84
+ part.type === "literal" ? (
85
+ part.value ? (
86
+ <span key={`sep-${index}`} class="hk-date-tag__sep">
87
+ {part.value}
88
+ </span>
89
+ ) : null
90
+ ) : (
91
+ <span
92
+ key={part.type}
93
+ class={part.type === "year" ? "hk-date-tag__seg hk-date-tag__seg--year" : "hk-date-tag__seg"}
94
+ >
95
+ {part.value}
96
+ </span>
97
+ ),
98
+ )}
99
+ {timeText.value !== null && (
100
+ <span key="time-sep" class="hk-date-tag__sep">
101
+ {" "}
89
102
  </span>
90
- ))}
103
+ )}
91
104
  {timeText.value !== null && (
92
- <span key="time" class="hk-date-tag__row hk-date-tag__row--time">{timeText.value}</span>
105
+ <span key="time" class="hk-date-tag__seg hk-date-tag__seg--time">
106
+ {timeText.value}
107
+ </span>
93
108
  )}
94
109
  </span>
95
110
  );