1import type { Component } from "vue";
2
3export interface Command {
4 id: string;
5 title: string;
6 /** Rendered inline after the title in muted ink; not part of fuzzy scoring. */
7 titleSuffix?: string;
8 group: string;
9 icon?: Component;
10 /** Props bound to `icon`, for icons that render from data (e.g. priority level). */
11 iconProps?: Record<string, unknown>;
12 subtitle?: string;
13 keywords?: string;
14 /** Combo string rendered by ShortcutKey, e.g. "Mod+." */
15 hint?: string;
16 /** Multiplies the fuzzy score; <1 sinks a row once the user types. */
17 weight?: number;
18 /** Tailwind class for the leading colour dot (status/priority rows). */
19 dotClass?: string;
20 /** Leading avatar (agent rows); `label` initials are the no-photo fallback. */
21 avatar?: { image?: string; label: string };
22 /** Fixed score, bypassing the fuzzy scorer. Server hits arrive pre-ranked. */
23 rank?: number;
24 /** Server-highlighted title with `<mark>` runs; `title` stays plain for scoring. */
25 marked?: string;
26 /** Renders a trailing tick: this row's value is already set on the ticket. */
27 checked?: boolean;
28 /** Perform, then pop back a level; the optimistic tick flips back if `perform` throws. */
29 keepOpen?: boolean;
30 /** Hidden until typed, and only a substring-or-better match reveals it. */
31 hideWhenEmpty?: boolean;
32 children?: () => Command[] | Promise<Command[]>;
33 perform?: () => void;
34}
35
36/** The record the palette acts on, shown as the removable chip above the input. */
37export interface PaletteContext {
38 /** Ticket name, passed to the command builders. */
39 id: string;
40 /** Short identifier on the chip, e.g. "#13". */
41 label: string;
42 /** Human title on the chip; empty until the ticket resource resolves. */
43 title: string;
44}
45
46export interface SearchItem {
47 doctype: string;
48 name: string;
49 title?: string;
50 content?: string;
51 status?: string;
52 priority?: string;
53 reference_ticket?: string;
54 reference_name?: string;
55}
56
57/** Server highlights matches with <mark>; palette rows render plain text. */
58export function stripTags(value: string): string {
59 return (value ?? "").replace(/<[^>]*>/g, "");
60}
61
62export interface TitleRun {
63 text: string;
64 /** Part of what the query matched, per the server's own highlighting. */
65 match: boolean;
66}
67
68/** Splits `<mark>` highlighting into runs, rendered as text nodes — no v-html,
69 * so ticket content cannot inject markup. */
70export function titleRuns(marked: string): TitleRun[] {
71 return (marked ?? "")
72 .split(/(<mark>[\s\S]*?<\/mark>)/g)
73 .map((part) => ({
74 text: stripTags(part),
75 match: part.startsWith("<mark>"),
76 }))
77 .filter((run) => run.text.length > 0);
78}
79
80/** Ticket-context rows outrank server search hits (fixed rank ~950). */
81export const CONTEXT_WEIGHT = 1.2;
82
83/** Above search hits, below the drill-down parent: "priority" leads with the
84 * picker, "urgent" with the row that sets it. */
85export const FLAT_OPTION_WEIGHT = 1.15;
86
87/** Untranslated: the palette translates group labels once, at render. */
88export const GROUP = {
89 ticket: "Ticket",
90 list: "Ticket list",
91 recent: "Recent tickets",
92 results: "Tickets",
93 navigate: "Navigate",
94 // Untitled: "New ticket" already says create, a header would repeat it.
95 create: "",
96 account: "Account",
97};
98
99