@celestia-island/hikari 0.47.0 → 0.47.1

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.47.0",
3
+ "version": "0.47.1",
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",
@@ -1,8 +1,12 @@
1
- /* Alternative sign-in methods as an icon button group (login card). The
2
- block container `.s-auth-methods` and its spacing are owned by
3
- HkAuthCard's slot styles (styles/admin-tokens.scss); this file only
4
- styles the divider and the icon group row the component renders into
5
- that slot. */
1
+ /* Alternative sign-in methods as CENTERED INDEPENDENT framed icon tiles
2
+ * (login card). The block container `.s-auth-methods` and its spacing are
3
+ * owned by HkAuthCard's slot styles (styles/admin-tokens.scss); this file
4
+ * only styles the divider and the tile row the component renders into that
5
+ * slot.
6
+ *
7
+ * Deliberately NOT a button group (2026-09-14 user direction): no shared
8
+ * track, no shared frame — each provider carries its own border so the row
9
+ * stays as wide as its content, centered in the card. */
6
10
 
7
11
  /* "Other ways to sign in" divider row between rules. */
8
12
  .s-auth-methods-divider {
@@ -24,16 +28,91 @@
24
28
  }
25
29
 
26
30
  /* The component's own wrapper is `display: contents` — layout-wise the
27
- divider and the group row remain direct children of HkAuthCard's
31
+ divider and the tile row remain direct children of HkAuthCard's
28
32
  `.s-auth-methods` slot container. */
29
33
  .s-auth-methods-list {
30
34
  display: contents;
31
35
  }
32
36
 
