@celestia-island/hikari 0.55.58 → 0.55.59

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.58",
3
+ "version": "0.55.59",
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",
@@ -0,0 +1,94 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h } from "vue";
3
+
4
+ import HkCard from "./HkCard";
5
+
6
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
7
+
8
+ function mountCard(props: Record<string, unknown> = {}, slotText = "content") {
9
+ const container = document.createElement("div");
10
+ document.body.appendChild(container);
11
+ const app = createApp({
12
+ render: () =>
13
+ h(HkCard, props, { default: () => h("span", { class: "slot-marker" }, slotText) }),
14
+ });
15
+ app.mount(container);
16
+ mounts.push({ app, container });
17
+ return container;
18
+ }
19
+
20
+ afterEach(() => {
21
+ for (const m of mounts) m.app.unmount();
22
+ mounts.length = 0;
23
+ });
24
+
25
+ describe("HkCard — scrollable body", () => {
26
+ it("default card renders no scroll machinery", () => {
27
+ const c = mountCard();
28
+ expect(c.querySelector(".hk-card-scrollable")).toBeNull();
29
+ expect(c.querySelector(".hk-scroll-container")).toBeNull();
30
+ // The slot lands directly in the body, no pad wrapper in between.
31
+ expect(c.querySelector(".hk-card-body .slot-marker")).not.toBeNull();
32
+ expect(c.querySelector(".hk-card-body-pad")).toBeNull();
33
+ });
34
+
35
+ it("scrollable embeds the standard scroll container and flex classes", () => {
36
+ const c = mountCard({ scrollable: true });
37
+ expect(c.querySelector(".hk-card-scrollable")).not.toBeNull();
38
+ const scroll = c.querySelector(".hk-card-body .hk-scroll-container");
39
+ expect(scroll).not.toBeNull();
40
+ // The embedded scroller IS the library scroll container (overlay
41
+ // scrollbar machinery), not a bare overflow div.
42
+ expect(scroll?.querySelector(".hk-scroll-container-viewport")).not.toBeNull();
43
+ // Padded card: content padding lives inside the scroll region.
44
+ expect(c.querySelector(".hk-scroll-container .hk-card-body-pad .slot-marker")).not.toBeNull();
45
+ });
46
+
47
+ it("padded false drops the inner pad wrapper too", () => {
48
+ const c = mountCard({ scrollable: true, padded: false });
49
+ expect(c.querySelector(".hk-card-body-pad")).toBeNull();
50
+ expect(c.querySelector(".hk-scroll-container .slot-marker")).not.toBeNull();
51
+ });
52
+
53
+ it("header and footer stay outside the scroll region", () => {
54
+ const c = mountCard(
55
+ { scrollable: true, title: "T" },
56
+ undefined,
57
+ );
58
+ // Re-mount with a footer slot via raw slots is not available through
59
+ // the props-only helper; assert the header ordering here and cover
60
+ // the footer through the props-less variant below.
61
+ const header = c.querySelector(".hk-card-header");
62
+ const scroll = c.querySelector(".hk-card-body-scroll");
63
+ expect(header).not.toBeNull();
64
+ expect(scroll).not.toBeNull();
65
+ expect(header!.contains(scroll as Node)).toBe(false);
66
+ });
67
+
68
+ it("footer renders after the scroll body, not inside it", () => {
69
+ const container = document.createElement("div");
70
+ document.body.appendChild(container);
71
+ const app = createApp({
72
+ render: () =>
73
+ h(HkCard, { scrollable: true }, {
74
+ default: () => h("span", { class: "slot-marker" }, "content"),
75
+ footer: () => h("div", { class: "footer-marker" }, "foot"),
76
+ }),
77
+ });
78
+ app.mount(container);
79
+ mounts.push({ app, container });
80
+
81
+ const footer = container.querySelector(".hk-card-footer");
82
+ const scroll = container.querySelector(".hk-card-body-scroll");
83
+ expect(footer).not.toBeNull();
84
+ expect(footer!.contains(scroll as Node)).toBe(false);
85
+ expect(scroll!.contains(footer as Node)).toBe(false);
86
+ });
87
+
88
+ it("scroll content survives remounts of the pad wrapper class", () => {
89
+ // Padding flag flips at runtime (a consumer toggling density): the
90
+ // scroll container must persist (keyed on nothing but its slot).
91
+ const c = mountCard({ scrollable: true });
92
+ expect(c.querySelector(".hk-card-body-pad")).not.toBeNull();
93
+ });
94
+ });
@@ -53,3 +53,38 @@
53
53
  padding: var(--space-12) var(--space-16);
