cursedbelt-core 2.12.0 → 2.13.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/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # cursedbelt-core
2
+
3
+ Framework-agnostic core of the cursedbelt split — isomorphic domain logic, types, schemas and
4
+ wire shapes both sides share. `cwip` is below it; `cursedbelt-server` and `cursedbelt` (the
5
+ React design system) sit above it.
6
+
7
+ ```sh
8
+ bun add cursedbelt-core
9
+ ```
10
+
11
+ ## The root export is a BARREL — import the leaf
12
+
13
+ `import … from "cursedbelt-core"` re-exports everything below it. Import the subpath that owns
14
+ the symbol instead — `cursedbelt-core/navigation`, `cursedbelt-core/media/ffmpeg`,
15
+ `cursedbelt-core/master-lock`, and so on. `package.json`'s `exports` map is the full list of
16
+ subpaths; there is no separate reference doc to keep in sync with it.
17
+
18
+ ## Docs in this repo
19
+
20
+ [`docs/skipped-tests.md`](docs/skipped-tests.md) — the table `src/skippedTestsAreAnswered.spec.ts`
21
+ keeps honest against every skip site in `src`.
@@ -40,6 +40,15 @@ export interface NavItem {
40
40
  * a way to dodge the coverage test for a real, discoverable feature.
41
41
  */
42
42
  deepLinkOnly?: true;
43
+ /**
44
+ * Found by search and the palette, but **never drawn** as a side-nav child row or a hub
45
+ * tile — the half of a page a reader types for rather than browses to: a tab (`?tab=agents`),
46
+ * a filter, a section of a long page. Without it, every such address either became a rail
47
+ * row (the sidebar grew with every tab) or was not findable by name at all (owner,
48
+ * 2026-10-09: *"Station is getting complicated to find things in with its dozens of sidebar
49
+ * items"*, and in the same breath, a search that finds a page by what it holds).
50
+ */
51
+ searchOnly?: true;
43
52
  /** Extra match terms for search / palette (synonyms, old names, abbreviations). */
44
53
  keywords?: string[];
45
54
  }
