@celestia-island/hikari 0.55.164 → 0.55.165

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.164",
3
+ "version": "0.55.165",
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,69 @@
1
+ import { defineComponent } from "vue";
2
+ import { Equal, EqualApproximately, EqualNot } from "lucide-vue-next";
3
+
4
+ import type { HkFilterOperator } from "./HkFilterPanel";
5
+
6
+ /**
7
+ * The operator glyph as an ICON, never a text character (user direction
8
+ * 2026-10-15: font stacks render U+2249 ≉ unevenly — the operator
9
+ * buttons are UI chrome, so they draw like every other icon in the
10
+ * family). Lucide ships =, ≠ and ≈; the ordered four and ≉ are
11
+ * hikari-authored in lucide's exact stroke grammar (24×24, stroke 2,
12
+ * currentColor) — ≉ is lucide's own ≈ path pair with EqualNot's slash
13
+ * composed in, so the set reads as one family.
14
+ */
15
+
16
+ // Lucide's EqualApproximately path pair (ISC, verbatim) + its EqualNot
17
+ // slash — composed rather than reinvented.
18
+ const NAPPROX_PATHS = [
19
+ "M5 15a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0",
20
+ "M5 9a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0",
21
+ "m5 19 14-14",
22
+ ];
23
+
24
+ const CUSTOM: Partial<Record<HkFilterOperator, string[]>> = {
25
+ napprox: NAPPROX_PATHS,
26
+ lt: ["m19 6-11 6 11 6"],
27
+ lte: ["m19 5-11 6 11 6", "M8 20h11"],
28
+ gt: ["m5 6 11 6-11 6"],
29
+ gte: ["m5 5 11 6-11 6", "M5 20h11"],
30
+ };
31
+
32
+ const LUCIDE: Partial<Record<HkFilterOperator, typeof Equal>> = {
33
+ eq: Equal,
34
+ ne: EqualNot,
35
+ approx: EqualApproximately,
36
+ };
37
+
38
+ export default defineComponent({
39
+ name: "HkFilterOperatorIcon",
40
+ props: {
41
+ op: { type: String as () => HkFilterOperator, required: true },
42
+ size: { type: Number, default: 14 },
43
+ },
44
+ setup(props) {
45
+ return () => {
46
+ const Lucide = LUCIDE[props.op];
47
+ if (Lucide) return <Lucide size={props.size} aria-hidden="true" />;
48
+ const paths = CUSTOM[props.op]!;
49
+ return (
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ width={props.size}
53
+ height={props.size}
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ stroke-linecap="round"
59
+ stroke-linejoin="round"
60
+ aria-hidden="true"
61
+ >
62
+ {paths.map((d) => (
63
+ <path key={d} d={d} />
64
+ ))}
65
+ </svg>
66
+ );
67
+ };
68
+ },
69
+ });
@@ -42,6 +42,14 @@
42
42
  /* Minimum outer margins for every column (user direction
43
43
  * 2026-10-12): the rows never touch the panel edge. */
44
44
  padding: var(--space-8, 0.5rem) var(--space-12, 0.75rem);
45
+ /* Empty panel collapses to the add verb's own width (user direction
46
+ * 2026-10-15): no rows means nothing to align columns against, so the
47
+ * popover hugs the 添加条件 button instead of holding the full-row
48
+ * width. Desktop only — the phone branch below re-widens to 100%. */
49
+ &[data-empty] {
50
+ width: max-content;
51
+ min-width: 0;
52
+ }
45
53
  }
46
54
 
