@celestia-island/hikari 0.55.4 → 0.55.6

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.4",
3
+ "version": "0.55.6",
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",
@@ -20,13 +20,19 @@ export interface AuthSubmitContext {
20
20
  * 3. `onCaptcha` (optional) — acquire a captcha verification token.
21
21
  * 4. Call `onSubmit` with the assembled `AuthSubmitContext`.
22
22
  *
23
- * While solving, the button shows its own spinner; `loading` stays
24
- * external so callers can reflect server-side submission progress.
23
+ * While solving or submitting, the button renders its loading look —
24
+ * a leading spinner beside the caller's label (chest-style; user report
25
+ * 2026-09-16: swapping the whole caption for a challenge string read as
26
+ * a different control). Hosts that want the verbose challenge caption
27
+ * can opt back in with `solvingLabel`.
25
28
  */
26
29
  export default defineComponent({
27
30
  name: "HkAuthSubmitButton",
28
31
  props: {
29
32
  label: { type: String, required: true },
33
+ /** Optional caption while the challenge is being solved; defaults to
34
+ * the regular label beside the spinner. */
35
+ solvingLabel: { type: String, default: undefined },
30
36
  loading: { type: Boolean, default: false },
31
37
  disabled: { type: Boolean, default: false },
32
38
  error: { type: String, default: undefined },
@@ -83,8 +89,8 @@ export default defineComponent({
83
89
  disabled={props.disabled || busy.value}
84
90
  onClick={() => void handleClick()}
85
91
  >
86
- {solving.value
87
- ? t("hikari::auth.solvingChallenge", "Verifying challenge…")
92
+ {solving.value && props.solvingLabel
93
+ ? props.solvingLabel
88
94
  : props.label}
89
95
  </HButton>
90
96
  {props.error && <p class="s-auth-submit-error">{props.error}</p>}
@@ -6,6 +6,25 @@
6
6
 
7
7
  .s-scheme-mode-switch {
8
8
  align-self: center;
9
+
10
+ /* The strip needs a track of its own: pill-on-dialog-surface is
11
+ same-on-same in dark mode and read as bare text links (user report
12
+ 2026-09-16), hiding that it is a segmented control — and that it
13
+ picks the EDITING target, not the applied app mode. */
14
+ .hk-tabs-list {
15
+ background: color-mix(in srgb, var(--hi-color-text-primary, rgb(var(--color-text))) 8%, transparent);
16
+ padding: var(--space-2, 2px);
17
+ border-radius: var(--hi-radius-sm, 4px);
18
+ }
19
+ }
20
+
21
+ /* Why-this-strip note: the tabs switch which palette you are EDITING;
22
+ the applied mode lives in the theme popup's color-mode strip. */
23
+ .s-scheme-mode-hint {
24
+ align-self: center;
25
+ font-size: var(--text-xs, 0.75rem);
26
+ color: rgb(var(--color-muted));
27
+ margin-top: calc(var(--space-8, 0.5rem) * -1);
9
28
  }
10
29
 
11
30
  .s-scheme-colors {
@@ -418,6 +418,7 @@ export const HkColorSchemeEditor = defineComponent({
418
418
  onUpdate:modelValue={(v: string) => { modeTab.value = v; }}
419
419
  tabs={modeTabs.value}
420
420
  />
421
+ <p class="s-scheme-mode-hint">{t("hikari::theme.modeTabHint")}</p>
421
422
  <div class="s-scheme-colors">
422
423
  {editableTokens.map((key) => (
423
424
  <HColorPicker
@@ -0,0 +1,95 @@
1
+ /**
2
+ * HkDockBar.scss — the shared dock surface chrome (single source of
3
+ * truth). `--hk-dock-inset` is declared by the foundation layer so every
4
+ * dock reads a defined value; change it once, all surfaces move
5
+ * together.
6
+ */
7
+
8
+ .hk-dock-bar {
9
+ /* Transparent anchor box — never intercepts canvas gestures around
10
+ * the docked surface. */
11
+ pointer-events: none;
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ gap: var(--space-4, 4px);
16
+
17
+ /* ── page anchor: absolute bottom-center of the nearest positioned
18
+ * ancestor, resting --hk-dock-inset above its content floor. */
19
+ &[data-anchor="page"] {
20
+ position: absolute;
21
+ left: 50%;
22
+ bottom: var(--hk-dock-inset, 12px);
23
+ transform: translateX(-50%);
24
+ z-index: 50;
25
+ }
26
+
27
+ /* ── plane anchor: in-flow inside a host plane (a fixed footer band);
28
+ * the host owns the positioning, this container centers the
29
+ * surface horizontally and lifts it by the shared dock inset. */
30
+ &[data-anchor="plane"] {
31
+ margin: 0 auto var(--hk-dock-inset, 12px);
32
+ }
33
+
34
+ /* ── corner anchors: absolute attachment with the same inset, for
35
+ * pagers, toolbars and HUD panels that live off-center. */
36
+ &[data-anchor="top-left"],
37
+ &[data-anchor="top-right"],
38
+ &[data-anchor="bottom-left"],
39
+ &[data-anchor="bottom-right"] {
40
+ position: absolute;
41
+ z-index: 50;
42
+ }
43
+
44
+ &[data-anchor="top-left"] {
45
+ top: var(--hk-dock-inset, 12px);
46
+ left: var(--hk-dock-inset, 12px);
47
+ align-items: flex-start;
48
+ }
49
+
50
+ &[data-anchor="top-right"] {
51
+ top: var(--hk-dock-inset, 12px);
52
+ right: var(--hk-dock-inset, 12px);
53
+ align-items: flex-end;
54
+ }
55
+
56
+ &[data-anchor="bottom-left"] {
57
+ bottom: var(--hk-dock-inset, 12px);
58
+ left: var(--hk-dock-inset, 12px);
59
+ align-items: flex-start;
60
+ }
61
+
62
+ &[data-anchor="bottom-right"] {
63
+ bottom: var(--hk-dock-inset, 12px);
64
+ right: var(--hk-dock-inset, 12px);
65
+ align-items: flex-end;
66
+ }
67
+ }
68
+
69
+ .hk-dock-bar-surface {
70
+ pointer-events: auto;
71
+ display: flex;
72
+ flex-direction: column;
73
+ align-items: stretch;
74
+ width: var(--dock-width, auto);
75
+ max-width: var(--dock-max-width, calc(100vw - var(--space-16, 16px)));
76
+ padding: var(--dock-padding, var(--space-8, 8px) var(--space-12, 12px));
77
+ background: rgb(var(--color-surface));
78
+ border: 1px solid var(--border-subtle);
79
+ border-radius: var(--radius-md);
80
+ /* Reverse soft light — dark mode: white halo; light mode: soft black
81
+ * shadow. Strength scales with --glow-scale (Style → Shadow). */
82
+ box-shadow: var(--glow-dock);
83
+ }
84
+
85
+ /* Glass surface (default) — blur the page canvas behind the dock. */
86
+ .hk-dock-bar[data-surface="glass"] .hk-dock-bar-surface {
87
+ backdrop-filter: blur(var(--blur-lg));
88
+ }
89
+
90
+ /* Solid surface — no blur, for moving 3D canvases (per-frame backdrop
91
+ * re-sampling over a live WebGL scene is expensive; the surface must
92
+ * stay opaque anyway for contrast over the scene). */
93
+ .hk-dock-bar[data-surface="solid"] .hk-dock-bar-surface {
94
+ backdrop-filter: none;
95
+ }
@@ -0,0 +1,76 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, defineComponent, h, nextTick } from "vue";
3
+
4
+ import HkDockBar from "./HkDockBar";
5
+
6
+ const apps: ReturnType<typeof createApp>[] = [];
7
+ const containers: HTMLElement[] = [];
8
+
9
+ afterEach(() => {
10
+ for (const app of apps.splice(0)) app.unmount();
11
+ for (const el of containers.splice(0)) el.remove();
12
+ document.body.innerHTML = "";
13
+ });
14
+
15
+ async function flush() {
16
+ await nextTick();
17
+ await nextTick();
18
+ }
19
+
20
+ /** Mount a dock with the given props and return its root section. */
21
+ async function mountDock(props: Record<string, unknown> = {}) {
22
+ const container = document.createElement("div");
23
+ document.body.appendChild(container);
24
+ containers.push(container);
25
+ const Host = defineComponent({
26
+ setup() {
27
+ return () =>
28
+ h(HkDockBar, props, { default: () => h("button", { class: "probe" }, "go") });
29
+ },
30
+ });
31
+ const app = createApp(Host);
32
+ apps.push(app);
33
+ app.mount(container);
34
+ await flush();
35
+ return container.querySelector<HTMLElement>(".hk-dock-bar")!;
36
+ }
37
+
38
+ describe("HkDockBar", () => {
39
+ it("renders slot content inside the surface", async () => {
40
+ const root = await mountDock();
41
+ const probe = root.querySelector<HTMLElement>(".probe");
42
+ expect(probe?.textContent).toBe("go");
43
+ expect(root.querySelector(".hk-dock-bar-surface")).toBeTruthy();
44
+ });
45
+
46
+ it("defaults to the page anchor with a glass surface", async () => {
47
+ const root = await mountDock();
48
+ expect(root.dataset.anchor).toBe("page");
49
+ expect(root.dataset.surface).toBe("glass");
50
+ });
51
+
52
+ it.each(["plane", "top-left", "top-right", "bottom-left", "bottom-right"] as const)(
53
+ "accepts the %s anchor",
54
+ async (anchor) => {
55
+ const root = await mountDock({ anchor });
56
+ expect(root.dataset.anchor).toBe(anchor);
57
+ },
58
+ );
59
+
60
+ it("exposes style hooks for width, max-width and padding", async () => {
61
+ const root = await mountDock({
62
+ width: "22rem",
63
+ maxWidth: "30rem",
64
+ padding: "4px 6px",
65
+ });
66
+ const style = root.style;
67
+ expect(style.getPropertyValue("--dock-width")).toBe("22rem");
68
+ expect(style.getPropertyValue("--dock-max-width")).toBe("30rem");
69
+ expect(style.getPropertyValue("--dock-padding")).toBe("4px 6px");
70
+ });
71
+
72
+ it("drops the blur on the solid surface", async () => {
73
+ const root = await mountDock({ surface: "solid" });
74
+ expect(root.dataset.surface).toBe("solid");
75
+ });
76
+ });
@@ -0,0 +1,77 @@
1
+ import { computed, defineComponent, type PropType } from "vue";
2
+
3
+ import "./HkDockBar.scss";
4
+
5
+ export type HkDockBarAnchor =
6
+ | "page"
7
+ | "plane"
8
+ | "top-left"
9
+ | "top-right"
10
+ | "bottom-left"
11
+ | "bottom-right";
12
+ export type HkDockBarSurface = "glass" | "solid";
13
+
14
+ /**
15
+ * HkDockBar — the ONE dock container. Every floating surface that docks
16
+ * to an edge or corner of a canvas — bottom-center cruise docks, corner
17
+ * step pagers, floating chat bars, nameplate pills — mounts its content
18
+ * inside THIS container; the page only provides slot content. One chrome
19
+ * (surface, blur, border, radius, reverse glow) means no dock surface
20
+ * ever re-declares its own background or shadow again.
21
+ *
22
+ * Anchors:
23
+ * - `page` (default): absolute bottom-center of the nearest positioned
24
+ * ancestor, `--hk-dock-inset` above its content floor.
25
+ * - `plane`: in-flow — the host provides the plane (e.g. a fixed
26
+ * footer band); this container centers the surface horizontally.
27
+ * - `top-left` / `top-right` / `bottom-left` / `bottom-right`:
28
+ * absolute corner attachment with the same inset, for pagers,
29
+ * toolbars and HUD panels that live off-center.
30
+ *
31
+ * Surfaces:
32
+ * - `glass` (default): translucent surface + backdrop blur.
33
+ * - `solid`: opaque, no blur — for moving canvases (WebGL scenes)
34
+ * where per-frame backdrop re-sampling is expensive.
35
+ *
36
+ * Pointer-events contract: the anchored box is transparent to canvas
37
+ * gestures (pointer-events: none); the surface catches them.
38
+ */
39
+ const HkDockBar = defineComponent({
40
+ name: "HkDockBar",
41
+ props: {
42
+ /** Which plane carries the dock: page (bottom-center), a host plane
43
+ * (in-flow), or one of the four corners. */
44
+ anchor: { type: String as PropType<HkDockBarAnchor>, default: "page" },
45
+ /** Surface finish: glass (blur) or solid (moving canvas). */
46
+ surface: { type: String as PropType<HkDockBarSurface>, default: "glass" },
47
+ /** CSS width of the surface; undefined = shrink-wrap content. */
48
+ width: { type: String, default: undefined },
49
+ /** CSS max-width of the surface (content caps, e.g. minimap clearance). */
50
+ maxWidth: { type: String, default: undefined },
51
+ /** CSS padding override; defaults to the shared chrome rhythm. */
52
+ padding: { type: String, default: undefined },
53
+ },
54
+ setup(props, { slots }) {
55
+ const style = computed(() => {
56
+ const s: Record<string, string> = {};
57
+ if (props.width) s["--dock-width"] = props.width;
58
+ if (props.maxWidth) s["--dock-max-width"] = props.maxWidth;
59
+ if (props.padding) s["--dock-padding"] = props.padding;
60
+ return s;
61
+ });
62
+
63
+ return () => (
64
+ <section
65
+ class="hk-dock-bar"
66
+ data-anchor={props.anchor}
67
+ data-surface={props.surface}
68
+ style={style.value}
69
+ >
70
+ <div class="hk-dock-bar-surface">{slots.default?.()}</div>
71
+ </section>
72
+ );
73
+ },
74
+ });
75
+
76
+ export default HkDockBar;
77
+ export { HkDockBar };
@@ -233,6 +233,17 @@
233
233
  flex-shrink: 0;
234
234
  }
235
235
 
236
+ // Caller's header actions (HkModal's headerActions slot): a gap-separated
237
+ // row of icon buttons / pills sitting at inline-end, one beat before the
238
+ // close button (which keeps its own flex-shrink: 0 and glyph).
239
+ .hk-modal-header-actions {
240
+ display: flex;
241
+ align-items: center;
242
+ gap: var(--space-2, 4px);
243
+ margin-inline-start: var(--space-8, 8px);
244
+ flex-shrink: 0;
245
+ }
246
+
236
247
  // ------
237
248
  // Sub-header (caller-provided slot)
238
249
  // ------
@@ -836,6 +836,16 @@ export default defineComponent({
836
836
  <h2 class="hk-modal-title">
837
837
  {slots.header ? (props.title ?? "") : (props.title ?? "")}
838
838
  </h2>
839
+ {slots.headerActions && (
840
+ // Caller's header actions (icon buttons, status
841
+ // pills) — parked at inline-end, just before the
842
+ // close button. The container participates in the
843
+ // header flex; the title's auto margin keeps the
844
+ // title+actions cluster apart.
845
+ <div class="hk-modal-header-actions">
846
+ {slots.headerActions()}
847
+ </div>
848
+ )}
839
849
  {props.closable && (
840
850
  // Unified window-close affordance: the shared
841
851
  // icon button + registry X glyph (see
@@ -239,7 +239,9 @@
239
239
  .hk-timeline-step {
240
240
  display: flex;
241
241
  align-items: center;
242
- gap: var(--space-8, 8px);
242
+ // Host-tunable rhythm: the node→label breathing room (a rail host with
243
+ // roomy rows raises it; the 8px stock keeps compact wizards tight).
244
+ gap: var(--hk-timeline-step-gap, var(--space-8, 8px));
243
245
  position: relative;
244
246
  flex: 1 1 auto;
245
247
  min-width: 0;
@@ -273,7 +275,9 @@
273
275
  }
274
276
 
275
277
  [data-el="label"] {
276
- font-size: var(--text-xs, 0.75rem);
278
+ // Host-tunable: rails in side columns read better a notch up from
279
+ // the compact --text-xs wizard label.
280
+ font-size: var(--hk-timeline-label-size, var(--text-xs, 0.75rem));
277
281
  white-space: nowrap;
278
282
  flex-shrink: 0;
279
283
  }
@@ -290,6 +294,17 @@
290
294
  flex: 0 0 auto;
291
295
  }
292
296
 
297
+ /* Disabled step: visible so the flow's shape reads, but dimmed and inert
298
+ (data-clickable is withheld upstream; cursor says so too). */
299
+ .hk-timeline-step[data-disabled] {
300
+ cursor: default;
301
+
302
+ & [data-el="label"],
303
+ & [data-el="indicator"] {
304
+ opacity: 0.45;
305
+ }
306
+ }
307
+
293
308
  .hk-timeline[data-orientation="vertical"][data-mode="full"] .hk-timeline-step {
294
309
  flex-direction: row;
295
310
  flex: 0 0 auto;
@@ -20,6 +20,10 @@ export interface TimelineStep {
20
20
  key: string;
21
21
  label: string;
22
22
  icon?: string;
23
+ /** Greyed and inert when set — a gate the host enforces elsewhere (a
24
+ * login wall, an unsatisfied precondition); the step stays visible so
25
+ * the flow's shape reads, it just cannot be clicked into. */
26
+ disabled?: boolean;
23
27
  }
24
28
 
25
29
  /**
@@ -246,17 +250,19 @@ export default defineComponent({
246
250
  data-status={status}
247
251
  data-last={opts.last || undefined}
248
252
  aria-current={status === "active" ? "step" : undefined}
249
- data-clickable={(props.clickable && status === "completed") || undefined}
250
- role={props.clickable ? "button" : undefined}
251
- tabindex={props.clickable && status === "completed" ? 0 : undefined}
253
+ data-disabled={step.disabled || undefined}
254
+ data-clickable={(props.clickable && status === "completed" && !step.disabled) || undefined}
255
+ role={props.clickable && !step.disabled ? "button" : undefined}
256
+ tabindex={props.clickable && status === "completed" && !step.disabled ? 0 : undefined}
252
257
  onClick={() => {
253
- if (props.clickable && status === "completed")
258
+ if (props.clickable && status === "completed" && !step.disabled)
254
259
  emit("select", step.key);
255
260
  }}
256
261
  onKeydown={(e: KeyboardEvent) => {
257
262
  if (
258
263
  props.clickable &&
259
264
  status === "completed" &&
265
+ !step.disabled &&
260
266
  (e.key === "Enter" || e.key === " ")
261
267
  ) {
262
268
  e.preventDefault();
package/src/index.ts CHANGED
@@ -18,6 +18,11 @@ export { HkColorSchemeEditor } from "./components/HkColorSchemeEditor";
18
18
  export { default as HConfirmDialog } from "./components/HkConfirmDialog";
19
19
  export { default as HCrossfade } from "./components/HkCrossfade";
20
20
  export { default as HDivider } from "./components/HkDivider";
21
+ export {
22
+ default as HDockBar,
23
+ type HkDockBarAnchor as HDockBarAnchor,
24
+ type HkDockBarSurface as HDockBarSurface,
25
+ } from "./components/HkDockBar";
21
26
  export { default as HDrawer } from "./components/HkDrawer";
22
27
  export { default as HEmptyState } from "./components/HkEmptyState";
23
28
  export { default as HExpansionPanel } from "./components/HkExpansionPanel";