@hoardodile/ui 0.1.5 → 0.1.7

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 (55) hide show
  1. package/dist/components/badge.d.ts +1 -1
  2. package/dist/components/breadcrumb.js +3 -0
  3. package/dist/components/breadcrumb.js.map +1 -1
  4. package/dist/components/caption-bar.js +3 -0
  5. package/dist/components/caption-bar.js.map +1 -1
  6. package/dist/components/color-picker.js +3 -0
  7. package/dist/components/color-picker.js.map +1 -1
  8. package/dist/components/combobox.js +3 -0
  9. package/dist/components/combobox.js.map +1 -1
  10. package/dist/components/dropdown-menu.js +3 -0
  11. package/dist/components/dropdown-menu.js.map +1 -1
  12. package/dist/components/dropdown-select.js +3 -0
  13. package/dist/components/dropdown-select.js.map +1 -1
  14. package/dist/components/font-picker.js +3 -0
  15. package/dist/components/font-picker.js.map +1 -1
  16. package/dist/components/image-crop-panel.js +3 -0
  17. package/dist/components/image-crop-panel.js.map +1 -1
  18. package/dist/components/pagination-bar.js +3 -0
  19. package/dist/components/pagination-bar.js.map +1 -1
  20. package/dist/components/pagination.js +3 -0
  21. package/dist/components/pagination.js.map +1 -1
  22. package/dist/components/panel-toolbar.js +3 -0
  23. package/dist/components/panel-toolbar.js.map +1 -1
  24. package/dist/components/search-field.js +3 -0
  25. package/dist/components/search-field.js.map +1 -1
  26. package/dist/components/sidebar.js +3 -0
  27. package/dist/components/sidebar.js.map +1 -1
  28. package/dist/components/toast.js +3 -0
  29. package/dist/components/toast.js.map +1 -1
  30. package/dist/icons/actions.js +3 -0
  31. package/dist/icons/actions.js.map +1 -1
  32. package/dist/icons/registry.d.ts +2 -1
  33. package/dist/icons/registry.js +9 -1
  34. package/dist/icons/registry.js.map +1 -1
  35. package/dist/res-card-template/format.d.ts +21 -0
  36. package/dist/res-card-template/format.js +23 -0
  37. package/dist/res-card-template/format.js.map +1 -0
  38. package/dist/res-card-template/icon.d.ts +38 -0
  39. package/dist/res-card-template/icon.js +41 -0
  40. package/dist/res-card-template/icon.js.map +1 -0
  41. package/dist/res-card-template/index.d.ts +4 -0
  42. package/dist/res-card-template/index.js +331 -0
  43. package/dist/res-card-template/index.js.map +1 -0
  44. package/dist/res-card-template/template.d.ts +48 -0
  45. package/dist/res-card-template/template.js +333 -0
  46. package/dist/res-card-template/template.js.map +1 -0
  47. package/package.json +9 -2
  48. package/src/icons/registry.ts +8 -0
  49. package/src/res-card-template/format.test.ts +45 -0
  50. package/src/res-card-template/format.ts +36 -0
  51. package/src/res-card-template/icon.test.ts +100 -0
  52. package/src/res-card-template/icon.ts +86 -0
  53. package/src/res-card-template/index.ts +22 -0
  54. package/src/res-card-template/template.test.ts +457 -0
  55. package/src/res-card-template/template.ts +429 -0