47
55
  /* A row is not a box — it donates its three cells to the panel grid so
@@ -73,6 +73,12 @@ describe("HkFilterPanel stylesheet contract", () => {
73
73
  expect(rule).toContain("justify-self: start");
74
74
  });
75
75
 
76
+ it("the empty panel collapses to the add verb's own width", () => {
77
+ // 2026-10-15: no rows = nothing to align columns against — the
78
+ // desktop popover hugs the 添加条件 button (data-empty marker).
79
+ expect(css).toMatch(/\.hk-filter-panel\[data-empty\]\s*\{[^}]*width:\s*max-content/);
80
+ });
81
+
76
82
  it("the phone operator column keeps its widened cap (≥ the old 4.25rem)", () => {
77
83
  expect(css).toContain("@media (max-width: 767px)");
78
84
  const media = css.slice(css.indexOf("@media (max-width: 767px)"));
@@ -45,6 +45,9 @@ describe("HkFilterPanel (array state, 2026-10-15)", () => {
45
45
  expect(c.querySelectorAll(".hk-filter-panel-row")).toHaveLength(0);
46
46
  expect(c.querySelector(".hk-filter-panel-add")).toBeTruthy();
47
47
  expect(c.querySelectorAll(".hk-filter-panel-field")).toHaveLength(0);
48
+ // The empty panel carries the collapse marker (width hugs the add
49
+ // verb on desktop — pinned in the styles contract test).
50
+ expect(c.querySelector(".hk-filter-panel")!.getAttribute("data-empty")).toBe("true");
48
51
 
49
52
  const state: HkFilterState = [
50
53
  { key: "tool", op: "approx", value: "web" },
@@ -188,13 +191,15 @@ describe("HkFilterPanel (array state, 2026-10-15)", () => {
188
191
  );
189
192
  const rows = [...c.querySelectorAll(".hk-filter-panel-row")];
190
193
  const opsOf = (row: Element) =>
191
- [...row.querySelectorAll(".hk-filter-panel-op")].map((b) => b.textContent);
194
+ [...row.querySelectorAll(".hk-filter-panel-op")].map((b) => b.getAttribute("title"));
192
195
  // Text speaks ONLY the fuzzy pair (2026-10-15: 等于其实是模糊匹配 —
193
196
  // label it ≈, and the exclusion ≉).
194
- expect(opsOf(rows[0]!)).toEqual(["≈", "≉"]);
195
- expect(opsOf(rows[1]!)).toEqual(["<", "≤", "=", "≥", ">"]);
196
- expect(opsOf(rows[2]!)).toEqual(["=", "≠"]);
197
- expect(opsOf(rows[3]!)).toEqual(["≥", "≤"]);
197
+ expect(opsOf(rows[0]!)).toEqual(["fuzzy match", "fuzzy non-match"]);
198
+ expect(opsOf(rows[1]!)).toEqual(["less than", "at most", "equals", "at least", "greater than"]);
199
+ // Select speaks ONLY the eq/ne pair — rendered as the fuzzy pair
200
+ // glyphs (2026-10-15 follow-up: 等于/不等于 换成 ≈/≉).
201
+ expect(opsOf(rows[2]!)).toEqual(["fuzzy match", "fuzzy non-match"]);
202
+ expect(opsOf(rows[3]!)).toEqual(["at least", "at most"]);
198
203
  });
199
204
 
200
205
  it("emits a new state when an operator is picked (live, no confirm)", async () => {
@@ -207,7 +212,7 @@ describe("HkFilterPanel (array state, 2026-10-15)", () => {
207
212
  }),
208
213
  );
209
214
  const notMatch = [...c.querySelectorAll(".hk-filter-panel-row")[0]!.querySelectorAll(".hk-filter-panel-op")]
210
- .find((b) => b.textContent === "≉")!;
215
+ .find((b) => b.getAttribute("title") === "fuzzy non-match")!;
211
216
  notMatch.dispatchEvent(new MouseEvent("click", { bubbles: true }));
212
217
  await Promise.resolve();
213
218
  expect(seen).toEqual([{ key: "tool", op: "napprox", value: "web" }]);
@@ -5,6 +5,7 @@ import HkDatePicker from "./HkDatePicker";
5
5
  import HkInput from "./HkInput";
6
6
  import HkPopover from "./HkPopover";
7
7
  import HkPopupSelect, { type HkPopupSelectOption } from "./HkPopupSelect";
8
+ import HkFilterOperatorIcon from "./HkFilterOperatorIcon";
8
9
  import HkScrollContainer from "./HkScrollContainer";
9
10
  import { useI18n } from "../i18n/context";
10
11
  import "./HkFilterPanel.scss";
@@ -53,16 +54,15 @@ export interface HkFilterCondition {
53
54
  * field chooser. */
54
55
  export type HkFilterState = HkFilterCondition[];
55
56
 
