@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
|
@@ -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
|
-
|
|
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
|
);
|