@@ -0,0 +1,333 @@
1
+ import { parseTemplateFragments, parseTemplateExpression } from '@hoardodile/sdk-types/template';
2
+ import prettyBytes from 'pretty-bytes';
3
+
4
+ // src/res-card-template/template.ts
5
+ function formatBytes(bytes) {
6
+ if (bytes === void 0) return "";
7
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
8
+ return prettyBytes(bytes, { binary: true }).replaceAll("iB", "B");
9
+ }
10
+ function formatClockDuration(ms) {
11
+ if (!Number.isFinite(ms) || ms < 0) return "";
12
+ const totalSeconds = Math.floor(ms / 1e3);
13
+ const hours = Math.floor(totalSeconds / 3600);
14
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
15
+ const seconds = totalSeconds % 60;
16
+ if (hours > 0) {
17
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
18
+ }
19
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
20
+ }
21
+
22
+ // src/res-card-template/icon.ts
23
+ var LEGACY_ICON_ALIASES = {
24
+ Files: "file",
25
+ Film: "video-frame",
26
+ Image: "gallery",
27
+ Info: "info-circle",
28
+ Music: "music-notes",
29
+ Search: "magnifier",
30
+ Sparkle: "star",
31
+ Video: "video-frame"
32
+ };
33
+ function normalizeSolarGlyphName(raw) {
34
+ const trimmed = raw.trim();
35
+ if (trimmed.length === 0) return void 0;
36
+ const aliased = LEGACY_ICON_ALIASES[trimmed];
37
+ if (aliased !== void 0) return aliased;
38
+ if (/^[a-z][a-z0-9-]*$/.test(trimmed)) return trimmed;
39
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(trimmed)) {
40
+ return trimmed.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
41
+ }
42
+ return void 0;
43
+ }
44
+ function parseIconRef(raw, pluginId, buildAssetUrl) {
45
+ const trimmed = raw.trim();
46
+ if (trimmed.length === 0) return void 0;
47
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("data:")) {
48
+ return void 0;
49
+ }
50
+ if (trimmed.includes(".") || trimmed.includes("/") || trimmed.includes("\\")) {
51
+ const rel = trimmed.replace(/^\.[\\/]/, "");
52
+ if (rel.length === 0 || rel.split("/").includes("..")) return void 0;
53
+ return { kind: "asset", url: buildAssetUrl(pluginId, rel) };
54
+ }
55
+ const name = normalizeSolarGlyphName(trimmed);
56
+ if (name === void 0) return void 0;
57
+ return { kind: "icon", name };
58
+ }
59
+
60
+ // src/res-card-template/template.ts
61
+ function resolvePath(segments, scope) {
62
+ const [namespace, ...rest] = segments;
63
+ const root = namespace === "file" ? scope.file : namespace === "searchMeta" ? scope.searchMeta : namespace === "data" ? scope.data : namespace === "coverMeta" ? scope.coverMeta : scope.source;
64
+ if (root === void 0 || root === null) return void 0;
65
+ let current = root;
66
+ for (const key of rest) {
67
+ if (typeof current !== "object" || current === null) return void 0;
68
+ current = current[key];
69
+ }
70
+ return current;
71
+ }
72
+ function resolveLocaleString(value, locale) {
73
+ if (typeof value === "string") return value;
74
+ const exact = value[locale];
75
+ if (exact !== void 0) return exact;
76
+ const base = locale.split("-")[0] ?? locale;
77
+ const partial = value[base];
78
+ if (partial !== void 0) return partial;
79
+ const first = Object.values(value)[0];
80
+ return first ?? "";
81
+ }
82
+ function evaluateExpression(expr, scope, ctx) {
83
+ if (expr.kind === "path") {
84
+ const value = resolvePath(expr.segments, scope);
85
+ if (value === void 0 || value === null) return "";
86
+ return String(value);
87
+ }
88
+ return evaluateCall(expr.name, expr.args, scope, ctx);
89
+ }
90
+ function evaluateCall(name, args, scope, ctx) {
91
+ switch (name) {
92
+ case "bytes":
93
+ return callPipe(
94
+ args,
95
+ scope,
96
+ ctx,
97
+ (value) => typeof value === "number" ? formatBytes(value) : ""
98
+ );
99
+ case "duration":
100
+ return callPipe(
101
+ args,
102
+ scope,
103
+ ctx,
104
+ (value) => typeof value === "number" ? formatClockDuration(value) : ""
105
+ );
106
+ case "number":
107
+ return callPipe(
108
+ args,
109
+ scope,
110
+ ctx,
111
+ (value) => typeof value === "number" && Number.isFinite(value) ? value.toLocaleString() : ""
112
+ );
113
+ case "inc":
114
+ return callPipe(
115
+ args,
116
+ scope,
117
+ ctx,
118
+ (value) => typeof value === "number" && Number.isFinite(value) ? String(value + 1) : ""
119
+ );
120
+ case "eq":
121
+ return compareCall(args, scope, ctx, (a, b) => a === b);
122
+ case "ne":
123
+ return compareCall(args, scope, ctx, (a, b) => a !== b);
124
+ case "gt":
125
+ return compareCall(args, scope, ctx, (a, b) => {
126
+ if (typeof a !== "number" || typeof b !== "number") return false;
127
+ return a > b;
128
+ });
129
+ case "lt":
130
+ return compareCall(args, scope, ctx, (a, b) => {
131
+ if (typeof a !== "number" || typeof b !== "number") return false;
132
+ return a < b;
133
+ });
134
+ case "gte":
135
+ return compareCall(args, scope, ctx, (a, b) => {
136
+ if (typeof a !== "number" || typeof b !== "number") return false;
137
+ return a >= b;
138
+ });
139
+ case "lte":
140
+ return compareCall(args, scope, ctx, (a, b) => {
141
+ if (typeof a !== "number" || typeof b !== "number") return false;
142
+ return a <= b;
143
+ });
144
+ case "if":
145
+ return callIf(args, scope, ctx);
146
+ case "t":
147
+ return callT(args, ctx);
148
+ case "icon":
149
+ return callIcon(args, ctx);
150
+ case "asset":
151
+ return callAsset(args, ctx);
152
+ case "kind":
153
+ return callKind(args, scope, ctx);
154
+ case "searchKindIcons":
155
+ return callSearchKindIcons(scope, ctx);
156
+ case "join":
157
+ return callJoin(args, scope, ctx);
158
+ default:
159
+ return "";
160
+ }
161
+ }
162
+ function callPipe(args, scope, ctx, pipeFn) {
163
+ const value = evaluateArgAsPrimitive(args[0], scope, ctx);
164
+ return pipeFn(value);
165
+ }
166
+ function callT(args, ctx) {
167
+ const key = evaluateArgAsString(args[0]);
168
+ if (key.length === 0) return "";
169
+ const map = ctx.manifest.i18n;
170
+ if (map === void 0) return "";
171
+ const value = map[key];
172
+ if (value === void 0) return "";
173
+ return resolveLocaleString(value, ctx.locale);
174
+ }
175
+ function callIcon(args, ctx) {
176
+ const name = evaluateArgAsString(args[0]);
177
+ if (name.length === 0) return null;
178
+ return ctx.renderIcon({ kind: "icon", name }, ctx.iconClassName);
179
+ }
180
+ function callAsset(args, ctx) {
181
+ const path = evaluateArgAsString(args[0]);
182
+ if (path.length === 0) return null;
183
+ const ref = parseIconRef(path, ctx.pluginId, ctx.buildAssetUrl);
184
+ if (ref === void 0) return null;
185
+ return ctx.renderIcon(ref, ctx.iconClassName);
186
+ }
187
+ function callKind(args, scope, ctx) {
188
+ const key = evaluateArgAsString(args[0]);
189
+ if (key.length === 0) return null;
190
+ const kinds = ctx.manifest.ui?.search?.kinds;
191
+ if (kinds === void 0) return null;
192
+ const match = kinds.find((k) => k.key === key);
193
+ if (match === void 0 || match.icon === void 0) return null;
194
+ const rendered = renderCardTemplate(match.icon, scope, ctx);
195
+ if (rendered === null || rendered === void 0 || rendered === "")
196
+ return null;
197
+ return rendered;
198
+ }
199
+ function callSearchKindIcons(scope, ctx) {
200
+ if (scope.searchMeta === void 0 || scope.searchMeta === null || typeof scope.searchMeta !== "object")
201
+ return [];
202
+ const facets = scope.searchMeta.facets;
203
+ if (facets === void 0 || typeof facets !== "object" || facets === null)
204
+ return [];
205
+ const kinds = ctx.manifest.ui?.search?.kinds;
206
+ if (kinds === void 0) return [];
207
+ const results = [];
208
+ for (const kind of kinds) {
209
+ if (kind.icon === void 0) continue;
210
+ const active = facets[kind.key];
211
+ if (active !== true) continue;
212
+ const rendered = renderCardTemplate(kind.icon, scope, ctx);
213
+ if (rendered === null || rendered === void 0 || rendered === "") continue;
214
+ results.push(rendered);
215
+ }
216
+ return results;
217
+ }
218
+ function callJoin(args, scope, ctx) {
219
+ if (args.length === 0) return "";
220
+ const separator = evaluateArgAsString(args[0]);
221
+ const items = [];
222
+ for (let i = 1; i < args.length; i++) {
223
+ const val = evaluateArg(args[i], scope, ctx);
224
+ if (Array.isArray(val)) {
225
+ for (const item of val) {
226
+ if (item !== null && item !== void 0 && item !== "") {
227
+ items.push(item);
228
+ }
229
+ }
230
+ continue;
231
+ }
232
+ if (val !== null && val !== void 0 && val !== "") {
233
+ items.push(val);
234
+ }
235
+ }
236
+ if (items.length === 0) return "";
237
+ if (items.every((item) => typeof item === "string")) {
238
+ return items.join(separator);
239
+ }
240
+ if (separator.length === 0) return items;
241
+ const interleaved = [];
242
+ for (let i = 0; i < items.length; i++) {
243
+ if (i > 0) interleaved.push(separator);
244
+ interleaved.push(items[i]);
245
+ }
246
+ return interleaved;
247
+ }
248
+ function evaluateArgAsPrimitive(arg, scope, ctx) {
249
+ if (arg === void 0) return void 0;
250
+ if (arg.kind === "string") return arg.value;
251
+ if (arg.expr.kind === "path") {
252
+ const segments = arg.expr.segments;
253
+ if (segments.length === 1) {
254
+ const seg = segments[0];
255
+ const num = Number(seg);
256
+ if (!Number.isNaN(num)) return num;
257
+ }
258
+ return resolvePath(segments, scope);
259
+ }
260
+ if (arg.expr.kind === "call") {
261
+ const result = evaluateCall(arg.expr.name, arg.expr.args, scope, ctx);
262
+ if (typeof result === "string" || typeof result === "number") return result;
263
+ if (Array.isArray(result)) return result;
264
+ return void 0;
265
+ }
266
+ return void 0;
267
+ }
268
+ function evaluateArg(arg, scope, ctx) {
269
+ if (arg === void 0) return void 0;
270
+ if (arg.kind === "string") return arg.value;
271
+ return evaluateExpression(arg.expr, scope, ctx);
272
+ }
273
+ function compareCall(args, scope, ctx, cmp) {
274
+ const a = evaluateArgAsPrimitive(args[0], scope, ctx);
275
+ const b = evaluateArgAsPrimitive(args[1], scope, ctx);
276
+ return cmp(a, b) ? "true" : "";
277
+ }
278
+ function callIf(args, scope, ctx) {
279
+ const cond = evaluateArgAsPrimitive(args[0], scope, ctx);
280
+ const truthy = cond !== void 0 && cond !== null && cond !== "" && cond !== false && cond !== 0;
281
+ if (truthy) {
282
+ const raw = evaluateArgAsPrimitive(args[1], scope, ctx);
283
+ if (raw !== void 0) return raw;
284
+ return evaluateArg(args[1], scope, ctx);
285
+ }
286
+ if (args.length > 2) {
287
+ const raw = evaluateArgAsPrimitive(args[2], scope, ctx);
288
+ if (raw !== void 0) return raw;
289
+ return evaluateArg(args[2], scope, ctx);
290
+ }
291
+ return "";
292
+ }
293
+ function evaluateArgAsString(arg) {
294
+ if (arg === void 0) return "";
295
+ if (arg.kind === "string") return arg.value;
296
+ return "";
297
+ }
298
+ function renderCardTemplate(template, scope, ctx) {
299
+ if (template.length === 0) return "";
300
+ const fragments = parseTemplateFragments(template);
301
+ if (fragments.length === 0) return "";
302
+ const results = [];
303
+ for (const frag of fragments) {
304
+ if (frag.kind === "text") {
305
+ results.push(frag.value);
306
+ continue;
307
+ }
308
+ const expr = parseTemplateExpression(frag.source);
309
+ if (expr === void 0) {
310
+ continue;
311
+ }
312
+ const value = evaluateExpression(expr, scope, ctx);
313
+ if (value !== null && value !== void 0 && value !== "") {
314
+ results.push(value);
315
+ }
316
+ }
317
+ if (results.length === 0) return null;
318
+ if (results.length === 1) return results[0];
319
+ const allStrings = results.every((r) => typeof r === "string");
320
+ if (allStrings) return results.join("");
321
+ return results;
322
+ }
323
+ function renderSlotBadges(slotValues, scope, ctx) {
324
+ return slotValues.map((template) => renderCardTemplate(template, scope, ctx)).filter((n) => {
325
+ if (n === null || n === void 0 || n === "") return false;
326
+ if (Array.isArray(n) && n.length === 0) return false;
327
+ return true;
328
+ });
329
+ }
330
+
331
+ export { renderCardTemplate, renderSlotBadges, resolveLocaleString };
332
+ //# sourceMappingURL=template.js.map
333
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/res-card-template/format.ts","../../src/res-card-template/icon.ts","../../src/res-card-template/template.ts"],"names":[],"mappings":"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","file":"template.js","sourcesContent":["import prettyBytes from \"pretty-bytes\"\n\n/**\n * Format a non-negative byte count as a short human-readable string\n * via {@link prettyBytes} with binary conversion (base 1024) and JEDEC\n * labels (`4.5 KB`, `1.2 MB`, `930 GB`) — IEC `KiB`/`MiB`/`GiB` mapped\n * to `KB`/`MB`/`GB`. Matches how Windows and most disk tools report\n * sizes, without switching to decimal (base 1000) units.\n *\n * Returns the empty string for `undefined` so callers can splat the value\n * into a template without conditional checks. Negative or non-finite\n * inputs are clamped to `0 B`.\n */\nexport function formatBytes(bytes: number | undefined): string {\n\tif (bytes === undefined) return \"\"\n\tif (!Number.isFinite(bytes) || bytes <= 0) return \"0 B\"\n\treturn prettyBytes(bytes, { binary: true }).replaceAll(\"iB\", \"B\")\n}\n\n/**\n * Format a duration in milliseconds as a media clock — `m:ss`, or\n * `h:mm:ss` past the hour. Used by the plugin card templates\n * (`{{duration(...)}}`) and the card's inline audio player, so both\n * read the same way.\n */\nexport function formatClockDuration(ms: number): string {\n\tif (!Number.isFinite(ms) || ms < 0) return \"\"\n\tconst totalSeconds = Math.floor(ms / 1000)\n\tconst hours = Math.floor(totalSeconds / 3600)\n\tconst minutes = Math.floor((totalSeconds % 3600) / 60)\n\tconst seconds = totalSeconds % 60\n\tif (hours > 0) {\n\t\treturn `${hours}:${String(minutes).padStart(2, \"0\")}:${String(seconds).padStart(2, \"0\")}`\n\t}\n\treturn `${minutes}:${String(seconds).padStart(2, \"0\")}`\n}\n","import type { ReactNode } from \"react\"\n\n/**\n * A resolved template-icon reference: a Solar glyph name, or an\n * asset image URL derived from a manifest-relative path.\n */\nexport type IconRef =\n\t| { readonly kind: \"icon\"; readonly name: string }\n\t| { readonly kind: \"asset\"; readonly url: string }\n\n/**\n * Legacy whitelist name → Solar kebab glyph. Derived names are handled by\n * the PascalCase conversion; only these diverge.\n */\nconst LEGACY_ICON_ALIASES: Readonly<Record<string, string>> = {\n\tFiles: \"file\",\n\tFilm: \"video-frame\",\n\tImage: \"gallery\",\n\tInfo: \"info-circle\",\n\tMusic: \"music-notes\",\n\tSearch: \"magnifier\",\n\tSparkle: \"star\",\n\tVideo: \"video-frame\",\n}\n\n/**\n * Normalize a raw icon name to Solar kebab-case, or `undefined` when the\n * string cannot name a glyph at all (schemes, separators, punctuation).\n * This is syntax only — membership in the Solar glyph index is left to\n * the renderer that consumes the name.\n */\nexport function normalizeSolarGlyphName(raw: string): string | undefined {\n\tconst trimmed = raw.trim()\n\tif (trimmed.length === 0) return undefined\n\tconst aliased = LEGACY_ICON_ALIASES[trimmed]\n\tif (aliased !== undefined) return aliased\n\tif (/^[a-z][a-z0-9-]*$/.test(trimmed)) return trimmed\n\tif (/^[A-Za-z][A-Za-z0-9]*$/.test(trimmed)) {\n\t\treturn trimmed.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase()\n\t}\n\treturn undefined\n}\n\n/**\n * Parse a manifest-level icon string into a render-ready ref.\n *\n * `<SolarGlyph>` — Solar glyph name (manifest/template icons are\n * Solar-only; PascalCase and the legacy whitelist\n * names normalize to the kebab glyph)\n * `<relative/path>` — resolved through `buildAssetUrl(pluginId, path)`\n * (leading `./` stripped)\n *\n * Empty inputs, schemes (`http(s)`, `data:`), `..`-shaped paths and any\n * string that cannot name a glyph return `undefined`. The renderer treats\n * that as \"nothing\" — icon resolution never throws.\n */\nexport function parseIconRef(\n\traw: string,\n\tpluginId: string,\n\tbuildAssetUrl: (pluginId: string, path: string) => string,\n): IconRef | undefined {\n\tconst trimmed = raw.trim()\n\tif (trimmed.length === 0) return undefined\n\tif (\n\t\ttrimmed.startsWith(\"http://\") ||\n\t\ttrimmed.startsWith(\"https://\") ||\n\t\ttrimmed.startsWith(\"data:\")\n\t) {\n\t\treturn undefined\n\t}\n\tif (\n\t\ttrimmed.includes(\".\") ||\n\t\ttrimmed.includes(\"/\") ||\n\t\ttrimmed.includes(\"\\\\\")\n\t) {\n\t\tconst rel = trimmed.replace(/^\\.[\\\\/]/, \"\")\n\t\tif (rel.length === 0 || rel.split(\"/\").includes(\"..\")) return undefined\n\t\treturn { kind: \"asset\", url: buildAssetUrl(pluginId, rel) }\n\t}\n\tconst name = normalizeSolarGlyphName(trimmed)\n\tif (name === undefined) return undefined\n\treturn { kind: \"icon\", name }\n}\n\n/** How a parsed {@link IconRef} is drawn inside a rendered badge/template. */\nexport type RenderIcon = (ref: IconRef, className?: string) => ReactNode\n","import {\n\ttype TemplateArg as Arg,\n\ttype TemplateExpr as Expr,\n\tparseTemplateExpression,\n\tparseTemplateFragments,\n} from \"@hoardodile/sdk-types/template\"\nimport type { ReactNode } from \"react\"\nimport { formatBytes, formatClockDuration } from \"./format.ts\"\nimport { parseIconRef, type RenderIcon } from \"./icon.ts\"\n\n// The template grammar (fragment splitting, tokenising, parsing) lives\n// in @hoardodile/sdk-types/template — shared verbatim with the CLI's\n// build-time template lint so the two can never drift. This module only\n// evaluates the AST. The parser is lenient: unparseable expressions\n// render as the empty string.\n\n// ── Evaluation context ───────────────────────────────────────────────────────\n\n/** How a referenced icon is drawn; the caller owns the SVG/image resolution. */\nexport type TemplateContext = {\n\treadonly locale: string\n\treadonly pluginId: string\n\treadonly manifest: {\n\t\treadonly i18n?: Record<string, string | Record<string, string>>\n\t\treadonly ui?: {\n\t\t\treadonly search?: {\n\t\t\t\treadonly kinds?: readonly {\n\t\t\t\t\treadonly key: string\n\t\t\t\t\treadonly icon?: string\n\t\t\t\t}[]\n\t\t\t}\n\t\t}\n\t}\n\treadonly iconClassName?: string\n\t/** Renders a parsed {@link IconRef} (glyph or asset) inside the template. */\n\treadonly renderIcon: RenderIcon\n\t/** Resolves a manifest-relative `asset('path')` reference to a URL. */\n\treadonly buildAssetUrl: (pluginId: string, path: string) => string\n}\n\ntype EvalValue = string | ReactNode\n\ntype TemplateScope = {\n\treadonly file: unknown\n\treadonly source: unknown\n\treadonly searchMeta?: unknown\n\treadonly data?: unknown\n\treadonly coverMeta?: unknown\n}\n\n// ── Scope resolution (paths) ─────────────────────────────────────────────────\n\nfunction resolvePath(\n\tsegments: readonly string[],\n\tscope: TemplateScope,\n): unknown {\n\tconst [namespace, ...rest] = segments\n\tconst root =\n\t\tnamespace === \"file\"\n\t\t\t? scope.file\n\t\t\t: namespace === \"searchMeta\"\n\t\t\t\t? scope.searchMeta\n\t\t\t\t: namespace === \"data\"\n\t\t\t\t\t? scope.data\n\t\t\t\t\t: namespace === \"coverMeta\"\n\t\t\t\t\t\t? scope.coverMeta\n\t\t\t\t\t\t: scope.source\n\tif (root === undefined || root === null) return undefined\n\tlet current: unknown = root\n\tfor (const key of rest) {\n\t\tif (typeof current !== \"object\" || current === null) return undefined\n\t\tcurrent = (current as Record<string, unknown>)[key]\n\t}\n\treturn current\n}\n\n// ── Locale resolution ────────────────────────────────────────────────────────\n\n/**\n * Resolve a locale-aware template value: if the value is a plain string,\n * return it directly; if it is a `Record<locale, string>`, pick the best\n * match for the current language. Falls back to the first available key\n * when nothing matches.\n */\nexport function resolveLocaleString(\n\tvalue: string | Record<string, string>,\n\tlocale: string,\n): string {\n\tif (typeof value === \"string\") return value\n\tconst exact = value[locale]\n\tif (exact !== undefined) return exact\n\tconst base = locale.split(\"-\")[0] ?? locale\n\tconst partial = value[base]\n\tif (partial !== undefined) return partial\n\tconst first = Object.values(value)[0]\n\treturn first ?? \"\"\n}\n\n// ── Expression evaluation ────────────────────────────────────────────────────\n\nfunction evaluateExpression(\n\texpr: Expr,\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): EvalValue {\n\tif (expr.kind === \"path\") {\n\t\tconst value = resolvePath(expr.segments, scope)\n\t\tif (value === undefined || value === null) return \"\"\n\t\treturn String(value)\n\t}\n\treturn evaluateCall(expr.name, expr.args, scope, ctx)\n}\n\nfunction evaluateCall(\n\tname: string,\n\targs: readonly Arg[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): EvalValue {\n\tswitch (name) {\n\t\tcase \"bytes\":\n\t\t\treturn callPipe(args, scope, ctx, (value) =>\n\t\t\t\ttypeof value === \"number\" ? formatBytes(value) : \"\",\n\t\t\t)\n\t\tcase \"duration\":\n\t\t\treturn callPipe(args, scope, ctx, (value) =>\n\t\t\t\ttypeof value === \"number\" ? formatClockDuration(value) : \"\",\n\t\t\t)\n\t\tcase \"number\":\n\t\t\treturn callPipe(args, scope, ctx, (value) =>\n\t\t\t\ttypeof value === \"number\" && Number.isFinite(value)\n\t\t\t\t\t? value.toLocaleString()\n\t\t\t\t\t: \"\",\n\t\t\t)\n\t\tcase \"inc\":\n\t\t\treturn callPipe(args, scope, ctx, (value) =>\n\t\t\t\ttypeof value === \"number\" && Number.isFinite(value)\n\t\t\t\t\t? String(value + 1)\n\t\t\t\t\t: \"\",\n\t\t\t)\n\t\tcase \"eq\":\n\t\t\treturn compareCall(args, scope, ctx, (a, b) => a === b)\n\t\tcase \"ne\":\n\t\t\treturn compareCall(args, scope, ctx, (a, b) => a !== b)\n\t\tcase \"gt\":\n\t\t\treturn compareCall(args, scope, ctx, (a, b) => {\n\t\t\t\tif (typeof a !== \"number\" || typeof b !== \"number\") return false\n\t\t\t\treturn a > b\n\t\t\t})\n\t\tcase \"lt\":\n\t\t\treturn compareCall(args, scope, ctx, (a, b) => {\n\t\t\t\tif (typeof a !== \"number\" || typeof b !== \"number\") return false\n\t\t\t\treturn a < b\n\t\t\t})\n\t\tcase \"gte\":\n\t\t\treturn compareCall(args, scope, ctx, (a, b) => {\n\t\t\t\tif (typeof a !== \"number\" || typeof b !== \"number\") return false\n\t\t\t\treturn a >= b\n\t\t\t})\n\t\tcase \"lte\":\n\t\t\treturn compareCall(args, scope, ctx, (a, b) => {\n\t\t\t\tif (typeof a !== \"number\" || typeof b !== \"number\") return false\n\t\t\t\treturn a <= b\n\t\t\t})\n\t\tcase \"if\":\n\t\t\treturn callIf(args, scope, ctx)\n\t\tcase \"t\":\n\t\t\treturn callT(args, ctx)\n\t\tcase \"icon\":\n\t\t\treturn callIcon(args, ctx)\n\t\tcase \"asset\":\n\t\t\treturn callAsset(args, ctx)\n\t\tcase \"kind\":\n\t\t\treturn callKind(args, scope, ctx)\n\t\tcase \"searchKindIcons\":\n\t\t\treturn callSearchKindIcons(scope, ctx)\n\t\tcase \"join\":\n\t\t\treturn callJoin(args, scope, ctx)\n\t\tdefault:\n\t\t\treturn \"\"\n\t}\n}\n\nfunction callPipe(\n\targs: readonly Arg[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n\tpipeFn: (value: unknown) => string,\n): string {\n\tconst value = evaluateArgAsPrimitive(args[0], scope, ctx)\n\treturn pipeFn(value)\n}\n\nfunction callT(args: readonly Arg[], ctx: TemplateContext): string {\n\tconst key = evaluateArgAsString(args[0])\n\tif (key.length === 0) return \"\"\n\tconst map = ctx.manifest.i18n\n\tif (map === undefined) return \"\"\n\tconst value = map[key]\n\tif (value === undefined) return \"\"\n\treturn resolveLocaleString(value, ctx.locale)\n}\n\nfunction callIcon(args: readonly Arg[], ctx: TemplateContext): ReactNode {\n\tconst name = evaluateArgAsString(args[0])\n\tif (name.length === 0) return null\n\treturn ctx.renderIcon({ kind: \"icon\", name }, ctx.iconClassName)\n}\n\nfunction callAsset(args: readonly Arg[], ctx: TemplateContext): ReactNode {\n\tconst path = evaluateArgAsString(args[0])\n\tif (path.length === 0) return null\n\tconst ref = parseIconRef(path, ctx.pluginId, ctx.buildAssetUrl)\n\tif (ref === undefined) return null\n\treturn ctx.renderIcon(ref, ctx.iconClassName)\n}\n\nfunction callKind(\n\targs: readonly Arg[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): ReactNode {\n\tconst key = evaluateArgAsString(args[0])\n\tif (key.length === 0) return null\n\tconst kinds = ctx.manifest.ui?.search?.kinds\n\tif (kinds === undefined) return null\n\tconst match = kinds.find((k) => k.key === key)\n\tif (match === undefined || match.icon === undefined) return null\n\tconst rendered = renderCardTemplate(match.icon, scope, ctx)\n\tif (rendered === null || rendered === undefined || rendered === \"\")\n\t\treturn null\n\treturn rendered\n}\n\nfunction callSearchKindIcons(\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): readonly ReactNode[] {\n\tif (\n\t\tscope.searchMeta === undefined ||\n\t\tscope.searchMeta === null ||\n\t\ttypeof scope.searchMeta !== \"object\"\n\t)\n\t\treturn []\n\tconst facets = (scope.searchMeta as Record<string, unknown>).facets\n\tif (facets === undefined || typeof facets !== \"object\" || facets === null)\n\t\treturn []\n\tconst kinds = ctx.manifest.ui?.search?.kinds\n\tif (kinds === undefined) return []\n\tconst results: ReactNode[] = []\n\tfor (const kind of kinds) {\n\t\tif (kind.icon === undefined) continue\n\t\tconst active = (facets as Record<string, boolean>)[kind.key]\n\t\tif (active !== true) continue\n\t\tconst rendered = renderCardTemplate(kind.icon, scope, ctx)\n\t\tif (rendered === null || rendered === undefined || rendered === \"\") continue\n\t\tresults.push(rendered)\n\t}\n\treturn results\n}\n\nfunction callJoin(\n\targs: readonly Arg[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): string | readonly ReactNode[] {\n\tif (args.length === 0) return \"\"\n\n\tconst separator = evaluateArgAsString(args[0])\n\tconst items: ReactNode[] = []\n\tfor (let i = 1; i < args.length; i++) {\n\t\tconst val = evaluateArg(args[i], scope, ctx)\n\t\tif (Array.isArray(val)) {\n\t\t\tfor (const item of val) {\n\t\t\t\tif (item !== null && item !== undefined && item !== \"\") {\n\t\t\t\t\titems.push(item)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (val !== null && val !== undefined && val !== \"\") {\n\t\t\titems.push(val)\n\t\t}\n\t}\n\tif (items.length === 0) return \"\"\n\tif (items.every((item) => typeof item === \"string\")) {\n\t\treturn (items as readonly string[]).join(separator)\n\t}\n\tif (separator.length === 0) return items\n\tconst interleaved: ReactNode[] = []\n\tfor (let i = 0; i < items.length; i++) {\n\t\tif (i > 0) interleaved.push(separator)\n\t\tinterleaved.push(items[i])\n\t}\n\treturn interleaved\n}\n\n// ── Argument helpers ─────────────────────────────────────────────────────────\n\nfunction evaluateArgAsPrimitive(\n\targ: Arg | undefined,\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): unknown {\n\tif (arg === undefined) return undefined\n\tif (arg.kind === \"string\") return arg.value\n\tif (arg.expr.kind === \"path\") {\n\t\tconst segments = arg.expr.segments\n\t\tif (segments.length === 1) {\n\t\t\tconst seg = segments[0]!\n\t\t\tconst num = Number(seg)\n\t\t\tif (!Number.isNaN(num)) return num\n\t\t}\n\t\treturn resolvePath(segments, scope)\n\t}\n\tif (arg.expr.kind === \"call\") {\n\t\tconst result = evaluateCall(arg.expr.name, arg.expr.args, scope, ctx)\n\t\tif (typeof result === \"string\" || typeof result === \"number\") return result\n\t\tif (Array.isArray(result)) return result\n\t\treturn undefined\n\t}\n\treturn undefined\n}\n\nfunction evaluateArg(\n\targ: Arg | undefined,\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): EvalValue {\n\tif (arg === undefined) return undefined\n\tif (arg.kind === \"string\") return arg.value\n\treturn evaluateExpression(arg.expr, scope, ctx)\n}\n\nfunction compareCall(\n\targs: readonly Arg[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n\tcmp: (a: unknown, b: unknown) => boolean,\n): string {\n\tconst a = evaluateArgAsPrimitive(args[0], scope, ctx)\n\tconst b = evaluateArgAsPrimitive(args[1], scope, ctx)\n\treturn cmp(a, b) ? \"true\" : \"\"\n}\n\nfunction callIf(\n\targs: readonly Arg[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): EvalValue {\n\tconst cond = evaluateArgAsPrimitive(args[0], scope, ctx)\n\tconst truthy =\n\t\tcond !== undefined &&\n\t\tcond !== null &&\n\t\tcond !== \"\" &&\n\t\tcond !== false &&\n\t\tcond !== 0\n\tif (truthy) {\n\t\tconst raw = evaluateArgAsPrimitive(args[1], scope, ctx)\n\t\tif (raw !== undefined) return raw as EvalValue\n\t\treturn evaluateArg(args[1], scope, ctx)\n\t}\n\tif (args.length > 2) {\n\t\tconst raw = evaluateArgAsPrimitive(args[2], scope, ctx)\n\t\tif (raw !== undefined) return raw as EvalValue\n\t\treturn evaluateArg(args[2], scope, ctx)\n\t}\n\treturn \"\"\n}\n\nfunction evaluateArgAsString(arg: Arg | undefined): string {\n\tif (arg === undefined) return \"\"\n\tif (arg.kind === \"string\") return arg.value\n\treturn \"\"\n}\n\n// ── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Render one res-card template string. Returns a ReactNode so that\n * icon-producing functions (`icon`, `asset`, `kind`) can inject\n * components inline with text.\n */\nexport function renderCardTemplate(\n\ttemplate: string,\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): ReactNode {\n\tif (template.length === 0) return \"\"\n\tconst fragments = parseTemplateFragments(template)\n\tif (fragments.length === 0) return \"\"\n\n\tconst results: ReactNode[] = []\n\tfor (const frag of fragments) {\n\t\tif (frag.kind === \"text\") {\n\t\t\tresults.push(frag.value)\n\t\t\tcontinue\n\t\t}\n\t\tconst expr = parseTemplateExpression(frag.source)\n\t\tif (expr === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tconst value = evaluateExpression(expr, scope, ctx)\n\t\tif (value !== null && value !== undefined && value !== \"\") {\n\t\t\tresults.push(value)\n\t\t}\n\t}\n\n\tif (results.length === 0) return null\n\tif (results.length === 1) return results[0]\n\tconst allStrings = results.every((r) => typeof r === \"string\")\n\tif (allStrings) return results.join(\"\")\n\treturn results\n}\n\n/** Render every badge in a {@link CoverKindUi} slot array. */\nexport function renderSlotBadges(\n\tslotValues: readonly string[],\n\tscope: TemplateScope,\n\tctx: TemplateContext,\n): readonly ReactNode[] {\n\treturn slotValues\n\t\t.map((template) => renderCardTemplate(template, scope, ctx))\n\t\t.filter((n): n is Exclude<typeof n, null | undefined | \"\"> => {\n\t\t\tif (n === null || n === undefined || n === \"\") return false\n\t\t\tif (Array.isArray(n) && n.length === 0) return false\n\t\t\treturn true\n\t\t})\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoardodile/ui",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "license": "MIT",
5
5
  "description": "shadcn/ui component library for hoardodile plugin iframes.",