@@ -33,8 +33,9 @@ export interface NavCrumb {
33
33
  /** The ordered domains visible under `gate` — the side-nav rail backbone. */
34
34
  export declare function deriveSideNavDomains(model: NavigationModel, gate?: GateFn): NavDomain[];
35
35
  /**
36
- * Every visible (gated, non-`deepLinkOnly`) item of `domain`, flattened across its
37
- * groups — for a simple HubTileGrid. Returns `[]` if the domain itself is gated out.
36
+ * Every visible (gated, non-`deepLinkOnly`, non-`searchOnly`) item of `domain`, flattened
37
+ * across its groups — for a simple HubTileGrid and the side nav's child rows. Returns `[]` if
38
+ * the domain itself is gated out.
38
39
  */
39
40
  export declare function deriveDomainItems(domain: NavDomain, gate?: GateFn): NavItem[];
40
41
  /**
@@ -54,7 +55,7 @@ export declare function deriveDomain(model: NavigationModel, domainId: string, g
54
55
  export declare function deriveBreadcrumbTrail(model: NavigationModel, path: string, gate?: GateFn): NavCrumb[];
55
56
  /**
56
57
  * The flat list of searchable destinations — every visible (gated,
57
- * non-`deepLinkOnly`) domain landing + item, each tagged with its domain/group so
58
+ * non-`deepLinkOnly`) domain landing + item, `searchOnly` ones included, each tagged with its domain/group so
58
59
  * results can be grouped. Drives HeaderSearch's instant pages layer *and* the
59
60
  * command palette; replaces every hand-listed search catalogue (so a feature is
60
61
  * findable the moment it's registered).
@@ -45,14 +45,17 @@ const itemCrumb = (it) => ({
45
45
  export function deriveSideNavDomains(model, gate = allowAllGate) {
46
46
  return (model?.domains ?? []).filter((d) => gate(d.gate));
47
47
  }
48
+ /** An item a reader can BROWSE to — drawn as a row or a tile, not only found by search. */
49
+ const isBrowsable = (it, gate) => !it.deepLinkOnly && !it.searchOnly && gate(it.gate);
48
50
  /**
49
- * Every visible (gated, non-`deepLinkOnly`) item of `domain`, flattened across its
50
- * groups — for a simple HubTileGrid. Returns `[]` if the domain itself is gated out.
51
+ * Every visible (gated, non-`deepLinkOnly`, non-`searchOnly`) item of `domain`, flattened
52
+ * across its groups — for a simple HubTileGrid and the side nav's child rows. Returns `[]` if
53
+ * the domain itself is gated out.
51
54
  */
52
55
  export function deriveDomainItems(domain, gate = allowAllGate) {
53
56
  if (!gate(domain.gate))
54
57
  return [];
55
- return domain.groups.flatMap((g) => g.items).filter((it) => !it.deepLinkOnly && gate(it.gate));
58
+ return domain.groups.flatMap((g) => g.items).filter((it) => isBrowsable(it, gate));
56
59
  }
57
60
  /**
58
61
  * The visible groups of `domain` (each with its visible items; empty groups
@@ -63,7 +66,7 @@ export function deriveDomainGroups(domain, gate = allowAllGate) {
63
66
  if (!gate(domain.gate))
64
67
  return [];
65
68
  return domain.groups
66
- .map((g) => ({ ...g, items: g.items.filter((it) => !it.deepLinkOnly && gate(it.gate)) }))
69
+ .map((g) => ({ ...g, items: g.items.filter((it) => isBrowsable(it, gate)) }))
67
70
  .filter((g) => g.items.length > 0);
68
71
  }
69
72
  /** Look up a domain by id (gated). */
@@ -113,7 +116,7 @@ export function deriveBreadcrumbTrail(model, path, gate = allowAllGate) {
113
116
  }
114
117
  /**
115
118
  * The flat list of searchable destinations — every visible (gated,
116
- * non-`deepLinkOnly`) domain landing + item, each tagged with its domain/group so
119
+ * non-`deepLinkOnly`) domain landing + item, `searchOnly` ones included, each tagged with its domain/group so
117
120
  * results can be grouped. Drives HeaderSearch's instant pages layer *and* the
118
121
  * command palette; replaces every hand-listed search catalogue (so a feature is
119
122
  * findable the moment it's registered).
@@ -11,6 +11,11 @@ export declare function searchInitials(label: string): string;
11
11
  * that carry domain/group tags for grouping — the extra fields ride straight through).
12
12
  * An empty/blank query returns `[]` (the palette shows its cold state instead).
13
13
  *
14
+ * Two tiers, the first always ahead of the second: an item whose NAME matches (label,
15
+ * initials, keywords), then one that matches only by what it HOLDS (description, group,
16
+ * place). So "frac" ranks Fractal first however many descriptions mention fractals, while
17
+ * a word that names no page still finds the pages that are about it.
18
+ *
14
19
  * Returns *all* matches in rank order; callers cap the count for display (the palette
15
20
  * shows 30). uFuzzy's contiguous scorer means "frac" ranks "Fractal" above "Format
16
21
  * Lab", and the initials augmentation means "qb" surfaces "Query Builder".
@@ -40,12 +40,38 @@ function haystackFor(item) {
40
40
  .filter(Boolean)
41
41
  .join(' ');
42
42
  }
43
+ /**
44
+ * The SECOND tier: what a destination holds rather than what it is called — its description,
45
+ * and the group and place it sits under when the entry carries them (catalogue entries do).
46
+ * Owner, 2026-10-09: *"I may not know where to go but typing something like 'api' brings up the
47
+ * relevant page links quickly"* — a page found only by its exact name is not found by someone
48
+ * who does not know the name.
49
+ */
50
+ function secondaryHaystackFor(item) {
51
+ return [item.description, item.groupLabel, item.domainLabel].filter(Boolean).join(' ');
52
+ }
53
+ /** uFuzzy's ranked indexes into `haystack` for `q`, `[]` when nothing matched. */
54
+ function rankedIndexes(haystack, q) {
55
+ const [idxs, , order] = uf.search(haystack, q);
56
+ // `idxs` is null when nothing matched and an empty array when filtered to zero; both
57
+ // mean "no results". `order` (the ranked iteration order into `idxs`) is present
58
+ // whenever there are matches under uFuzzy's info threshold (1e3 — far above any nav
59
+ // set); fall back to raw `idxs` order if it's ever absent.
60
+ if (!idxs || idxs.length === 0)
61
+ return [];
62
+ return order ? order.map((o) => idxs[o]) : [...idxs];
63
+ }
43
64
  /**
44
65
  * Rank `items` against `query`, best match first; non-matches dropped. Generic over the
45
66
  * item type (so callers can pass plain {@link NavItem}s *or* richer catalogue entries
46
67
  * that carry domain/group tags for grouping — the extra fields ride straight through).
47
68
  * An empty/blank query returns `[]` (the palette shows its cold state instead).
48
69
  *
70
+ * Two tiers, the first always ahead of the second: an item whose NAME matches (label,
71
+ * initials, keywords), then one that matches only by what it HOLDS (description, group,
72
+ * place). So "frac" ranks Fractal first however many descriptions mention fractals, while
73
+ * a word that names no page still finds the pages that are about it.
74
+ *
49
75
  * Returns *all* matches in rank order; callers cap the count for display (the palette
50
76
  * shows 30). uFuzzy's contiguous scorer means "frac" ranks "Fractal" above "Format
51
77
  * Lab", and the initials augmentation means "qb" surfaces "Query Builder".
@@ -54,17 +80,11 @@ export function fuzzyRankItems(query, items) {
54
80
  const q = query.trim();
55
81
  if (!q || items.length === 0)
56
82
  return [];
57
- const haystack = items.map(haystackFor);
58
- const [idxs, , order] = uf.search(haystack, q);
59
- // `idxs` is null when nothing matched and an empty array when filtered to zero; both
60
- // mean "no results". `order` (the ranked iteration order into `idxs`) is present
61
- // whenever there are matches under uFuzzy's info threshold (1e3 — far above any nav
62
- // set); fall back to raw `idxs` order if it's ever absent.
63
- if (!idxs || idxs.length === 0)
64
- return [];
65
- const ranked = order ? order.map((o) => idxs[o]) : idxs;
83
+ const primary = rankedIndexes(items.map(haystackFor), q);
84
+ const named = new Set(primary);
85
+ const secondary = rankedIndexes(items.map(secondaryHaystackFor), q).filter((i) => !named.has(i));
66
86
  const out = [];
67
- for (const haystackIdx of ranked) {
87
+ for (const haystackIdx of [...primary, ...secondary]) {
68
88
  const item = items[haystackIdx];
69
89
  if (item)
70
90
  out.push(item);
@@ -15,7 +15,10 @@ export interface ParsedHotkey extends HotkeyMods {
15
15
  * returns `false` when there's no `navigator`.
16
16
  */
17
17
  export declare function isApplePlatform(): boolean;
18
- /** Split a `'+'`-separated binding into its required modifiers + final key. */
18
+ /**
19
+ * Split a `'+'`-separated binding into its required modifiers + final key. `+` is itself a
20
+ * key — a bare `'+'` or `'$mod++'` splits into empty parts, and the key is the `+`.
21
+ */
19
22
  export declare function parseHotkey(binding: string, isApple?: boolean): ParsedHotkey;
20
23
  /** Does a keyboard event satisfy `binding` — exact modifier set + the key? */
21
24
  export declare function matchHotkey(e: KeyboardEvent, binding: string, isApple?: boolean): boolean;
@@ -21,14 +21,18 @@ export function isApplePlatform() {
21
21
  const probe = `${navigator.platform ?? ''} ${navigator.userAgent ?? ''}`;
22
22
  return /mac|iphone|ipad|ipod/i.test(probe);
23
23
  }
24
- /** Split a `'+'`-separated binding into its required modifiers + final key. */
24
+ /**
25
+ * Split a `'+'`-separated binding into its required modifiers + final key. `+` is itself a
26
+ * key — a bare `'+'` or `'$mod++'` splits into empty parts, and the key is the `+`.
27
+ */
25
28
  export function parseHotkey(binding, isApple = isApplePlatform()) {
26
- const parts = binding
29
+ const split = binding
27
30
  .toLowerCase()
28
31
  .split('+')
29
- .map((p) => p.trim())
30
- .filter(Boolean);
31
- const key = parts.pop() ?? '';
32
+ .map((p) => p.trim());
33
+ const plusKey = split.length > 1 && split[split.length - 1] === '';
34
+ const parts = split.filter(Boolean);
35
+ const key = plusKey ? '+' : (parts.pop() ?? '');
32
36
  const tokens = new Set(parts);
33
37
  const mod = tokens.has('$mod');
34
38
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-core",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
@@ -424,5 +424,9 @@
424
424
  "why": "build starts with `rm -rf dist`, and leafSubpathsImportNothing / shippedFilesAreTracked read dist/; racing it they read a directory that is not there yet"
425
425
  }
426
426
  ]
