@agent-native/toolkit 0.20.8 → 0.21.0-nightly-20260925012751

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.
Files changed (233) hide show
  1. package/agent-native.eject.json +30 -0
  2. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  3. package/dist/composer/TiptapComposer.js +101 -59
  4. package/dist/composer/TiptapComposer.js.map +1 -1
  5. package/dist/composer/TiptapComposer.spec.js +102 -10
  6. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  7. package/dist/icons/ResourceIcon.d.ts +14 -0
  8. package/dist/icons/ResourceIcon.d.ts.map +1 -0
  9. package/dist/icons/ResourceIcon.js +43 -0
  10. package/dist/icons/ResourceIcon.js.map +1 -0
  11. package/dist/icons/ResourceIcon.spec.d.ts +2 -0
  12. package/dist/icons/ResourceIcon.spec.d.ts.map +1 -0
  13. package/dist/icons/ResourceIcon.spec.js +23 -0
  14. package/dist/icons/ResourceIcon.spec.js.map +1 -0
  15. package/dist/icons/ResourceIconPicker.d.ts +52 -0
  16. package/dist/icons/ResourceIconPicker.d.ts.map +1 -0
  17. package/dist/icons/ResourceIconPicker.js +344 -0
  18. package/dist/icons/ResourceIconPicker.js.map +1 -0
  19. package/dist/icons/catalog.d.ts +17 -0
  20. package/dist/icons/catalog.d.ts.map +1 -0
  21. package/dist/icons/catalog.js +114 -0
  22. package/dist/icons/catalog.js.map +1 -0
  23. package/dist/icons/catalog.spec.d.ts +2 -0
  24. package/dist/icons/catalog.spec.d.ts.map +1 -0
  25. package/dist/icons/catalog.spec.js +29 -0
  26. package/dist/icons/catalog.spec.js.map +1 -0
  27. package/dist/icons/emoji-catalog.d.ts +12 -0
  28. package/dist/icons/emoji-catalog.d.ts.map +1 -0
  29. package/dist/icons/emoji-catalog.js +59 -0
  30. package/dist/icons/emoji-catalog.js.map +1 -0
  31. package/dist/icons/emoji-catalog.spec.d.ts +2 -0
  32. package/dist/icons/emoji-catalog.spec.d.ts.map +1 -0
  33. package/dist/icons/emoji-catalog.spec.js +29 -0
  34. package/dist/icons/emoji-catalog.spec.js.map +1 -0
  35. package/dist/icons/icon-colors.d.ts +3 -0
  36. package/dist/icons/icon-colors.d.ts.map +1 -0
  37. package/dist/icons/icon-colors.js +12 -0
  38. package/dist/icons/icon-colors.js.map +1 -0
  39. package/dist/icons/icon-persistence.spec.d.ts +2 -0
  40. package/dist/icons/icon-persistence.spec.d.ts.map +1 -0
  41. package/dist/icons/icon-persistence.spec.js +38 -0
  42. package/dist/icons/icon-persistence.spec.js.map +1 -0
  43. package/dist/icons/index.d.ts +5 -0
  44. package/dist/icons/index.d.ts.map +1 -0
  45. package/dist/icons/index.js +5 -0
  46. package/dist/icons/index.js.map +1 -0
  47. package/dist/icons/recents.d.ts +6 -0
  48. package/dist/icons/recents.d.ts.map +1 -0
  49. package/dist/icons/recents.js +23 -0
  50. package/dist/icons/recents.js.map +1 -0
  51. package/dist/icons/recents.spec.d.ts +2 -0
  52. package/dist/icons/recents.spec.d.ts.map +1 -0
  53. package/dist/icons/recents.spec.js +29 -0
  54. package/dist/icons/recents.spec.js.map +1 -0
  55. package/dist/icons/server.d.ts +7 -0
  56. package/dist/icons/server.d.ts.map +1 -0
  57. package/dist/icons/server.js +25 -0
  58. package/dist/icons/server.js.map +1 -0
  59. package/dist/icons/tabler-catalog-data.d.ts +31 -0
  60. package/dist/icons/tabler-catalog-data.d.ts.map +1 -0
  61. package/dist/icons/tabler-catalog-data.js +30121 -0
  62. package/dist/icons/tabler-catalog-data.js.map +1 -0
  63. package/dist/icons/tabler-chunk-loaders.d.ts +30 -0
  64. package/dist/icons/tabler-chunk-loaders.d.ts.map +1 -0
  65. package/dist/icons/tabler-chunk-loaders.js +30 -0
  66. package/dist/icons/tabler-chunk-loaders.js.map +1 -0
  67. package/dist/icons/tabler-chunks/a.d.ts +450 -0
  68. package/dist/icons/tabler-chunks/a.d.ts.map +1 -0
  69. package/dist/icons/tabler-chunks/a.js +451 -0
  70. package/dist/icons/tabler-chunks/a.js.map +1 -0
  71. package/dist/icons/tabler-chunks/b.d.ts +849 -0
  72. package/dist/icons/tabler-chunks/b.d.ts.map +1 -0
  73. package/dist/icons/tabler-chunks/b.js +850 -0
  74. package/dist/icons/tabler-chunks/b.js.map +1 -0
  75. package/dist/icons/tabler-chunks/c.d.ts +922 -0
  76. package/dist/icons/tabler-chunks/c.d.ts.map +1 -0
  77. package/dist/icons/tabler-chunks/c.js +923 -0
  78. package/dist/icons/tabler-chunks/c.js.map +1 -0
  79. package/dist/icons/tabler-chunks/d.d.ts +380 -0
  80. package/dist/icons/tabler-chunks/d.d.ts.map +1 -0
  81. package/dist/icons/tabler-chunks/d.js +381 -0
  82. package/dist/icons/tabler-chunks/d.js.map +1 -0
  83. package/dist/icons/tabler-chunks/e.d.ts +112 -0
  84. package/dist/icons/tabler-chunks/e.d.ts.map +1 -0
  85. package/dist/icons/tabler-chunks/e.js +113 -0
  86. package/dist/icons/tabler-chunks/e.js.map +1 -0
  87. package/dist/icons/tabler-chunks/f.d.ts +341 -0
  88. package/dist/icons/tabler-chunks/f.d.ts.map +1 -0
  89. package/dist/icons/tabler-chunks/f.js +342 -0
  90. package/dist/icons/tabler-chunks/f.js.map +1 -0
  91. package/dist/icons/tabler-chunks/g.d.ts +101 -0
  92. package/dist/icons/tabler-chunks/g.d.ts.map +1 -0
  93. package/dist/icons/tabler-chunks/g.js +102 -0
  94. package/dist/icons/tabler-chunks/g.js.map +1 -0
  95. package/dist/icons/tabler-chunks/h.d.ts +276 -0
  96. package/dist/icons/tabler-chunks/h.d.ts.map +1 -0
  97. package/dist/icons/tabler-chunks/h.js +277 -0
  98. package/dist/icons/tabler-chunks/h.js.map +1 -0
  99. package/dist/icons/tabler-chunks/i.d.ts +79 -0
  100. package/dist/icons/tabler-chunks/i.d.ts.map +1 -0
  101. package/dist/icons/tabler-chunks/i.js +80 -0
  102. package/dist/icons/tabler-chunks/i.js.map +1 -0
  103. package/dist/icons/tabler-chunks/j.d.ts +17 -0
  104. package/dist/icons/tabler-chunks/j.d.ts.map +1 -0
  105. package/dist/icons/tabler-chunks/j.js +18 -0
  106. package/dist/icons/tabler-chunks/j.js.map +1 -0
  107. package/dist/icons/tabler-chunks/k.d.ts +25 -0
  108. package/dist/icons/tabler-chunks/k.d.ts.map +1 -0
  109. package/dist/icons/tabler-chunks/k.js +26 -0
  110. package/dist/icons/tabler-chunks/k.js.map +1 -0
  111. package/dist/icons/tabler-chunks/l.d.ts +298 -0
  112. package/dist/icons/tabler-chunks/l.d.ts.map +1 -0
  113. package/dist/icons/tabler-chunks/l.js +299 -0
  114. package/dist/icons/tabler-chunks/l.js.map +1 -0
  115. package/dist/icons/tabler-chunks/m.d.ts +413 -0
  116. package/dist/icons/tabler-chunks/m.d.ts.map +1 -0
  117. package/dist/icons/tabler-chunks/m.js +414 -0
  118. package/dist/icons/tabler-chunks/m.js.map +1 -0
  119. package/dist/icons/tabler-chunks/n.d.ts +174 -0
  120. package/dist/icons/tabler-chunks/n.d.ts.map +1 -0
  121. package/dist/icons/tabler-chunks/n.js +175 -0
  122. package/dist/icons/tabler-chunks/n.js.map +1 -0
  123. package/dist/icons/tabler-chunks/o.d.ts +31 -0
  124. package/dist/icons/tabler-chunks/o.d.ts.map +1 -0
  125. package/dist/icons/tabler-chunks/o.js +32 -0
  126. package/dist/icons/tabler-chunks/o.js.map +1 -0
  127. package/dist/icons/tabler-chunks/p.d.ts +374 -0
  128. package/dist/icons/tabler-chunks/p.d.ts.map +1 -0
  129. package/dist/icons/tabler-chunks/p.js +375 -0
  130. package/dist/icons/tabler-chunks/p.js.map +1 -0
  131. package/dist/icons/tabler-chunks/q.d.ts +14 -0
  132. package/dist/icons/tabler-chunks/q.d.ts.map +1 -0
  133. package/dist/icons/tabler-chunks/q.js +15 -0
  134. package/dist/icons/tabler-chunks/q.js.map +1 -0
  135. package/dist/icons/tabler-chunks/r.d.ts +183 -0
  136. package/dist/icons/tabler-chunks/r.d.ts.map +1 -0
  137. package/dist/icons/tabler-chunks/r.js +184 -0
  138. package/dist/icons/tabler-chunks/r.js.map +1 -0
  139. package/dist/icons/tabler-chunks/s.d.ts +656 -0
  140. package/dist/icons/tabler-chunks/s.d.ts.map +1 -0
  141. package/dist/icons/tabler-chunks/s.js +657 -0
  142. package/dist/icons/tabler-chunks/s.js.map +1 -0
  143. package/dist/icons/tabler-chunks/t.d.ts +242 -0
  144. package/dist/icons/tabler-chunks/t.d.ts.map +1 -0
  145. package/dist/icons/tabler-chunks/t.js +243 -0
  146. package/dist/icons/tabler-chunks/t.js.map +1 -0
  147. package/dist/icons/tabler-chunks/u.d.ts +62 -0
  148. package/dist/icons/tabler-chunks/u.d.ts.map +1 -0
  149. package/dist/icons/tabler-chunks/u.js +63 -0
  150. package/dist/icons/tabler-chunks/u.js.map +1 -0
  151. package/dist/icons/tabler-chunks/v.d.ts +60 -0
  152. package/dist/icons/tabler-chunks/v.d.ts.map +1 -0
  153. package/dist/icons/tabler-chunks/v.js +61 -0
  154. package/dist/icons/tabler-chunks/v.js.map +1 -0
  155. package/dist/icons/tabler-chunks/w.d.ts +115 -0
  156. package/dist/icons/tabler-chunks/w.d.ts.map +1 -0
  157. package/dist/icons/tabler-chunks/w.js +116 -0
  158. package/dist/icons/tabler-chunks/w.js.map +1 -0
  159. package/dist/icons/tabler-chunks/x.d.ts +18 -0
  160. package/dist/icons/tabler-chunks/x.d.ts.map +1 -0
  161. package/dist/icons/tabler-chunks/x.js +19 -0
  162. package/dist/icons/tabler-chunks/x.js.map +1 -0
  163. package/dist/icons/tabler-chunks/y.d.ts +7 -0
  164. package/dist/icons/tabler-chunks/y.d.ts.map +1 -0
  165. package/dist/icons/tabler-chunks/y.js +4 -0
  166. package/dist/icons/tabler-chunks/y.js.map +1 -0
  167. package/dist/icons/tabler-chunks/z.d.ts +51 -0
  168. package/dist/icons/tabler-chunks/z.d.ts.map +1 -0
  169. package/dist/icons/tabler-chunks/z.js +52 -0
  170. package/dist/icons/tabler-chunks/z.js.map +1 -0
  171. package/dist/icons/tabler-groups.d.ts +30 -0
  172. package/dist/icons/tabler-groups.d.ts.map +1 -0
  173. package/dist/icons/tabler-groups.js +65 -0
  174. package/dist/icons/tabler-groups.js.map +1 -0
  175. package/dist/icons/types.d.ts +25 -0
  176. package/dist/icons/types.d.ts.map +1 -0
  177. package/dist/icons/types.js +2 -0
  178. package/dist/icons/types.js.map +1 -0
  179. package/dist/index.d.ts +1 -0
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +1 -0
  182. package/dist/index.js.map +1 -1
  183. package/dist/styles.css +2 -0
  184. package/dist/theme/resource-icons.css +24 -0
  185. package/package.json +11 -2
  186. package/src/composer/TiptapComposer.spec.ts +142 -15
  187. package/src/composer/TiptapComposer.tsx +109 -58
  188. package/src/icons/ResourceIcon.spec.tsx +38 -0
  189. package/src/icons/ResourceIcon.tsx +91 -0
  190. package/src/icons/ResourceIconPicker.tsx +812 -0
  191. package/src/icons/catalog.spec.ts +50 -0
  192. package/src/icons/catalog.ts +153 -0
  193. package/src/icons/emoji-catalog.spec.ts +46 -0
  194. package/src/icons/emoji-catalog.ts +86 -0
  195. package/src/icons/icon-colors.ts +13 -0
  196. package/src/icons/icon-persistence.spec.ts +45 -0
  197. package/src/icons/index.ts +4 -0
  198. package/src/icons/recents.spec.ts +37 -0
  199. package/src/icons/recents.ts +37 -0
  200. package/src/icons/server.tsx +53 -0
  201. package/src/icons/tabler-catalog-data.ts +30122 -0
  202. package/src/icons/tabler-chunk-loaders.ts +29 -0
  203. package/src/icons/tabler-chunks/a.ts +897 -0
  204. package/src/icons/tabler-chunks/b.ts +1695 -0
  205. package/src/icons/tabler-chunks/c.ts +1841 -0
  206. package/src/icons/tabler-chunks/d.ts +757 -0
  207. package/src/icons/tabler-chunks/e.ts +221 -0
  208. package/src/icons/tabler-chunks/f.ts +679 -0
  209. package/src/icons/tabler-chunks/g.ts +199 -0
  210. package/src/icons/tabler-chunks/h.ts +549 -0
  211. package/src/icons/tabler-chunks/i.ts +155 -0
  212. package/src/icons/tabler-chunks/j.ts +31 -0
  213. package/src/icons/tabler-chunks/k.ts +47 -0
  214. package/src/icons/tabler-chunks/l.ts +593 -0
  215. package/src/icons/tabler-chunks/m.ts +823 -0
  216. package/src/icons/tabler-chunks/n.ts +345 -0
  217. package/src/icons/tabler-chunks/o.ts +59 -0
  218. package/src/icons/tabler-chunks/p.ts +745 -0
  219. package/src/icons/tabler-chunks/q.ts +25 -0
  220. package/src/icons/tabler-chunks/r.ts +363 -0
  221. package/src/icons/tabler-chunks/s.ts +1309 -0
  222. package/src/icons/tabler-chunks/t.ts +481 -0
  223. package/src/icons/tabler-chunks/u.ts +121 -0
  224. package/src/icons/tabler-chunks/v.ts +117 -0
  225. package/src/icons/tabler-chunks/w.ts +227 -0
  226. package/src/icons/tabler-chunks/x.ts +33 -0
  227. package/src/icons/tabler-chunks/y.ts +3 -0
  228. package/src/icons/tabler-chunks/z.ts +99 -0
  229. package/src/icons/tabler-groups.ts +70 -0
  230. package/src/icons/types.ts +35 -0
  231. package/src/index.ts +1 -0
  232. package/src/styles.css +2 -0
  233. package/src/theme/resource-icons.css +24 -0