6
6
  "keywords": [
@@ -46,6 +46,11 @@
46
46
  "development": "./src/hooks/*",
47
47
  "default": "./dist/hooks/*.js"
48
48
  },
49
+ "./res-card-template": {
50
+ "types": "./dist/res-card-template/index.d.ts",
51
+ "development": "./src/res-card-template/index.ts",
52
+ "default": "./dist/res-card-template/index.js"
53
+ },
49
54
  "./viewport": {
50
55
  "types": "./dist/viewport.d.ts",
51
56
  "development": "./src/viewport.ts",
@@ -72,12 +77,14 @@
72
77
  "@dnd-kit/utilities": "^3.2.2",
73
78
  "@solar-icons/react": "^2.1.0",
74
79
  "class-variance-authority": "^0.7.1",
80
+ "pretty-bytes": "^7.1.1",
75
81
  "clsx": "^2.1.1",
76
82
  "react-hook-form": "^7.86.0",
77
83
  "react-image-crop": "^11.1.2",
78
84
  "react-use": "^17.6.1",
79
85
  "tailwind-merge": "^3.6.0",
80
- "@hoardodile/i18n": "0.1.5"
86
+ "@hoardodile/i18n": "0.1.7",
87
+ "@hoardodile/sdk-types": "0.1.7"
81
88
  },