33
- /* The icon button group row: fill the card column and center the
34
- provider tiles inside it (descendant match off `.s-auth-methods` —
35
- the wrapper generates no box of its own). */
36
- .s-auth-methods .hk-icon-group.s-auth-methods-group {
37
+ /* The tile row: fill the card column and center the independent tiles
38
+ inside it. The gap between tiles is the INDEPENDENT-action spacing
39
+ (--space-12) — comfortable air, unlike a group's shoulder-to-shoulder
40
+ track gap. */
41
+ .s-auth-methods .s-auth-methods-tiles {
42
+ display: flex;
43
+ flex-wrap: wrap;
44
+ align-items: center;
45
+ justify-content: center;
46
+ gap: var(--space-12, 0.75rem);
37
47
  width: 100%;
48
+ }
49
+
50
+ /* The HkTooltip wrapper span is the flex child between tiles — let it
51
+ shrink to the button so the row gap (not the wrapper) spaces them. */
52
+ .s-auth-methods .s-auth-methods-tiles > .hk-tooltip-wrapper {
53
+ display: inline-flex;
54
+ flex: none;
55
+ }
56
+
57
+ /* One provider tile: a framed 44px square (the login-chooser tile size,
58
+ same as the icon group's md item). Each tile owns its whole frame —
59
+ border, hover wash, focus ring — with no track around the row. */
60
+ .s-auth-methods-tile {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ width: 2.75rem;
65
+ height: 2.75rem;
66
+ padding: 0;
67
+ font: inherit;
68
+ color: rgb(var(--color-text, 25 30 40));
69
+ background: rgb(var(--color-surface, 255 255 255) / 55%);
70
+ border: 1px solid rgb(var(--color-border, 222 224 228) / 55%);
71
+ border-radius: var(--radius-md, 8px);
72
+ cursor: pointer;
73
+ outline: none;
74
+ transition-property: background-color, border-color, color, box-shadow, opacity, transform;
75
+ transition-duration: var(--duration-short, 0.15s);
76
+ transition-timing-function: ease;
77
+
78
+ /* A real <button disabled> still matches :hover — guard the dead ones. */
79
+ &:hover:not(:disabled) {
80
+ color: rgb(var(--color-text, 25 30 40));
81
+ background: var(--c-primary-faint, rgb(var(--color-primary, 90 140 255) / 5%));
82
+ border-color: var(--c-primary-medium, rgb(var(--color-primary, 90 140 255) / 25%));
83
+ }
84
+
85
+ /* Tactile press feedback — the button-family cadence. */
86
+ &:active:not(:disabled) {
87
+ transform: scale(0.96);
88
+ }
89
+
90
+ /* Outset double ring, same grammar as .hk-btn. */
91
+ &:focus-visible {
92
+ box-shadow:
93
+ 0 0 0 2px rgb(var(--color-surface, 255 255 255)),
94
+ 0 0 0 4px rgb(var(--color-primary, 90 140 255));
95
+ }
96
+
97
+ &:disabled {
98
+ opacity: 0.5;
99
+ cursor: not-allowed;
100
+ }
101
+ }
102
+
103
+ .s-auth-methods-tile-icon {
104
+ display: inline-flex;
105
+ align-items: center;
38
106
  justify-content: center;
107
+ line-height: 0;
108
+ /* Brand marks (svg/img vnodes) at their intrinsic size; lucide comps
109
+ sized by the host. The box never grows past the square. */
110
+ max-width: 100%;
111
+ max-height: 100%;
112
+ }
113
+
114
+ .s-auth-methods-tile-initial {
115
+ font-size: var(--text-sm, 0.8125rem);
116
+ font-weight: 600;
117
+ line-height: 1;
39
118
  }
@@ -28,27 +28,39 @@ const methods = [
28
28
  ];
29
29
 
30
30
  describe("HkAuthMethodList", () => {
31
- it("renders the provider row as an icon button group with an optional divider", () => {
31
+ it("renders centered independent framed tiles with an optional divider", () => {
32
32
  // The card's `methods` slot owns the `.s-auth-methods` container —
33
- // reproduce that context here (the divider and the group row are layout
33
+ // reproduce that context here (the divider and the tile row are layout
34
34
  // children of it through the component's display:contents wrapper).
35
35
  const c = mount(
36
36
  h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { divider: "其他方式登录", methods })),
37
37
  );
38
38
  const divider = c.querySelector(".s-auth-methods-divider");
39
39
  expect(divider?.textContent).toContain("其他方式登录");
40
- const group = c.querySelector<HTMLElement>(".s-auth-methods .hk-icon-group");
41
- expect(group).toBeTruthy();
42
- expect(group!.classList.contains("hk-icon-group-buttons")).toBe(true);
43
- const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .hk-icon-group-item");
44
- expect(buttons.length).toBe(2);
40
+ const tiles = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .s-auth-methods-tile");
41
+ expect(tiles.length).toBe(2);
45
42
  // Icon-only: the label is the accessible name + tooltip, never visible text.
46
- expect(buttons[0]!.getAttribute("aria-label")).toBe("GitHub");
47
- expect(buttons[1]!.getAttribute("aria-label")).toBe("LinuxDo");
48
- expect(buttons[0]!.textContent).not.toContain("GitHub");
43
+ expect(tiles[0]!.getAttribute("aria-label")).toBe("GitHub");
44
+ expect(tiles[1]!.getAttribute("aria-label")).toBe("LinuxDo");
45
+ expect(tiles[0]!.textContent).not.toContain("GitHub");
49
46
  });
50
47
 
51
- it("wraps every item in a tooltip carrying the provider label", () => {
48
+ it("never wraps the providers in a button-group track", () => {
49
+ // 2026-09-14 user direction: a shared group frame (one wide bordered
50
+ // track with the icons floating inside) is NOT this component's
51
+ // grammar — every provider is an independent framed tile. A regression
52
+ // back to HkIconButtonGroup (or any .hk-icon-group markup) must fail
53
+ // here, because the button group is reserved for selectors and tight
54
+ // action strips.
55
+ const c = mount(
56
+ h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })),
57
+ );
58
+ expect(c.querySelector(".s-auth-methods .hk-icon-group")).toBeNull();
59
+ expect(c.querySelector(".s-auth-methods [role='group']")).toBeNull();
60
+ expect(c.querySelector(".s-auth-methods [role='radiogroup']")).toBeNull();
61
+ });
62
+
63
+ it("wraps every tile in a tooltip carrying the provider label", () => {
52
64
  const c = mount(
53
65
  h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })),
54
66
  );
@@ -71,12 +83,12 @@ describe("HkAuthMethodList", () => {
71
83
  },
72
84
  })),
73
85
  );
74
- const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .hk-icon-group-item");
75
- buttons[1]!.click();
86
+ const tiles = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .s-auth-methods-tile");
87
+ tiles[1]!.click();
76
88
  await nextTick();
77
89
  // Both rows are asserted so a constant-literal emit cannot ride an
78
90
  // accidental fixture coincidence.
79
- buttons[0]!.click();
91
+ tiles[0]!.click();
80
92
  await nextTick();
