@apliteni/apliteni-ui 0.58.2 → 0.60.0

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.
@@ -1,3 +1,5 @@
1
+ import { SCORE, scoreCommand, rankCommands, rankGroups, paletteHotkey, norm } from '../logic/command-palette.js';
2
+ export { SCORE, scoreCommand, rankCommands, rankGroups, paletteHotkey } from '../logic/command-palette.js';
1
3
  // Command palette — one overlay, a text box and a ranked list of things to run
2
4
  // or go to.
3
5
  //
@@ -16,134 +18,6 @@ const cx = (...a) => a.filter(Boolean).join(' ');
16
18
  let _uid = 0;
17
19
  const nextId = (p = 'cmdk') => `${p}-${++_uid}`;
18
20
 
19
- /**
20
- * How a match is worth more than another match.
21
- *
22
- * Six ways a query can meet an item, scored so that the whole beats the part
23
- * and the name beats the note. The numbers are a ladder and not a measurement —
24
- * what matters is the order, which is why src/components/command-palette.test.js
25
- * asserts the ORDER of the results rather than any of these values.
26
- *
27
- * cmdk's command-score is the other published answer: one continuous score
28
- * built out of SCORE_CONTINUE_MATCH 1, SCORE_SPACE_WORD_JUMP 0.9,
29
- * SCORE_NON_SPACE_WORD_JUMP 0.8 and SCORE_CHARACTER_JUMP 0.17. It ranks
30
- * beautifully and cannot be explained to a reader who asks why their item is
31
- * third. A ladder can: the kit's palettes hold tens of items, not thousands.
32
- */
33
- export const SCORE = {
34
- exact: 100,
35
- prefix: 90,
36
- wordStart: 80,
37
- contains: 70,
38
- keyword: 60,
39
- description: 40,
40
- subsequence: 20,
41
- };
42
-
43
- const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
44
-
45
- // Every character of `q`, in order, somewhere in `s` — "nc" finding "New
46
- // campaign". The weakest match the kit accepts, and the only one that can pair
47
- // a two-letter query with a twenty-letter label.
48
- function subsequence(s, q) {
49
- let at = 0;
50
- for (const ch of q) {
51
- at = s.indexOf(ch, at);
52
- if (at === -1) return false;
53
- at += 1;
54
- }
55
- return true;
56
- }
57
-
58
- // A word here starts after a space or one of the separators a product name uses,
59
- // so "camp" reaches the second word of "New campaign" and the "utm" in
60
- // "url-utm_source".
61
- const wordStarts = (s) => s.split(/[\s/\-_.:,]+/).filter(Boolean);
62
-
63
- function scoreField({ label, description, keywords }, q) {
64
- const name = norm(label);
65
- if (!name && !description && !(keywords || []).length) return 0;
66
- if (name === q) return SCORE.exact;
67
- if (name.startsWith(q)) return SCORE.prefix;
68
- if (wordStarts(name).some((w) => w.startsWith(q))) return SCORE.wordStart;
69
- if (name.includes(q)) return SCORE.contains;
70
- for (const k of keywords || []) {
71
- const key = norm(k);
72
- if (key === q || key.startsWith(q)) return SCORE.keyword;
73
- }
74
- if (norm(description).includes(q)) return SCORE.description;
75
- if (subsequence(name, q)) return SCORE.subsequence;
76
- return 0;
77
- }
78
-
79
- /**
80
- * What one item is worth against one query. 0 means it is not a result.
81
- *
82
- * An empty query scores everything the same, so a palette nobody has typed into
83
- * shows what the caller passed, in the caller's order — which is where a
84
- * product puts the four things somebody actually does here.
85
- *
86
- * A query with a space in it has to match as a whole OR token by token, every
87
- * token landing somewhere: "new camp" reaches "New campaign" as a whole, and
88
- * "camp new" reaches it one token at a time. The weakest token decides, because
89
- * a result is only as good as the part of the query it answers worst.
90
- */
91
- export function scoreCommand(item, query) {
92
- const q = norm(query);
93
- if (!q) return 1;
94
- const whole = scoreField(item, q);
95
- const tokens = q.split(' ');
96
- if (tokens.length < 2) return whole;
97
- let weakest = Infinity;
98
- for (const t of tokens) {
99
- const s = scoreField(item, t);
100
- if (!s) { weakest = 0; break; }
101
- weakest = Math.min(weakest, s);
102
- }
103
- return Math.max(whole, weakest === Infinity ? 0 : weakest);
104
- }
105
-
106
- /**
107
- * The items that answer `query`, best first.
108
- *
109
- * Ties keep the caller's order — that is the whole of the kit's ordering
110
- * opinion, and it is what lets a product put its four most-used commands at the
111
- * top of the list it passes and have them stay there.
112
- */
113
- export function rankCommands(items, query) {
114
- return (items || [])
115
- .map((item, i) => ({ item, i, score: scoreCommand(item, query) }))
116
- .filter((r) => r.score > 0)
117
- .sort((a, b) => b.score - a.score || a.i - b.i)
118
- .map((r) => r.item);
119
- }
120
-
121
- /**
122
- * The groups that answer `query`, best group first, each holding its own ranked
123
- * rows and nothing that scored zero.
124
- *
125
- * A group is carried by its best row, and the caller's order breaks the tie at
126
- * both levels — so a palette nobody has typed into is exactly the list that was
127
- * passed in. cmdk ranks groups the same way; the tie-break is the kit's, and it
128
- * is what keeps a heading from moving under a reader who has typed nothing.
129
- */
130
- export function rankGroups(groups, query) {
131
- return (groups || [])
132
- .map((g, i) => {
133
- const items = rankCommands(g.items || [], query);
134
- const best = items.length ? scoreCommand(items[0], query) : 0;
135
- return { group: { ...g, items }, i, best };
136
- })
137
- .filter((g) => g.group.items.length > 0)
138
- .sort((a, b) => b.best - a.best || a.i - b.i)
139
- .map((g) => g.group);
140
- }
141
-
142
- /** The label for the key that opens it, on the platform the caller is on. */
143
- export function paletteHotkey(platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
144
- return /mac|iphone|ipad|ipod/i.test(String(platform || '')) ? '⌘K' : 'Ctrl K';
145
- }
146
-
147
21
  // The three fields a query is matched against, written onto the row so the