427
+ },
428
+ "repository": {
429
+ "type": "git",
430
+ "url": "git+https://github.com/wizardmordamir/cursedforge-cursedbelt-core.git"
427
431
  }
428
432
  }
@@ -13,6 +13,7 @@ import {
13
13
  groupCatalogueHits,
14
14
  type LegacyNavPrefs,
15
15
  migrateNavPreferences,
16
+ type NavDomain,
16
17
  type NavGate,
17
18
  type NavigationModel,
18
19
  normalizeNavPath,
@@ -180,6 +181,30 @@ describe('deriveDomainItems / deriveDomainGroups', () => {
180
181
  const ctgrGate: GateFn = (g?: NavGate) => !g?.hiddenInCtgr;
181
182
  expect(deriveDomainItems(admin, ctgrGate).map((i) => i.id)).toEqual(['admin-users']);
182
183
  });
184
+ test('a searchOnly item is found by search but never drawn as a row or a tile', () => {
185
+ const perf: NavDomain = {
186
+ id: 'perf',
187
+ label: 'Performance',
188
+ icon: 'activity',
189
+ path: '/performance',
190
+ groups: [
191
+ {
192
+ id: 'perf-pages',
193
+ label: 'Pages',
194
+ items: [
195
+ { id: 'perf-overview', label: 'Overview', path: '/performance/overview' },
196
+ { id: 'perf-agents', label: 'Agents', path: '/performance?tab=agents', searchOnly: true },
197
+ ],
198
+ },
199
+ { id: 'perf-tabs', label: 'Tabs', items: [{ id: 'perf-cost', label: 'Cost', path: '/performance?tab=cost', searchOnly: true }] },
200
+ ],
201
+ };
202
+ expect(deriveDomainItems(perf).map((i) => i.id)).toEqual(['perf-overview']);
203
+ // A group whose every item is searchOnly is not a heading on a hub landing.
204
+ expect(deriveDomainGroups(perf).map((g) => g.id)).toEqual(['perf-pages']);
205
+ const found = deriveSearchCatalogue({ domains: [perf] }).map((e) => e.id);
206
+ expect(found).toEqual(['perf', 'perf-overview', 'perf-agents', 'perf-cost']);
207
+ });
183
208
  });
