@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.
- package/agent-native.eject.json +30 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +101 -59
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/TiptapComposer.spec.js +102 -10
- package/dist/composer/TiptapComposer.spec.js.map +1 -1
- package/dist/icons/ResourceIcon.d.ts +14 -0
- package/dist/icons/ResourceIcon.d.ts.map +1 -0
- package/dist/icons/ResourceIcon.js +43 -0
- package/dist/icons/ResourceIcon.js.map +1 -0
- package/dist/icons/ResourceIcon.spec.d.ts +2 -0
- package/dist/icons/ResourceIcon.spec.d.ts.map +1 -0
- package/dist/icons/ResourceIcon.spec.js +23 -0
- package/dist/icons/ResourceIcon.spec.js.map +1 -0
- package/dist/icons/ResourceIconPicker.d.ts +52 -0
- package/dist/icons/ResourceIconPicker.d.ts.map +1 -0
- package/dist/icons/ResourceIconPicker.js +344 -0
- package/dist/icons/ResourceIconPicker.js.map +1 -0
- package/dist/icons/catalog.d.ts +17 -0
- package/dist/icons/catalog.d.ts.map +1 -0
- package/dist/icons/catalog.js +114 -0
- package/dist/icons/catalog.js.map +1 -0
- package/dist/icons/catalog.spec.d.ts +2 -0
- package/dist/icons/catalog.spec.d.ts.map +1 -0
- package/dist/icons/catalog.spec.js +29 -0
- package/dist/icons/catalog.spec.js.map +1 -0
- package/dist/icons/emoji-catalog.d.ts +12 -0
- package/dist/icons/emoji-catalog.d.ts.map +1 -0
- package/dist/icons/emoji-catalog.js +59 -0
- package/dist/icons/emoji-catalog.js.map +1 -0
- package/dist/icons/emoji-catalog.spec.d.ts +2 -0
- package/dist/icons/emoji-catalog.spec.d.ts.map +1 -0
- package/dist/icons/emoji-catalog.spec.js +29 -0
- package/dist/icons/emoji-catalog.spec.js.map +1 -0
- package/dist/icons/icon-colors.d.ts +3 -0
- package/dist/icons/icon-colors.d.ts.map +1 -0
- package/dist/icons/icon-colors.js +12 -0
- package/dist/icons/icon-colors.js.map +1 -0
- package/dist/icons/icon-persistence.spec.d.ts +2 -0
- package/dist/icons/icon-persistence.spec.d.ts.map +1 -0
- package/dist/icons/icon-persistence.spec.js +38 -0
- package/dist/icons/icon-persistence.spec.js.map +1 -0
- package/dist/icons/index.d.ts +5 -0
- package/dist/icons/index.d.ts.map +1 -0
- package/dist/icons/index.js +5 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/icons/recents.d.ts +6 -0
- package/dist/icons/recents.d.ts.map +1 -0
- package/dist/icons/recents.js +23 -0
- package/dist/icons/recents.js.map +1 -0
- package/dist/icons/recents.spec.d.ts +2 -0
- package/dist/icons/recents.spec.d.ts.map +1 -0
- package/dist/icons/recents.spec.js +29 -0
- package/dist/icons/recents.spec.js.map +1 -0
- package/dist/icons/server.d.ts +7 -0
- package/dist/icons/server.d.ts.map +1 -0
- package/dist/icons/server.js +25 -0
- package/dist/icons/server.js.map +1 -0
- package/dist/icons/tabler-catalog-data.d.ts +31 -0
- package/dist/icons/tabler-catalog-data.d.ts.map +1 -0
- package/dist/icons/tabler-catalog-data.js +30121 -0
- package/dist/icons/tabler-catalog-data.js.map +1 -0
- package/dist/icons/tabler-chunk-loaders.d.ts +30 -0
- package/dist/icons/tabler-chunk-loaders.d.ts.map +1 -0
- package/dist/icons/tabler-chunk-loaders.js +30 -0
- package/dist/icons/tabler-chunk-loaders.js.map +1 -0
- package/dist/icons/tabler-chunks/a.d.ts +450 -0
- package/dist/icons/tabler-chunks/a.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/a.js +451 -0
- package/dist/icons/tabler-chunks/a.js.map +1 -0
- package/dist/icons/tabler-chunks/b.d.ts +849 -0
- package/dist/icons/tabler-chunks/b.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/b.js +850 -0
- package/dist/icons/tabler-chunks/b.js.map +1 -0
- package/dist/icons/tabler-chunks/c.d.ts +922 -0
- package/dist/icons/tabler-chunks/c.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/c.js +923 -0
- package/dist/icons/tabler-chunks/c.js.map +1 -0
- package/dist/icons/tabler-chunks/d.d.ts +380 -0
- package/dist/icons/tabler-chunks/d.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/d.js +381 -0
- package/dist/icons/tabler-chunks/d.js.map +1 -0
- package/dist/icons/tabler-chunks/e.d.ts +112 -0
- package/dist/icons/tabler-chunks/e.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/e.js +113 -0
- package/dist/icons/tabler-chunks/e.js.map +1 -0
- package/dist/icons/tabler-chunks/f.d.ts +341 -0
- package/dist/icons/tabler-chunks/f.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/f.js +342 -0
- package/dist/icons/tabler-chunks/f.js.map +1 -0
- package/dist/icons/tabler-chunks/g.d.ts +101 -0
- package/dist/icons/tabler-chunks/g.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/g.js +102 -0
- package/dist/icons/tabler-chunks/g.js.map +1 -0
- package/dist/icons/tabler-chunks/h.d.ts +276 -0
- package/dist/icons/tabler-chunks/h.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/h.js +277 -0
- package/dist/icons/tabler-chunks/h.js.map +1 -0
- package/dist/icons/tabler-chunks/i.d.ts +79 -0
- package/dist/icons/tabler-chunks/i.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/i.js +80 -0
- package/dist/icons/tabler-chunks/i.js.map +1 -0
- package/dist/icons/tabler-chunks/j.d.ts +17 -0
- package/dist/icons/tabler-chunks/j.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/j.js +18 -0
- package/dist/icons/tabler-chunks/j.js.map +1 -0
- package/dist/icons/tabler-chunks/k.d.ts +25 -0
- package/dist/icons/tabler-chunks/k.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/k.js +26 -0
- package/dist/icons/tabler-chunks/k.js.map +1 -0
- package/dist/icons/tabler-chunks/l.d.ts +298 -0
- package/dist/icons/tabler-chunks/l.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/l.js +299 -0
- package/dist/icons/tabler-chunks/l.js.map +1 -0
- package/dist/icons/tabler-chunks/m.d.ts +413 -0
- package/dist/icons/tabler-chunks/m.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/m.js +414 -0
- package/dist/icons/tabler-chunks/m.js.map +1 -0
- package/dist/icons/tabler-chunks/n.d.ts +174 -0
- package/dist/icons/tabler-chunks/n.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/n.js +175 -0
- package/dist/icons/tabler-chunks/n.js.map +1 -0
- package/dist/icons/tabler-chunks/o.d.ts +31 -0
- package/dist/icons/tabler-chunks/o.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/o.js +32 -0
- package/dist/icons/tabler-chunks/o.js.map +1 -0
- package/dist/icons/tabler-chunks/p.d.ts +374 -0
- package/dist/icons/tabler-chunks/p.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/p.js +375 -0
- package/dist/icons/tabler-chunks/p.js.map +1 -0
- package/dist/icons/tabler-chunks/q.d.ts +14 -0
- package/dist/icons/tabler-chunks/q.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/q.js +15 -0
- package/dist/icons/tabler-chunks/q.js.map +1 -0
- package/dist/icons/tabler-chunks/r.d.ts +183 -0
- package/dist/icons/tabler-chunks/r.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/r.js +184 -0
- package/dist/icons/tabler-chunks/r.js.map +1 -0
- package/dist/icons/tabler-chunks/s.d.ts +656 -0
- package/dist/icons/tabler-chunks/s.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/s.js +657 -0
- package/dist/icons/tabler-chunks/s.js.map +1 -0
- package/dist/icons/tabler-chunks/t.d.ts +242 -0
- package/dist/icons/tabler-chunks/t.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/t.js +243 -0
- package/dist/icons/tabler-chunks/t.js.map +1 -0
- package/dist/icons/tabler-chunks/u.d.ts +62 -0
- package/dist/icons/tabler-chunks/u.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/u.js +63 -0
- package/dist/icons/tabler-chunks/u.js.map +1 -0
- package/dist/icons/tabler-chunks/v.d.ts +60 -0
- package/dist/icons/tabler-chunks/v.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/v.js +61 -0
- package/dist/icons/tabler-chunks/v.js.map +1 -0
- package/dist/icons/tabler-chunks/w.d.ts +115 -0
- package/dist/icons/tabler-chunks/w.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/w.js +116 -0
- package/dist/icons/tabler-chunks/w.js.map +1 -0
- package/dist/icons/tabler-chunks/x.d.ts +18 -0
- package/dist/icons/tabler-chunks/x.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/x.js +19 -0
- package/dist/icons/tabler-chunks/x.js.map +1 -0
- package/dist/icons/tabler-chunks/y.d.ts +7 -0
- package/dist/icons/tabler-chunks/y.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/y.js +4 -0
- package/dist/icons/tabler-chunks/y.js.map +1 -0
- package/dist/icons/tabler-chunks/z.d.ts +51 -0
- package/dist/icons/tabler-chunks/z.d.ts.map +1 -0
- package/dist/icons/tabler-chunks/z.js +52 -0
- package/dist/icons/tabler-chunks/z.js.map +1 -0
- package/dist/icons/tabler-groups.d.ts +30 -0
- package/dist/icons/tabler-groups.d.ts.map +1 -0
- package/dist/icons/tabler-groups.js +65 -0
- package/dist/icons/tabler-groups.js.map +1 -0
- package/dist/icons/types.d.ts +25 -0
- package/dist/icons/types.d.ts.map +1 -0
- package/dist/icons/types.js +2 -0
- package/dist/icons/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/theme/resource-icons.css +24 -0
- package/package.json +11 -2
- package/src/composer/TiptapComposer.spec.ts +142 -15
- package/src/composer/TiptapComposer.tsx +109 -58
- package/src/icons/ResourceIcon.spec.tsx +38 -0
- package/src/icons/ResourceIcon.tsx +91 -0
- package/src/icons/ResourceIconPicker.tsx +812 -0
- package/src/icons/catalog.spec.ts +50 -0
- package/src/icons/catalog.ts +153 -0
- package/src/icons/emoji-catalog.spec.ts +46 -0
- package/src/icons/emoji-catalog.ts +86 -0
- package/src/icons/icon-colors.ts +13 -0
- package/src/icons/icon-persistence.spec.ts +45 -0
- package/src/icons/index.ts +4 -0
- package/src/icons/recents.spec.ts +37 -0
- package/src/icons/recents.ts +37 -0
- package/src/icons/server.tsx +53 -0
- package/src/icons/tabler-catalog-data.ts +30122 -0
- package/src/icons/tabler-chunk-loaders.ts +29 -0
- package/src/icons/tabler-chunks/a.ts +897 -0
- package/src/icons/tabler-chunks/b.ts +1695 -0
- package/src/icons/tabler-chunks/c.ts +1841 -0
- package/src/icons/tabler-chunks/d.ts +757 -0
- package/src/icons/tabler-chunks/e.ts +221 -0
- package/src/icons/tabler-chunks/f.ts +679 -0
- package/src/icons/tabler-chunks/g.ts +199 -0
- package/src/icons/tabler-chunks/h.ts +549 -0
- package/src/icons/tabler-chunks/i.ts +155 -0
- package/src/icons/tabler-chunks/j.ts +31 -0
- package/src/icons/tabler-chunks/k.ts +47 -0
- package/src/icons/tabler-chunks/l.ts +593 -0
- package/src/icons/tabler-chunks/m.ts +823 -0
- package/src/icons/tabler-chunks/n.ts +345 -0
- package/src/icons/tabler-chunks/o.ts +59 -0
- package/src/icons/tabler-chunks/p.ts +745 -0
- package/src/icons/tabler-chunks/q.ts +25 -0
- package/src/icons/tabler-chunks/r.ts +363 -0
- package/src/icons/tabler-chunks/s.ts +1309 -0
- package/src/icons/tabler-chunks/t.ts +481 -0
- package/src/icons/tabler-chunks/u.ts +121 -0
- package/src/icons/tabler-chunks/v.ts +117 -0
- package/src/icons/tabler-chunks/w.ts +227 -0
- package/src/icons/tabler-chunks/x.ts +33 -0
- package/src/icons/tabler-chunks/y.ts +3 -0
- package/src/icons/tabler-chunks/z.ts +99 -0
- package/src/icons/tabler-groups.ts +70 -0
- package/src/icons/types.ts +35 -0
- package/src/index.ts +1 -0
- package/src/styles.css +2 -0
- 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,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
|
+
}
|