148
22
  // wiring can re-rank what is already rendered without being handed the data a
149
23
  // second time. Three attributes and not one blob: the ranking weighs a name
@@ -1,3 +1,5 @@
1
+ import { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
2
+ export { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
1
3
  // Dropdown — the kit's one popover-list primitive. A trigger opens a panel of
2
4
  // item rows; two flavours share the same panel and the same open/close JS:
3
5
  //
@@ -79,22 +81,6 @@ function ddBody({ items, sections }, listbox, sx) {
79
81
  // why: docs/specification.md#a-dropdown-with-a-search-field
80
82
  // NFD takes the mark off é or ö; ł, ø, đ and the rest are letters of their own
81
83
  // with nothing to take off, so they are mapped by hand.
82
- const FOLD = { ł: 'l', ø: 'o', đ: 'd', ð: 'd', ß: 'ss', æ: 'ae', œ: 'oe', ı: 'i', þ: 'th' };
83
- const fold = (s) => String(s == null ? '' : s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
84
- .replace(/[łøđðßæœıþ]/g, (c) => FOLD[c]);
85
-
86
- /**
87
- * Is this query narrowing anything? A query of spaces, or of marks with no letter
88
- * under them, is not — it leaves every row showing, and the separators with them.
89
- *
90
- * Public because a second implementation of this dropdown cannot write its own: the
91
- * React <Dropdown> asks these two, the way <CommandPalette> asks rankGroups(), so a
92
- * list a server rendered and the same list after a keystroke hide the same rows.
93
- */
94
- export const dropdownFiltering = (query) => fold(query).trim() !== '';
95
-
96
- /** Does `label` match `query`? A substring, anywhere, ignoring case and accents. */
97
- export const dropdownMatch = (label, query) => fold(label).includes(fold(query).trim());
98
84
  const ddIsRow = (it) => it && it !== '---' && !it.separator;
99
85
 
100
86
  let _ddSeq = 0;
@@ -1,3 +1,5 @@
1
+ import { calloutIcons } from '../logic/callout.js';
2
+ export { calloutIcons } from '../logic/callout.js';
1
3
  // apliteni-ui component factories — each returns an HTML string, matching the
2
4
  // viz/ server-render idiom so the portal can adopt them with no framework.
3
5
  import { icon } from '../assets/icons.js';
@@ -218,7 +220,7 @@ export function switchToggle({ checked = false, disabled = false, name, label =
218
220
  }
219
221
 
220
222
  // ---- Callout / toast / success ------------------------------------------
221
- export const calloutIcons = { info: 'info', success: 'check', warn: 'alert', danger: 'alert', neutral: 'info' };
223
+
222
224
 
223
225
  export function callout({ variant, icon: ic = calloutIcons[variant] || 'info', body, actions } = {}) {
224
226
  return `<div class="${esc(cx('ui-callout', variant && variant !== 'neutral' && `ui-callout--${variant}`))}"><span class="ui-callout__icon">${icon(ic)}</span><div class="ui-callout__body">${body}${actions != null ? `<div class="ui-toolbar ui-callout__actions">${actions}</div>` : ''}</div></div>`;
@@ -1,3 +1,5 @@
1
+ import { PAGE_SIZES, DEFAULT_PAGE_SIZE } from '../logic/pagination.js';
2
+ export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from '../logic/pagination.js';
1
3
  // Pagination — the strip under a table or a list, as an HTML string.
2
4
  //
3
5
  // It renders a page the CALLER computed. Rows never come in here: `page`,
@@ -15,8 +17,7 @@ import { safeUrl } from '../html.js';
15
17
  // AG Grid defaults to 100, and the finance portal's two data-heavy surfaces
16
18
  // already page at 100. A 250 step was in the first draft of this component and
17
19
  // no surveyed kit offers one, so it went rather than being invented.
18
- export const PAGE_SIZES = [25, 50, 100];
19
- export const DEFAULT_PAGE_SIZE = 100;
20
+
20
21
 
21
22
  // The classes button({ variant: 'ghost', size: 'sm' }) emits, written out because
22
23
  // button() takes no extra class and every control here needs one of its own for
@@ -1,12 +1,5 @@
1
- export function segmentedNextIndex(key, index, length) {
2
- if (!length) return null;
3
- if (key === 'ArrowRight' || key === 'ArrowDown') return (index + 1) % length;
4
- if (key === 'ArrowLeft' || key === 'ArrowUp') return (index - 1 + length) % length;
5
- if (key === 'Home') return 0;
6
- if (key === 'End') return length - 1;
7
- return null;
8
- }
9
-
1
+ import { segmentedNextIndex } from '../logic/segmented.js';
2
+ export { segmentedNextIndex } from '../logic/segmented.js';
10
3
  export function initSegmented(root = document) {
11
4
  const cleanups = [];
12
5
  root.querySelectorAll('[data-seg]').forEach(group => {
@@ -1,14 +1,14 @@
1
+ import { formatNumericValue, formatDeltaValue } from '../logic/table-values.js';
1
2
  import { esc } from './index.js';
2
3
  import { safeUrl } from '../html.js';
3
- export function numericValue({ value, unit = '', missing = 'Not available' } = {}) {
4
- if (value == null || value === '') return `<span class="ui-value" aria-label="${esc(missing)}">—</span>`;
5
- return `<span class="ui-value">${esc(value)}${unit ? `<span class="ui-value__unit">${esc(unit)}</span>` : ''}</span>`;
4
+ export function numericValue(options = {}) {
5
+ const value = formatNumericValue(options);
6
+ return `<span class="ui-value"${value.missing !== undefined ? ` aria-label="${esc(value.missing)}"` : ''}>${esc(value.text)}${value.unit ? `<span class="ui-value__unit">${esc(value.unit)}</span>` : ''}</span>`;
6
7
  }
7
8
 
8
- export function deltaValue({ value, tone = 'neutral', basisId, missing = 'No earlier figure' } = {}) {
9
- const present = value != null && value !== '';
10
- const judged = present && ['success', 'danger'].includes(tone) && !/^[+−-]?0+(?:[.,]0+)?(?:[^\d.,].*)?$/.test(String(value).replace(/\s/g, ''));
11
- return `<span class="ui-delta${judged ? ` ui-delta--${tone}` : ''}"${basisId ? ` aria-describedby="${esc(basisId)}"` : ''}>${esc(present ? value : missing)}</span>`;
9
+ export function deltaValue(options = {}) {
10
+ const value = formatDeltaValue(options);
11
+ return `<span class="${value.className}"${value.basisId ? ` aria-describedby="${esc(value.basisId)}"` : ''}>${esc(value.text)}</span>`;
12
12
  }
13
13
 
14
14
  export function rowIdentity({ symbol = '', name = '', logo, href } = {}) {
package/src/index.d.ts ADDED
@@ -0,0 +1,128 @@
1
+ export declare function esc(value: unknown): string;
2
+ export declare function field(opts?: Record<string, unknown>): string;
3
+ export declare function input(opts?: Record<string, unknown>): string;
4
+ export declare function textarea(opts?: Record<string, unknown>): string;
5
+ export declare function select(opts?: Record<string, unknown>): string;
6
+ export declare function numericValue(opts?: Record<string, unknown>): string;
7
+ export declare function deltaValue(opts?: Record<string, unknown>): string;
8
+ export declare function filterBar(opts?: Record<string, unknown>): string;
9
+ export declare function initFilterBar(host: Element, opts?: Record<string, unknown>): { update: (opts: Record<string, unknown>) => void; destroy: () => void };
10
+ export declare function segmentedNextIndex(key: string, index: number, length: number): number | null;
11
+ export declare function icon(name: string, className?: string): string;
12
+ export declare function snippet(opts?: Record<string, unknown>): string;
13
+ export declare function button(opts?: Record<string, unknown>): string;
14
+ export declare function badge(label: string, variant?: string): string;
15
+ export declare const calloutIcons: Record<'neutral' | 'info' | 'success' | 'warn' | 'danger', string>;
16
+ export declare function callout(opts?: Record<string, unknown>): string;
17
+ export declare function card(opts?: Record<string, unknown>): string;
18
+ export declare function pagination(opts?: Record<string, unknown>): string;
19
+ export declare function drawerSection(opts?: Record<string, unknown>): string;
20
+ export declare function drawer(opts?: Record<string, unknown>): string;
21
+ export declare function dropdown(opts?: Record<string, unknown>): string;
22
+ export declare function wireDropdown(root?: Document | Element): void;
23
+ /** The kit's own match, asked rather than re-implemented by <Dropdown>. */
24
+ export declare function dropdownMatch(label: unknown, query: unknown): boolean;
25
+ export declare function dropdownFiltering(query: unknown): boolean;
26
+ export declare function backLink(opts?: Record<string, unknown>): string;
27
+ export declare function statBand(opts?: Record<string, unknown>): string;
28
+ export declare function commandPalette(opts?: Record<string, unknown>): string;
29
+ export declare function rankGroups<G>(groups: readonly G[], query: string): G[];
30
+ export declare function rankCommands<I>(items: readonly I[], query: string): I[];
31
+ export declare function scoreCommand(item: Record<string, unknown>, query: string): number;
32
+ export declare function paletteHotkey(platform?: string): string;
33
+ export declare const PAGE_SIZES: number[];
34
+ export declare const DEFAULT_PAGE_SIZE: number;
35
+ export type NumericValueOptions = { value?: string | number | null; unit?: string; missing?: string };
36
+ export type DeltaValueOptions = { value?: string | null; tone?: 'success' | 'danger' | 'neutral'; basisId?: string; missing?: string };
37
+ export declare function formatNumericValue(options?: NumericValueOptions): { text: string; unit: string; missing: string | undefined };
38
+ export declare function formatDeltaValue(options?: DeltaValueOptions): { text: string; className: string; basisId: string | undefined };
39
+ export declare const SCORE: Record<'exact' | 'prefix' | 'wordStart' | 'contains' | 'keyword' | 'description' | 'subsequence', number>;
40
+ export declare const iconNames: string[];
41
+ export declare const iconCategories: { name: string; names: string[] }[];
42
+ export declare const iconOnlyAllowed: Record<string, string>;
43
+ export declare const iconMeanings: Record<string, string>;
44
+ export declare const sun: string;
45
+ export declare const moon: string;
46
+ export declare function prism(prefix?: string, size?: number): string;
47
+ export declare function seedling(prefix?: string, size?: number): string;
48
+ export declare function brand(options?: { p?: string; word?: string; size?: number; href?: string }): string;
49
+ export declare function illo(name: string): string;
50
+ export * from './motion.js';
51
+ export declare const illoNames: string[];
52
+
53
+ // Vanilla factories accept their existing option bags and return HTML.
54
+ export declare function accentPicker(opts?: Record<string, unknown>): string;
55
+ export declare function accountMenu(opts?: Record<string, unknown>): string;
56
+ export declare function accountShell(opts?: Record<string, unknown>): string;
57
+ export declare function appShell(opts?: Record<string, unknown>): string;
58
+ export declare function breadcrumbs(opts?: Record<string, unknown>): string;
59
+ export declare function busyRegion(opts?: Record<string, unknown>): string;
60
+ export declare function checkbox(opts?: Record<string, unknown>): string;
61
+ export declare function confirm(opts?: Record<string, unknown>): string;
62
+ export declare function deniedState(opts?: Record<string, unknown>): string;
63
+ export declare function emptyState(opts?: Record<string, unknown>): string;
64
+ export declare function feedbackWidget(opts?: Record<string, unknown>): string;
65
+ export declare function footer(opts?: Record<string, unknown>): string;
66
+ export declare function nav(opts?: Record<string, unknown>): string;
67
+ export declare function navTabs(opts?: Record<string, unknown>): string;
68
+ export declare function rowIdentity(opts?: Record<string, unknown>): string;
69
+ export declare function segmented(opts?: Record<string, unknown>): string;
70
+ export declare function sidebarNav(opts?: Record<string, unknown>): string;
71
+ export declare function skeleton(opts?: Record<string, unknown>): string;
72
+ export declare function skeletonTable(opts?: Record<string, unknown>): string;
73
+ export declare function success(opts?: Record<string, unknown>): string;
74
+ export declare function successPanel(opts?: Record<string, unknown>): string;
75
+ export declare function switchToggle(opts?: Record<string, unknown>): string;
76
+ export declare function tabs(opts?: Record<string, unknown>): string;
77
+ export declare function toast(opts?: Record<string, unknown>): string;
78
+ export declare function tooltip(opts?: Record<string, unknown>): string;
79
+ export declare function topbar(opts?: Record<string, unknown>): string;
80
+ export declare function commandPaletteList(groups?: readonly Record<string, unknown>[], opts?: { uid?: string; from?: number }): string;
81
+ export declare function deckTextSwitch(active?: string): string;
82
+ export declare function hlShell(raw: string): string;
83
+ export declare function pill(label: string, variant?: string): string;
84
+ export declare function statusDot(live?: boolean): string;
85
+ export declare function successCheck(): string;
86
+ export declare function themeIcon(theme: string): string;
87
+ export declare function themeName(theme: string): string;
88
+ export declare function themeToggle(theme?: string): string;
89
+ export declare function versionSwitcher(versions?: { label: string; meta?: string; badge?: string }[], activeIdx?: number): string;
90
+
91
+ export declare const ACCENTS: string[];
92
+ export declare const ACCOUNT_NAV: { id: string; icon: string; label: string }[];
93
+ export declare const RAIL_COOKIE: 'apliteni-ui-rail';
94
+ export declare const STAT_TONES: string[];
95
+ export declare const STAT_VARIANTS: string[];
96
+
97
+ export declare function applyAccent(name: string, root?: HTMLElement): void;
98
+ export declare function applyTheme(theme: string, root?: HTMLElement): void;
99
+ export declare function railCollapsed(cookies?: string): boolean | null;
100
+ export declare function closeCommandPalette(root: Element | null): void;
101
+ export declare function closeConfirm(root: Element | null): void;
102
+ export declare function closeDrawer(root: Element | null): void;
103
+ export declare function openCommandPalette(root: Element | null, returnFocusTo?: HTMLElement | null): void;
104
+ export declare function openConfirm(root: Element | null, returnFocusTo?: HTMLElement | null): void;
105
+ export declare function openDrawer(root: Element | null, returnFocusTo?: HTMLElement | null): void;
106
+ export declare function setPaletteResults(root: HTMLElement, groups: readonly Record<string, unknown>[]): void;
107
+ export declare function hideTooltip(host: HTMLElement): void;
108
+ export declare function showTooltip(host: HTMLElement, mark: Element): void;
109
+ export declare function initRowIdentity(root?: Document | Element): void;
110
+ export declare function initSegmented(root?: Document | Element): () => void;
111
+ export declare function initTabs(root?: Document | Element): void;
112
+ export declare function wireCommandPalette(root?: Document | Element): void;
113
+ export declare function wireConfirm(root?: Document | Element): void;
114
+ export declare function wireDrawer(root?: Document | Element): void;
115
+ export declare function wireFeedback(opts?: Record<string, unknown>): void;
116
+ export declare function wireNav(root?: Document | Element): void;
117
+ export declare function wireShell(root?: Document | Element, opts?: { persist?: boolean }): void;
118
+ export declare function wireTooltip(root?: Document | Element): void;
119
+ export declare function wireTopbar(root?: Document | Element): void;
120
+ export declare function wireSuccess(root: Element, opts?: { onDone?: () => void }): () => void;
121
+ export declare function wirePagination(root?: Document | Element | string | null, opts?: { onPage?: (page: number) => void; onPageSize?: (size: number) => void }): () => void;
122
+ export declare function nearestSection(node: Node | null, root?: Element | null): Element | null;
123
+ export declare function dismissToast(element: HTMLElement): void;
124
+ export declare function wireToastStack(container: Element | string | null): Element | null;
125
+ export declare function pushToast(container: Element | string | null, opts?: Record<string, unknown>): Element | null;
126
+ export declare function setBusy(root: Element | string | null, opts?: { busy?: boolean; message?: string; body?: string }): Element | null;
127
+ export declare function setButtonBusy(element: HTMLElement, opts?: { busy?: boolean }): void;
128
+ export declare function setPagerStatus(root: Element | string | null, text: string): Element | null;
package/src/index.js CHANGED
@@ -26,3 +26,10 @@ export { setButtonBusy } from './components/button-busy.js';
26
26
  export * from './components/table-values.js';
27
27
  export * from './components/segmented.js';
28
28
  export * from './components/filter-bar.js';
29
+
30
+ export { dropdownMatch, dropdownFiltering } from './logic/dropdown.js';
31
+ export { SCORE, rankGroups, rankCommands, scoreCommand, paletteHotkey } from './logic/command-palette.js';
32
+ export { segmentedNextIndex } from './logic/segmented.js';
33
+ export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from './logic/pagination.js';
34
+ export { calloutIcons } from './logic/callout.js';
35
+ export { formatNumericValue, formatDeltaValue } from './logic/table-values.js';
@@ -0,0 +1 @@
1
+ export const calloutIcons = { info: 'info', success: 'check', warn: 'alert', danger: 'alert', neutral: 'info' };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * How a match is worth more than another match.
3
+ *
4
+ * Six ways a query can meet an item, scored so that the whole beats the part
5
+ * and the name beats the note. The numbers are a ladder and not a measurement —
6
+ * what matters is the order, which is why src/components/command-palette.test.js
7
+ * asserts the ORDER of the results rather than any of these values.
8
+ *
9
+ * cmdk's command-score is the other published answer: one continuous score
10
+ * built out of SCORE_CONTINUE_MATCH 1, SCORE_SPACE_WORD_JUMP 0.9,
11
+ * SCORE_NON_SPACE_WORD_JUMP 0.8 and SCORE_CHARACTER_JUMP 0.17. It ranks
12
+ * beautifully and cannot be explained to a reader who asks why their item is
13
+ * third. A ladder can: the kit's palettes hold tens of items, not thousands.
14
+ */
15
+ export const SCORE = {
16
+ exact: 100,
17
+ prefix: 90,
18
+ wordStart: 80,
19
+ contains: 70,
20
+ keyword: 60,
21
+ description: 40,
22
+ subsequence: 20,
23
+ };
24
+
25
+ export const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
26
+
27
+ // Every character of `q`, in order, somewhere in `s` — "nc" finding "New
28
+ // campaign". The weakest match the kit accepts, and the only one that can pair
29
+ // a two-letter query with a twenty-letter label.
30
+ function subsequence(s, q) {
31
+ let at = 0;
32
+ for (const ch of q) {
33
+ at = s.indexOf(ch, at);
34
+ if (at === -1) return false;
35
+ at += 1;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ // A word here starts after a space or one of the separators a product name uses,
41
+ // so "camp" reaches the second word of "New campaign" and the "utm" in
42
+ // "url-utm_source".
43
+ const wordStarts = (s) => s.split(/[\s/\-_.:,]+/).filter(Boolean);
44
+
45
+ function scoreField({ label, description, keywords }, q) {
46
+ const name = norm(label);
47
+ if (!name && !description && !(keywords || []).length) return 0;
48
+ if (name === q) return SCORE.exact;
49
+ if (name.startsWith(q)) return SCORE.prefix;
50
+ if (wordStarts(name).some((w) => w.startsWith(q))) return SCORE.wordStart;
51
+ if (name.includes(q)) return SCORE.contains;
52
+ for (const k of keywords || []) {
53
+ const key = norm(k);
54
+ if (key === q || key.startsWith(q)) return SCORE.keyword;
55
+ }
56
+ if (norm(description).includes(q)) return SCORE.description;
57
+ if (subsequence(name, q)) return SCORE.subsequence;
58
+ return 0;
59
+ }
60
+
61
+ /**
62
+ * What one item is worth against one query. 0 means it is not a result.
63
+ *
64
+ * An empty query scores everything the same, so a palette nobody has typed into
65
+ * shows what the caller passed, in the caller's order — which is where a
66
+ * product puts the four things somebody actually does here.
67
+ *
68
+ * A query with a space in it has to match as a whole OR token by token, every
69
+ * token landing somewhere: "new camp" reaches "New campaign" as a whole, and
70
+ * "camp new" reaches it one token at a time. The weakest token decides, because
71
+ * a result is only as good as the part of the query it answers worst.
72
+ */
73
+ export function scoreCommand(item, query) {
74
+ const q = norm(query);
75
+ if (!q) return 1;
76
+ const whole = scoreField(item, q);
77
+ const tokens = q.split(' ');
78
+ if (tokens.length < 2) return whole;
79
+ let weakest = Infinity;
80
+ for (const t of tokens) {
81
+ const s = scoreField(item, t);
82
+ if (!s) { weakest = 0; break; }
83
+ weakest = Math.min(weakest, s);
84
+ }
85
+ return Math.max(whole, weakest === Infinity ? 0 : weakest);
86
+ }
87
+
88
+ /**
89
+ * The items that answer `query`, best first.
90
+ *
91
+ * Ties keep the caller's order — that is the whole of the kit's ordering
92
+ * opinion, and it is what lets a product put its four most-used commands at the
93
+ * top of the list it passes and have them stay there.
94
+ */
95
+ export function rankCommands(items, query) {
96
+ return (items || [])
97
+ .map((item, i) => ({ item, i, score: scoreCommand(item, query) }))
98
+ .filter((r) => r.score > 0)
99
+ .sort((a, b) => b.score - a.score || a.i - b.i)
100
+ .map((r) => r.item);
101
+ }
102
+
103
+ /**
104
+ * The groups that answer `query`, best group first, each holding its own ranked
105
+ * rows and nothing that scored zero.
106
+ *
107
+ * A group is carried by its best row, and the caller's order breaks the tie at
108
+ * both levels — so a palette nobody has typed into is exactly the list that was
109
+ * passed in. cmdk ranks groups the same way; the tie-break is the kit's, and it
110
+ * is what keeps a heading from moving under a reader who has typed nothing.
111
+ */
112
+ export function rankGroups(groups, query) {
113
+ return (groups || [])
114
+ .map((g, i) => {
115
+ const items = rankCommands(g.items || [], query);
116
+ const best = items.length ? scoreCommand(items[0], query) : 0;
117
+ return { group: { ...g, items }, i, best };
118
+ })
119
+ .filter((g) => g.group.items.length > 0)
120
+ .sort((a, b) => b.best - a.best || a.i - b.i)
121
+ .map((g) => g.group);
122
+ }
123
+
124
+ /** The label for the key that opens it, on the platform the caller is on. */
125
+ export function paletteHotkey(platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
126
+ return /mac|iphone|ipad|ipod/i.test(String(platform || '')) ? '⌘K' : 'Ctrl K';
127
+ }
@@ -0,0 +1,16 @@
1
+ const FOLD = { ł: 'l', ø: 'o', đ: 'd', ð: 'd', ß: 'ss', æ: 'ae', œ: 'oe', ı: 'i', þ: 'th' };
2
+ const fold = (s) => String(s == null ? '' : s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
3
+ .replace(/[łøđðßæœıþ]/g, (c) => FOLD[c]);
4
+
5
+ /**
6
+ * Is this query narrowing anything? A query of spaces, or of marks with no letter
7
+ * under them, is not — it leaves every row showing, and the separators with them.
8
+ *
9
+ * Public because a second implementation of this dropdown cannot write its own: the
10
+ * React <Dropdown> asks these two, the way <CommandPalette> asks rankGroups(), so a
11
+ * list a server rendered and the same list after a keystroke hide the same rows.
12
+ */
13
+ export const dropdownFiltering = (query) => fold(query).trim() !== '';
14
+
15
+ /** Does `label` match `query`? A substring, anywhere, ignoring case and accents. */
16
+ export const dropdownMatch = (label, query) => fold(label).includes(fold(query).trim());
@@ -0,0 +1,2 @@
1
+ export const PAGE_SIZES = [25, 50, 100];
2
+ export const DEFAULT_PAGE_SIZE = 100;
@@ -0,0 +1,8 @@
1
+ export function segmentedNextIndex(key, index, length) {
2
+ if (!length) return null;
3
+ if (key === 'ArrowRight' || key === 'ArrowDown') return (index + 1) % length;
4
+ if (key === 'ArrowLeft' || key === 'ArrowUp') return (index - 1 + length) % length;
5
+ if (key === 'Home') return 0;
6
+ if (key === 'End') return length - 1;
7
+ return null;
8
+ }
@@ -0,0 +1,10 @@
1
+ export function formatNumericValue({ value, unit = '', missing = 'Not available' } = {}) {
2
+ const present = value != null && value !== '';
3
+ return { text: present ? String(value) : '—', unit: present ? unit : '', missing: present ? undefined : missing };
4
+ }
5
+
6
+ export function formatDeltaValue({ value, tone = 'neutral', basisId, missing = 'No earlier figure' } = {}) {
7
+ const present = value != null && value !== '';
8
+ const judged = present && ['success', 'danger'].includes(tone) && !/^[+−-]?0+(?:[.,]0+)?(?:[^\d.,].*)?$/.test(String(value).replace(/\s/g, ''));
9
+ return { text: String(present ? value : missing), className: `ui-delta${judged ? ` ui-delta--${tone}` : ''}`, basisId };
10
+ }
@@ -0,0 +1,6 @@
1
+ export declare function prefersReducedMotion(): boolean;
2
+ export declare function staggerDelay(index: number, stepMs?: number): number;
3
+ export declare function initReveal(root?: Document | Element): IntersectionObserver | undefined;
4
+ export declare const ENTRANCE_FALLBACK_MS: number;
5
+ export declare function playEntrance(element: Element | null, className?: string): void;
6
+ export declare function replay(element: HTMLElement | null): void;