@celestia-island/hikari 0.40.4 → 0.40.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.40.4",
3
+ "version": "0.40.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",
@@ -5,6 +5,7 @@ import HkAffixPicker, { type HkAffixOption } from "./HkAffixPicker";
5
5
 
6
6
  const OPTIONS: HkAffixOption[] = [
7
7
  { key: "cn", label: "China", meta: "+86", flag: "🇨🇳", keywords: "zhongguo 中国 0086" },
8
+ { key: "cn-main", label: "中华人民共和国", meta: "+86", flag: "🇨🇳" },
8
9
  { key: "jp", label: "Japan", meta: "+81", flag: "🇯🇵" },
9
10
  { key: "us", label: "United States", meta: "+1", flag: "🇺🇸" },
10
11
  ];
@@ -154,9 +155,9 @@ describe("HkAffixPicker", () => {
154
155
  it("single mode lists every option, marks the active one, closes on pick", async () => {
155
156
  const { events, container } = mountPicker({ selected: "cn" });
156
157
  await openPopup(container);
157
- expect(rows()).toHaveLength(3);
158
+ expect(rows()).toHaveLength(4);
158
159
  expect(rows()[0].hasAttribute("data-active")).toBe(true);
159
- rows()[1].click();
160
+ rows()[2].click();
160
161
  await nextTick();
161
162
  expect(events.select.at(-1)).toBe("jp");
162
163
  // Single mode closes after the pick (leave transition may linger).
@@ -176,6 +177,30 @@ describe("HkAffixPicker", () => {
176
177
  expect(document.querySelector(".hk-affix-empty")?.textContent).toContain("No matches");
177
178
  });
178
179
 
180
+ it("fuzzy fallback finds a renamed CJK label by in-order subsequence", async () => {
181
+ const { container } = mountPicker();
182
+ await openPopup(container);
183
+ // 中国 is NOT a substring of 中华人民共和国 — only the in-order
184
+ // subsequence fallback (中 at 0, 国 at 6) can surface the cn-main
185
+ // row the substring pass misses.
186
+ await typeQuery("中国");
187
+ const labels = rows().map((r) => r.textContent);
188
+ // Both the China row (keyword substring match) and the renamed
189
+ // cn-main row (fuzzy subsequence) surface.
190
+ expect(labels).toContain("🇨🇳China+86");
191
+ expect(labels.some((l) => l.includes("中华人民共和国"))).toBe(true);
192
+ });
193
+
194
+ it("order-violating query does not fuzzy-match the CJK label", async () => {
195
+ const { container } = mountPicker();
196
+ await openPopup(container);
197
+ // 国民 is out of order in 中华人民共和国 (国 comes after 民) — the
198
+ // subsequence pass must NOT surface the row.
199
+ await typeQuery("国民");
200
+ expect(rows()).toHaveLength(0);
201
+ expect(document.querySelector(".hk-affix-empty")?.textContent).toContain("No matches");
202
+ });
203
+
179
204
  it("re-attaches the row list when the empty-state query is cleared", async () => {
180
205
  const { container } = mountPicker();
181
206
  await openPopup(container);
@@ -190,6 +215,7 @@ describe("HkAffixPicker", () => {
190
215
  expect(document.querySelector(".hk-affix-empty")).toBeNull();
191
216
  expect(rows().map((r) => r.textContent)).toEqual([
192
217
  expect.stringContaining("China"),
218
+ expect.stringContaining("中华人民共和国"),
193
219
  expect.stringContaining("Japan"),
194
220
  expect.stringContaining("United States"),
195
221
  ]);
@@ -197,7 +223,12 @@ describe("HkAffixPicker", () => {
197
223
  expect(document.querySelector(".hk-affix-list")).toBeTruthy();
198
224
  // Re-filtering after the remount keeps working on the live list.
199
225
  await typeQuery("+86");
200
- expect(rows().map((r) => r.textContent)).toEqual([expect.stringContaining("China")]);
226
+ // Both +86 rows match by substring (the cn-main row via its meta)
227
+ // the original fixtured "China" row is still surfaced.
228
+ expect(rows().map((r) => r.textContent)).toEqual([
229
+ expect.stringContaining("China"),
230
+ expect.stringContaining("中华人民共和国"),
231
+ ]);
201
232
  });
202
233
 
203
234
  it("multi mode renders selected tags and offers only the remaining rows", async () => {
@@ -211,13 +242,17 @@ describe("HkAffixPicker", () => {
211
242
  expect(tags()[0].querySelector(".hk-affix-tag-dot")).toBeTruthy();
212
243
  expect(tags()[1].querySelector(".hk-affix-tag-dot")).toBeNull();
213
244
  // Rows exclude the already-selected keys.
214
- expect(rows().map((r) => r.textContent)).toEqual([expect.stringContaining("United States")]);
245
+ expect(rows().map((r) => r.textContent)).toEqual([
246
+ expect.stringContaining("中华人民共和国"),
247
+ expect.stringContaining("United States"),
248
+ ]);
215
249
  });
216
250
 
217
251
  it("multi mode keeps the popup open when a row is picked", async () => {
218
252
  const { events, container } = mountPicker({ mode: "multi", selected: ["cn"] });
219
253
  await openPopup(container);
220
- rows()[0].click();
254
+ // With cn selected, the remaining rows are [cn-main, jp, us]; pick jp.
255
+ rows()[1].click();
221
256
  await nextTick();
222
257
  expect(events.select.at(-1)).toBe("jp");
223
258
  expect(rows().length).toBeGreaterThan(0);
@@ -38,6 +38,18 @@ export interface HkAffixOption {
38
38
  disabled?: boolean;
39
39
  }
40
40
 
41
+ /** True when every character of `query` appears in `text` in the same
42
+ * order — gaps allowed ("中国" finds 中华人民共和国). Callers lowercase
43
+ * both sides first, matching the substring pass. */
44
+ function isSubsequence(query: string, text: string): boolean {
45
+ let i = 0;
46
+ for (const ch of text) {
47
+ if (ch === query[i]) i++;
48
+ if (i === query.length) return true;
49
+ }
50
+ return false;
51
+ }
52
+
41
53
  /**
42
54
  * HkAffixPicker — the standardized "affix chip + searchable popup list"
43
55
  * control. A chip rides the prefix (left) or suffix (right) slot of a
@@ -190,7 +202,16 @@ export const HkAffixPicker = defineComponent({
190
202
  );
191
203
 
192
204
  /** Rows of the pick list. Multi hides already-selected keys (they
193
- * live in the tag list instead); single always shows everything. */
205
+ * live in the tag list instead); single always shows everything.
206
+ *
207
+ * Filtering is TWO-PASS over a single field at a time (never across
208
+ * concatenated fields, which produces noise):
209
+ * 1. an exact substring match for precision (label / meta / key /
210
+ * keywords, case-folded);
211
+ * 2. an in-order character subsequence fallback (gaps allowed) —
212
+ * so a renamed or CJK-composed label like "中华人民共和国" is
213
+ * still found by its short form "中国" (中 at 0, 国 at 6).
214
+ */
194
215
  const filteredRows = computed<readonly HkAffixOption[]>(() => {
195
216
  const base =
196
217
  props.mode === "multi"
@@ -202,7 +223,12 @@ export const HkAffixPicker = defineComponent({
202
223
  if (o.label.toLowerCase().includes(q)) return true;
203
224
  if (o.meta && o.meta.toLowerCase().includes(q)) return true;
204
225
  if (o.key.toLowerCase().includes(q)) return true;
205
- return !!o.keywords && o.keywords.toLowerCase().includes(q);
226
+ if (o.keywords && o.keywords.toLowerCase().includes(q)) return true;
227
+ // Fuzzy fallback — per field, in-order subsequence, gaps allowed.
228
+ if (isSubsequence(q, o.label.toLowerCase())) return true;
229
+ if (o.meta && isSubsequence(q, o.meta.toLowerCase())) return true;
230
+ if (isSubsequence(q, o.key.toLowerCase())) return true;
231
+ return !!o.keywords && isSubsequence(q, o.keywords.toLowerCase());
206
232
  });
207
233
  });
208
234
 
@@ -117,12 +117,12 @@ describe("HkPhoneInput", () => {
117
117
  }
118
118
  });
119
119
 
120
- it("still finds the PRC row when searching the short alias", async () => {
120
+ it("still finds the PRC row when searching the short form 中国", async () => {
121
121
  const { container } = mountPhone();
122
122
  await openPicker(container);
123
- // The row renders the formal zh name (中华人民共和国), which no longer
124
- // contains 中国 as a substring the catalog's zhAlias keeps the old
125
- // short form reachable from the search field.
123
+ // The row renders the formal zh name (中华人民共和国), which contains
124
+ // 中国 only as an in-order subsequence (中 at 0, at 6) — the affix
125
+ // picker's fuzzy fallback surfaces the +86 row, no alias needed.
126
126
  const search = document.querySelector<HTMLInputElement>(
127
127
  ".hk-affix-search .hk-input-element",
128
128
  );
@@ -102,15 +102,14 @@ export const HkPhoneInput = defineComponent({
102
102
  );
103
103
 
104
104
  /** Picker rows come from the shared affix catalog shape; the
105
- * keywords haystack keeps the old filter reach: en/zh names plus
106
- * the optional zh search alias, ISO code, bare and zero-prefixed
107
- * dial digits ("86"/"0086"). */
105
+ * keywords haystack keeps the old filter reach: en/zh names, ISO
106
+ * code, bare and zero-prefixed dial digits ("86"/"0086"). */
108
107
  const dialOptions = computed<readonly HkAffixOption[]>(() =>
109
108
  props.countries.map((c) => ({
110
109
  key: c.iso,
111
110
  label: dialCodeName(c, locale),
112
111
  meta: `+${c.dial}`,
113
- keywords: `${c.en} ${c.zh} ${c.zhAlias ?? ""} ${c.iso} +${c.dial} 00${c.dial}`,
112
+ keywords: `${c.en} ${c.zh} ${c.iso} +${c.dial} 00${c.dial}`,
114
113
  })),
115
114
  );
116
115
 
@@ -17,7 +17,6 @@ describe("dialCodes catalog", () => {
17
17
  dial: "86",
18
18
  en: "China",
19
19
  zh: "中华人民共和国",
20
- zhAlias: "中国",
21
20
  });
22
21
  for (const entry of DIAL_CODES) {
23
22
  expect(entry.iso).toMatch(/^[a-z]{2}$/);
@@ -19,12 +19,6 @@ export interface DialCodeEntry {
19
19
  en: string;
20
20
  /** Simplified Chinese country name. */
21
21
  zh: string;
22
- /** Search-only Chinese alias — never rendered as the display name.
23
- * HkPhoneInput folds it into the popup's keyword haystack so the old
24
- * short form still finds the entry after a formal rename (e.g. the
25
- * PRC row renamed to 中华人民共和国 no longer contains 中国 as a
26
- * substring). */
27
- zhAlias?: string;
28
22
  }
29
23
 
30
24
  /** Flag glyph for an ISO 3166-1 alpha-2 code (regional indicators). */
@@ -49,7 +43,7 @@ export function dialCodeName(entry: DialCodeEntry, locale: string): string {
49
43
  * prefixes like +1 / +7).
50
44
  */
51
45
  export const DIAL_CODES: readonly DialCodeEntry[] = [
52
- { iso: "cn", dial: "86", en: "China", zh: "中华人民共和国", zhAlias: "中国" },
46
+ { iso: "cn", dial: "86", en: "China", zh: "中华人民共和国" },
53
47
  { iso: "hk", dial: "852", en: "Hong Kong (China)", zh: "中国香港" },
54
48
  { iso: "mo", dial: "853", en: "Macau (China)", zh: "中国澳门" },
55
49
  { iso: "tw", dial: "886", en: "Taiwan (China)", zh: "中国台湾" },