@celestia-island/hikari 0.55.245 → 0.55.246

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.245",
3
+ "version": "0.55.246",
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",
@@ -0,0 +1,33 @@
1
+ // AI-generated: developed with the DeepSeek Harness agent (model DeepSeek V4.1 Flash); human-directed.
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. */
7
+
8
+ .hk-date-tag {
9
+ display: inline-flex;
10
+ flex-direction: column;
11
+ align-items: center;
12
+ padding: var(--space-2, 2px) var(--space-10, 0.625rem);
13
+ border-radius: var(--radius-full, 999px);
14
+ background: rgb(var(--color-text) / 6%);
15
+ white-space: nowrap;
16
+ }
17
+
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);
23
+ font-variant-numeric: tabular-nums;
24
+ color: rgb(var(--color-text));
25
+ }
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 {
31
+ color: rgb(var(--color-text) / 55%);
32
+ font-size: calc(var(--text-2xs, 0.625rem) - 1px);
33
+ }
@@ -0,0 +1,135 @@
1
+ // AI-generated: developed with the DeepSeek Harness agent (model DeepSeek V4.1 Flash); human-directed.
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { createApp, h, nextTick } from "vue";
4
+
5
+ import HkDateTagDefault, { HkDateTag } from "./HkDateTag";
6
+ import { setLocale } from "../i18n/context";
7
+
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.
14
+ */
15
+
16
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
17
+
18
+ // 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
+ const SHIP_DATE = new Date(2024, 9, 6, 14, 30);
21
+
22
+ function mountTag(props: { value: string | number | Date; time?: boolean }): HTMLElement {
23
+ const container = document.createElement("div");
24
+ document.body.appendChild(container);
25
+ const app = createApp({ render: () => h(HkDateTag, props) });
26
+ app.mount(container);
27
+ mounts.push({ app, container });
28
+ return container;
29
+ }
30
+
31
+ function rowsOf(container: HTMLElement): HTMLElement[] {
32
+ return [...container.querySelectorAll<HTMLElement>(".hk-date-tag__row")];
33
+ }
34
+
35
+ function capsuleOf(container: HTMLElement): HTMLElement {
36
+ const el = container.querySelector<HTMLElement>(".hk-date-tag");
37
+ expect(el, "capsule renders").toBeTruthy();
38
+ return el!;
39
+ }
40
+
41
+ // setLocale mutates the module-global i18n state — restore the default
42
+ // after every case (the HkSolarLocationSetting suite's idiom).
43
+ afterEach(async () => {
44
+ for (const { app, container } of mounts.splice(0)) {
45
+ app.unmount();
46
+ container.remove();
47
+ }
48
+ await setLocale("en");
49
+ });
50
+
51
+ describe("HkDateTag locale ordering", () => {
52
+ it("renders year/month/day rows top to bottom for zh-Hans", async () => {
53
+ await setLocale("zh-Hans");
54
+ const container = mountTag({ value: SHIP_DATE });
55
+
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);
63
+ });
64
+
65
+ it("orders month/day/year for en — the year goes LAST", async () => {
66
+ await setLocale("en");
67
+ const container = mountTag({ value: SHIP_DATE });
68
+
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);
72
+ });
73
+
74
+ it("re-orders the rows live when the locale flips under a mounted capsule", async () => {
75
+ await setLocale("zh-Hans");
76
+ const container = mountTag({ value: SHIP_DATE });
77
+ expect(rowsOf(container).map((r) => r.textContent)).toEqual(["2024", "10", "06"]);
78
+
79
+ await setLocale("en");
80
+ await nextTick();
81
+ expect(rowsOf(container).map((r) => r.textContent)).toEqual(["10", "06", "2024"]);
82
+
83
+ await setLocale("zh-Hans");
84
+ await nextTick();
85
+ expect(rowsOf(container).map((r) => r.textContent)).toEqual(["2024", "10", "06"]);
86
+ });
87
+ });
88
+
89
+ describe("HkDateTag time row", () => {
90
+ it("appends the muted time row only when asked", async () => {
91
+ await setLocale("zh-Hans");
92
+ const withTime = rowsOf(mountTag({ value: SHIP_DATE, time: true }));
93
+ expect(withTime).toHaveLength(4);
94
+ expect(withTime[3].classList.contains("hk-date-tag__row--time")).toBe(true);
95
+ expect(withTime[3].textContent).toBe("14:30");
96
+
97
+ const withoutTime = rowsOf(mountTag({ value: SHIP_DATE }));
98
+ expect(withoutTime).toHaveLength(3);
99
+ expect(withoutTime.some((r) => r.classList.contains("hk-date-tag__row--time"))).toBe(false);
100
+ });
101
+
102
+ it("formats the time row for the active locale", async () => {
103
+ 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");
107
+ });
108
+ });
109
+
110
+ describe("HkDateTag value normalization", () => {
111
+ it("accepts epoch milliseconds and date strings like the Date form", async () => {
112
+ await setLocale("en");
113
+ const expected = ["10", "06", "2024"];
114
+ expect(rowsOf(mountTag({ value: SHIP_DATE })).map((r) => r.textContent)).toEqual(expected);
115
+ // 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);
118
+ });
119
+
120
+ it("renders the empty capsule shell for unparseable input", async () => {
121
+ await setLocale("zh-Hans");
122
+ const container = mountTag({ value: "not-a-date" });
123
+
124
+ const capsule = capsuleOf(container);
125
+ expect(capsule.children).toHaveLength(0);
126
+ expect(rowsOf(container)).toHaveLength(0);
127
+ });
128
+ });
129
+
130
+ describe("HkDateTag module surface", () => {
131
+ it("exports the same component as the default and named binding", () => {
132
+ expect(HkDateTagDefault).toBe(HkDateTag);
133
+ expect((HkDateTag as unknown as { name: string }).name).toBe("HkDateTag");
134
+ });
135
+ });
@@ -0,0 +1,99 @@
1
+ // AI-generated: developed with the DeepSeek Harness agent (model DeepSeek V4.1 Flash); human-directed.
2
+ import { computed, defineComponent, type PropType } from "vue";
3
+
4
+ import { activeLocale } from "../i18n/context";
5
+ import "./HkDateTag.scss";
6
+
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).
14
+ *
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.
19
+ *
20
+ * Contract:
21
+ * - `value` accepts a Date, an epoch-MILLISECONDS number or a
22
+ * date-ish string, normalized with `new Date(value)` (so a number is
23
+ * NEVER seconds — pass what `Date.now()` returns). Unparseable input
24
+ * renders NOTHING but the empty capsule shell; the caller owns the
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.
34
+ */
35
+ export const HkDateTag = defineComponent({
36
+ name: "HkDateTag",
37
+ props: {
38
+ /** The date to render — Date, epoch milliseconds or a parseable
39
+ * string. Invalid input renders the empty capsule. */
40
+ value: {
41
+ type: [String, Number, Date] as PropType<string | number | Date>,
42
+ required: true,
43
+ },
44
+ /** Append the muted locale time row (hour + minute, 2-digit). */
45
+ time: { type: Boolean, default: false },
46
+ },
47
+ setup(props) {
48
+ const date = computed<Date | null>(() => {
49
+ const d = new Date(props.value);
50
+ return Number.isNaN(d.getTime()) ? null : d;
51
+ });
52
+
53
+ // The locale is read through the reactive i18n state (activeLocale
54
+ // reads it on every evaluation), so this computed re-runs — and the
55
+ // rows re-order — whenever the host calls setLocale. One formatter
56
+ // per shape per locale, rebuilt only on a locale/value change (the
57
+ // HkCalendarHeatmap idiom).
58
+ const locale = computed(() => activeLocale());
59
+
60
+ /** The year/month/day segments in the locale's own order. */
61
+ const rows = computed<Intl.DateTimeFormatPart[]>(() => {
62
+ const d = date.value;
63
+ if (d === null) return [];
64
+ return new Intl.DateTimeFormat(locale.value, {
65
+ year: "numeric",
66
+ month: "2-digit",
67
+ day: "2-digit",
68
+ })
69
+ .formatToParts(d)
70
+ .filter((part) => part.type === "year" || part.type === "month" || part.type === "day");
71
+ });
72
+
73
+ /** The locale time (hour + minute, 2-digit) when the capsule opts
74
+ * in — null without `time` or for an invalid date. */
75
+ const timeText = computed<string | null>(() => {
76
+ const d = date.value;
77
+ if (!props.time || d === null) return null;
78
+ return new Intl.DateTimeFormat(locale.value, { hour: "2-digit", minute: "2-digit" }).format(d);
79
+ });
80
+
81
+ return () => (
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}
89
+ </span>
90
+ ))}
91
+ {timeText.value !== null && (
92
+ <span key="time" class="hk-date-tag__row hk-date-tag__row--time">{timeText.value}</span>
93
+ )}
94
+ </span>
95
+ );
96
+ },
97
+ });
98
+
99
+ export default HkDateTag;
package/src/index.ts CHANGED
@@ -555,6 +555,7 @@ export { HkNavSidebar as HNavSidebar } from "./components/HkNavSidebar";
555
555
  export { HkThemeToggle as HThemeToggle, type ThemeItemScope } from "./components/HkThemeToggle";
