@celestia-island/hikari 0.55.184 → 0.55.185

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.184",
3
+ "version": "0.55.185",
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",
@@ -108,6 +108,23 @@
108
108
  }
109
109
  }
110
110
 
111
+ /* The per-row actions lane: renders only when the host supplies the
112
+ * `actions` slot (render-side guard), as a SIBLING of the content lane
113
+ * inside the row's flex layout — never a child of it, so the content
114
+ * lane's `> :only-child` fill above keeps stretching single-root rows
115
+ * even when actions ride along. Pinning is structural: the content lane
116
+ * keeps flex: 1 and this lane only refuses to shrink, so the actions
117
+ * land on the row's far edge — no margin-left:auto patch (it would
118
+ * paper over a content-lane regression instead of letting the fill rule
119
+ * fail loudly) and no directional property, so the lane follows the
120
+ * row's inline direction under RTL too. */
121
+ .hk-draggable-list-actions {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: var(--space-8, 0.5rem);
125
+ flex-shrink: 0;
126
+ }
127
+
111
128
  .hk-draggable-list-ghost {
112
129
  position: fixed;
113
130
  top: 0;
@@ -81,3 +81,30 @@ describe("HkDraggableList content-lane fill sheet", () => {
81
81
  expect(fillRules[0]).toContain(":only-child");
82
82
  });
83
83
  });
84
+
85
+ describe("HkDraggableList actions-lane sheet", () => {
86
+ const css = listCss();
87
+
88
+ it("keeps the actions lane fixed at its natural width", () => {
89
+ const body = ruleBody(css, ".hk-draggable-list-actions");
90
+ expect(body, "the actions-lane rule must exist").not.toBeNull();
91
+ expect(body).toContain("display: flex");
92
+ expect(body).toContain("align-items: center");
93
+ expect(body).toContain("flex-shrink: 0");
94
+ // Multiple host buttons in one lane need the lane's own gap so they
95
+ // never fuse into one cluster against the row edge.
96
+ expect(body).toContain("gap:");
97
+ });
98
+
99
+ // The right-edge pinning is STRUCTURAL, not a margin patch: the item
100
+ // row is a flex row and the content lane keeps flex: 1, so the lane
101
+ // needs nothing beyond shrink-resistance. A margin-left: auto here
102
+ // would paper over a future content-lane regression instead of
103
+ // letting the fill sheet fail loudly — pin by ownership, not by patch.
104
+ it("pins by the content lane's flex: 1, not a margin patch", () => {
105
+ const body = ruleBody(css, ".hk-draggable-list-actions");
106
+ expect(body).not.toContain("margin-left: auto");
107
+ const content = ruleBody(css, ".hk-draggable-list-content");
108
+ expect(content).toContain("flex: 1");
109
+ });
110
+ });
@@ -390,3 +390,138 @@ describe("HkDraggableList press ergonomics (clickable rows)", () => {
390
390
  }
391
391
  });
392
392
  });