184
209
 
185
210
  describe('deriveBreadcrumbTrail', () => {
@@ -54,6 +54,15 @@ export interface NavItem {
54
54
  * a way to dodge the coverage test for a real, discoverable feature.
55
55
  */
56
56
  deepLinkOnly?: true;
57
+ /**
58
+ * Found by search and the palette, but **never drawn** as a side-nav child row or a hub
59
+ * tile — the half of a page a reader types for rather than browses to: a tab (`?tab=agents`),
60
+ * a filter, a section of a long page. Without it, every such address either became a rail
61
+ * row (the sidebar grew with every tab) or was not findable by name at all (owner,
62
+ * 2026-10-09: *"Station is getting complicated to find things in with its dozens of sidebar
63
+ * items"*, and in the same breath, a search that finds a page by what it holds).
64
+ */
65
+ searchOnly?: true;
57
66
  /** Extra match terms for search / palette (synonyms, old names, abbreviations). */
58
67
  keywords?: string[];
59
68
  }
@@ -79,13 +79,17 @@ export function deriveSideNavDomains(
79
79
  return (model?.domains ?? []).filter((d) => gate(d.gate));
80
80
  }
81
81
 
82
+ /** An item a reader can BROWSE to — drawn as a row or a tile, not only found by search. */
83
+ const isBrowsable = (it: NavItem, gate: GateFn): boolean => !it.deepLinkOnly && !it.searchOnly && gate(it.gate);
84
+
82
85
  /**
83
- * Every visible (gated, non-`deepLinkOnly`) item of `domain`, flattened across its
84
- * groups — for a simple HubTileGrid. Returns `[]` if the domain itself is gated out.
86
+ * Every visible (gated, non-`deepLinkOnly`, non-`searchOnly`) item of `domain`, flattened
87
+ * across its groups — for a simple HubTileGrid and the side nav's child rows. Returns `[]` if
88
+ * the domain itself is gated out.
85
89
  */
86
90
  export function deriveDomainItems(domain: NavDomain, gate: GateFn = allowAllGate): NavItem[] {
87
91
  if (!gate(domain.gate)) return [];
88
- return domain.groups.flatMap((g) => g.items).filter((it) => !it.deepLinkOnly && gate(it.gate));
92
+ return domain.groups.flatMap((g) => g.items).filter((it) => isBrowsable(it, gate));
89
93
  }
90
94
 
91
95
  /**
@@ -96,7 +100,7 @@ export function deriveDomainItems(domain: NavDomain, gate: GateFn = allowAllGate
96
100
  export function deriveDomainGroups(domain: NavDomain, gate: GateFn = allowAllGate): NavGroup[] {
97
101
  if (!gate(domain.gate)) return [];
98
102
  return domain.groups
99
- .map((g) => ({ ...g, items: g.items.filter((it) => !it.deepLinkOnly && gate(it.gate)) }))
103
+ .map((g) => ({ ...g, items: g.items.filter((it) => isBrowsable(it, gate)) }))
100
104
  .filter((g) => g.items.length > 0);
101
105
  }
102
106
 
@@ -154,7 +158,7 @@ export function deriveBreadcrumbTrail(
154
158
 
155
159
  /**
156
160
  * The flat list of searchable destinations — every visible (gated,
157
- * non-`deepLinkOnly`) domain landing + item, each tagged with its domain/group so
161
+ * non-`deepLinkOnly`) domain landing + item, `searchOnly` ones included, each tagged with its domain/group so
158
162
  * results can be grouped. Drives HeaderSearch's instant pages layer *and* the
159
163
  * command palette; replaces every hand-listed search catalogue (so a feature is
160
164
  * findable the moment it's registered).
@@ -46,12 +46,39 @@ function haystackFor(item: NavItem): string {
46
46
  .join(' ');
47
47
  }
48
48
 
49
+ /**
50
+ * The SECOND tier: what a destination holds rather than what it is called — its description,
51
+ * and the group and place it sits under when the entry carries them (catalogue entries do).
52
+ * Owner, 2026-10-09: *"I may not know where to go but typing something like 'api' brings up the
53
+ * relevant page links quickly"* — a page found only by its exact name is not found by someone
54
+ * who does not know the name.
55
+ */
56
+ function secondaryHaystackFor(item: NavItem & { groupLabel?: string; domainLabel?: string }): string {
57
+ return [item.description, item.groupLabel, item.domainLabel].filter(Boolean).join(' ');
58
+ }
59
+
60
+ /** uFuzzy's ranked indexes into `haystack` for `q`, `[]` when nothing matched. */
61
+ function rankedIndexes(haystack: string[], q: string): number[] {
62
+ const [idxs, , order] = uf.search(haystack, q);
63
+ // `idxs` is null when nothing matched and an empty array when filtered to zero; both
64
+ // mean "no results". `order` (the ranked iteration order into `idxs`) is present
65
+ // whenever there are matches under uFuzzy's info threshold (1e3 — far above any nav
66
+ // set); fall back to raw `idxs` order if it's ever absent.
67
+ if (!idxs || idxs.length === 0) return [];
68
+ return order ? order.map((o) => idxs[o] as number) : [...idxs];
69
+ }
70
+
49
71
  /**
50
72
  * Rank `items` against `query`, best match first; non-matches dropped. Generic over the
51
73
  * item type (so callers can pass plain {@link NavItem}s *or* richer catalogue entries
52
74
  * that carry domain/group tags for grouping — the extra fields ride straight through).
53
75
  * An empty/blank query returns `[]` (the palette shows its cold state instead).
54
76
  *
77
+ * Two tiers, the first always ahead of the second: an item whose NAME matches (label,
78
+ * initials, keywords), then one that matches only by what it HOLDS (description, group,
79
+ * place). So "frac" ranks Fractal first however many descriptions mention fractals, while
80
+ * a word that names no page still finds the pages that are about it.
81
+ *
55
82
  * Returns *all* matches in rank order; callers cap the count for display (the palette
56
83
  * shows 30). uFuzzy's contiguous scorer means "frac" ranks "Fractal" above "Format
57
84
  * Lab", and the initials augmentation means "qb" surfaces "Query Builder".
@@ -60,17 +87,12 @@ export function fuzzyRankItems<T extends NavItem>(query: string, items: T[]): T[
60
87
  const q = query.trim();
61
88
  if (!q || items.length === 0) return [];
62
89
 
63
- const haystack = items.map(haystackFor);
64
- const [idxs, , order] = uf.search(haystack, q);
65
- // `idxs` is null when nothing matched and an empty array when filtered to zero; both
66
- // mean "no results". `order` (the ranked iteration order into `idxs`) is present
67
- // whenever there are matches under uFuzzy's info threshold (1e3 — far above any nav
68
- // set); fall back to raw `idxs` order if it's ever absent.
69
- if (!idxs || idxs.length === 0) return [];
70
- const ranked = order ? order.map((o) => idxs[o]) : idxs;
90
+ const primary = rankedIndexes(items.map(haystackFor), q);
91
+ const named = new Set(primary);
92
+ const secondary = rankedIndexes(items.map(secondaryHaystackFor), q).filter((i) => !named.has(i));
71
93
 
72
94
  const out: T[] = [];
73
- for (const haystackIdx of ranked) {
95
+ for (const haystackIdx of [...primary, ...secondary]) {
74
96
  const item = items[haystackIdx];
75
97
  if (item) out.push(item);
76
98
  }
@@ -35,14 +35,18 @@ export function isApplePlatform(): boolean {
35
35
  return /mac|iphone|ipad|ipod/i.test(probe);
36
36
  }
37
37
 
38
- /** Split a `'+'`-separated binding into its required modifiers + final key. */
38
+ /**
39
+ * Split a `'+'`-separated binding into its required modifiers + final key. `+` is itself a
40
+ * key — a bare `'+'` or `'$mod++'` splits into empty parts, and the key is the `+`.
41
+ */
39
42
  export function parseHotkey(binding: string, isApple = isApplePlatform()): ParsedHotkey {
40
- const parts = binding
43
+ const split = binding
41
44
  .toLowerCase()
42
45
  .split('+')
43
- .map((p) => p.trim())
44
- .filter(Boolean);
45
- const key = parts.pop() ?? '';
46
+ .map((p) => p.trim());
47
+ const plusKey = split.length > 1 && split[split.length - 1] === '';
48
+ const parts = split.filter(Boolean);
49
+ const key = plusKey ? '+' : (parts.pop() ?? '');
46
50
  const tokens = new Set(parts);
47
51
  const mod = tokens.has('$mod');
48
52
  return {
@@ -66,6 +66,21 @@ describe('fuzzyRankItems', () => {
66
66
  expect(fuzzyRankItems('zzqzzx', ITEMS)).toEqual([]);
67
67
  });
68
68
 
69
+ test('what a page HOLDS finds it: a description-only word ranks after every name match', () => {
70
+ const pages: (NavItem & { domainLabel?: string })[] = [
71
+ { ...item('Connections', ['google', 'github']), description: 'The sign-ins and API tokens your apps run on' },
72
+ { ...item('Traffic'), description: 'What this Mac served' },
73
+ { ...item('Agents'), domainLabel: 'Performance', description: 'What each agent cost' },
74
+ { ...item('API keys') },
75
+ ];
76
+ // "api" names "API keys" — that is first — and is only HELD by Connections, which follows.
77
+ expect(labels(fuzzyRankItems('api', pages))).toEqual(['API keys', 'Connections']);
78
+ // The place an entry sits under counts as what it holds: "performance" finds its tab.
79
+ expect(labels(fuzzyRankItems('performance', pages))).toEqual(['Agents']);
80
+ // Never the same item twice when it matches in both tiers.
81
+ expect(labels(fuzzyRankItems('agent', pages))).toEqual(['Agents']);
82
+ });
83
+
69
84
  test('carries through extra fields on richer entry types (domain tags)', () => {
70
85
  type Tagged = NavItem & { domainId: string };
71
86
  const tagged: Tagged[] = [{ ...item('Fractal'), domainId: 'play' }];
@@ -118,6 +133,14 @@ describe('formatHotkey', () => {
118
133
  test('multiple modifiers order Apple-style (⌃⌥⇧⌘)', () => {
119
134
  expect(formatHotkey('$mod+shift+p', true)).toBe('⇧⌘P');
120
135
  });
136
+ test('`+` is a key too — a bare `+` or `shift++` draws a + cap, never an empty one', () => {
137
+ expect(formatHotkey('+', true)).toBe('+');
138
+ expect(formatHotkey('+', false)).toBe('+');
139
+ expect(formatHotkey('$mod++', true)).toBe('⌘+');
140
+ expect(formatHotkey('$mod++', false)).toBe('Ctrl +');
141
+ expect(parseHotkey('shift++')).toMatchObject({ shift: true, key: '+' });
142
+ expect(matchHotkey(kd({ key: '+' }), '+', true)).toBe(true);
143
+ });
121
144
  });
122
145
 
123
146
  // ── ShortcutRegistry ──────────────────────────────────────────────────────────