81
93
  expect(picked).toEqual(["linuxdo", "github"]);
82
94
  });
@@ -95,15 +107,15 @@ describe("HkAuthMethodList", () => {
95
107
  },
96
108
  })),
97
109
  );
98
- const buttons = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .hk-icon-group-item");
99
- expect(buttons.length).toBe(3);
100
- // The prebuilt brand vnode renders inside the icon column.
101
- expect(buttons[0]!.querySelector(".brand-gh")).toBeTruthy();
110
+ const tiles = c.querySelectorAll<HTMLButtonElement>(".s-auth-methods .s-auth-methods-tile");
111
+ expect(tiles.length).toBe(3);
112
+ // The prebuilt brand vnode renders inside the icon box.
113
+ expect(tiles[0]!.querySelector(".brand-gh")).toBeTruthy();
102
114
  // A missing icon falls back to the label initial (never an empty box).
103
- expect(buttons[2]!.querySelector<HTMLElement>(".hk-icon-group-item-initial")?.textContent).toBe("F");
115
+ expect(tiles[2]!.querySelector<HTMLElement>(".s-auth-methods-tile-initial")?.textContent).toBe("F");
104
116
  // Disabled entries render dead and swallow clicks.
105
- expect(buttons[1]!.disabled).toBe(true);
106
- buttons[1]!.click();
117
+ expect(tiles[1]!.disabled).toBe(true);
118
+ tiles[1]!.click();
107
119
  await nextTick();
108
120
  expect(picked).toBe("");
109
121
  });
@@ -1,23 +1,27 @@
1
1
  import { defineComponent } from "vue";
2
- import HkIconButtonGroup from "./HkIconButtonGroup";
2
+ import HkTooltip from "./HkTooltip";
3
3
  import "./HkAuthMethodList.scss";
4
4
 
5
5
  /**
6
6
  * HkAuthMethodList — the auth card's third-party sign-in block (the
7
7
  * "other ways to sign in" row under the credentials form).
8
8
  *
9
- * Since 2026-09-13 (user direction) the block renders as the ICON
10
- * BUTTON GROUP by default: one centered `HkIconButtonGroup` row of
11
- * icon-only provider buttons (mode "buttons", size md) instead of the
12
- * stacked full-width [icon | label] buttons. Provider identity is
13
- * revealed on hover — every item wraps itself in an HkTooltip with the
14
- * provider label — which matches the platform login-chooser pattern
15
- * (Windows/macOS account tiles) and keeps the card compact when a
16
- * deployment offers several providers. The `select` event carries the
17
- * provider `key` exactly as before, so consumers swap rendering
18
- * without touching their OAuth flow.
9
+ * Since 2026-09-14 (user direction) the block renders as CENTERED
10
+ * INDEPENDENT icon tiles: every provider is its own framed 44px icon
11
+ * button with its own tooltip — NOT a button group. A group track (one
12
+ * shared frame stretched across the card with the icons floating in
13
+ * the middle) reads as an oversized empty box; the button-group
14
+ * component is reserved for its two fundamental jobs — a selector
15
+ * (single/multiple) and a tight action strip (see HkIconButtonGroup).
19
16
  *
20
- * The wrapper stays `display: contents`: the divider and the group row
17
+ * Provider identity is revealed on hover: each tile wraps itself in an
18
+ * HkTooltip carrying the provider label, which matches the platform
19
+ * login-chooser pattern (Windows/macOS account tiles) and keeps the
20
+ * card compact when a deployment offers several providers. The
21
+ * `select` event carries the provider `key` exactly as before, so
22
+ * consumers swap rendering without touching their OAuth flow.
23
+ *
24
+ * The wrapper stays `display: contents`: the divider and the tile row
21
25
  * remain layout children of HkAuthCard's `.s-auth-methods` slot
22
26
  * container (it owns the side padding and vertical rhythm).
23
27
  */