393
+
394
+ describe("HkDraggableList actions lane", () => {
395
+ /** The standard row-actions contract: hosts inject trailing buttons
396
+ * through the scoped `actions` slot instead of hand-rolling
397
+ * ml-auto/flex-1 spacers inside the row content (chest's pricing
398
+ * ladder shipped exactly such a hand-rolled group and its buttons
399
+ * hugged the row's text mid-card). The lane renders as a SIBLING of
400
+ * the content lane — never inside it — so the content sheet's
401
+ * `> :only-child` fill keeps matching single-root rows even when
402
+ * actions ride along (sheet twin: HkDraggableList.styles.test.ts). */
403
+
404
+ function mountWithActions(items: { key: string }[], opts?: { lockedKeys?: string[] }): HTMLElement {
405
+ const container = document.createElement("div");
406
+ document.body.appendChild(container);
407
+ const app = createApp({
408
+ render: () =>
409
+ h(
410
+ HDraggableList,
411
+ { items, lockedKeys: opts?.lockedKeys ?? [] },
412
+ {
413
+ item: ({ item }: { item: { key: string } }) =>
414
+ h("span", { class: "row-body" }, item.key),
415
+ actions: ({
416
+ item,
417
+ index,
418
+ locked,
419
+ }: {
420
+ item: { key: string };
421
+ index: number;
422
+ locked: boolean;
423
+ }) =>
424
+ h("span", {
425
+ class: "row-actions",
426
+ "data-key": item.key,
427
+ "data-index": index,
428
+ "data-locked": locked,
429
+ }),
430
+ },
431
+ ),
432
+ });
433
+ app.mount(container);
434
+ mounts.push({ app, container });
435
+ return container.querySelector(".hk-draggable-list") as HTMLElement;
436
+ }
437
+
438
+ it("renders a right-lane sibling per row only when the slot is provided", () => {
439
+ const root = mountWithActions([{ key: "a" }, { key: "b" }]);
440
+
441
+ const lanes = Array.from(root.querySelectorAll(".hk-draggable-list-actions"));
442
+ expect(lanes).toHaveLength(2);
443
+ for (const lane of lanes) {
444
+ // Sibling of the content lane, not a child of it — the :only-child
445
+ // fill contract above must keep matching the row body.
446
+ expect(lane.closest(".hk-draggable-list-content")).toBeNull();
447
+ expect(lane.previousElementSibling?.classList.contains("hk-draggable-list-content")).toBe(
448
+ true,
449
+ );
450
+ }
451
+ });
452
+
453
+ it("keeps the content lane's :only-child fill intact alongside the lane", () => {
454
+ const root = mountWithActions([{ key: "a" }]);
455
+
456
+ const content = root.querySelector(".hk-draggable-list-content") as HTMLElement;
457
+ const elementChildren = Array.from(content.children);
458
+ expect(elementChildren).toHaveLength(1);
459
+ expect(elementChildren[0]!.classList.contains("row-body")).toBe(true);
460
+ });
461
+
462
+ it("scopes the slot with item, index, and locked-ness", () => {
463
+ const root = mountWithActions([{ key: "a" }, { key: "b" }], { lockedKeys: ["b"] });
464
+
465
+ const marks = Array.from(root.querySelectorAll(".row-actions")) as HTMLElement[];
466
+ expect(marks).toHaveLength(2);
467
+ expect(marks.map((m) => `${m.dataset.key}:${m.dataset.index}:${m.dataset.locked}`).join(",")).toBe(
468
+ "a:0:false,b:1:true",
469
+ );
470
+ });
471
+
472
+ it("renders no lane for hosts that do not adopt the slot", () => {
473
+ const root = mountList(["a"], []);
474
+
475
+ expect(root.querySelectorAll(".hk-draggable-list-actions")).toHaveLength(0);
476
+ });
477
+
478
+ it("keeps a press on a lane button from arming a drag — the handle still drags", async () => {
479
+ /** The lane exists to host buttons, so the row's interactive-content
480
+ * exclusion (button, a, input, textarea, select, [contenteditable])
481
+ * must own every press that starts on one — and a lane FULL of
482
+ * buttons must leave the handle's drag path untouched. */
483
+ const PointerEventCtor: typeof MouseEvent =
484
+ (globalThis as { PointerEvent?: typeof MouseEvent }).PointerEvent ?? MouseEvent;
485
+ const fire = (el: Element, type: string, x = 0, y = 0) =>
486
+ el.dispatchEvent(
487
+ new PointerEventCtor(type, { bubbles: true, cancelable: true, button: 0, clientX: x, clientY: y }),
488
+ );
489
+ const fireWin = (type: string, x = 0, y = 0) =>
490
+ window.dispatchEvent(new PointerEventCtor(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }));
491
+
492
+ const container = document.createElement("div");
493
+ document.body.appendChild(container);
494
+ const app = createApp({
495
+ render: () =>
496
+ h(HDraggableList, { items: [{ key: "a" }] }, {
497
+ item: ({ item }: { item: { key: string } }) => h("span", { class: "row-body" }, item.key),
498
+ actions: () => [
499
+ h("button", { class: "lane-button", type: "button" }, "✏"),
500
+ h("button", { class: "lane-button", type: "button" }, "⋯"),
501
+ ],
502
+ }),
503
+ });
504
+ app.mount(container);
505
+ mounts.push({ app, container });
506
+ const root = container.querySelector(".hk-draggable-list") as HTMLElement;
507
+ const row = root.querySelector(".hk-draggable-list-item") as HTMLElement;
508
+ const button = row.querySelector(".lane-button") as HTMLElement;
509
+ const handle = row.querySelector(".hk-draggable-list-handle") as HTMLElement;
510
+
511
+ // Press ON the button (target = button, bubbling to the row) and
512
+ // travel far past the arm threshold: no drag may publish.
513
+ fire(button, "pointerdown", 10, 10);
514
+ fireWin("pointermove", 30, 40);
515
+ await nextTick();
516
+ expect(row.hasAttribute("data-dragging")).toBe(false);
517
+
518
+ // The same gesture on the handle still arms — a button-filled lane
519
+ // never breaks the drag path.
520
+ fire(handle, "pointerdown", 10, 10);
521
+ fireWin("pointermove", 30, 40);
522
+ await nextTick();
523
+ expect(row.getAttribute("data-dragging")).not.toBeNull();
524
+
525
+ fireWin("pointerup");
526
+ });
527
+ });
@@ -334,6 +334,19 @@ export default defineComponent({
334
334
  <div class="hk-draggable-list-content">
335
335
  {slots.item?.({ item, index, locked })}
336
336
  </div>
337
+ {/* The standard row-actions lane: when the host provides
338
+ * an `actions` slot, every row renders this right-pinned
339
+ * lane (the content lane keeps flex: 1, so the buttons
340
+ * land on the row's far edge structurally — hosts stop
341
+ * hand-rolling ml-auto/flex-1 spacers). The lane is
342
+ * host-decided content: locked rows may still carry
343
+ * actions (chest's pricing ladder), so locked-ness is
344
+ * only reported through the slot scope. */}
345
+ {slots.actions ? (
346
+ <div class="hk-draggable-list-actions">
347
+ {slots.actions({ item, index, locked })}
348
+ </div>
349
+ ) : null}
337
350
  </div>
338
351
  );
339
352
  })}