@celestia-island/hikari 0.55.197 → 0.55.198

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.197",
3
+ "version": "0.55.198",
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,75 @@
1
+ // HkWindowControls — the two deliberate modes (2026-10-23 user ruling).
2
+ //
3
+ // standard: borderless ghost buttons at the header chrome's own size
4
+ // step (2rem boxes, 16px glyphs, 0.375rem gaps) — no borders, no
5
+ // dividers, no container chrome; the close hover is a small rounded
6
+ // red wash. Reads as one family with gear/search icon buttons.
7
+ //
8
+ // flush: edge-riding squares for thin captions — the host zeroes its
9
+ // own right padding (the s-header-bar--chrome contract pattern); the
10
+ // buttons stretch the host height, radius 0, wash-on-hover only.
11
+
12
+ .hk-window-controls {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: 0.375rem;
16
+ align-self: center;
17
+ }
18
+
19
+ .hk-window-controls__btn {
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ width: 2rem;
24
+ height: 2rem;
25
+ padding: 0;
26
+ border: none;
27
+ border-radius: 0.375rem;
28
+ background: transparent;
29
+ /* Inherit the host's foreground — a fixed gray literal is invisible
30
+ * on light floors and wrong in dark mode. */
31
+ color: inherit;
32
+ cursor: pointer;
33
+ transition: background 0.12s ease, color 0.12s ease;
34
+
35
+ &:hover {
36
+ background: rgb(var(--color-muted, 108 108 108) / 16%);
37
+ }
38
+
39
+ &--close {
40
+ &:hover {
41
+ background: rgb(232 17 35);
42
+ color: #fff;
43
+ }
44
+ }
45
+ }
46
+
47
+ // ── flush (thin captions) ───────────────────────────────────────────
48
+ .hk-window-controls--flush {
49
+ gap: 0;
50
+ align-self: stretch;
51
+ margin-left: 0.25rem;
52
+
53
+ .hk-window-controls__btn {
54
+ width: 2.75rem;
55
+ height: auto;
56
+ min-height: 2.25rem;
57
+ border-radius: 0;
58
+
59
+ & + & {
60
+ /* No dividers in flush mode either — wash-on-hover only. */
61
+ border-left: none;
62
+ }
63
+ }
64
+ }
65
+
66
+ // Phones: the cluster shrinks so the full action row fits.
67
+ @media (max-width: 767px) {
68
+ .hk-window-controls {
69
+ gap: 0.25rem;
70
+ }
71
+ .hk-window-controls__btn {
72
+ width: 1.75rem;
73
+ height: 1.75rem;
74
+ }
75
+ }
@@ -0,0 +1,93 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createApp, defineComponent, h, type App } from "vue";
3
+
4
+ import HkWindowControls, { isApplePlatform } from "./HkWindowControls";
5
+
6
+ const containers: HTMLElement[] = [];
7
+
8
+ function mountControls(props: Record<string, unknown> = {}): {
9
+ app: App;
10
+ container: HTMLElement;
11
+ } {
12
+ const container = document.createElement("div");
13
+ document.body.appendChild(container);
14
+ containers.push(container);
15
+ const Wrapper = defineComponent({
16
+ render: () => h(HkWindowControls as never, props),
17
+ });
18
+ const app = createApp(Wrapper);
19
+ app.mount(container);
20
+ return { app, container };
21
+ }
22
+
23
+ afterEach(() => {
24
+ for (const c of containers.splice(0)) c.remove();
25
+ vi.unstubAllGlobals();
26
+ });
27
+
28
+ /** A fake __TAURI__ window API so the render gates pass off-desktop. */
29
+ function stubTauriWindow(): void {
30
+ vi.stubGlobal("__TAURI__", {
31
+ window: {
32
+ getCurrentWindow: () => ({
33
+ minimize: () => Promise.resolve(),
34
+ toggleMaximize: () => Promise.resolve(),
35
+ close: () => Promise.resolve(),
36
+ isMaximized: () => Promise.resolve(false),
37
+ onResized: () => Promise.resolve(() => {}),
38
+ }),
39
+ },
40
+ });
41
+ }
42
+
43
+ describe("HkWindowControls (two deliberate modes, explicit None)", () => {
44
+ it("renders the three buttons in standard mode", () => {
45
+ stubTauriWindow();
46
+ const t = mountControls({ variant: "standard" });
47
+ const btns = t.container.querySelectorAll<HTMLButtonElement>(
48
+ ".hk-window-controls__btn",
49
+ );
50
+ expect(btns).toHaveLength(3);
51
+ expect(t.container.querySelector(".hk-window-controls--flush")).toBeNull();
52
+ t.app.unmount();
53
+ });
54
+
55
+ it("renders the flush modifier in flush mode", () => {
56
+ stubTauriWindow();
57
+ const t = mountControls({ variant: "flush" });
58
+ expect(t.container.querySelector(".hk-window-controls--flush")).not.toBeNull();
59
+ expect(
60
+ t.container.querySelectorAll(".hk-window-controls__btn"),
61
+ ).toHaveLength(3);
62
+ t.app.unmount();
63
+ });
64
+
65
+ it("renders NOTHING without a variant (the explicit-None contract)", () => {
66
+ stubTauriWindow();
67
+ const t = mountControls({});
68
+ expect(t.container.querySelector(".hk-window-controls")).toBeNull();
69
+ t.app.unmount();
70
+ });
71
+
72
+ it("renders nothing for an invalid variant (validator escape)", () => {
73
+ stubTauriWindow();
74
+ const t = mountControls({ variant: "rounded" });
75
+ expect(t.container.querySelector(".hk-window-controls")).toBeNull();
76
+ t.app.unmount();
77
+ });
78
+
79
+ it("renders nothing on Apple platforms (the OS owns the traffic lights)", () => {
80
+ stubTauriWindow();
81
+ vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" });
82
+ expect(isApplePlatform()).toBe(true);
83
+ const t = mountControls({ variant: "standard" });
84
+ expect(t.container.querySelector(".hk-window-controls")).toBeNull();
85
+ t.app.unmount();
86
+ });
87
+
88
+ it("renders nothing without a Tauri window API (browser preview)", () => {
89
+ const t = mountControls({ variant: "standard" });
90
+ expect(t.container.querySelector(".hk-window-controls")).toBeNull();
91
+ t.app.unmount();
92
+ });
93
+ });
@@ -0,0 +1,144 @@
1
+ import { defineComponent, onMounted, onUnmounted, ref, type PropType } from "vue";
2
+
3
+ import { Minus, Square, X, Copy } from "lucide-vue-next";
4
+
5
+ import "./HkWindowControls.scss";
6
+
7
+ /**
8
+ * HkWindowControls — the minimize / maximize / close cluster for hosts
9
+ * that own their window chrome (Tauri custom titlebars, Electron
10
+ * frameless). Three modes, and the choice is DELIBERATELY explicit
11
+ * (user direction 2026-10-23):
12
+ *
13
+ * - `standard` — three borderless ghost icon buttons at the same size
14
+ * step as the header's other chrome (ghost/32, 16px glyphs), spaced
15
+ * with comfortable gaps. The whole top-right reads as one family of
16
+ * identical buttons; the close hover is a small rounded red wash.
17
+ * THE mode for content-height headers (3rem-class bars).
18
+ *
19
+ * - `flush` — edge-riding squares that fill the host's height (the
20
+ * host must zero its own right padding so the close button rides
21
+ * the window edge; wash-on-hover only, radius 0). THE mode for real
22
+ * thin captions (32px-class native-titlebar strips).
23
+ *
24
+ * - **omitted → nothing renders.** There is NO default variant: a
25
+ * forgetful host shows no control buttons (the app stays closable
26
+ * via the taskbar/OS), which fails loudly at design review instead
27
+ * of silently shipping the wrong chrome.
28
+ *
29
+ * macOS: the traffic lights belong to the OS titlebar — this component
30
+ * renders nothing on Apple platforms and the header degrades to its
31
+ * browser-face look automatically.
32
+ *
33
+ * The window API rides `__TAURI__.window.getCurrentWindow()` when
34
+ * present; on a non-Tauri host (browser preview) the buttons render
35
+ * but the calls no-op, keeping visual parity for storybook/tests.
36
+ */
37
+
38
+ interface TauriWindowLike {
39
+ minimize(): Promise<void>;
40
+ toggleMaximize(): Promise<void>;
41
+ close(): Promise<void>;
42
+ isMaximized(): Promise<boolean>;
43
+ onResized(cb: () => void): Promise<() => void>;
44
+ }
45
+
46
+ function currentWindow(): TauriWindowLike | null {
47
+ const w = (
48
+ window as unknown as {
49
+ __TAURI__?: { window?: { getCurrentWindow?: () => TauriWindowLike } };
50
+ }
51
+ ).__TAURI__?.window;
52
+ return w?.getCurrentWindow?.() ?? null;
53
+ }
54
+
55
+ /** Apple platforms own their traffic lights — we never render there. */
56
+ export function isApplePlatform(): boolean {
57
+ return /Mac|iPhone|iPad|iPod/.test(
58
+ (typeof navigator !== "undefined" && navigator.userAgent) || "",
59
+ );
60
+ }
61
+
62
+ export type WindowControlsVariant = "standard" | "flush";
63
+
64
+ export const HkWindowControls = defineComponent({
65
+ name: "HkWindowControls",
66
+ props: {
67
+ /** REQUIRED — no default. One of the two deliberate modes; omit it
68
+ * and the component renders nothing (the explicit-None contract). */
69
+ variant: {
70
+ type: String as PropType<WindowControlsVariant>,
71
+ required: true,
72
+ validator: (v: unknown) => v === "standard" || v === "flush",
73
+ },
74
+ },
75
+ setup(props) {
76
+ const maximized = ref(false);
77
+ let unlisten: (() => void) | null = null;
78
+
79
+ onMounted(async () => {
80
+ const win = currentWindow();
81
+ if (!win) return;
82
+ try {
83
+ const refresh = () => void win.isMaximized().then((m) => (maximized.value = m));
84
+ refresh();
85
+ const off = await win.onResized(refresh);
86
+ unlisten = off;
87
+ } catch {
88
+ /* the window API is optional furniture */
89
+ }
90
+ });
91
+
92
+ onUnmounted(() => {
93
+ unlisten?.();
94
+ });
95
+
96
+ return () => {
97
+ // Two render gates before any chrome: Apple owns its traffic
98
+ // lights, and a host that forgot the variant gets NOTHING (the
99
+ // explicit-None contract — belt for runtime JS where TS cannot
100
+ // enforce the required prop).
101
+ if (isApplePlatform()) return null;
102
+ if (props.variant !== "standard" && props.variant !== "flush") return null;
103
+ const win = currentWindow();
104
+ if (!win) return null;
105
+
106
+ const flush = props.variant === "flush";
107
+ return (
108
+ <div
109
+ class={{ "hk-window-controls": true, "hk-window-controls--flush": flush }}
110
+ data-tauri-drag-region={false}
111
+ >
112
+ <button
113
+ type="button"
114
+ class="hk-window-controls__btn"
115
+ aria-label="Minimize"
116
+ onClick={() => void win.minimize()}
117
+ >
118
+ <Minus size={flush ? 14 : 16} />
119
+ </button>
120
+ <button
121
+ type="button"
122
+ class="hk-window-controls__btn"
123
+ aria-label={maximized.value ? "Restore" : "Maximize"}
124
+ onClick={() => void win.toggleMaximize()}
125
+ >
126
+ {maximized.value
127
+ ? <Copy size={flush ? 12 : 14} style={{ transform: "rotate(180deg)" }} />
128
+ : <Square size={flush ? 12 : 14} />}
129
+ </button>
130
+ <button
131
+ type="button"
132
+ class="hk-window-controls__btn hk-window-controls__btn--close"
133
+ aria-label="Close"
134
+ onClick={() => void win.close()}
135
+ >
136
+ <X size={flush ? 14 : 16} />
137
+ </button>
138
+ </div>
139
+ );
140
+ };
141
+ },
142
+ });
143
+
144
+ export default HkWindowControls;
package/src/index.ts CHANGED
@@ -614,6 +614,8 @@ export { default as HkCopyBadge } from "./components/HkCopyBadge";
614
614
  export { default as HkConfirmDialog } from "./components/HkConfirmDialog";
615
615
  export { default as HkCrossfade } from "./components/HkCrossfade";
616
616
  export { default as HkTransitionContainer } from "./components/HkTransitionContainer";
617
+ export { default as HkWindowControls, isApplePlatform } from "./components/HkWindowControls";
618
+ export type { WindowControlsVariant } from "./components/HkWindowControls";
617
619
  export { default as HkDivider } from "./components/HkDivider";
618
620
  export { default as HkDiffViewer } from "./components/HkDiffViewer";
619
621
  export { parseUnifiedDiff, computeGaps } from "./components/diffParse";