@@ -35,11 +39,11 @@ export default defineComponent({
35
39
  /** Accessible name + tooltip text (the hover reveal). */
36
40
  label: string;
37
41
  /** Prebuilt icon vnode (brand SVG, <img>, …) rendered inside the
38
- * icon-only button. Typed loose on purpose (same as
39
- * HkIconButtonGroup options): hosts materialize hikari against
40
- * their own vue store, and a hard VNode type breaks typecheck
41
- * whenever the host's vue minor differs. A missing icon falls
42
- * back to the label's initial. */
42
+ * framed tile. Typed loose on purpose (same as HkIconButtonGroup
43
+ * options): hosts materialize hikari against their own vue store,
44
+ * and a hard VNode type breaks typecheck whenever the host's vue
45
+ * minor differs. A missing icon falls back to the label's
46
+ * initial. */
43
47
  icon?: unknown;
44
48
  disabled?: boolean;
45
49
  }>,
@@ -47,7 +51,7 @@ export default defineComponent({
47
51
  },
48
52
  },
49
53
  emits: {
50
- /** A provider button was clicked. */
54
+ /** A provider tile was clicked. */
51
55
  select: (_key: string) => true,
52
56
  },
53
57
  setup(props, { emit }) {
@@ -58,18 +62,42 @@ export default defineComponent({
58
62
  <span>{props.divider}</span>
59
63
  </div>
60
64
  )}
61
- <HkIconButtonGroup
62
- class="s-auth-methods-group"
63
- mode="buttons"
64
- size="md"
65
- options={props.methods.map((method) => ({
66
- key: method.key,
67
- label: method.label,
68
- icon: method.icon,
69
- disabled: method.disabled === true,
70
- }))}
71
- onSelect={(key: string) => emit("select", key)}
72
- />
65
+ {/* A plain row, deliberately NOT role="group": every tile is an
66
+ independent action (its own frame, its own tooltip), and the
67
+ divider above names the section for sighted users. Group
68
+ semantics belong to the selector/tight-strip components. */}
69
+ <div class="s-auth-methods-tiles">
70
+ {props.methods.map((method) => {
71
+ const tile = (
72
+ <button
73
+ key={method.key}
74
+ type="button"
75
+ class="s-auth-methods-tile"
76
+ data-key={method.key}
77
+ aria-label={method.label}
78
+ title={undefined} // the HkTooltip popup owns the hover text
79
+ disabled={method.disabled === true}
80
+ onClick={() => emit("select", method.key)}
81
+ >
82
+ <span class="s-auth-methods-tile-icon" aria-hidden="true">
83
+ {method.icon != null
84
+ ? method.icon
85
+ : (
86
+ <span class="s-auth-methods-tile-initial">
87
+ {method.label.charAt(0).toUpperCase()}
88
+ </span>
89
+ )
90
+ }
91
+ </span>
92
+ </button>
93
+ );
94
+ return (
95
+ <HkTooltip key={method.key} text={method.label} placement="top" delay={300}>
96
+ {tile}
97
+ </HkTooltip>
98
+ );
99
+ })}
100
+ </div>
73
101
  </div>
74
102
  );
75
103
  },
@@ -25,13 +25,21 @@ export interface HkIconButtonGroupOption {
25
25
  * HkIconButtonGroup — the icon-only variant of the button-group family
26
26
  * (the same centered strip grammar as HkTabs, minus the text).
27
27
  *
28
- * Three working modes:
29
- * - "buttons" a plain action group (no selection state) — the auth
30
- * card's OAuth provider row
31
- * - "single" a radiogroup: one active key (v-model), re-clicking the
32
- * active key is a no-op — radio semantics
33
- * - "multiple" a toggle group: v-model is a string[] of active keys,
34
- * every item carries aria-pressed
28
+ * The group has exactly TWO fundamental jobs (2026-09-14 user direction
29
+ * — a group is a semantic component, not a generic box to throw buttons
30
+ * into), carried by three working modes:
31
+ * - a SELECTOR: "single" (radiogroup: one active key via v-model,
32
+ * re-clicking the active key is a no-op — radio semantics) or
33
+ * "multiple" (toggle set: v-model is a string[] of active keys,
34
+ * every item carries aria-pressed)
35
+ * - a TIGHT ACTION STRIP: "buttons" (no selection state) — related
36
+ * actions packed shoulder-to-shoulder in ONE shared track, the
37
+ * toolbar grammar
38
+ *
39
+ * Anything else is NOT a group: independent actions that merely live on
40
+ * the same row (e.g. a login card's third-party provider tiles) render
41
+ * as separate framed buttons with comfortable spacing — see
42
+ * HkAuthMethodList for the reference pattern.
35
43
  *
36
44
  * Every item is icon-only and rides one size taller than a standard
37
45
  * icon button (md = 44px vs the 32px icon button / 40px text button) —