@celestia-island/hikari 0.44.0 → 0.45.0

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.44.0",
3
+ "version": "0.45.0",
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",
@@ -112,6 +112,15 @@ describe("HkAdminHeader", () => {
112
112
  expect(barText).not.toContain("alice");
113
113
  });
114
114
 
115
+ it("renders the letter fallback on the shared avatar primitive, no outline ring", () => {
116
+ const c = mount(headerNode({ title: "Providers" }));
117
+ const btn = avatarButton(c) as HTMLButtonElement;
118
+ // The old border-2 ring made the console avatar read as a different
119
+ // control from the chat frontend's (user report 2026-09-12).
120
+ expect(btn.className).not.toContain("border-2");
121
+ expect(btn.querySelector(".s-user-avatar")?.textContent).toBe("A");
122
+ });
123
+
115
124
  it("hides the title node entirely when it is empty", () => {
116
125
  const withTitle = mount(headerNode({ title: "Dashboard" }));
117
126
  expect(withTitle.textContent).toContain("Dashboard");
@@ -181,12 +181,7 @@ export const HkAdminHeader = defineComponent({
181
181
 
182
182
  <div ref={userTriggerRef} class="flex items-center gap-2 min-w-0">
183
183
  <button
184
- class={[
185
- "w-7 h-7 rounded-full overflow-hidden shrink-0 cursor-pointer transition-opacity relative group p-0 border-0",
186
- props.avatarUrl
187
- ? ""
188
- : "bg-primary/10 border-2 border-primary/15 hover:border-primary/30",
189
- ]}
184
+ class="w-7 h-7 rounded-full overflow-hidden shrink-0 cursor-pointer transition-opacity relative group p-0 border-0"
190
185
  aria-label={props.avatarTriggerLabel ?? t("hikari::adminHeader.avatarTrigger", "Account menu")}
191
186
  aria-haspopup={props.avatarAction === "drawer" ? "dialog" : "menu"}
192
187
  aria-expanded={props.avatarAction === "menu" ? userMenuOpen.value : undefined}
@@ -200,7 +195,12 @@ export const HkAdminHeader = defineComponent({
200
195
  onError={() => { avatarFailed.value = true; }}
201
196
  />
202
197
  ) : (
203
- <span class="text-xs font-bold text-primary/70">
198
+ // Letter fallback on the shared avatar primitive — the
199
+ // same filled-circle grammar as the chat frontend's
200
+ // trigger. The old border-2 ring made the console's
201
+ // avatar read as a different control (user report
202
+ // 2026-09-12: 前后台头像不一致).
203
+ <span class="s-user-avatar">
204
204
  {props.username?.charAt(0).toUpperCase() || "?"}
205
205
  </span>
206
206
  )}
@@ -789,6 +789,85 @@ describe("HkMenu sidebar variant", () => {
789
789
  expect(container.textContent).toContain("Products");
790
790
  expect(container.textContent).not.toContain("General");
791
791
  });
792
+
793
+ it("renders the header slot above the rows", () => {
794
+ const container = document.createElement("div");
795
+ document.body.appendChild(container);
796
+ containers.push(container);
797
+ const app = createApp({
798
+ render: () =>
799
+ h(
800
+ HkMenu,
801
+ { variant: "sidebar", open: true, items: navItems },
802
+ { header: () => h("div", { class: "sidebar-identity" }, "me@example.com") },
803
+ ),
804
+ });
805
+ mounts.push(app);
806
+ app.mount(container);
807
+
808
+ // The identity block leads the same inline surface (drawer account
809
+ // menu grammar) — first child of the nav, before any row.
810
+ const nav = container.querySelector(".hk-menu-sidebar");
811
+ const identity = nav?.querySelector(".sidebar-identity");
812
+ expect(identity).not.toBeNull();
813
+ expect(nav!.firstElementChild).toBe(identity);
814
+ });
815
+
816
+ it("renders the check column on rows that opt in (checked !== undefined)", async () => {
817
+ const selected: string[] = [];
818
+ const container = document.createElement("div");
819
+ document.body.appendChild(container);
820
+ containers.push(container);
821
+ const app = createApp({
822
+ render: () =>
823
+ h(HkMenu, {
824
+ variant: "sidebar",
825
+ open: true,
826
+ items: [
827
+ {
828
+ key: "locale",
829
+ label: "Language",
830
+ children: [
831
+ { key: "en", label: "English", checked: false },
832
+ { key: "zh", label: "中文", checked: true },
833
+ ],
834
+ },
835
+ { key: "logout", label: "Log out", danger: true },
836
+ ],
837
+ onSelect: (key: string) => selected.push(key),
838
+ }),
839
+ });
840
+ mounts.push(app);
841
+ app.mount(container);
842
+ await settle();
843
+
844
+ // Groups default collapsed — expand the locale group.
845
+ const toggle = container.querySelector(
846
+ ".hk-menu-sidebar-group-toggle",
847
+ ) as HTMLButtonElement;
848
+ toggle.click();
849
+ await settle();
850
+
851
+ const rowOf = (label: string) =>
852
+ [...container.querySelectorAll(".hk-menu-sidebar-row")].find(
853
+ (r) => r.textContent?.includes(label),
854
+ ) as HTMLButtonElement;
855
+ const en = rowOf("English");
856
+ const zh = rowOf("中文");
857
+ const logout = rowOf("Log out");
858
+
859
+ // Checked rows carry the cell; true renders the mark, false keeps
860
+ // the empty placeholder so labels stay aligned. Rows without the
861
+ // opt-in get no cell at all.
862
+ expect(en.querySelector(".hk-menu-check")).not.toBeNull();
863
+ expect(en.querySelector(".hk-menu-check")?.hasAttribute("data-on")).toBe(false);
864
+ expect(zh.querySelector(".hk-menu-check")?.hasAttribute("data-on")).toBe(true);
865
+ expect(logout.querySelector(".hk-menu-check")).toBeNull();
866
+
867
+ en.click();
868
+ await settle();
869
+ expect(selected).toEqual(["en"]);
870
+ });
792
871
  });
793
872
 
794
873
  describe("HkMenu close linger (leave-transition window)", () => {
@@ -723,6 +723,7 @@ export default defineComponent({
723
723
  class="hk-menu-sidebar-row"
724
724
  data-depth={depth || undefined}
725
725
  data-active={active || undefined}
726
+ data-checked={item.checked || undefined}
726
727
  data-danger={item.danger || undefined}
727
728
  data-disabled={item.disabled || undefined}
728
729
  style={
@@ -735,6 +736,15 @@ export default defineComponent({
735
736
  emit("select", item.key, item);
736
737
  }}
737
738
  >
739
+ {/* Check column on opt-in rows (checked !== undefined): the
740
+ same fixed cell grammar the popup rows use, so a sidebar
741
+ locale/account picker shows its active entry in place.
742
+ false renders the empty placeholder to keep labels aligned. */}
743
+ {item.checked !== undefined && (
744
+ <span class="hk-menu-check" data-on={item.checked || undefined} aria-hidden="true">
745
+ {item.checked ? <Check size={12} /> : null}
746
+ </span>
747
+ )}
738
748
  {item.icon && (
739
749
  <span class="hk-menu-item-icon">{h(item.icon, { size: 16 })}</span>
740
750
  )}
@@ -782,6 +792,11 @@ export default defineComponent({
782
792
  function renderSidebar() {
783
793
  return (
784
794
  <nav class="hk-menu-sidebar" aria-label={props.title || "menu"}>
795
+ {/* Header slot (identity block, section label, …) above the
796
+ rows — mirrors the popup variant's level-0 header, so an
797
+ account menu renders the same grammar whether it pops up
798
+ on desktop or lives inside a drawer on mobile. */}
799
+ {slots.header?.()}
785
800
  {props.items.map((it) => renderSidebarItem(it, 0))}
786
801
  </nav>
787
802
  );