56
- const OPERATOR_SYMBOL: Record<HkFilterOperator, string> = {
57
- eq: "=",
58
- ne: "≠",
59
- approx: "≈",
60
- napprox: "≉",
61
- lt: "<",
62
- lte: "≤",
63
- gte: "≥",
64
- gt: ">",
65
- };
57
+ /** Which ICON the operator renders as: fields that speak ONLY the
58
+ * eq/ne pair (select) render the fuzzy pair ≈/≉ (2026-10-15 — 等于
59
+ * 其实是模糊匹配 applies to them too); number's exact = keeps the
60
+ * plain equals icon inside the ordered five. */
61
+ function glyphOpFor(kind: HkFilterFieldDef["kind"], op: HkFilterOperator): HkFilterOperator {
62
+ if (kind === "select" && op === "eq") return "approx";
63
+ if (kind === "select" && op === "ne") return "napprox";
64
+ return op;
65
+ }
66
66
 
67
67
  const OPERATOR_LABEL: Record<HkFilterOperator, { key: string; fallback: string }> = {
68
68
  eq: { key: "hikari::filterPanel.op.eq", fallback: "equals" },
@@ -309,7 +309,7 @@ export default defineComponent({
309
309
  return () => {
310
310
  const label = props.label ?? t("hikari::filterBar.trigger", "Filters");
311
311
  return (
312
- <div class="hk-filter-panel" data-testid="hk-filter-panel" data-active={activeCount.value > 0 || undefined} aria-label={label}>
312
+ <div class="hk-filter-panel" data-testid="hk-filter-panel" data-active={activeCount.value > 0 || undefined} data-empty={rows.value.length === 0 || undefined} aria-label={label}>
313
313
  {rows.value.map((cond, index) => {
314
314
  const field = fieldByKey(cond.key);
315
315
  // An undeclared key (the page renamed a field under live
@@ -322,20 +322,23 @@ export default defineComponent({
322
322
  <div key={index} class="hk-filter-panel-row" data-kind={kind}>
323
323
  <span class="hk-filter-panel-field">{labelText}</span>
324
324
  <div class="hk-filter-panel-ops" role="group" aria-label={`${labelText} — ${label}`}>
325
- {ops.map((op) => (
325
+ {ops.map((op) => {
326
+ const glyph = glyphOpFor(kind, op);
327
+ return (
326
328
  <button
327
329
  key={op}
328
330
  type="button"
329
331
  class="hk-filter-panel-op"
330
332
  aria-pressed={cond.op === op}
331
- aria-label={t(OPERATOR_LABEL[op].key, OPERATOR_LABEL[op].fallback)}
332
- title={t(OPERATOR_LABEL[op].key, OPERATOR_LABEL[op].fallback)}
333
+ aria-label={t(OPERATOR_LABEL[glyph].key, OPERATOR_LABEL[glyph].fallback)}
334
+ title={t(OPERATOR_LABEL[glyph].key, OPERATOR_LABEL[glyph].fallback)}
333
335
  disabled={props.disabled || !field}
334
336
  onClick={() => emitCondition(index, { op })}
335
337
  >
336
- {OPERATOR_SYMBOL[op]}
338
+ <HkFilterOperatorIcon op={glyph} />
337
339
  </button>
338
- ))}
340
+ );
341
+ })}
339
342
  </div>
340
343
  <div class="hk-filter-panel-value">
341
344
  <div class="hk-filter-panel-control">
@@ -319,7 +319,7 @@ describe("HkFilterTrigger", () => {
319
319
  // stand down for it, not swallow it. (Pick the ≈ toggle; the first
320
320
  // op button is the already-pressed =.)
321
321
  const op = [...document.body.querySelectorAll<HTMLButtonElement>(".hk-filter-panel .hk-filter-panel-op")].find(
322
- (b) => b.textContent === "≈",
322
+ (b) => b.getAttribute("title") === "fuzzy match",
323
323
  )!;
324
324
  op.dispatchEvent(new MouseEvent("click", { bubbles: true }));
325
325
  await Promise.resolve();
@@ -1,5 +1,5 @@
1
1
  import { computed, defineComponent, ref, type PropType } from "vue";
2
- import { SlidersHorizontal } from "lucide-vue-next";
2
+ import { Filter } from "lucide-vue-next";
3
3
 
4
4
  import HkButton from "./HkButton";
5
5
  import HkCornerBadge from "./HkCornerBadge";
@@ -74,7 +74,7 @@ export default defineComponent({
74
74
  open.value = !open.value;
75
75
  }}
76
76
  >
77
- {{ icon: () => <SlidersHorizontal size={16} /> }}
77
+ {{ icon: () => <Filter size={16} /> }}
78
78
  </HkIconButton>
79
79
  </HkCornerBadge>
80
80
  <HkPopover