@@ -0,0 +1,50 @@
1
+ import { icons } from "@tabler/icons-react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ loadTablerCatalog,
6
+ loadTablerIcon,
7
+ searchTablerIcons,
8
+ TABLER_ICON_CATEGORIES,
9
+ tablerExportName,
10
+ } from "./catalog.js";
11
+ import { TABLER_ICON_GROUPS } from "./tabler-groups.js";
12
+
13
+ describe("Tabler catalog", () => {
14
+ it("includes every canonical outline and filled icon once, with a renderable component", async () => {
15
+ const catalog = await loadTablerCatalog();
16
+ expect(catalog.map((entry) => tablerExportName(entry.name)).sort()).toEqual(
17
+ Object.keys(icons).sort(),
18
+ );
19
+ expect(new Set(catalog.map((entry) => entry.name)).size).toBe(
20
+ catalog.length,
21
+ );
22
+ expect(catalog.length).toBeGreaterThan(6_000);
23
+ const components = await Promise.all(
24
+ catalog.map((entry) => loadTablerIcon(entry.name)),
25
+ );
26
+ expect(components.every((component) => component !== null)).toBe(true);
27
+ expect(new Set(catalog.map((entry) => entry.category))).toEqual(
28
+ new Set(TABLER_ICON_CATEGORIES),
29
+ );
30
+ });
31
+
32
+ it("does not truncate browsing or broad searches, and searches tags", async () => {
33
+ expect(await searchTablerIcons("")).toHaveLength(
34
+ (await loadTablerCatalog()).length,
35
+ );
36
+ expect((await searchTablerIcons("arrow")).length).toBeGreaterThan(120);
37
+ expect(await searchTablerIcons("arrow", 12)).toHaveLength(12);
38
+ expect(await searchTablerIcons("programming")).toContain("a-b");
39
+ expect(await loadTablerIcon("not-a-real-icon")).toBeNull();
40
+ });
41
+
42
+ it("places every source category in exactly one reader-facing group", () => {
43
+ const assigned = TABLER_ICON_GROUPS.flatMap((group) => group.categories);
44
+ expect(assigned).toHaveLength(TABLER_ICON_CATEGORIES.length);
45
+ expect(new Set(assigned)).toEqual(new Set(TABLER_ICON_CATEGORIES));
46
+ expect(new Set(TABLER_ICON_GROUPS.map((group) => group.id)).size).toBe(
47
+ TABLER_ICON_GROUPS.length,
48
+ );
49
+ });
50
+ });
@@ -0,0 +1,153 @@
1
+ import type { ComponentType, SVGProps } from "react";
2
+
3
+ export type TablerIconComponent = ComponentType<
4
+ Omit<SVGProps<SVGSVGElement>, "stroke"> & {
5
+ size?: number | string;
6
+ stroke?: number;
7
+ }
8
+ >;
9
+
10
+ type TablerModule = Record<string, unknown>;
11
+ type ViteImportMeta = ImportMeta & { env: { SSR: boolean; MODE: string } };
12
+
13
+ export const TABLER_ICON_CATEGORIES = [
14
+ "system",
15
+ "communication",
16
+ "document",
17
+ "media",
18
+ "design",
19
+ "development",
20
+ "devices",
21
+ "computers",
22
+ "database",
23
+ "charts",
24
+ "arrows",
25
+ "shapes",
26
+ "symbols",
27
+ "text",
28
+ "letters",
29
+ "numbers",
30
+ "math",
31
+ "logic",
32
+ "version-control",
33
+ "extensions",
34
+ "e-commerce",
35
+ "currencies",
36
+ "buildings",
37
+ "map",
38
+ "vehicles",
39
+ "nature",
40
+ "animals",
41
+ "weather",
42
+ "food",
43
+ "health",
44
+ "sport",
45
+ "games",
46
+ "photography",
47
+ "mood",
48
+ "gestures",
49
+ "gender",
50
+ "badges",
51
+ "electrical",
52
+ "laundry",
53
+ "zodiac",
54
+ "brand",
55
+ ] as const;
56
+
57
+ export type TablerIconCategory = (typeof TABLER_ICON_CATEGORIES)[number];
58
+
59
+ export interface TablerCatalogEntry {
60
+ name: string;
61
+ category: TablerIconCategory;
62
+ search: string;
63
+ }
64
+
65
+ let metadataPromise: Promise<TablerCatalogEntry[]> | undefined;
66
+
67
+ export function tablerExportName(name: string): string {
68
+ return `Icon${name
69
+ .split("-")
70
+ .filter(Boolean)
71
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
72
+ .join("")}`;
73
+ }
74
+
75
+ export async function loadTablerIcon(
76
+ name: string,
77
+ ): Promise<TablerIconComponent | null> {
78
+ if (
79
+ (import.meta as ViteImportMeta).env.SSR &&
80
+ (import.meta as ViteImportMeta).env.MODE !== "test"
81
+ ) {
82
+ throw new Error("Tabler icons are available only in the browser");
83
+ }
84
+ const initial = name.charAt(0).toLowerCase();
85
+ const { default: loaders } = await import("./tabler-chunk-loaders.js");
86
+ const loadChunk = (
87
+ loaders as Record<string, () => Promise<{ default: TablerModule }>>
88
+ )[initial];
89
+ if (!loadChunk) return null;
90
+ const { default: catalog } = await loadChunk();
91
+ const candidate = catalog[tablerExportName(name)];
92
+ return typeof candidate === "object" || typeof candidate === "function"
93
+ ? (candidate as TablerIconComponent)
94
+ : null;
95
+ }
96
+
97
+ export async function searchTablerIcons(
98
+ query: string,
99
+ limit?: number,
100
+ ): Promise<string[]> {
101
+ const catalog = await loadTablerCatalog();
102
+ const normalized = query.trim().toLowerCase();
103
+ return catalog
104
+ .filter((entry) => entry.search.includes(normalized))
105
+ .map((entry) => entry.name)
106
+ .slice(0, limit);
107
+ }
108
+
109
+ export function loadTablerCatalog(): Promise<TablerCatalogEntry[]> {
110
+ if (
111
+ (import.meta as ViteImportMeta).env.SSR &&
112
+ (import.meta as ViteImportMeta).env.MODE !== "test"
113
+ ) {
114
+ throw new Error("Tabler catalog is available only in the browser");
115
+ }
116
+ metadataPromise ??= import("./tabler-catalog-data.js")
117
+ .then(({ default: metadata }) => {
118
+ return metadata
119
+ .flatMap(([baseName, category, tags, styles]): TablerCatalogEntry[] => {
120
+ if (
121
+ !TABLER_ICON_CATEGORIES.includes(category as TablerIconCategory)
122
+ ) {
123
+ throw new Error(`Unknown Tabler icon category: ${category}`);
124
+ }
125
+ return styles.split(" ").map((style) => {
126
+ const name = `${baseName}${style === "filled" ? "-filled" : ""}`;
127
+ return {
128
+ name,
129
+ category: category as TablerIconCategory,
130
+ search: [
131
+ name,
132
+ name.replaceAll("-", " "),
133
+ category.replaceAll("-", " "),
134
+ tags,
135
+ ]
136
+ .join(" ")
137
+ .toLowerCase(),
138
+ };
139
+ });
140
+ })
141
+ .sort(
142
+ (left, right) =>
143
+ TABLER_ICON_CATEGORIES.indexOf(left.category) -
144
+ TABLER_ICON_CATEGORIES.indexOf(right.category) ||
145
+ left.name.localeCompare(right.name),
146
+ );
147
+ })
148
+ .catch((error) => {
149
+ metadataPromise = undefined;
150
+ throw error;
151
+ });
152
+ return metadataPromise;
153
+ }
@@ -0,0 +1,46 @@
1
+ import data from "emojibase-data/en/data.json";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { EMOJI_CATEGORIES, loadEmojiCatalog } from "./emoji-catalog.js";
5
+
6
+ describe("emoji catalog", () => {
7
+ it("loads the maintained dataset with joined and skin-tone sequences", async () => {
8
+ const catalog = await loadEmojiCatalog();
9
+ expect(catalog.length).toBeGreaterThan(1_000);
10
+ expect(catalog.some((entry) => entry.emoji.includes("\u200d"))).toBe(true);
11
+ expect(
12
+ catalog.some((entry) => /[\u{1F3FB}-\u{1F3FF}]/u.test(entry.emoji)),
13
+ ).toBe(true);
14
+ });
15
+
16
+ it("browses the complete Unicode catalog in group order without standalone components", async () => {
17
+ const catalog = await loadEmojiCatalog();
18
+ const expected = data
19
+ .filter(
20
+ (entry) =>
21
+ entry.group !== undefined &&
22
+ entry.group !== 2 &&
23
+ entry.order !== undefined,
24
+ )
25
+ .flatMap((entry) => [entry, ...(entry.skins ?? [])])
26
+ .map((entry) => entry.emoji);
27
+ expect(catalog.map((entry) => entry.emoji).sort()).toEqual(expected.sort());
28
+ expect(catalog[0].emoji).toBe("😀");
29
+ expect(catalog.some((entry) => entry.emoji === "🇦")).toBe(false);
30
+ expect(catalog.some((entry) => entry.emoji === "🏻")).toBe(false);
31
+ expect(
32
+ catalog.some(
33
+ (entry) => entry.emoji === "🇿🇼" && entry.category === "flags",
34
+ ),
35
+ ).toBe(true);
36
+ expect(
37
+ catalog.some((entry) => entry.emoji === "👋🏽" && entry.skinTone === 3),
38
+ ).toBe(true);
39
+ expect(catalog.map((entry) => entry.order)).toEqual(
40
+ catalog.map((entry) => entry.order).sort((a, b) => a - b),
41
+ );
42
+ expect(new Set(catalog.map((entry) => entry.category))).toEqual(
43
+ new Set(EMOJI_CATEGORIES),
44
+ );
45
+ });
46
+ });
@@ -0,0 +1,86 @@
1
+ export const EMOJI_CATEGORIES = [
2
+ "smileys-emotion",
3
+ "people-body",
4
+ "animals-nature",
5
+ "food-drink",
6
+ "travel-places",
7
+ "activities",
8
+ "objects",
9
+ "symbols",
10
+ "flags",
11
+ ] as const;
12
+
13
+ export type EmojiCategory = (typeof EMOJI_CATEGORIES)[number];
14
+
15
+ export interface EmojiCatalogEntry {
16
+ emoji: string;
17
+ label: string;
18
+ search: string;
19
+ category: EmojiCategory;
20
+ order: number;
21
+ skinTone?: number | number[];
22
+ }
23
+
24
+ interface EmojibaseEntry {
25
+ emoji?: string;
26
+ label?: string;
27
+ tags?: string[];
28
+ group?: number;
29
+ order?: number;
30
+ tone?: number | number[];
31
+ skins?: EmojibaseEntry[];
32
+ }
33
+
34
+ type ViteImportMeta = ImportMeta & { env: { SSR: boolean; MODE: string } };
35
+
36
+ let emojiCatalogPromise: Promise<EmojiCatalogEntry[]> | undefined;
37
+
38
+ export function loadEmojiCatalog(): Promise<EmojiCatalogEntry[]> {
39
+ if (
40
+ (import.meta as ViteImportMeta).env.SSR &&
41
+ (import.meta as ViteImportMeta).env.MODE !== "test"
42
+ ) {
43
+ throw new Error("Emoji catalog is available only in the browser");
44
+ }
45
+ emojiCatalogPromise ??= import("emojibase-data/en/data.json")
46
+ .then((module) => {
47
+ const rows = (module.default ?? module) as EmojibaseEntry[];
48
+ return rows
49
+ .flatMap((row) => {
50
+ // Components (including regional letters) are not standalone emoji choices.
51
+ if (
52
+ row.group === undefined ||
53
+ row.group === 2 ||
54
+ row.order === undefined
55
+ ) {
56
+ return [];
57
+ }
58
+ const category =
59
+ EMOJI_CATEGORIES[row.group > 2 ? row.group - 1 : row.group];
60
+ if (!category) throw new Error(`Unknown emoji group: ${row.group}`);
61
+ const variants = [row, ...(row.skins ?? [])];
62
+ return variants.flatMap((variant) => {
63
+ if (!variant.emoji) return [];
64
+ const label = variant.label ?? row.label ?? variant.emoji;
65
+ return [
66
+ {
67
+ emoji: variant.emoji,
68
+ label,
69
+ search: [label, ...(row.tags ?? [])].join(" ").toLowerCase(),
70
+ category,
71
+ order: variant.order ?? row.order!,
72
+ ...(variant.tone === undefined
73
+ ? {}
74
+ : { skinTone: variant.tone }),
75
+ },
76
+ ];
77
+ });
78
+ })
79
+ .sort((left, right) => left.order - right.order);
80
+ })
81
+ .catch((error) => {
82
+ emojiCatalogPromise = undefined;
83
+ throw error;
84
+ });
85
+ return emojiCatalogPromise;
86
+ }
@@ -0,0 +1,13 @@
1
+ import type { ResourceIconColor } from "./types.js";
2
+
3
+ export const resourceIconColorClasses: Record<ResourceIconColor, string> = {
4
+ gray: "text-[var(--resource-icon-gray)]",
5
+ brown: "text-[var(--resource-icon-brown)]",
6
+ orange: "text-[var(--resource-icon-orange)]",
7
+ yellow: "text-[var(--resource-icon-yellow)]",
8
+ green: "text-[var(--resource-icon-green)]",
9
+ blue: "text-[var(--resource-icon-blue)]",
10
+ purple: "text-[var(--resource-icon-purple)]",
11
+ pink: "text-[var(--resource-icon-pink)]",
12
+ red: "text-[var(--resource-icon-red)]",
13
+ };
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { persistedIconValue } from "./ResourceIconPicker.js";
4
+ import type { ResourceIconImage } from "./types.js";
5
+
6
+ describe("persistedIconValue", () => {
7
+ it("removes preview-only metadata before saving an uploaded image", () => {
8
+ const uploaded: ResourceIconImage = {
9
+ version: 1,
10
+ kind: "image",
11
+ authority: "url",
12
+ assetId: "https://cdn.example.com/logo.png",
13
+ alt: "Team logo",
14
+ previewUrl: "blob:temporary-preview",
15
+ };
16
+
17
+ expect(persistedIconValue(uploaded)).toEqual({
18
+ version: 1,
19
+ kind: "image",
20
+ authority: "url",
21
+ assetId: "https://cdn.example.com/logo.png",
22
+ alt: "Team logo",
23
+ });
24
+ expect(uploaded.previewUrl).toBe("blob:temporary-preview");
25
+ });
26
+
27
+ it("repairs image recents saved by the previous picker", () => {
28
+ const recent = JSON.parse(
29
+ JSON.stringify({
30
+ version: 1,
31
+ kind: "image",
32
+ authority: "url",
33
+ assetId: "https://cdn.example.com/logo.png",
34
+ previewUrl: "https://cdn.example.com/logo.png",
35
+ }),
36
+ );
37
+
38
+ expect(Object.keys(persistedIconValue(recent)).sort()).toEqual([
39
+ "assetId",
40
+ "authority",
41
+ "kind",
42
+ "version",
43
+ ]);
44
+ });
45
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./ResourceIcon.js";
2
+ export * from "./ResourceIconPicker.js";
3
+ export * from "./recents.js";
4
+ export * from "./types.js";
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { addResourceIconRecent, resourceIconKey } from "./recents.js";
4
+ import type { ResourceIconValue } from "./types.js";
5
+
6
+ describe("resource icon recents", () => {
7
+ it("deduplicates the complete value while preserving colored variants", () => {
8
+ const blue: ResourceIconValue = {
9
+ version: 1,
10
+ kind: "library",
11
+ library: "tabler",
12
+ name: "book",
13
+ color: "blue",
14
+ };
15
+ const red: ResourceIconValue = { ...blue, color: "red" };
16
+ expect(addResourceIconRecent([blue, red], blue)).toEqual([blue, red]);
17
+ expect(resourceIconKey(blue)).not.toBe(resourceIconKey(red));
18
+ });
19
+
20
+ it("bounds recents to 24 by default", () => {
21
+ const recents: ResourceIconValue[] = Array.from(
22
+ { length: 30 },
23
+ (_, index) => ({
24
+ version: 1,
25
+ kind: "emoji",
26
+ emoji: String(index),
27
+ }),
28
+ );
29
+ expect(
30
+ addResourceIconRecent(recents, {
31
+ version: 1,
32
+ kind: "emoji",
33
+ emoji: "new",
34
+ }),
35
+ ).toHaveLength(24);
36
+ });
37
+ });
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+
3
+ import type { ResourceIconValue } from "./types.js";
4
+
5
+ export const RESOURCE_ICON_RECENTS_LIMIT = 24;
6
+
7
+ export function resourceIconKey(value: ResourceIconValue): string {
8
+ if (value.kind === "emoji") return `emoji:${value.emoji}`;
9
+ if (value.kind === "library") {
10
+ return `library:${value.library}:${value.name}:${value.variant ?? "outline"}:${value.color ?? "default"}`;
11
+ }
12
+ return `image:${value.authority}:${value.assetId}`;
13
+ }
14
+
15
+ export function addResourceIconRecent(
16
+ recents: readonly ResourceIconValue[],
17
+ value: ResourceIconValue,
18
+ limit = RESOURCE_ICON_RECENTS_LIMIT,
19
+ ): ResourceIconValue[] {
20
+ const key = resourceIconKey(value);
21
+ return [
22
+ value,
23
+ ...recents.filter((recent) => resourceIconKey(recent) !== key),
24
+ ].slice(0, limit);
25
+ }
26
+
27
+ export function useResourceIconRecents(
28
+ recents: readonly ResourceIconValue[],
29
+ onRecentsChange?: (recents: ResourceIconValue[]) => void,
30
+ ) {
31
+ return React.useCallback(
32
+ (value: ResourceIconValue) => {
33
+ onRecentsChange?.(addResourceIconRecent(recents, value));
34
+ },
35
+ [onRecentsChange, recents],
36
+ );
37
+ }
@@ -0,0 +1,53 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../utils.js";
4
+ import type { ResourceIconProps } from "./ResourceIcon.js";
5
+
6
+ export * from "./recents.js";
7
+ export * from "./types.js";
8
+
9
+ export function ResourceIcon({
10
+ value,
11
+ size = 20,
12
+ fallback = null,
13
+ label,
14
+ className,
15
+ resolveImageUrl,
16
+ }: ResourceIconProps) {
17
+ const accessibility = label
18
+ ? { role: "img", "aria-label": label }
19
+ : { "aria-hidden": true as const };
20
+
21
+ if (!value) return fallback;
22
+ if (value.kind === "emoji") {
23
+ return (
24
+ <span
25
+ {...accessibility}
26
+ className={cn(
27
+ "inline-flex shrink-0 items-center justify-center",
28
+ className,
29
+ )}
30
+ style={{ fontSize: size, lineHeight: 1 }}
31
+ >
32
+ {value.emoji}
33
+ </span>
34
+ );
35
+ }
36
+ if (value.kind === "image") {
37
+ const src = resolveImageUrl?.(value);
38
+ if (!src) return fallback;
39
+ return (
40
+ <img
41
+ src={src}
42
+ alt={label ?? value.alt ?? ""}
43
+ className={cn("inline-block shrink-0 object-contain", className)}
44
+ style={{ width: size, height: size }}
45
+ />
46
+ );
47
+ }
48
+ return fallback;
49
+ }
50
+
51
+ export function ResourceIconPicker() {
52
+ return null;
53
+ }