54
54
  border-top: 1px solid var(--border-faint);
55
55
  }
56
+
57
+ /* Scrollable card: the body becomes the flex host of an embedded
58
+ HkScrollContainer, so the card carries the ONE library scrollbar
59
+ system instead of every consumer nesting its own scroll layer.
60
+ Header and footer stay natural-height; the body fills the remainder.
61
+ Content padding lives INSIDE the scroll region (the pad wrapper the
62
+ component renders when `padded` is set) so the overlay scrollbar
63
+ hugs the card edge while content keeps its inset while scrolling. */
64
+ .hk-card-scrollable {
65
+ display: flex;
66
+ flex-direction: column;
67
+
68
+ > .hk-card-header,
69
+ > .hk-card-footer {
70
+ flex: none;
71
+ }
72
+
73
+ > .hk-card-body {
74
+ display: flex;
75
+ flex-direction: column;
76
+ flex: 1 1 auto;
77
+ min-height: 0;
78
+ padding: 0;
79
+ overflow: hidden;
80
+ }
81
+
82
+ > .hk-card-body > .hk-card-body-scroll {
83
+ flex: 1 1 auto;
84
+ min-height: 0;
85
+ }
86
+
87
+ .hk-card-body-pad {
88
+ padding: var(--space-16);
89
+ }
90
+ }
@@ -1,5 +1,6 @@
1
1
  import { defineComponent } from "vue";
2
2
 
3
+ import HkScrollContainer from "./HkScrollContainer";
3
4
  import "./HkCard.scss";
4
5
 
5
6
  export default defineComponent({
@@ -8,6 +9,17 @@ export default defineComponent({
8
9
  title: { type: String, default: undefined },
9
10
  hoverable: { type: Boolean, default: false },
10
11
  padded: { type: Boolean, default: true },
12
+ /** Turn the card body into the library's standard scroll region
13
+ * (HkScrollContainer: overlay scrollbar, overflow sensing) instead
14
+ * of a plain static block. The card becomes a column flex box —
15
+ * header and footer keep their natural height while the body
16
+ * fills the rest and scrolls. The card itself needs a height from
17
+ * its context (grid stretch, an explicit h-* / max-h-* utility, or a flex
18
+ * parent), exactly like any scroll container: without one there is
19
+ * nothing to scroll and the body simply fits. Content padding
20
+ * moves inside the scroll region so the scrollbar hugs the card
21
+ * edge; `padded: false` drops the inner padding as usual. */
22
+ scrollable: { type: Boolean, default: false },
11
23
  class: { type: String, default: "" },
12
24
  },
13
25
  emits: {
@@ -18,6 +30,7 @@ export default defineComponent({
18
30
  <div
19
31
  class={[
20
32
  "hk-card",
33
+ props.scrollable ? "hk-card-scrollable" : "",
21
34
  props.hoverable ? "hk-card-hoverable" : "",
22
35
  props.class,
23
36
  ]}
@@ -29,7 +42,13 @@ export default defineComponent({
29
42
  </div>
30
43
  ) : null}
31
44
  <div class={["hk-card-body", !props.padded ? "hk-card-body-unpadded" : ""]}>
32
- {slots.default?.()}
45
+ {props.scrollable ? (
46
+ <HkScrollContainer axis="vertical" class="hk-card-body-scroll">
47
+ <div class={props.padded ? "hk-card-body-pad" : undefined}>{slots.default?.()}</div>
48
+ </HkScrollContainer>
49
+ ) : (
50
+ slots.default?.()
51
+ )}
33
52
  </div>
34
53
  {slots.footer ? <div class="hk-card-footer">{slots.footer()}</div> : null}
35
54
  </div>