556
556
  export { HkThemePanelContent as HThemePanelContent } from "./components/HkThemePanelContent";
557
557
  export { HkSolarLocationSetting as HSolarLocationSetting } from "./components/HkSolarLocationSetting";
558
+ export { HkDateTag as HDateTag } from "./components/HkDateTag";
558
559
  export { HkAuthCard as HAuthCard } from "./components/HkAuthCard";
559
560
  export { default as HAuthMethodList } from "./components/HkAuthMethodList";
560
561
  export { HkSignInCard as HSignInCard } from "./components/HkSignInCard";
@@ -836,6 +837,7 @@ export { HkNavSidebar as HkNavSidebar } from "./components/HkNavSidebar";
836
837
  export { HkThemeToggle as HkThemeToggle } from "./components/HkThemeToggle";
837
838
  export { HkThemePanelContent as HkThemePanelContent } from "./components/HkThemePanelContent";
838
839
  export { HkSolarLocationSetting as HkSolarLocationSetting } from "./components/HkSolarLocationSetting";
840
+ export { HkDateTag as HkDateTag } from "./components/HkDateTag";
839
841
  export { HkAuthCard as HkAuthCard } from "./components/HkAuthCard";
840
842
  export { HkSignInCard as HkSignInCard } from "./components/HkSignInCard";
841
843
  export { HkThirdPartySignInCard as HkThirdPartySignInCard } from "./components/HkThirdPartySignInCard";