@hoardodile/ui 0.1.6 → 0.1.8
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/dist/chunk-2BVZEYNW.js +97 -0
- package/dist/chunk-2BVZEYNW.js.map +1 -0
- package/dist/chunk-2DTR6CD5.js +47 -0
- package/dist/chunk-2DTR6CD5.js.map +1 -0
- package/dist/chunk-2HWSCY4T.js +20 -0
- package/dist/chunk-2HWSCY4T.js.map +1 -0
- package/dist/chunk-363CCKQ7.js +75 -0
- package/dist/chunk-363CCKQ7.js.map +1 -0
- package/dist/chunk-3ZQJWBVB.js +30 -0
- package/dist/chunk-3ZQJWBVB.js.map +1 -0
- package/dist/chunk-4D36OO5W.js +277 -0
- package/dist/chunk-4D36OO5W.js.map +1 -0
- package/dist/chunk-4UITFLIN.js +31 -0
- package/dist/chunk-4UITFLIN.js.map +1 -0
- package/dist/chunk-6HAIUXNZ.js +11 -0
- package/dist/chunk-6HAIUXNZ.js.map +1 -0
- package/dist/chunk-7JMMFY67.js +62 -0
- package/dist/chunk-7JMMFY67.js.map +1 -0
- package/dist/chunk-7MWWHI6T.js +1149 -0
- package/dist/chunk-7MWWHI6T.js.map +1 -0
- package/dist/chunk-AEHTAUAQ.js +65 -0
- package/dist/chunk-AEHTAUAQ.js.map +1 -0
- package/dist/chunk-ASRGQ5BD.js +130 -0
- package/dist/chunk-ASRGQ5BD.js.map +1 -0
- package/dist/chunk-BFGOE5AI.js +187 -0
- package/dist/chunk-BFGOE5AI.js.map +1 -0
- package/dist/chunk-BRT25IDQ.js +46 -0
- package/dist/chunk-BRT25IDQ.js.map +1 -0
- package/dist/chunk-BU5DQOPM.js +29 -0
- package/dist/chunk-BU5DQOPM.js.map +1 -0
- package/dist/chunk-BWASOT5X.js +266 -0
- package/dist/chunk-BWASOT5X.js.map +1 -0
- package/dist/chunk-F32V4Z7Q.js +26 -0
- package/dist/chunk-F32V4Z7Q.js.map +1 -0
- package/dist/chunk-F7RHDRRJ.js +24 -0
- package/dist/chunk-F7RHDRRJ.js.map +1 -0
- package/dist/chunk-FH5RNCFR.js +21 -0
- package/dist/chunk-FH5RNCFR.js.map +1 -0
- package/dist/chunk-GKSX5ULZ.js +513 -0
- package/dist/chunk-GKSX5ULZ.js.map +1 -0
- package/dist/chunk-JPFDNRCA.js +26 -0
- package/dist/chunk-JPFDNRCA.js.map +1 -0
- package/dist/chunk-KNPRUZ4H.js +13 -0
- package/dist/chunk-KNPRUZ4H.js.map +1 -0
- package/dist/chunk-KOFWEPPB.js +254 -0
- package/dist/chunk-KOFWEPPB.js.map +1 -0
- package/dist/chunk-M5OJN54O.js +10 -0
- package/dist/chunk-M5OJN54O.js.map +1 -0
- package/dist/chunk-OEVALKQ5.js +51 -0
- package/dist/chunk-OEVALKQ5.js.map +1 -0
- package/dist/chunk-OW5FWLPH.js +33 -0
- package/dist/chunk-OW5FWLPH.js.map +1 -0
- package/dist/chunk-POX7TXBH.js +41 -0
- package/dist/chunk-POX7TXBH.js.map +1 -0
- package/dist/chunk-PWJBHUM2.js +58 -0
- package/dist/chunk-PWJBHUM2.js.map +1 -0
- package/dist/chunk-QGMDWGJT.js +23 -0
- package/dist/chunk-QGMDWGJT.js.map +1 -0
- package/dist/chunk-QGTQQNUL.js +36 -0
- package/dist/chunk-QGTQQNUL.js.map +1 -0
- package/dist/chunk-SAY52ZKI.js +152 -0
- package/dist/chunk-SAY52ZKI.js.map +1 -0
- package/dist/chunk-SKDPGSRL.js +166 -0
- package/dist/chunk-SKDPGSRL.js.map +1 -0
- package/dist/chunk-T56ARCUK.js +38 -0
- package/dist/chunk-T56ARCUK.js.map +1 -0
- package/dist/chunk-TDDHQZ62.js +227 -0
- package/dist/chunk-TDDHQZ62.js.map +1 -0
- package/dist/chunk-TMQ5OMBK.js +29 -0
- package/dist/chunk-TMQ5OMBK.js.map +1 -0
- package/dist/chunk-UEVND7AS.js +23 -0
- package/dist/chunk-UEVND7AS.js.map +1 -0
- package/dist/chunk-UPWHCG4S.js +40 -0
- package/dist/chunk-UPWHCG4S.js.map +1 -0
- package/dist/chunk-VWUNF7VX.js +3 -0
- package/dist/chunk-VWUNF7VX.js.map +1 -0
- package/dist/chunk-WI6O7GZK.js +45 -0
- package/dist/chunk-WI6O7GZK.js.map +1 -0
- package/dist/chunk-WVEK3WCX.js +21 -0
- package/dist/chunk-WVEK3WCX.js.map +1 -0
- package/dist/chunk-XBPQEZES.js +48 -0
- package/dist/chunk-XBPQEZES.js.map +1 -0
- package/dist/chunk-ZYAGKKR7.js +142 -0
- package/dist/chunk-ZYAGKKR7.js.map +1 -0
- package/dist/components/alert.js +2 -21
- package/dist/components/alert.js.map +1 -1
- package/dist/components/app-dialog.js +10 -654
- package/dist/components/app-dialog.js.map +1 -1
- package/dist/components/badge.js +2 -23
- package/dist/components/badge.js.map +1 -1
- package/dist/components/breadcrumb.js +5 -501
- package/dist/components/breadcrumb.js.map +1 -1
- package/dist/components/button.js +3 -76
- package/dist/components/button.js.map +1 -1
- package/dist/components/caption-bar.js +7 -518
- package/dist/components/caption-bar.js.map +1 -1
- package/dist/components/checkbox.js +4 -74
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip-button.js +4 -85
- package/dist/components/chip-button.js.map +1 -1
- package/dist/components/close-confirm-dialog.js +12 -662
- package/dist/components/close-confirm-dialog.js.map +1 -1
- package/dist/components/color-picker.js +7 -530
- package/dist/components/color-picker.js.map +1 -1
- package/dist/components/combobox.js +15 -934
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-by-typing-dialog.js +12 -669
- package/dist/components/confirm-by-typing-dialog.js.map +1 -1
- package/dist/components/confirm-dialog.js +11 -637
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/count-badge.js +2 -21
- package/dist/components/count-badge.js.map +1 -1
- package/dist/components/dialog.js +8 -569
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/dropdown-menu.js +9 -1004
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/dropdown-select.js +9 -880
- package/dist/components/dropdown-select.js.map +1 -1
- package/dist/components/empty.js +2 -21
- package/dist/components/empty.js.map +1 -1
- package/dist/components/filter-rail.js +7 -145
- package/dist/components/filter-rail.js.map +1 -1
- package/dist/components/fixed-action-bar.js +2 -21
- package/dist/components/fixed-action-bar.js.map +1 -1
- package/dist/components/font-picker.js +11 -586
- package/dist/components/font-picker.js.map +1 -1
- package/dist/components/form.js +9 -66
- package/dist/components/form.js.map +1 -1
- package/dist/components/icon-tile.js +3 -43
- package/dist/components/icon-tile.js.map +1 -1
- package/dist/components/icon-toggle.js +3 -43
- package/dist/components/icon-toggle.js.map +1 -1
- package/dist/components/icon.js +3 -47
- package/dist/components/icon.js.map +1 -1
- package/dist/components/image-crop-panel.js +19 -772
- package/dist/components/image-crop-panel.js.map +1 -1
- package/dist/components/image-cropper.js +1 -164
- package/dist/components/image-cropper.js.map +1 -1
- package/dist/components/input-group.js +6 -268
- package/dist/components/input-group.js.map +1 -1
- package/dist/components/input.js +3 -58
- package/dist/components/input.js.map +1 -1
- package/dist/components/label.js +3 -38
- package/dist/components/label.js.map +1 -1
- package/dist/components/list-empty-row.js +2 -21
- package/dist/components/list-empty-row.js.map +1 -1
- package/dist/components/management-empty.js +2 -21
- package/dist/components/management-empty.js.map +1 -1
- package/dist/components/management-skeleton.js +4 -59
- package/dist/components/management-skeleton.js.map +1 -1
- package/dist/components/meta-chip.js +2 -21
- package/dist/components/meta-chip.js.map +1 -1
- package/dist/components/mobile-drawer.js +7 -268
- package/dist/components/mobile-drawer.js.map +1 -1
- package/dist/components/page-header.js +3 -42
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/page-scaffold.js +5 -76
- package/dist/components/page-scaffold.js.map +1 -1
- package/dist/components/pagination-bar.js +13 -614
- package/dist/components/pagination-bar.js.map +1 -1
- package/dist/components/pagination.js +6 -681
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/panel-toolbar.js +11 -713
- package/dist/components/panel-toolbar.js.map +1 -1
- package/dist/components/pill-tabs.js +2 -21
- package/dist/components/pill-tabs.js.map +1 -1
- package/dist/components/plugin-download-consent.js +13 -663
- package/dist/components/plugin-download-consent.js.map +1 -1
- package/dist/components/popover.js +5 -242
- package/dist/components/popover.js.map +1 -1
- package/dist/components/preview-card.js +2 -21
- package/dist/components/preview-card.js.map +1 -1
- package/dist/components/progress.js +2 -20
- package/dist/components/progress.js.map +1 -1
- package/dist/components/radio-group.js +2 -21
- package/dist/components/radio-group.js.map +1 -1
- package/dist/components/scroll-area.js +2 -21
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/search-field.js +6 -587
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/secondary-stat.js +3 -40
- package/dist/components/secondary-stat.js.map +1 -1
- package/dist/components/section-header.js +3 -43
- package/dist/components/section-header.js.map +1 -1
- package/dist/components/section-label.js +3 -44
- package/dist/components/section-label.js.map +1 -1
- package/dist/components/section-tabs.js +2 -21
- package/dist/components/section-tabs.js.map +1 -1
- package/dist/components/separator.js +3 -47
- package/dist/components/separator.js.map +1 -1
- package/dist/components/sheet.js +8 -467
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/sidebar.js +24 -1044
- package/dist/components/sidebar.js.map +1 -1
- package/dist/components/skeleton.js +3 -39
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +2 -21
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sortable-chip-list.js +2 -21
- package/dist/components/sortable-chip-list.js.map +1 -1
- package/dist/components/special-tag-surface.js +3 -531
- package/dist/components/special-tag-surface.js.map +1 -1
- package/dist/components/spinner.js +3 -63
- package/dist/components/spinner.js.map +1 -1
- package/dist/components/surface.js +3 -49
- package/dist/components/surface.js.map +1 -1
- package/dist/components/switch.js +3 -51
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.js +2 -20
- package/dist/components/table.js.map +1 -1
- package/dist/components/tag-chip.js +6 -617
- package/dist/components/tag-chip.js.map +1 -1
- package/dist/components/textarea.js +3 -56
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toast.js +8 -806
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle.js +2 -20
- package/dist/components/toggle.js.map +1 -1
- package/dist/components/tooltip.js +3 -80
- package/dist/components/tooltip.js.map +1 -1
- package/dist/hooks/use-dialog-footer-actions.js +1 -9
- package/dist/hooks/use-dialog-footer-actions.js.map +1 -1
- package/dist/hooks/use-form-field.js +1 -34
- package/dist/hooks/use-form-field.js.map +1 -1
- package/dist/hooks/use-mobile.js +2 -27
- package/dist/hooks/use-mobile.js.map +1 -1
- package/dist/hooks/useMobileBackToClose.js +3 -232
- package/dist/hooks/useMobileBackToClose.js.map +1 -1
- package/dist/icons/actions.js +4 -521
- package/dist/icons/actions.js.map +1 -1
- package/dist/icons/icon-style.js +1 -49
- package/dist/icons/icon-style.js.map +1 -1
- package/dist/icons/marks.js +1 -46
- package/dist/icons/marks.js.map +1 -1
- package/dist/icons/registry.d.ts +2 -1
- package/dist/icons/registry.js +2 -1186
- package/dist/icons/registry.js.map +1 -1
- package/dist/index.js +10 -651
- package/dist/index.js.map +1 -1
- package/dist/lib/colors.js +1 -63
- package/dist/lib/colors.js.map +1 -1
- package/dist/lib/merge-config.js +1 -18
- package/dist/lib/merge-config.js.map +1 -1
- package/dist/lib/pagination.js +1 -24
- package/dist/lib/pagination.js.map +1 -1
- package/dist/lib/tag-surface.js +5 -613
- package/dist/lib/tag-surface.js.map +1 -1
- package/dist/lib/utils.js +2 -24
- package/dist/lib/utils.js.map +1 -1
- package/dist/res-card-template/format.d.ts +21 -0
- package/dist/res-card-template/format.js +3 -0
- package/dist/res-card-template/format.js.map +1 -0
- package/dist/res-card-template/icon.d.ts +38 -0
- package/dist/res-card-template/icon.js +3 -0
- package/dist/res-card-template/icon.js.map +1 -0
- package/dist/res-card-template/index.d.ts +4 -0
- package/dist/res-card-template/index.js +5 -0
- package/dist/res-card-template/index.js.map +1 -0
- package/dist/res-card-template/template.d.ts +48 -0
- package/dist/res-card-template/template.js +5 -0
- package/dist/res-card-template/template.js.map +1 -0
- package/dist/viewport.js +1 -11
- package/dist/viewport.js.map +1 -1
- package/package.json +9 -2
- package/src/components/image-crop-panel.dist.test.tsx +54 -0
- package/src/icons/registry.ts +8 -0
- package/src/res-card-template/format.test.ts +45 -0
- package/src/res-card-template/format.ts +36 -0
- package/src/res-card-template/icon.test.ts +100 -0
- package/src/res-card-template/icon.ts +86 -0
- package/src/res-card-template/index.ts +22 -0
- package/src/res-card-template/template.test.ts +457 -0
- package/src/res-card-template/template.ts +429 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A resolved template-icon reference: a Solar glyph name, or an
|
|
5
|
+
* asset image URL derived from a manifest-relative path.
|
|
6
|
+
*/
|
|
7
|
+
export type IconRef =
|
|
8
|
+
| { readonly kind: "icon"; readonly name: string }
|
|
9
|
+
| { readonly kind: "asset"; readonly url: string }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Legacy whitelist name → Solar kebab glyph. Derived names are handled by
|
|
13
|
+
* the PascalCase conversion; only these diverge.
|
|
14
|
+
*/
|
|
15
|
+
const LEGACY_ICON_ALIASES: Readonly<Record<string, string>> = {
|
|
16
|
+
Files: "file",
|
|
17
|
+
Film: "video-frame",
|
|
18
|
+
Image: "gallery",
|
|
19
|
+
Info: "info-circle",
|
|
20
|
+
Music: "music-notes",
|
|
21
|
+
Search: "magnifier",
|
|
22
|
+
Sparkle: "star",
|
|
23
|
+
Video: "video-frame",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a raw icon name to Solar kebab-case, or `undefined` when the
|
|
28
|
+
* string cannot name a glyph at all (schemes, separators, punctuation).
|
|
29
|
+
* This is syntax only — membership in the Solar glyph index is left to
|
|
30
|
+
* the renderer that consumes the name.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeSolarGlyphName(raw: string): string | undefined {
|
|
33
|
+
const trimmed = raw.trim()
|
|
34
|
+
if (trimmed.length === 0) return undefined
|
|
35
|
+
const aliased = LEGACY_ICON_ALIASES[trimmed]
|
|
36
|
+
if (aliased !== undefined) return aliased
|
|
37
|
+
if (/^[a-z][a-z0-9-]*$/.test(trimmed)) return trimmed
|
|
38
|
+
if (/^[A-Za-z][A-Za-z0-9]*$/.test(trimmed)) {
|
|
39
|
+
return trimmed.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
40
|
+
}
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse a manifest-level icon string into a render-ready ref.
|
|
46
|
+
*
|
|
47
|
+
* `<SolarGlyph>` — Solar glyph name (manifest/template icons are
|
|
48
|
+
* Solar-only; PascalCase and the legacy whitelist
|
|
49
|
+
* names normalize to the kebab glyph)
|
|
50
|
+
* `<relative/path>` — resolved through `buildAssetUrl(pluginId, path)`
|
|
51
|
+
* (leading `./` stripped)
|
|
52
|
+
*
|
|
53
|
+
* Empty inputs, schemes (`http(s)`, `data:`), `..`-shaped paths and any
|
|
54
|
+
* string that cannot name a glyph return `undefined`. The renderer treats
|
|
55
|
+
* that as "nothing" — icon resolution never throws.
|
|
56
|
+
*/
|
|
57
|
+
export function parseIconRef(
|
|
58
|
+
raw: string,
|
|
59
|
+
pluginId: string,
|
|
60
|
+
buildAssetUrl: (pluginId: string, path: string) => string,
|
|
61
|
+
): IconRef | undefined {
|
|
62
|
+
const trimmed = raw.trim()
|
|
63
|
+
if (trimmed.length === 0) return undefined
|
|
64
|
+
if (
|
|
65
|
+
trimmed.startsWith("http://") ||
|
|
66
|
+
trimmed.startsWith("https://") ||
|
|
67
|
+
trimmed.startsWith("data:")
|
|
68
|
+
) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
trimmed.includes(".") ||
|
|
73
|
+
trimmed.includes("/") ||
|
|
74
|
+
trimmed.includes("\\")
|
|
75
|
+
) {
|
|
76
|
+
const rel = trimmed.replace(/^\.[\\/]/, "")
|
|
77
|
+
if (rel.length === 0 || rel.split("/").includes("..")) return undefined
|
|
78
|
+
return { kind: "asset", url: buildAssetUrl(pluginId, rel) }
|
|
79
|
+
}
|
|
80
|
+
const name = normalizeSolarGlyphName(trimmed)
|
|
81
|
+
if (name === undefined) return undefined
|
|
82
|
+
return { kind: "icon", name }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** How a parsed {@link IconRef} is drawn inside a rendered badge/template. */
|
|
86
|
+
export type RenderIcon = (ref: IconRef, className?: string) => ReactNode
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Res-card template renderer, shared by the host app and the offline
|
|
3
|
+
* workbench. Evaluates the plugin manifest's `{{...}}` template strings
|
|
4
|
+
* (`ui.card.*`, `ui.search.*`, `ui.message.anchor`) over a resource's
|
|
5
|
+
* hook snapshot, returning ReactNode (so icon-producing directives can
|
|
6
|
+
* inject components inline). Icon drawing and asset-URL building are
|
|
7
|
+
* injected by the caller, so this module never imports host-specific
|
|
8
|
+
* code — see the grammar in `@hoardodile/sdk-types/template`.
|
|
9
|
+
*/
|
|
10
|
+
export { formatBytes, formatClockDuration } from "./format.ts"
|
|
11
|
+
export {
|
|
12
|
+
type IconRef,
|
|
13
|
+
normalizeSolarGlyphName,
|
|
14
|
+
parseIconRef,
|
|
15
|
+
type RenderIcon,
|
|
16
|
+
} from "./icon.ts"
|
|
17
|
+
export {
|
|
18
|
+
renderCardTemplate,
|
|
19
|
+
renderSlotBadges,
|
|
20
|
+
resolveLocaleString,
|
|
21
|
+
type TemplateContext,
|
|
22
|
+
} from "./template.ts"
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createElement } from "react"
|
|
6
|
+
import { describe, expect, test } from "vitest"
|
|
7
|
+
import { type IconRef, normalizeSolarGlyphName } from "./icon.ts"
|
|
8
|
+
import {
|
|
9
|
+
type RenderIcon,
|
|
10
|
+
renderCardTemplate,
|
|
11
|
+
renderSlotBadges,
|
|
12
|
+
resolveLocaleString,
|
|
13
|
+
type TemplateContext,
|
|
14
|
+
} from "./index.ts"
|
|
15
|
+
|
|
16
|
+
// A deterministic in-memory icon renderer: a fixed set of "known" Solar
|
|
17
|
+
// glyphs (what the app's renderer considers resolvable) draw a React
|
|
18
|
+
// element (so mixed templates stay arrays), unknown glyphs and
|
|
19
|
+
// unresolvable assets return null. This keeps the renderer tests focused
|
|
20
|
+
// on evaluation, not the icon implementation.
|
|
21
|
+
const KNOWN_GLYPHS = new Set([
|
|
22
|
+
"video-frame",
|
|
23
|
+
"music-notes",
|
|
24
|
+
"gallery",
|
|
25
|
+
"heart",
|
|
26
|
+
"play",
|
|
27
|
+
"file-text",
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
function renderIcon(ref: IconRef): ReturnType<RenderIcon> {
|
|
31
|
+
if (ref.kind === "asset") {
|
|
32
|
+
return createElement("span", null, `asset:${ref.url}`)
|
|
33
|
+
}
|
|
34
|
+
const name = normalizeSolarGlyphName(ref.name)
|
|
35
|
+
return name !== undefined && KNOWN_GLYPHS.has(name)
|
|
36
|
+
? createElement("span", null, `icon:${name}`)
|
|
37
|
+
: null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
41
|
+
return {
|
|
42
|
+
locale: "en",
|
|
43
|
+
pluginId: "test-plugin",
|
|
44
|
+
manifest: {
|
|
45
|
+
i18n: {
|
|
46
|
+
name: { en: "Test Plugin", zh: "测试插件" },
|
|
47
|
+
label: { en: "Label", zh: "标签" },
|
|
48
|
+
},
|
|
49
|
+
ui: {
|
|
50
|
+
search: {
|
|
51
|
+
kinds: [
|
|
52
|
+
{ key: "video", icon: "{{icon('Video')}}" },
|
|
53
|
+
{ key: "audio", icon: "{{icon('Music')}}" },
|
|
54
|
+
{ key: "image", icon: "{{icon('Image')}}" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
iconClassName: "h-4 w-4",
|
|
60
|
+
renderIcon,
|
|
61
|
+
buildAssetUrl: (pluginId, path) => `/assets/${pluginId}/${path}`,
|
|
62
|
+
...overrides,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("renderCardTemplate", () => {
|
|
67
|
+
test("substitutes a simple field", () => {
|
|
68
|
+
const result = renderCardTemplate(
|
|
69
|
+
"{{file.sizeBytes}}",
|
|
70
|
+
{
|
|
71
|
+
file: { sizeBytes: 1024 },
|
|
72
|
+
source: undefined,
|
|
73
|
+
},
|
|
74
|
+
makeCtx(),
|
|
75
|
+
)
|
|
76
|
+
expect(result).toBe("1024")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("applies bytes formatting", () => {
|
|
80
|
+
const result = renderCardTemplate(
|
|
81
|
+
"{{bytes(file.sizeBytes)}}",
|
|
82
|
+
{ file: { sizeBytes: 1536 }, source: undefined },
|
|
83
|
+
makeCtx(),
|
|
84
|
+
)
|
|
85
|
+
expect(result).toBe("1.5 KB")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("applies duration formatting", () => {
|
|
89
|
+
const result = renderCardTemplate(
|
|
90
|
+
"{{duration(source.durationMs)}}",
|
|
91
|
+
{ file: undefined, source: { durationMs: 125_000 } },
|
|
92
|
+
makeCtx(),
|
|
93
|
+
)
|
|
94
|
+
expect(result).toBe("2:05")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("formats long durations with hours", () => {
|
|
98
|
+
const result = renderCardTemplate(
|
|
99
|
+
"{{duration(source.durationMs)}}",
|
|
100
|
+
{ file: undefined, source: { durationMs: 3_661_000 } },
|
|
101
|
+
makeCtx(),
|
|
102
|
+
)
|
|
103
|
+
expect(result).toBe("1:01:01")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("applies number formatting", () => {
|
|
107
|
+
const result = renderCardTemplate(
|
|
108
|
+
"{{number(file.count)}}",
|
|
109
|
+
{ file: { count: 1234 }, source: undefined },
|
|
110
|
+
makeCtx(),
|
|
111
|
+
)
|
|
112
|
+
expect(result).toBe("1,234")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test("reads from source namespace", () => {
|
|
116
|
+
const result = renderCardTemplate(
|
|
117
|
+
"{{source.width}}×{{source.height}}",
|
|
118
|
+
{ file: undefined, source: { width: 1920, height: 1080 } },
|
|
119
|
+
makeCtx(),
|
|
120
|
+
)
|
|
121
|
+
expect(result).toBe("1920×1080")
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("renders empty for missing fields", () => {
|
|
125
|
+
const result = renderCardTemplate(
|
|
126
|
+
"{{source.missing}}",
|
|
127
|
+
{ file: undefined, source: {}, searchMeta: undefined },
|
|
128
|
+
makeCtx(),
|
|
129
|
+
)
|
|
130
|
+
expect(result).toBeNull()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("renders empty when namespace root is undefined", () => {
|
|
134
|
+
const result = renderCardTemplate(
|
|
135
|
+
"{{file.sizeBytes}}",
|
|
136
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
137
|
+
makeCtx(),
|
|
138
|
+
)
|
|
139
|
+
expect(result).toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("leaves plain text untouched", () => {
|
|
143
|
+
const result = renderCardTemplate(
|
|
144
|
+
"hello world",
|
|
145
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
146
|
+
makeCtx(),
|
|
147
|
+
)
|
|
148
|
+
expect(result).toBe("hello world")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("t() resolves i18n key", () => {
|
|
152
|
+
const result = renderCardTemplate(
|
|
153
|
+
"{{t('name')}}",
|
|
154
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
155
|
+
makeCtx(),
|
|
156
|
+
)
|
|
157
|
+
expect(result).toBe("Test Plugin")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("t() falls back to first key when locale missing", () => {
|
|
161
|
+
const result = renderCardTemplate(
|
|
162
|
+
"{{t('name')}}",
|
|
163
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
164
|
+
makeCtx({ locale: "fr" }),
|
|
165
|
+
)
|
|
166
|
+
expect(result).toBe("Test Plugin")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("t() returns empty for missing key", () => {
|
|
170
|
+
const result = renderCardTemplate(
|
|
171
|
+
"{{t('missing')}}",
|
|
172
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
173
|
+
makeCtx(),
|
|
174
|
+
)
|
|
175
|
+
expect(result).toBeNull()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test("kind() renders known kind icon", () => {
|
|
179
|
+
const result = renderCardTemplate(
|
|
180
|
+
"{{kind('video')}}",
|
|
181
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
182
|
+
makeCtx(),
|
|
183
|
+
)
|
|
184
|
+
expect(result).not.toBeNull()
|
|
185
|
+
expect(result).not.toBe("")
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("kind() returns null for unknown kind", () => {
|
|
189
|
+
const result = renderCardTemplate(
|
|
190
|
+
"{{kind('unknown')}}",
|
|
191
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
192
|
+
makeCtx(),
|
|
193
|
+
)
|
|
194
|
+
expect(result).toBeNull()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test("icon() renders known icon", () => {
|
|
198
|
+
const result = renderCardTemplate(
|
|
199
|
+
"{{icon('Heart')}}",
|
|
200
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
201
|
+
makeCtx(),
|
|
202
|
+
)
|
|
203
|
+
expect(result).not.toBeNull()
|
|
204
|
+
expect(result).not.toBe("")
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("icon() returns null for unknown name", () => {
|
|
208
|
+
const result = renderCardTemplate(
|
|
209
|
+
"{{icon('NotInRegistry')}}",
|
|
210
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
211
|
+
makeCtx(),
|
|
212
|
+
)
|
|
213
|
+
expect(result).toBeNull()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test("asset() renders img node", () => {
|
|
217
|
+
const result = renderCardTemplate(
|
|
218
|
+
"{{asset('icons/heart.gif')}}",
|
|
219
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
220
|
+
makeCtx({ pluginId: "my-plugin" }),
|
|
221
|
+
)
|
|
222
|
+
expect(result).not.toBeNull()
|
|
223
|
+
expect(result).not.toBe("")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("mixed text and icon returns array", () => {
|
|
227
|
+
const result = renderCardTemplate(
|
|
228
|
+
"'{{icon('Play')}}",
|
|
229
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
230
|
+
makeCtx(),
|
|
231
|
+
)
|
|
232
|
+
expect(Array.isArray(result)).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test("invalid expression renders empty", () => {
|
|
236
|
+
const result = renderCardTemplate(
|
|
237
|
+
"{{}}",
|
|
238
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
239
|
+
makeCtx(),
|
|
240
|
+
)
|
|
241
|
+
expect(result).toBeNull()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("malformed expression renders empty", () => {
|
|
245
|
+
const result = renderCardTemplate(
|
|
246
|
+
"{{!!!}}",
|
|
247
|
+
{ file: undefined, source: undefined, searchMeta: undefined },
|
|
248
|
+
makeCtx(),
|
|
249
|
+
)
|
|
250
|
+
expect(result).toBeNull()
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe("resolveLocaleString", () => {
|
|
255
|
+
test("returns a plain string as-is", () => {
|
|
256
|
+
expect(resolveLocaleString("hello", "en")).toBe("hello")
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test("picks the exact locale match", () => {
|
|
260
|
+
expect(resolveLocaleString({ en: "hello", zh: "你好" }, "zh")).toBe("你好")
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test("falls back to base language for region codes", () => {
|
|
264
|
+
expect(resolveLocaleString({ en: "hello", zh: "你好" }, "zh-CN")).toBe(
|
|
265
|
+
"你好",
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test("falls back to first available when nothing matches", () => {
|
|
270
|
+
expect(resolveLocaleString({ de: "Hallo" }, "en")).toBe("Hallo")
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe("renderSlotBadges", () => {
|
|
275
|
+
test("renders and filters empty results", () => {
|
|
276
|
+
const result = renderSlotBadges(
|
|
277
|
+
["{{bytes(file.sizeBytes)}}", "{{source.missing}}"],
|
|
278
|
+
{ file: { sizeBytes: 1024 }, source: {}, searchMeta: undefined },
|
|
279
|
+
makeCtx(),
|
|
280
|
+
)
|
|
281
|
+
expect(result).toHaveLength(1)
|
|
282
|
+
expect(result[0]).toBe("1 KB")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test("slot value embeds {{t('key')}}", () => {
|
|
286
|
+
const result = renderSlotBadges(
|
|
287
|
+
["Label: {{t('label')}}, Bytes: {{bytes(file.sizeBytes)}}"],
|
|
288
|
+
{ file: { sizeBytes: 1024 }, source: {}, searchMeta: undefined },
|
|
289
|
+
makeCtx({ locale: "zh" }),
|
|
290
|
+
)
|
|
291
|
+
expect(result).toHaveLength(1)
|
|
292
|
+
expect(result[0]).toBe("Label: 标签, Bytes: 1 KB")
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test("kind() paints without facets; searchKindIcons() does not", () => {
|
|
296
|
+
const scope = {
|
|
297
|
+
file: undefined,
|
|
298
|
+
source: undefined,
|
|
299
|
+
searchMeta: undefined,
|
|
300
|
+
}
|
|
301
|
+
expect(
|
|
302
|
+
renderSlotBadges(["{{kind('video')}}"], scope, makeCtx()),
|
|
303
|
+
).toHaveLength(1)
|
|
304
|
+
expect(
|
|
305
|
+
renderSlotBadges(
|
|
306
|
+
["{{join(' ', searchKindIcons(), number(source.animationCount))}}"],
|
|
307
|
+
scope,
|
|
308
|
+
makeCtx(),
|
|
309
|
+
),
|
|
310
|
+
).toHaveLength(0)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test("searchKindIcons() only includes active facets", () => {
|
|
314
|
+
const result = renderSlotBadges(
|
|
315
|
+
["{{join(' ', searchKindIcons(), number(source.animationCount))}}"],
|
|
316
|
+
{
|
|
317
|
+
file: undefined,
|
|
318
|
+
source: { animationCount: 3 },
|
|
319
|
+
searchMeta: { v: 1, facets: { video: true, audio: false } },
|
|
320
|
+
},
|
|
321
|
+
makeCtx(),
|
|
322
|
+
)
|
|
323
|
+
expect(result).toHaveLength(1)
|
|
324
|
+
expect(result[0]).not.toBe("")
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
describe("condition syntax", () => {
|
|
329
|
+
test("eq compares primitives", () => {
|
|
330
|
+
expect(
|
|
331
|
+
renderCardTemplate(
|
|
332
|
+
"{{if(eq(file.count, 2), 'yes')}}",
|
|
333
|
+
{ file: { count: 2 }, source: {}, searchMeta: undefined },
|
|
334
|
+
makeCtx(),
|
|
335
|
+
),
|
|
336
|
+
).toBe("yes")
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test("eq returns empty when not equal", () => {
|
|
340
|
+
expect(
|
|
341
|
+
renderCardTemplate(
|
|
342
|
+
"{{if(eq(file.count, 2), 'yes')}}",
|
|
343
|
+
{ file: { count: 3 }, source: {}, searchMeta: undefined },
|
|
344
|
+
makeCtx(),
|
|
345
|
+
),
|
|
346
|
+
).toBeNull()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test("gt compares numbers", () => {
|
|
350
|
+
expect(
|
|
351
|
+
renderCardTemplate(
|
|
352
|
+
"{{if(gt(file.count, 1), file.count)}}",
|
|
353
|
+
{ file: { count: 5 }, source: {}, searchMeta: undefined },
|
|
354
|
+
makeCtx(),
|
|
355
|
+
),
|
|
356
|
+
).toBe(5)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test("gt returns empty when not greater", () => {
|
|
360
|
+
expect(
|
|
361
|
+
renderCardTemplate(
|
|
362
|
+
"{{if(gt(file.count, 1), file.count)}}",
|
|
363
|
+
{ file: { count: 1 }, source: {}, searchMeta: undefined },
|
|
364
|
+
makeCtx(),
|
|
365
|
+
),
|
|
366
|
+
).toBeNull()
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test("lt compares numbers", () => {
|
|
370
|
+
expect(
|
|
371
|
+
renderCardTemplate(
|
|
372
|
+
"{{if(lt(file.count, 3), 'small')}}",
|
|
373
|
+
{ file: { count: 2 }, source: {}, searchMeta: undefined },
|
|
374
|
+
makeCtx(),
|
|
375
|
+
),
|
|
376
|
+
).toBe("small")
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test("gte compares numbers", () => {
|
|
380
|
+
expect(
|
|
381
|
+
renderCardTemplate(
|
|
382
|
+
"{{if(gte(file.count, 1), 'ok')}}",
|
|
383
|
+
{ file: { count: 1 }, source: {}, searchMeta: undefined },
|
|
384
|
+
makeCtx(),
|
|
385
|
+
),
|
|
386
|
+
).toBe("ok")
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test("lte compares numbers", () => {
|
|
390
|
+
expect(
|
|
391
|
+
renderCardTemplate(
|
|
392
|
+
"{{if(lte(file.count, 1), 'single')}}",
|
|
393
|
+
{ file: { count: 1 }, source: {}, searchMeta: undefined },
|
|
394
|
+
makeCtx(),
|
|
395
|
+
),
|
|
396
|
+
).toBe("single")
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test("ne compares primitives", () => {
|
|
400
|
+
expect(
|
|
401
|
+
renderCardTemplate(
|
|
402
|
+
"{{if(ne(file.count, 1), 'many')}}",
|
|
403
|
+
{ file: { count: 5 }, source: {}, searchMeta: undefined },
|
|
404
|
+
makeCtx(),
|
|
405
|
+
),
|
|
406
|
+
).toBe("many")
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test("if with else branch", () => {
|
|
410
|
+
expect(
|
|
411
|
+
renderCardTemplate(
|
|
412
|
+
"{{if(gt(file.count, 1), file.count, 'one')}}",
|
|
413
|
+
{ file: { count: 1 }, source: {}, searchMeta: undefined },
|
|
414
|
+
makeCtx(),
|
|
415
|
+
),
|
|
416
|
+
).toBe("one")
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test("number literals as arguments", () => {
|
|
420
|
+
expect(
|
|
421
|
+
renderCardTemplate(
|
|
422
|
+
"{{if(eq(file.count, 1), 'single')}}",
|
|
423
|
+
{ file: { count: 1 }, source: {}, searchMeta: undefined },
|
|
424
|
+
makeCtx(),
|
|
425
|
+
),
|
|
426
|
+
).toBe("single")
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test("nested calls", () => {
|
|
430
|
+
expect(
|
|
431
|
+
renderCardTemplate(
|
|
432
|
+
"{{bytes(if(gt(file.sizeBytes, 1024), file.sizeBytes))}}",
|
|
433
|
+
{ file: { sizeBytes: 2048 }, source: {}, searchMeta: undefined },
|
|
434
|
+
makeCtx(),
|
|
435
|
+
),
|
|
436
|
+
).toBe("2 KB")
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test("hide count when single file", () => {
|
|
440
|
+
const result = renderSlotBadges(
|
|
441
|
+
["{{if(gt(file.count, 1), file.count)}}"],
|
|
442
|
+
{ file: { count: 1 }, source: {}, searchMeta: undefined },
|
|
443
|
+
makeCtx(),
|
|
444
|
+
)
|
|
445
|
+
expect(result).toHaveLength(0)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test("show count when multiple files", () => {
|
|
449
|
+
const result = renderSlotBadges(
|
|
450
|
+
["{{if(gt(file.count, 1), file.count)}}"],
|
|
451
|
+
{ file: { count: 5 }, source: {}, searchMeta: undefined },
|
|
452
|
+
makeCtx(),
|
|
453
|
+
)
|
|
454
|
+
expect(result).toHaveLength(1)
|
|
455
|
+
expect(result[0]).toBe(5)
|
|
456
|
+
})
|
|
457
|
+
})
|