82
89
  "peerDependencies": {
83
90
  "i18next": "^26.4.0",
@@ -170,6 +170,7 @@ import { UserPlusIcon as UserPlusBoldWeight } from "@solar-icons/react/bold/user
170
170
  import { UsersGroupRoundedIcon as UsersGroupRoundedBoldWeight } from "@solar-icons/react/bold/users-group-rounded"
171
171
  import { UsersGroupTwoRoundedIcon as UsersGroupTwoRoundedBoldWeight } from "@solar-icons/react/bold/users-group-two-rounded"
172
172
  import { VideoFrameIcon as VideoFrameBoldWeight } from "@solar-icons/react/bold/video-frame"
173
+ import { VideoFramePlayHorizontalIcon as VideoFramePlayHorizontalBoldWeight } from "@solar-icons/react/bold/video-frame-play-horizontal"
173
174
  import { Widget2Icon as Widget2BoldWeight } from "@solar-icons/react/bold/widget-2"
174
175
  import { Widget5Icon as Widget5BoldWeight } from "@solar-icons/react/bold/widget-5"
175
176
  import { WindowFrameIcon as WindowFrameBoldWeight } from "@solar-icons/react/bold/window-frame"
@@ -312,6 +313,7 @@ import { UserPlusIcon as UserPlusBoldDuotone } from "@solar-icons/react/bold-duo
312
313
  import { UsersGroupRoundedIcon as UsersGroupRoundedBoldDuotone } from "@solar-icons/react/bold-duotone/users-group-rounded"
313
314
  import { UsersGroupTwoRoundedIcon as UsersGroupTwoRoundedBoldDuotone } from "@solar-icons/react/bold-duotone/users-group-two-rounded"
314
315
  import { VideoFrameIcon as VideoFrameBoldDuotone } from "@solar-icons/react/bold-duotone/video-frame"
316
+ import { VideoFramePlayHorizontalIcon as VideoFramePlayHorizontalBoldDuotone } from "@solar-icons/react/bold-duotone/video-frame-play-horizontal"
315
317
  import { Widget2Icon as Widget2BoldDuotone } from "@solar-icons/react/bold-duotone/widget-2"
316
318
  import { Widget5Icon as Widget5BoldDuotone } from "@solar-icons/react/bold-duotone/widget-5"
317
319
  import { WindowFrameIcon as WindowFrameBoldDuotone } from "@solar-icons/react/bold-duotone/window-frame"
@@ -453,6 +455,7 @@ import { UserPlusIcon as UserPlusLinear } from "@solar-icons/react/linear/user-p
453
455
  import { UsersGroupRoundedIcon as UsersGroupRoundedLinear } from "@solar-icons/react/linear/users-group-rounded"
454
456
  import { UsersGroupTwoRoundedIcon as UsersGroupTwoRoundedLinear } from "@solar-icons/react/linear/users-group-two-rounded"
455
457
  import { VideoFrameIcon as VideoFrameLinear } from "@solar-icons/react/linear/video-frame"
458
+ import { VideoFramePlayHorizontalIcon as VideoFramePlayHorizontalLinear } from "@solar-icons/react/linear/video-frame-play-horizontal"
456
459
  import { Widget2Icon as Widget2Linear } from "@solar-icons/react/linear/widget-2"
457
460
  import { Widget5Icon as Widget5Linear } from "@solar-icons/react/linear/widget-5"
458
461
  import { WindowFrameIcon as WindowFrameLinear } from "@solar-icons/react/linear/window-frame"
@@ -1154,6 +1157,11 @@ export const VideoFrame = createIcon({
1154
1157
  boldDuotone: VideoFrameBoldDuotone,
1155
1158
  linear: VideoFrameLinear,
1156
1159
  })
1160
+ export const VideoFramePlayHorizontal = createIcon({
1161
+ bold: VideoFramePlayHorizontalBoldWeight,
1162
+ boldDuotone: VideoFramePlayHorizontalBoldDuotone,
1163
+ linear: VideoFramePlayHorizontalLinear,
1164
+ })
1157
1165
  export const Widget2 = createIcon({
1158
1166
  bold: Widget2BoldWeight,
1159
1167
  boldDuotone: Widget2BoldDuotone,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+
5
+ import { describe, expect, test } from "vitest"
6
+ import { formatBytes, formatClockDuration } from "./index.ts"
7
+
8
+ describe("formatBytes", () => {
9
+ test("returns empty for undefined", () => {
10
+ expect(formatBytes(undefined)).toBe("")
11
+ })
12
+
13
+ test("clamps zero, negative and non-finite to 0 B", () => {
14
+ expect(formatBytes(0)).toBe("0 B")
15
+ expect(formatBytes(-1)).toBe("0 B")
16
+ expect(formatBytes(Number.NaN)).toBe("0 B")
17
+ expect(formatBytes(Number.POSITIVE_INFINITY)).toBe("0 B")
18
+ })
19
+
20
+ test("formats kibibytes with JEDEC labels", () => {
21
+ expect(formatBytes(1024)).toBe("1 KB")
22
+ expect(formatBytes(1536)).toBe("1.5 KB")
23
+ })
24
+
25
+ test("formats megabytes and gigabytes", () => {
26
+ expect(formatBytes(1024 ** 2)).toBe("1 MB")
27
+ expect(formatBytes(1024 ** 3)).toBe("1 GB")
28
+ })
29
+ })
30
+
31
+ describe("formatClockDuration", () => {
32
+ test("formats sub-hour as m:ss", () => {
33
+ expect(formatClockDuration(0)).toBe("0:00")
34
+ expect(formatClockDuration(125_000)).toBe("2:05")
35
+ })
36
+
37
+ test("formats past the hour as h:mm:ss", () => {
38
+ expect(formatClockDuration(3_661_000)).toBe("1:01:01")
39
+ })
40
+
41
+ test("returns empty for negative or non-finite input", () => {
42
+ expect(formatClockDuration(-1)).toBe("")
43
+ expect(formatClockDuration(Number.NaN)).toBe("")
44
+ })
45
+ })
@@ -0,0 +1,36 @@
1
+ import prettyBytes from "pretty-bytes"
2
+
3
+ /**
4
+ * Format a non-negative byte count as a short human-readable string
5
+ * via {@link prettyBytes} with binary conversion (base 1024) and JEDEC
6
+ * labels (`4.5 KB`, `1.2 MB`, `930 GB`) — IEC `KiB`/`MiB`/`GiB` mapped
7
+ * to `KB`/`MB`/`GB`. Matches how Windows and most disk tools report
8
+ * sizes, without switching to decimal (base 1000) units.
9
+ *
10
+ * Returns the empty string for `undefined` so callers can splat the value
11
+ * into a template without conditional checks. Negative or non-finite
12
+ * inputs are clamped to `0 B`.
13
+ */
14
+ export function formatBytes(bytes: number | undefined): string {
15
+ if (bytes === undefined) return ""
16
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
17
+ return prettyBytes(bytes, { binary: true }).replaceAll("iB", "B")
18
+ }
19
+
20
+ /**
21
+ * Format a duration in milliseconds as a media clock — `m:ss`, or
22
+ * `h:mm:ss` past the hour. Used by the plugin card templates
23
+ * (`{{duration(...)}}`) and the card's inline audio player, so both
24
+ * read the same way.
25
+ */
26
+ export function formatClockDuration(ms: number): string {
27
+ if (!Number.isFinite(ms) || ms < 0) return ""
28
+ const totalSeconds = Math.floor(ms / 1000)
29
+ const hours = Math.floor(totalSeconds / 3600)
30
+ const minutes = Math.floor((totalSeconds % 3600) / 60)
31
+ const seconds = totalSeconds % 60
32
+ if (hours > 0) {
33
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
34
+ }
35
+ return `${minutes}:${String(seconds).padStart(2, "0")}`
36
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+
5
+ import { describe, expect, test } from "vitest"
6
+ import { normalizeSolarGlyphName, parseIconRef } from "./index.ts"
7
+
8
+ const buildAssetUrl = (pluginId: string, path: string) =>
9
+ `/api/plugins/${pluginId}/${path}`
10
+
11
+ describe("normalizeSolarGlyphName", () => {
12
+ test("passes kebab through", () => {
13
+ expect(normalizeSolarGlyphName("video-frame")).toBe("video-frame")
14
+ })
15
+
16
+ test("converts PascalCase to kebab", () => {
17
+ expect(normalizeSolarGlyphName("Heart")).toBe("heart")
18
+ expect(normalizeSolarGlyphName("VideoFrame")).toBe("video-frame")
19
+ })
20
+
21
+ test("maps legacy whitelist aliases", () => {
22
+ expect(normalizeSolarGlyphName("Image")).toBe("gallery")
23
+ expect(normalizeSolarGlyphName("FileText")).toBe("file-text")
24
+ expect(normalizeSolarGlyphName("Film")).toBe("video-frame")
25
+ })
26
+
27
+ test("returns undefined for empty, whitespace and prefixed input", () => {
28
+ expect(normalizeSolarGlyphName("")).toBeUndefined()
29
+ expect(normalizeSolarGlyphName(" ")).toBeUndefined()
30
+ expect(normalizeSolarGlyphName("icon:Heart")).toBeUndefined()
31
+ })
32
+ })
33
+
34
+ describe("parseIconRef", () => {
35
+ test("plain name → normalized Solar glyph name", () => {
36
+ expect(parseIconRef("Heart", "pid", buildAssetUrl)).toEqual({
37
+ kind: "icon",
38
+ name: "heart",
39
+ })
40
+ })
41
+
42
+ test("trims whitespace", () => {
43
+ expect(parseIconRef(" Heart ", "pid", buildAssetUrl)).toEqual({
44
+ kind: "icon",
45
+ name: "heart",
46
+ })
47
+ })
48
+
49
+ test("legacy whitelist names normalize to their Solar glyph", () => {
50
+ expect(parseIconRef("Image", "pid", buildAssetUrl)).toEqual({
51
+ kind: "icon",
52
+ name: "gallery",
53
+ })
54
+ expect(parseIconRef("FileText", "pid", buildAssetUrl)).toEqual({
55
+ kind: "icon",
56
+ name: "file-text",
57
+ })
58
+ })
59
+
60
+ test("the name: prefix is rejected outright", () => {
61
+ expect(parseIconRef("icon:Heart", "pid", buildAssetUrl)).toBeUndefined()
62
+ })
63
+
64
+ test("parses a relative asset path through the URL builder", () => {
65
+ expect(parseIconRef("icons/heart.gif", "pid", buildAssetUrl)).toEqual({
66
+ kind: "asset",
67
+ url: "/api/plugins/pid/icons/heart.gif",
68
+ })
69
+ })
70
+
71
+ test("strips leading ./ from asset path", () => {
72
+ expect(parseIconRef("./icons/heart.gif", "pid", buildAssetUrl)).toEqual({
73
+ kind: "asset",
74
+ url: "/api/plugins/pid/icons/heart.gif",
75
+ })
76
+ })
77
+
78
+ test("returns undefined for empty and dot asset paths", () => {
79
+ expect(parseIconRef("./", "pid", buildAssetUrl)).toBeUndefined()
80
+ expect(parseIconRef("", "pid", buildAssetUrl)).toBeUndefined()
81
+ expect(parseIconRef(" ", "pid", buildAssetUrl)).toBeUndefined()
82
+ })
83
+
84
+ test("returns undefined for .. asset paths", () => {
85
+ expect(parseIconRef("../x.svg", "pid", buildAssetUrl)).toBeUndefined()
86
+ expect(parseIconRef("icons/../x.svg", "pid", buildAssetUrl)).toBeUndefined()
87
+ })
88
+
89
+ test("returns undefined for http/https/data schemes", () => {
90
+ expect(
91
+ parseIconRef("http://example.com/icon.png", "pid", buildAssetUrl),
92
+ ).toBeUndefined()
93
+ expect(
94
+ parseIconRef("https://example.com/icon.png", "pid", buildAssetUrl),
95
+ ).toBeUndefined()
96
+ expect(
97
+ parseIconRef("data:image/png;base64,abc", "pid", buildAssetUrl),
98
+ ).toBeUndefined()
99
+ })
100
+ })