@traky-app/ui 1.6.0 → 1.6.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.
@@ -0,0 +1,202 @@
1
+ import type { Icon } from "@tabler/icons-react";
2
+
3
+ export type InternalChatMentionKind = "guide" | "package";
4
+
5
+ export type InternalChatMentionItem = {
6
+ id: string;
7
+ kind: InternalChatMentionKind;
8
+ label: string;
9
+ subtitle: string;
10
+ insertLabel: string;
11
+ icon: Icon;
12
+ };
13
+
14
+ export type InternalChatMentionCategoryMeta = {
15
+ label: string;
16
+ hint: string;
17
+ keywords: string[];
18
+ };
19
+
20
+ export type InternalChatMentionListEntry =
21
+ | {
22
+ type: "category";
23
+ kind: InternalChatMentionKind;
24
+ label: string;
25
+ hint: string;
26
+ icon: Icon;
27
+ }
28
+ | { type: "item"; data: InternalChatMentionItem };
29
+
30
+ export const DEFAULT_MENTION_KIND_ORDER: InternalChatMentionKind[] = [
31
+ "guide",
32
+ "package",
33
+ ];
34
+
35
+ export const DEFAULT_MENTION_KIND_LABEL: Record<
36
+ InternalChatMentionKind,
37
+ string
38
+ > = {
39
+ guide: "Guías",
40
+ package: "Paquetes",
41
+ };
42
+
43
+ export type InternalChatMentionConfig = {
44
+ kindOrder: InternalChatMentionKind[];
45
+ categoryMeta: Record<
46
+ InternalChatMentionKind,
47
+ InternalChatMentionCategoryMeta
48
+ >;
49
+ kindIcons: Record<InternalChatMentionKind, Icon>;
50
+ kindAliases?: Record<string, InternalChatMentionKind>;
51
+ };
52
+
53
+ function resolveKindAlias(
54
+ raw: string,
55
+ config: InternalChatMentionConfig,
56
+ ): InternalChatMentionKind | null {
57
+ const s = raw.trim().toLowerCase();
58
+ if (!s) return null;
59
+
60
+ const aliases: Record<string, InternalChatMentionKind> = {
61
+ guide: "guide",
62
+ guía: "guide",
63
+ guia: "guide",
64
+ paquete: "package",
65
+ paquetes: "package",
66
+ package: "package",
67
+ ...config.kindAliases,
68
+ };
69
+
70
+ const direct = aliases[s];
71
+ if (direct) return direct;
72
+
73
+ for (const kind of config.kindOrder) {
74
+ const meta = config.categoryMeta[kind];
75
+ if (kind === s) return kind;
76
+ if (meta.label.toLowerCase() === s) return kind;
77
+ if (meta.keywords.some((k) => k === s)) return kind;
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ export function parseMentionSegment(
84
+ query: string,
85
+ config: InternalChatMentionConfig,
86
+ ):
87
+ | { phase: "category"; filter: string }
88
+ | { phase: "item"; kind: InternalChatMentionKind; filter: string } {
89
+ const colon = query.indexOf(":");
90
+ if (colon === -1) {
91
+ return { phase: "category", filter: query };
92
+ }
93
+ const head = query.slice(0, colon).trim().toLowerCase();
94
+ const tail = query.slice(colon + 1);
95
+ if (head === "") {
96
+ return { phase: "category", filter: query.replace(":", "") };
97
+ }
98
+ const kind = resolveKindAlias(head, config);
99
+ if (!kind) {
100
+ return { phase: "category", filter: query };
101
+ }
102
+ return { phase: "item", kind, filter: tail };
103
+ }
104
+
105
+ export function parseSlashMention(
106
+ draft: string,
107
+ cursor: number,
108
+ ): { start: number; query: string } | null {
109
+ const before = draft.slice(0, cursor);
110
+ const lastSlash = before.lastIndexOf("/");
111
+ if (lastSlash === -1) return null;
112
+ const charBefore = lastSlash === 0 ? " " : draft[lastSlash - 1];
113
+ if (charBefore !== " " && charBefore !== "\n" && lastSlash !== 0) {
114
+ return null;
115
+ }
116
+ const afterSlash = before.slice(lastSlash + 1);
117
+ if (afterSlash.includes("\n")) return null;
118
+ return { start: lastSlash, query: afterSlash };
119
+ }
120
+
121
+ function filterCategoryEntries(
122
+ filterText: string,
123
+ config: InternalChatMentionConfig,
124
+ ): InternalChatMentionListEntry[] {
125
+ const q = filterText.trim().toLowerCase();
126
+ const rows: InternalChatMentionListEntry[] = config.kindOrder.map((kind) => ({
127
+ type: "category",
128
+ kind,
129
+ label: config.categoryMeta[kind].label,
130
+ hint: config.categoryMeta[kind].hint,
131
+ icon: config.kindIcons[kind],
132
+ }));
133
+ if (!q) return rows;
134
+ const byAlias = resolveKindAlias(q, config);
135
+ return rows.filter((row) => {
136
+ if (row.type !== "category") return false;
137
+ if (byAlias && row.kind === byAlias) return true;
138
+ return (
139
+ row.label.toLowerCase().includes(q) ||
140
+ row.hint.toLowerCase().includes(q) ||
141
+ config.categoryMeta[row.kind].keywords.some(
142
+ (k) => k.includes(q) || q.includes(k),
143
+ )
144
+ );
145
+ });
146
+ }
147
+
148
+ function filterItemsByKind(
149
+ kind: InternalChatMentionKind,
150
+ filterText: string,
151
+ catalog: InternalChatMentionItem[],
152
+ ): InternalChatMentionListEntry[] {
153
+ const q = filterText.trim().toLowerCase();
154
+ let items = catalog.filter((i) => i.kind === kind);
155
+ if (q) {
156
+ items = items.filter(
157
+ (item) =>
158
+ item.label.toLowerCase().includes(q) ||
159
+ item.subtitle.toLowerCase().includes(q) ||
160
+ item.insertLabel.toLowerCase().includes(q),
161
+ );
162
+ }
163
+ return items.map((data) => ({ type: "item", data }));
164
+ }
165
+
166
+ export function buildMentionList(
167
+ query: string,
168
+ guideItems: InternalChatMentionItem[],
169
+ packageItems: InternalChatMentionItem[],
170
+ config: InternalChatMentionConfig,
171
+ ): InternalChatMentionListEntry[] {
172
+ const seg = parseMentionSegment(query, config);
173
+ if (seg.phase === "category") {
174
+ return filterCategoryEntries(seg.filter, config);
175
+ }
176
+ const pool = seg.kind === "guide" ? guideItems : packageItems;
177
+ return filterItemsByKind(seg.kind, seg.filter, pool);
178
+ }
179
+
180
+ export function roomInitialLetter(name: string): string {
181
+ const t = name.trim();
182
+ if (!t) return "?";
183
+ const ch = [...t][0];
184
+ return ch && /\p{L}/u.test(ch) ? ch.toUpperCase() : "?";
185
+ }
186
+
187
+ export function initialsFromDisplayName(name: string): string {
188
+ const t = name.trim();
189
+ if (!t) return "?";
190
+ const parts = t.split(/\s+/).filter(Boolean);
191
+ if (parts.length === 0) return "?";
192
+ if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();
193
+ return `${parts[0]![0]!}${parts[parts.length - 1]![0]!}`.toUpperCase();
194
+ }
195
+
196
+ export function formatMessageTime(iso: string, locale = "es"): string {
197
+ return new Intl.DateTimeFormat(locale, {
198
+ hour: "2-digit",
199
+ minute: "2-digit",
200
+ hour12: false,
201
+ }).format(new Date(iso));
202
+ }