@moewolf/moe-icons-cli 0.0.1 → 0.0.2
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/README.md +243 -9
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +538 -3395
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
|
@@ -2,13 +2,44 @@ import { catalog, findCatalogIcon } from "../catalog/catalog.js";
|
|
|
2
2
|
import { reportLibraryExportNameCollisions, reportProxyNameCollisions, toLibraryExportName, toProxyName, } from "../core/icon-names.js";
|
|
3
3
|
import { selectBitmapVariantAssets } from "../core/bitmap-assets.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { posix } from "node:path";
|
|
5
6
|
import { decodeUtf8 } from "../project/tar-gz.js";
|
|
7
|
+
import { prepareSvg, rootPaintAttributes } from "./svg-model.js";
|
|
6
8
|
import { CN_HELPER_SOURCE } from "./cn.js";
|
|
9
|
+
import { resolveIconTheme, themeHasIcon } from "../core/icon-selection.js";
|
|
7
10
|
import { bitmapWrapperImportName, reactBitmapWrapperSource, resolveThemes, svgInternalImportName, vueBitmapWrapperSource, } from "./theme-resolve.js";
|
|
8
11
|
export { toLibraryExportName, toProxyName };
|
|
9
12
|
/** @deprecated Use toProxyName; kept so existing generator tests keep compiling. */
|
|
10
13
|
export const toPascalCase = toProxyName;
|
|
11
14
|
const OWNER_HEADER = "// Generated by moeicons CLI. Do not edit manually; re-run `moeicons generate`.";
|
|
15
|
+
/**
|
|
16
|
+
* Vanilla project top-level index re-exports each selected style group as a
|
|
17
|
+
* namespace (`export * as MoeOutline from './moe-outline'`), mirroring the
|
|
18
|
+
* code-library layer. Two style groups must therefore never map to the same
|
|
19
|
+
* namespace identifier, and the namespace must not shadow the reserved
|
|
20
|
+
* runtime/types exports.
|
|
21
|
+
*/
|
|
22
|
+
function reportVanillaNamespaceCollisions(config) {
|
|
23
|
+
const reserved = new Set(["createMoeiconsRuntime", "Theme", "VanillaIconOptions", "default"]);
|
|
24
|
+
const seen = new Map();
|
|
25
|
+
const problems = [];
|
|
26
|
+
const styleGroups = [...new Set(Object.values(config.themes).map((entry) => entry.styleGroup))];
|
|
27
|
+
for (const styleGroup of styleGroups) {
|
|
28
|
+
const ns = toProxyName(styleGroup);
|
|
29
|
+
if (reserved.has(ns)) {
|
|
30
|
+
problems.push(`vanilla style group "${styleGroup}" maps to reserved export "${ns}"`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const owner = seen.get(ns);
|
|
34
|
+
if (owner) {
|
|
35
|
+
problems.push(`vanilla style groups "${owner}" and "${styleGroup}" both map to namespace "${ns}"`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
seen.set(ns, styleGroup);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return problems;
|
|
42
|
+
}
|
|
12
43
|
function themeClassExpr(themes) {
|
|
13
44
|
const entries = themes
|
|
14
45
|
.map((theme) => {
|
|
@@ -18,7 +49,23 @@ function themeClassExpr(themes) {
|
|
|
18
49
|
.join("\n");
|
|
19
50
|
return `const themeClassName: Partial<Record<Theme, string | undefined>> = {\n${entries}\n};\n`;
|
|
20
51
|
}
|
|
21
|
-
function
|
|
52
|
+
function themeDefaultsExpr(themes) {
|
|
53
|
+
return `const themeDefaultSize: Record<Theme, number> = {\n${themes.map((theme) => ` ${JSON.stringify(theme.theme)}: ${theme.entry.defaultSize ?? 24},`).join("\n")}\n};\n` +
|
|
54
|
+
`const themeStrokeWidth: Partial<Record<Theme, number>> = {\n${themes.map((theme) => ` ${JSON.stringify(theme.theme)}: ${theme.entry.strokeWidth === undefined ? "undefined" : theme.entry.strokeWidth},`).join("\n")}\n};\n`;
|
|
55
|
+
}
|
|
56
|
+
function installedIconImport(outputDir, target, group, iconId) {
|
|
57
|
+
const file = `${toProxyName(iconId)}${target === "vue" ? ".vue" : ""}.js`;
|
|
58
|
+
const relative = posix.relative(outputDir.replace(/\/$/, ""), `.moeicons/artifact/${target}/${group}/${file}`);
|
|
59
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
60
|
+
}
|
|
61
|
+
function effectiveThemeForIcon(iconId, logical, themes, config, sourceCatalog) {
|
|
62
|
+
const selected = resolveIconTheme(config, sourceCatalog, logical.theme, iconId);
|
|
63
|
+
const actual = themes.find((theme) => theme.theme === selected);
|
|
64
|
+
if (!actual)
|
|
65
|
+
throw new Error(`icon "${iconId}" has no selected variant for theme "${logical.theme}"`);
|
|
66
|
+
return actual;
|
|
67
|
+
}
|
|
68
|
+
function appendReactFiles(files, rel, config, themes, sourceCatalog) {
|
|
22
69
|
files.push({
|
|
23
70
|
path: rel("cn.ts"),
|
|
24
71
|
content: CN_HELPER_SOURCE,
|
|
@@ -26,12 +73,24 @@ function appendReactFiles(files, rel, config, themes) {
|
|
|
26
73
|
files.push({
|
|
27
74
|
path: rel("types.ts"),
|
|
28
75
|
content: `${OWNER_HEADER}
|
|
29
|
-
|
|
76
|
+
import type * as React from "react";
|
|
77
|
+
export interface IconProps extends React.AriaAttributes, React.DOMAttributes<Element> {
|
|
30
78
|
className?: string;
|
|
79
|
+
style?: React.CSSProperties;
|
|
31
80
|
size?: number;
|
|
81
|
+
width?: number | string;
|
|
82
|
+
height?: number | string;
|
|
32
83
|
strokeWidth?: number;
|
|
33
|
-
|
|
34
|
-
|
|
84
|
+
color?: string;
|
|
85
|
+
fill?: string;
|
|
86
|
+
stroke?: string;
|
|
87
|
+
focusable?: boolean | "auto" | "true" | "false";
|
|
88
|
+
alt?: string;
|
|
89
|
+
title?: string;
|
|
90
|
+
role?: React.AriaRole;
|
|
91
|
+
draggable?: boolean;
|
|
92
|
+
id?: string;
|
|
93
|
+
tabIndex?: number;
|
|
35
94
|
}
|
|
36
95
|
export type Theme =
|
|
37
96
|
${Object.keys(config.themes)
|
|
@@ -39,97 +98,95 @@ ${Object.keys(config.themes)
|
|
|
39
98
|
.join("\n")};
|
|
40
99
|
`,
|
|
41
100
|
});
|
|
42
|
-
|
|
101
|
+
// Each public icon proxy imports only its own theme variants. A generated
|
|
102
|
+
// global registry would eagerly import every configured icon.
|
|
43
103
|
for (const resolved of themes) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
importLines.push(`import { ${wrapperName} } from "${wrapperPath}";`);
|
|
55
|
-
files.push({
|
|
56
|
-
path: rel(`wrappers/${wrapperName}.tsx`),
|
|
57
|
-
content: `${OWNER_HEADER}
|
|
58
|
-
${reactBitmapWrapperSource(iconId, wrapperName, resolved.variant, resolved.entry.className)}`,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
104
|
+
for (const iconId of config.icons) {
|
|
105
|
+
const actual = effectiveThemeForIcon(iconId, resolved, themes, config, sourceCatalog);
|
|
106
|
+
if (actual.kind !== "bitmap" || !actual.variant)
|
|
107
|
+
continue;
|
|
108
|
+
const wrapperName = bitmapWrapperImportName(resolved.theme, iconId);
|
|
109
|
+
files.push({
|
|
110
|
+
path: rel(`wrappers/${wrapperName}.tsx`),
|
|
111
|
+
content: `${OWNER_HEADER}
|
|
112
|
+
${reactBitmapWrapperSource(iconId, wrapperName, actual.variant, resolved.entry.className)}`,
|
|
113
|
+
});
|
|
61
114
|
}
|
|
62
115
|
}
|
|
63
|
-
files.push({
|
|
64
|
-
path: rel("registry.ts"),
|
|
65
|
-
content: `${OWNER_HEADER}
|
|
66
|
-
import type { IconProps, Theme } from "./types";
|
|
67
|
-
import type * as React from "react";
|
|
68
|
-
${importLines.join("\n")}
|
|
69
|
-
|
|
70
|
-
export type IconId =
|
|
71
|
-
${config.icons.map((id) => ` | "${id}"`).join("\n")};
|
|
72
|
-
|
|
73
|
-
export interface ThemeRegistry {
|
|
74
|
-
${config.icons.map((id) => ` readonly ${toPascalCase(id)}: React.ComponentType<IconProps>;`).join("\n")}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const registry: Record<Theme, ThemeRegistry> = {
|
|
78
|
-
${themes
|
|
79
|
-
.map((resolved) => {
|
|
80
|
-
const entries = config.icons
|
|
81
|
-
.map((id) => {
|
|
82
|
-
const component = resolved.kind === "bitmap"
|
|
83
|
-
? bitmapWrapperImportName(resolved.theme, id)
|
|
84
|
-
: svgInternalImportName(resolved.theme, resolved.entry.styleGroup, id);
|
|
85
|
-
return ` ${toPascalCase(id)}: ${component},`;
|
|
86
|
-
})
|
|
87
|
-
.join("\n");
|
|
88
|
-
return ` "${resolved.theme}": {\n${entries}\n },`;
|
|
89
|
-
})
|
|
90
|
-
.join("\n")}
|
|
91
|
-
};
|
|
92
|
-
`,
|
|
93
|
-
});
|
|
94
116
|
files.push({
|
|
95
117
|
path: rel("provider.tsx"),
|
|
96
118
|
content: `${OWNER_HEADER}
|
|
97
|
-
import { createContext, useContext, useState, type PropsWithChildren } from "react";
|
|
119
|
+
import { createContext, useCallback, useContext, useMemo, useState, type PropsWithChildren } from "react";
|
|
98
120
|
import type { Theme } from "./types";
|
|
99
121
|
|
|
100
122
|
interface MoeiconsThemeState { readonly theme: Theme; readonly setTheme: (theme: Theme) => void; }
|
|
101
123
|
const MoeiconsThemeContext = createContext<MoeiconsThemeState | undefined>(undefined);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
const fallbackState: MoeiconsThemeState = { theme: ${JSON.stringify(config.defaultTheme)}, setTheme: () => {} };
|
|
125
|
+
export function MoeiconsProvider(props: PropsWithChildren<{ theme?: Theme; defaultTheme?: Theme; onThemeChange?: (theme: Theme) => void }>) {
|
|
126
|
+
const [localTheme, setLocalTheme] = useState<Theme>(props.defaultTheme ?? ${JSON.stringify(config.defaultTheme)});
|
|
127
|
+
const theme = props.theme ?? localTheme;
|
|
128
|
+
const setTheme = useCallback((next: Theme) => {
|
|
129
|
+
if (props.theme === undefined) setLocalTheme(next);
|
|
130
|
+
props.onThemeChange?.(next);
|
|
131
|
+
}, [props.theme, props.onThemeChange]);
|
|
132
|
+
const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
|
|
133
|
+
return <MoeiconsThemeContext.Provider value={value}>{props.children}</MoeiconsThemeContext.Provider>;
|
|
105
134
|
}
|
|
106
135
|
export function useMoeiconsTheme(): MoeiconsThemeState {
|
|
107
|
-
|
|
108
|
-
if (!state) throw new Error("useMoeiconsTheme must be used inside <MoeiconsProvider>");
|
|
109
|
-
return state;
|
|
136
|
+
return useContext(MoeiconsThemeContext) ?? fallbackState;
|
|
110
137
|
}
|
|
111
138
|
`,
|
|
112
139
|
});
|
|
113
140
|
for (const iconId of config.icons) {
|
|
114
141
|
const pascal = toPascalCase(iconId);
|
|
142
|
+
const ownImports = themes.map((theme) => {
|
|
143
|
+
const actual = effectiveThemeForIcon(iconId, theme, themes, config, sourceCatalog);
|
|
144
|
+
if (actual.kind === "bitmap") {
|
|
145
|
+
const symbol = bitmapWrapperImportName(theme.theme, iconId);
|
|
146
|
+
return `import { ${symbol} } from "../wrappers/${symbol}";`;
|
|
147
|
+
}
|
|
148
|
+
const symbol = svgInternalImportName(theme.theme, actual.entry.styleGroup, iconId);
|
|
149
|
+
return `import ${symbol} from "${installedIconImport(posix.join(config.outputDir, "icons"), "react", actual.entry.styleGroup, iconId)}";`;
|
|
150
|
+
}).join("\n");
|
|
151
|
+
const renderBranches = themes.map((theme) => {
|
|
152
|
+
const actual = effectiveThemeForIcon(iconId, theme, themes, config, sourceCatalog);
|
|
153
|
+
const symbol = actual.kind === "bitmap"
|
|
154
|
+
? bitmapWrapperImportName(theme.theme, iconId)
|
|
155
|
+
: svgInternalImportName(theme.theme, actual.entry.styleGroup, iconId);
|
|
156
|
+
const refType = actual.kind === "bitmap" ? "HTMLImageElement" : "SVGSVGElement";
|
|
157
|
+
return ` case ${JSON.stringify(theme.theme)}:
|
|
158
|
+
return <${symbol} ref={ref as React.Ref<${refType}>} width={resolvedSize} height={resolvedSize} strokeWidth={strokeWidth ?? themeStrokeWidth[${JSON.stringify(theme.theme)}]} aria-label={rest["aria-label"] ?? alt} {...rest} className={cn("moe-icon", themeClassName[${JSON.stringify(theme.theme)}], className)} />;`;
|
|
159
|
+
}).join("\n");
|
|
160
|
+
const defaultTheme = themes.find((theme) => theme.theme === config.defaultTheme);
|
|
161
|
+
const defaultActual = effectiveThemeForIcon(iconId, defaultTheme, themes, config, sourceCatalog);
|
|
162
|
+
const defaultSymbol = defaultActual.kind === "bitmap"
|
|
163
|
+
? bitmapWrapperImportName(defaultTheme.theme, iconId)
|
|
164
|
+
: svgInternalImportName(defaultTheme.theme, defaultActual.entry.styleGroup, iconId);
|
|
165
|
+
const defaultRefType = defaultActual.kind === "bitmap" ? "HTMLImageElement" : "SVGSVGElement";
|
|
166
|
+
const singleTheme = themes.length === 1;
|
|
115
167
|
files.push({
|
|
116
168
|
path: rel(`icons/${pascal}.tsx`),
|
|
117
169
|
content: `${OWNER_HEADER}
|
|
118
170
|
import type { IconProps } from "../types";
|
|
119
|
-
import {
|
|
120
|
-
import { useMoeiconsTheme } from "../provider";
|
|
171
|
+
${singleTheme ? "" : 'import { useMoeiconsTheme } from "../provider";'}
|
|
121
172
|
import { cn } from "../cn";
|
|
122
|
-
import type { Theme } from "../types";
|
|
123
|
-
|
|
124
|
-
${
|
|
173
|
+
${singleTheme ? "" : 'import type { Theme } from "../types";'}
|
|
174
|
+
import * as React from "react";
|
|
175
|
+
${ownImports}
|
|
125
176
|
|
|
126
|
-
|
|
127
|
-
|
|
177
|
+
${singleTheme ? "" : themeClassExpr(themes)}
|
|
178
|
+
${singleTheme ? "" : themeDefaultsExpr(themes)}
|
|
179
|
+
export const ${pascal} = /* @__PURE__ */ React.forwardRef<SVGSVGElement | HTMLImageElement, IconProps>((props, ref) => {
|
|
180
|
+
const { className, size, strokeWidth, alt, ...rest } = props;
|
|
181
|
+
${singleTheme ? ` return <${defaultSymbol} ref={ref as React.Ref<${defaultRefType}>} width={size ?? ${defaultTheme.entry.defaultSize ?? 24}} height={size ?? ${defaultTheme.entry.defaultSize ?? 24}} strokeWidth={strokeWidth ?? ${defaultTheme.entry.strokeWidth === undefined ? "undefined" : defaultTheme.entry.strokeWidth}} aria-label={rest["aria-label"] ?? alt} {...rest} className={cn("moe-icon", ${JSON.stringify(defaultTheme.entry.className ?? "")}, className)} />;` : `
|
|
128
182
|
const { theme } = useMoeiconsTheme();
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
183
|
+
const resolvedSize = size ?? themeDefaultSize[theme] ?? 24;
|
|
184
|
+
switch (theme) {
|
|
185
|
+
${renderBranches}
|
|
186
|
+
default:
|
|
187
|
+
return <${defaultSymbol} ref={ref as React.Ref<${defaultRefType}>} width={resolvedSize} height={resolvedSize} strokeWidth={strokeWidth ?? themeStrokeWidth[${JSON.stringify(config.defaultTheme)}]} aria-label={rest["aria-label"] ?? alt} {...rest} className={cn("moe-icon", themeClassName[${JSON.stringify(config.defaultTheme)}], className)} />;
|
|
188
|
+
}`}
|
|
189
|
+
});
|
|
133
190
|
`,
|
|
134
191
|
});
|
|
135
192
|
}
|
|
@@ -143,7 +200,7 @@ ${config.icons.map((id) => `export { ${toPascalCase(id)} } from "./icons/${toPas
|
|
|
143
200
|
`,
|
|
144
201
|
});
|
|
145
202
|
}
|
|
146
|
-
function appendVueFiles(files, rel, config, themes) {
|
|
203
|
+
function appendVueFiles(files, rel, config, themes, sourceCatalog) {
|
|
147
204
|
files.push({
|
|
148
205
|
path: rel("cn.ts"),
|
|
149
206
|
content: CN_HELPER_SOURCE,
|
|
@@ -151,12 +208,22 @@ function appendVueFiles(files, rel, config, themes) {
|
|
|
151
208
|
files.push({
|
|
152
209
|
path: rel("types.ts"),
|
|
153
210
|
content: `${OWNER_HEADER}
|
|
154
|
-
|
|
155
|
-
|
|
211
|
+
import type { SVGAttributes } from "vue";
|
|
212
|
+
export interface IconProps extends Omit<SVGAttributes, "class" | "focusable" | "draggable"> {
|
|
213
|
+
class?: import("clsx").ClassValue;
|
|
156
214
|
size?: number;
|
|
215
|
+
width?: number | string;
|
|
216
|
+
height?: number | string;
|
|
157
217
|
strokeWidth?: number;
|
|
218
|
+
color?: string;
|
|
219
|
+
fill?: string;
|
|
220
|
+
stroke?: string;
|
|
221
|
+
focusable?: boolean | "auto" | "true" | "false";
|
|
158
222
|
"aria-label"?: string;
|
|
159
|
-
|
|
223
|
+
alt?: string;
|
|
224
|
+
title?: string;
|
|
225
|
+
"aria-hidden"?: boolean | "true" | "false";
|
|
226
|
+
draggable?: boolean;
|
|
160
227
|
}
|
|
161
228
|
|
|
162
229
|
export type Theme =
|
|
@@ -179,73 +246,39 @@ export interface MoeiconsThemeState {
|
|
|
179
246
|
export const MOEICONS_THEME_KEY: InjectionKey<MoeiconsThemeState> = Symbol("moeicons-theme");
|
|
180
247
|
`,
|
|
181
248
|
});
|
|
182
|
-
const importLines = [];
|
|
183
249
|
for (const resolved of themes) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
files.push({
|
|
195
|
-
path: rel(`wrappers/${wrapperName}.ts`),
|
|
196
|
-
content: `${OWNER_HEADER}
|
|
197
|
-
${vueBitmapWrapperSource(iconId, wrapperName, resolved.variant, resolved.entry.className)}`,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
250
|
+
for (const iconId of config.icons) {
|
|
251
|
+
const actual = effectiveThemeForIcon(iconId, resolved, themes, config, sourceCatalog);
|
|
252
|
+
if (actual.kind !== "bitmap" || !actual.variant)
|
|
253
|
+
continue;
|
|
254
|
+
const wrapperName = bitmapWrapperImportName(resolved.theme, iconId);
|
|
255
|
+
files.push({
|
|
256
|
+
path: rel(`wrappers/${wrapperName}.ts`),
|
|
257
|
+
content: `${OWNER_HEADER}
|
|
258
|
+
${vueBitmapWrapperSource(iconId, wrapperName, actual.variant, resolved.entry.className)}`,
|
|
259
|
+
});
|
|
200
260
|
}
|
|
201
261
|
}
|
|
202
|
-
files.push({
|
|
203
|
-
path: rel("registry.ts"),
|
|
204
|
-
content: `${OWNER_HEADER}
|
|
205
|
-
import type { Component } from "vue";
|
|
206
|
-
import type { Theme } from "./types";
|
|
207
|
-
${importLines.join("\n")}
|
|
208
|
-
|
|
209
|
-
export type IconId =
|
|
210
|
-
${config.icons.map((id) => ` | "${id}"`).join("\n")};
|
|
211
|
-
|
|
212
|
-
export interface ThemeRegistry {
|
|
213
|
-
${config.icons.map((id) => ` readonly ${toPascalCase(id)}: Component;`).join("\n")}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export const registry: Record<Theme, ThemeRegistry> = {
|
|
217
|
-
${themes
|
|
218
|
-
.map((resolved) => {
|
|
219
|
-
const entries = config.icons
|
|
220
|
-
.map((id) => {
|
|
221
|
-
const component = resolved.kind === "bitmap"
|
|
222
|
-
? bitmapWrapperImportName(resolved.theme, id)
|
|
223
|
-
: svgInternalImportName(resolved.theme, resolved.entry.styleGroup, id);
|
|
224
|
-
return ` ${toPascalCase(id)}: ${component},`;
|
|
225
|
-
})
|
|
226
|
-
.join("\n");
|
|
227
|
-
return ` "${resolved.theme}": {\n${entries}\n },`;
|
|
228
|
-
})
|
|
229
|
-
.join("\n")}
|
|
230
|
-
};
|
|
231
|
-
`,
|
|
232
|
-
});
|
|
233
262
|
files.push({
|
|
234
263
|
path: rel("provider.ts"),
|
|
235
264
|
content: `${OWNER_HEADER}
|
|
236
|
-
import { defineComponent, provide, ref, type PropType } from "vue";
|
|
265
|
+
import { computed, defineComponent, provide, ref, type PropType } from "vue";
|
|
237
266
|
import type { Theme } from "./types";
|
|
238
267
|
import { MOEICONS_THEME_KEY, type MoeiconsThemeState } from "./theme";
|
|
239
268
|
|
|
240
269
|
export const MoeiconsProvider = defineComponent({
|
|
241
270
|
name: "MoeiconsProvider",
|
|
242
271
|
props: {
|
|
243
|
-
theme: { type: String as PropType<Theme>, default:
|
|
272
|
+
theme: { type: String as PropType<Theme>, default: undefined },
|
|
273
|
+
defaultTheme: { type: String as PropType<Theme>, default: ${JSON.stringify(config.defaultTheme)} as Theme },
|
|
244
274
|
},
|
|
245
|
-
|
|
246
|
-
|
|
275
|
+
emits: ["update:theme"],
|
|
276
|
+
setup(props, { slots, emit }) {
|
|
277
|
+
const localTheme = ref<Theme>(props.defaultTheme);
|
|
278
|
+
const theme = computed<Theme>(() => props.theme ?? localTheme.value);
|
|
247
279
|
const setTheme = (next: Theme) => {
|
|
248
|
-
theme.value = next;
|
|
280
|
+
if (props.theme === undefined) localTheme.value = next;
|
|
281
|
+
emit("update:theme", next);
|
|
249
282
|
};
|
|
250
283
|
const state: MoeiconsThemeState = { theme, setTheme };
|
|
251
284
|
provide(MOEICONS_THEME_KEY, state);
|
|
@@ -257,46 +290,115 @@ export const MoeiconsProvider = defineComponent({
|
|
|
257
290
|
files.push({
|
|
258
291
|
path: rel("composable.ts"),
|
|
259
292
|
content: `${OWNER_HEADER}
|
|
260
|
-
import { inject } from "vue";
|
|
293
|
+
import { inject, ref } from "vue";
|
|
261
294
|
import { MOEICONS_THEME_KEY } from "./theme";
|
|
262
295
|
import type { MoeiconsThemeState } from "./theme";
|
|
296
|
+
import type { Theme } from "./types";
|
|
263
297
|
|
|
264
298
|
export function useMoeiconsTheme(): MoeiconsThemeState {
|
|
265
|
-
const state = inject(MOEICONS_THEME_KEY);
|
|
266
|
-
|
|
267
|
-
throw new Error("useMoeiconsTheme must be used inside <MoeiconsProvider>");
|
|
268
|
-
}
|
|
269
|
-
return state;
|
|
299
|
+
const state = inject(MOEICONS_THEME_KEY, undefined);
|
|
300
|
+
return state ?? { theme: ref<Theme>(${JSON.stringify(config.defaultTheme)}), setTheme: () => {} };
|
|
270
301
|
}
|
|
271
302
|
`,
|
|
272
303
|
});
|
|
273
304
|
for (const iconId of config.icons) {
|
|
274
305
|
const pascal = toPascalCase(iconId);
|
|
306
|
+
const ownImports = themes.map((theme) => {
|
|
307
|
+
const actual = effectiveThemeForIcon(iconId, theme, themes, config, sourceCatalog);
|
|
308
|
+
if (actual.kind === "bitmap") {
|
|
309
|
+
const symbol = bitmapWrapperImportName(theme.theme, iconId);
|
|
310
|
+
return `import { ${symbol} } from "../wrappers/${symbol}";`;
|
|
311
|
+
}
|
|
312
|
+
const symbol = svgInternalImportName(theme.theme, actual.entry.styleGroup, iconId);
|
|
313
|
+
return `import ${symbol} from "${installedIconImport(posix.join(config.outputDir, "icons"), "vue", actual.entry.styleGroup, iconId)}";`;
|
|
314
|
+
}).join("\n");
|
|
315
|
+
const ownVariants = themes.map((theme) => {
|
|
316
|
+
const actual = effectiveThemeForIcon(iconId, theme, themes, config, sourceCatalog);
|
|
317
|
+
const symbol = actual.kind === "bitmap"
|
|
318
|
+
? bitmapWrapperImportName(theme.theme, iconId)
|
|
319
|
+
: svgInternalImportName(theme.theme, actual.entry.styleGroup, iconId);
|
|
320
|
+
return ` ${JSON.stringify(theme.theme)}: ${symbol},`;
|
|
321
|
+
}).join("\n");
|
|
322
|
+
const singleTheme = themes.length === 1;
|
|
323
|
+
const onlyTheme = themes[0];
|
|
324
|
+
const onlyActual = effectiveThemeForIcon(iconId, onlyTheme, themes, config, sourceCatalog);
|
|
325
|
+
const onlySymbol = onlyActual.kind === "bitmap"
|
|
326
|
+
? bitmapWrapperImportName(onlyTheme.theme, iconId)
|
|
327
|
+
: svgInternalImportName(onlyTheme.theme, onlyActual.entry.styleGroup, iconId);
|
|
275
328
|
files.push({
|
|
276
329
|
path: rel(`icons/${pascal}.ts`),
|
|
277
330
|
content: `${OWNER_HEADER}
|
|
278
|
-
import { computed, defineComponent, h, inject } from "vue";
|
|
279
|
-
import { MOEICONS_THEME_KEY, type MoeiconsThemeState } from "../theme";
|
|
280
|
-
import { registry } from "../registry";
|
|
331
|
+
import { ${singleTheme ? "defineComponent, h" : "computed, defineComponent, h, inject, type Component"} } from "vue";
|
|
332
|
+
${singleTheme ? "" : 'import { MOEICONS_THEME_KEY, type MoeiconsThemeState } from "../theme";'}
|
|
281
333
|
import { cn } from "../cn";
|
|
282
|
-
import type { Theme } from "../types";
|
|
334
|
+
${singleTheme ? "" : 'import type { Theme } from "../types";'}
|
|
335
|
+
${ownImports}
|
|
283
336
|
|
|
284
|
-
${themeClassExpr(themes)}
|
|
337
|
+
${singleTheme ? "" : themeClassExpr(themes)}
|
|
338
|
+
${singleTheme ? "" : themeDefaultsExpr(themes)}
|
|
339
|
+
${singleTheme ? "" : `
|
|
340
|
+
const variants: Record<Theme, Component> = {
|
|
341
|
+
${ownVariants}
|
|
342
|
+
};`}
|
|
285
343
|
|
|
286
|
-
export const ${pascal} = defineComponent({
|
|
344
|
+
export const ${pascal} = /* @__PURE__ */ defineComponent({
|
|
287
345
|
name: "${pascal}",
|
|
288
|
-
|
|
346
|
+
inheritAttrs: false,
|
|
347
|
+
props: {
|
|
348
|
+
size: Number,
|
|
349
|
+
width: [Number, String],
|
|
350
|
+
height: [Number, String],
|
|
351
|
+
strokeWidth: Number,
|
|
352
|
+
color: String,
|
|
353
|
+
fill: String,
|
|
354
|
+
stroke: String,
|
|
355
|
+
focusable: { type: [Boolean, String], default: undefined },
|
|
356
|
+
alt: String,
|
|
357
|
+
title: String,
|
|
358
|
+
draggable: { type: Boolean, default: undefined },
|
|
359
|
+
},
|
|
360
|
+
setup(props, { attrs }) {
|
|
361
|
+
${singleTheme ? "" : `
|
|
289
362
|
const state = inject<MoeiconsThemeState | undefined>(MOEICONS_THEME_KEY, undefined);
|
|
290
|
-
const component = computed(() =>
|
|
363
|
+
const component = computed(() => variants[(state?.theme.value ?? ${JSON.stringify(config.defaultTheme)}) as Theme] ?? variants[${JSON.stringify(config.defaultTheme)}]);
|
|
364
|
+
`}
|
|
291
365
|
return () => {
|
|
292
|
-
const className =
|
|
293
|
-
const size =
|
|
294
|
-
|
|
366
|
+
const className = attrs.class as Parameters<typeof cn>[number];
|
|
367
|
+
const size = props.size;
|
|
368
|
+
${singleTheme ? `
|
|
369
|
+
const resolvedSize = size ?? ${onlyTheme.entry.defaultSize ?? 24};
|
|
370
|
+
return h(${onlySymbol}, {
|
|
371
|
+
...attrs,
|
|
372
|
+
...(attrs["aria-label"] === undefined && props.alt === undefined ? {} : { "aria-label": attrs["aria-label"] ?? props.alt }),
|
|
373
|
+
width: props.width ?? resolvedSize,
|
|
374
|
+
height: props.height ?? resolvedSize,
|
|
375
|
+
strokeWidth: props.strokeWidth ?? ${onlyTheme.entry.strokeWidth === undefined ? "undefined" : onlyTheme.entry.strokeWidth},
|
|
376
|
+
...(props.color === undefined ? {} : { color: props.color }),
|
|
377
|
+
...(props.fill === undefined ? {} : { fill: props.fill }),
|
|
378
|
+
...(props.stroke === undefined ? {} : { stroke: props.stroke }),
|
|
379
|
+
...(props.focusable === undefined ? {} : { focusable: props.focusable }),
|
|
380
|
+
...(props.alt === undefined ? {} : { alt: props.alt }),
|
|
381
|
+
...(props.title === undefined ? {} : { title: props.title }),
|
|
382
|
+
...(props.draggable === undefined ? {} : { draggable: props.draggable }),
|
|
383
|
+
class: cn("moe-icon", ${JSON.stringify(onlyTheme.entry.className ?? "")}, className),
|
|
384
|
+
});` : `
|
|
295
385
|
const theme = (state?.theme.value ?? ${JSON.stringify(config.defaultTheme)}) as Theme;
|
|
386
|
+
const resolvedSize = size ?? themeDefaultSize[theme] ?? 24;
|
|
296
387
|
return h(component.value, {
|
|
297
388
|
...attrs,
|
|
298
|
-
|
|
299
|
-
|
|
389
|
+
...(attrs["aria-label"] === undefined && props.alt === undefined ? {} : { "aria-label": attrs["aria-label"] ?? props.alt }),
|
|
390
|
+
width: props.width ?? resolvedSize,
|
|
391
|
+
height: props.height ?? resolvedSize,
|
|
392
|
+
strokeWidth: props.strokeWidth ?? themeStrokeWidth[theme],
|
|
393
|
+
...(props.color === undefined ? {} : { color: props.color }),
|
|
394
|
+
...(props.fill === undefined ? {} : { fill: props.fill }),
|
|
395
|
+
...(props.stroke === undefined ? {} : { stroke: props.stroke }),
|
|
396
|
+
...(props.focusable === undefined ? {} : { focusable: props.focusable }),
|
|
397
|
+
...(props.alt === undefined ? {} : { alt: props.alt }),
|
|
398
|
+
...(props.title === undefined ? {} : { title: props.title }),
|
|
399
|
+
...(props.draggable === undefined ? {} : { draggable: props.draggable }),
|
|
400
|
+
class: cn("moe-icon", themeClassName[theme], className),
|
|
401
|
+
});`}
|
|
300
402
|
};
|
|
301
403
|
},
|
|
302
404
|
});
|
|
@@ -317,55 +419,17 @@ ${config.icons
|
|
|
317
419
|
`,
|
|
318
420
|
});
|
|
319
421
|
}
|
|
320
|
-
function parseSvgNodes(source) {
|
|
321
|
-
const root = { children: [] };
|
|
322
|
-
const stack = [root];
|
|
323
|
-
const svg = source.match(/<svg\b[^>]*>/i);
|
|
324
|
-
if (!svg)
|
|
325
|
-
throw new Error("SVG root element is missing");
|
|
326
|
-
const viewBox = svg[0].match(/\bviewBox\s*=\s*["']([^"']+)["']/i)?.[1] ?? "0 0 24 24";
|
|
327
|
-
const inner = source.slice((svg.index ?? 0) + svg[0].length).replace(/<\/svg>\s*$/i, "");
|
|
328
|
-
const tokenRe = /<!--[\s\S]*?-->|<[^>]+>|[^<]+/g;
|
|
329
|
-
for (const match of inner.matchAll(tokenRe)) {
|
|
330
|
-
const token = match[0];
|
|
331
|
-
if (token.startsWith("<!--") || !token.trim())
|
|
332
|
-
continue;
|
|
333
|
-
if (token.startsWith("</")) {
|
|
334
|
-
if (stack.length > 1)
|
|
335
|
-
stack.pop();
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (!token.startsWith("<")) {
|
|
339
|
-
stack[stack.length - 1]?.children.push({ text: token });
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
const selfClosing = /\/\s*>$/.test(token);
|
|
343
|
-
const body = token.slice(1, token.length - (selfClosing ? 2 : 1)).trim();
|
|
344
|
-
const name = body.match(/^[A-Za-z][\w:.-]*/)?.[0];
|
|
345
|
-
if (!name)
|
|
346
|
-
continue;
|
|
347
|
-
const attrs = [];
|
|
348
|
-
body.slice(name.length).replace(/([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g, (_full, attr, doubleValue, singleValue, bareValue) => {
|
|
349
|
-
attrs.push([attr, doubleValue ?? singleValue ?? bareValue ?? ""]);
|
|
350
|
-
return "";
|
|
351
|
-
});
|
|
352
|
-
const node = { name, attrs, children: [] };
|
|
353
|
-
stack[stack.length - 1]?.children.push(node);
|
|
354
|
-
if (!selfClosing)
|
|
355
|
-
stack.push(node);
|
|
356
|
-
}
|
|
357
|
-
return { viewBox, children: root.children };
|
|
358
|
-
}
|
|
359
422
|
function vanillaFactorySource(name, source, strategy) {
|
|
360
|
-
const parsed =
|
|
361
|
-
const paint =
|
|
362
|
-
const graphic = new Set(["path", "circle", "ellipse", "line", "polyline", "polygon", "rect", "text", "use", "image"]);
|
|
423
|
+
const parsed = prepareSvg(source, strategy);
|
|
424
|
+
const paint = rootPaintAttributes(parsed.rootAttrs, strategy);
|
|
363
425
|
const lines = [
|
|
364
426
|
"const SVG_NS = 'http://www.w3.org/2000/svg';", "",
|
|
365
427
|
"export interface VanillaIconOptions extends Record<string, string | number | undefined> { className?: string; strokeWidth?: number; }", "",
|
|
366
428
|
`export function create${name}(options: VanillaIconOptions = {}): SVGElement {`,
|
|
367
429
|
" const svg = document.createElementNS(SVG_NS, 'svg');",
|
|
368
430
|
` svg.setAttribute('viewBox', ${JSON.stringify(parsed.viewBox)});`,
|
|
431
|
+
" svg.setAttribute('width', '24');",
|
|
432
|
+
" svg.setAttribute('height', '24');",
|
|
369
433
|
" svg.setAttribute('class', options.className ? `moe-icon ${options.className}` : 'moe-icon');",
|
|
370
434
|
];
|
|
371
435
|
for (const [attr, value] of paint)
|
|
@@ -381,15 +445,9 @@ function vanillaFactorySource(name, source, strategy) {
|
|
|
381
445
|
}
|
|
382
446
|
const variable = `node${counter++}`;
|
|
383
447
|
lines.push(` const ${variable} = document.createElementNS(SVG_NS, ${JSON.stringify(node.name)});`);
|
|
384
|
-
for (const [attr, value] of node.attrs)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (strategy === "outline" && !node.attrs.some(([attr]) => attr.toLowerCase() === "stroke"))
|
|
388
|
-
lines.push(` ${variable}.setAttribute('stroke', 'currentColor');`);
|
|
389
|
-
if (strategy === "outline" && !node.attrs.some(([attr]) => attr.toLowerCase() === "fill"))
|
|
390
|
-
lines.push(` ${variable}.setAttribute('fill', 'none');`);
|
|
391
|
-
if (strategy === "solid" && !node.attrs.some(([attr]) => attr.toLowerCase() === "fill"))
|
|
392
|
-
lines.push(` ${variable}.setAttribute('fill', 'currentColor');`);
|
|
448
|
+
for (const [attr, value] of node.attrs) {
|
|
449
|
+
const namespace = attr.startsWith("xlink:") ? "http://www.w3.org/1999/xlink" : attr.startsWith("xml:") ? "http://www.w3.org/XML/1998/namespace" : undefined;
|
|
450
|
+
lines.push(namespace ? ` ${variable}.setAttributeNS(${JSON.stringify(namespace)}, ${JSON.stringify(attr)}, ${JSON.stringify(value)});` : ` ${variable}.setAttribute(${JSON.stringify(attr)}, ${JSON.stringify(value)});`);
|
|
393
451
|
}
|
|
394
452
|
emit(node.children, variable);
|
|
395
453
|
lines.push(` ${parent}.appendChild(${variable});`);
|
|
@@ -399,6 +457,119 @@ function vanillaFactorySource(name, source, strategy) {
|
|
|
399
457
|
lines.push(" return svg;", "}", "", `export default create${name};`, "");
|
|
400
458
|
return lines.join("\n");
|
|
401
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* CLI project-layer Vanilla runtime (E2E-C4). Underlying code-library factories
|
|
462
|
+
* stay theme-less; this runtime maps logical themes → style groups and updates
|
|
463
|
+
* already-mounted DOM nodes on setTheme.
|
|
464
|
+
*/
|
|
465
|
+
function vanillaRuntimeSource(config, themes, sourceCatalog) {
|
|
466
|
+
const defaultTheme = config.defaultTheme;
|
|
467
|
+
const imports = [];
|
|
468
|
+
const factoryEntries = [];
|
|
469
|
+
for (const resolved of themes) {
|
|
470
|
+
const theme = resolved.theme;
|
|
471
|
+
const iconEntries = [];
|
|
472
|
+
for (const iconId of config.icons) {
|
|
473
|
+
const group = effectiveThemeForIcon(iconId, resolved, themes, config, sourceCatalog).entry.styleGroup;
|
|
474
|
+
const pascal = toPascalCase(iconId);
|
|
475
|
+
const alias = `create${pascal}_${theme.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
476
|
+
imports.push(`import { create${pascal} as ${alias} } from "./${group}/${pascal}";`);
|
|
477
|
+
iconEntries.push(` ${JSON.stringify(iconId)}: ${alias}`);
|
|
478
|
+
}
|
|
479
|
+
factoryEntries.push(` ${JSON.stringify(theme)}: {\n${iconEntries.join(",\n")}\n }`);
|
|
480
|
+
}
|
|
481
|
+
return `${imports.join("\n")}
|
|
482
|
+
import type { Theme, VanillaIconOptions } from "./types";
|
|
483
|
+
|
|
484
|
+
const defaultTheme = ${JSON.stringify(defaultTheme)} as Theme;
|
|
485
|
+
const themeDefaults: Record<Theme, VanillaIconOptions> = {
|
|
486
|
+
${themes.map((theme) => ` ${JSON.stringify(theme.theme)}: { width: ${theme.entry.defaultSize ?? 24}, height: ${theme.entry.defaultSize ?? 24}${theme.entry.strokeWidth === undefined ? "" : `, strokeWidth: ${theme.entry.strokeWidth}`}${theme.entry.className === undefined ? "" : `, className: ${JSON.stringify(theme.entry.className)}`} },`).join("\n")}
|
|
487
|
+
};
|
|
488
|
+
const factories: Record<Theme, Record<string, (options?: VanillaIconOptions) => SVGElement>> = {
|
|
489
|
+
${factoryEntries.join(",\n")}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
function resolveTheme(theme: string | undefined): Theme {
|
|
493
|
+
if (theme && theme in factories) return theme as Theme;
|
|
494
|
+
return defaultTheme;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function iconOptions(theme: Theme, options: VanillaIconOptions): VanillaIconOptions {
|
|
498
|
+
return { ...themeDefaults[theme], ...options };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface MoeiconsRuntime {
|
|
502
|
+
readonly getTheme: () => Theme;
|
|
503
|
+
readonly setTheme: (theme: Theme | string) => void;
|
|
504
|
+
readonly mount: (host: Element, iconId: string, options?: VanillaIconOptions) => MoeiconsMount;
|
|
505
|
+
readonly mountIcon: (host: Element, iconId: string, options?: VanillaIconOptions) => SVGElement;
|
|
506
|
+
readonly destroy: () => void;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export interface MoeiconsMount {
|
|
510
|
+
readonly node: SVGElement;
|
|
511
|
+
readonly update: (options: VanillaIconOptions) => void;
|
|
512
|
+
readonly on: (type: string, listener: EventListener) => () => void;
|
|
513
|
+
readonly unmount: () => void;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Project-layer runtime: theme ↔ styleGroup mapping with live DOM updates. */
|
|
517
|
+
export function createMoeiconsRuntime(options: { theme?: Theme | string } = {}): MoeiconsRuntime {
|
|
518
|
+
let theme = resolveTheme(options.theme);
|
|
519
|
+
const mounts = new Set<{ host: Element; iconId: string; options: VanillaIconOptions; node: SVGElement; listeners: Map<string, Set<EventListener>> }>();
|
|
520
|
+
const render = (record: { iconId: string; options: VanillaIconOptions; node: SVGElement; listeners: Map<string, Set<EventListener>> }) => {
|
|
521
|
+
const factory = factories[theme]?.[record.iconId] ?? factories[defaultTheme]?.[record.iconId];
|
|
522
|
+
if (!factory) throw new Error(\`unknown icon "\${record.iconId}"\`);
|
|
523
|
+
const nextNode = factory(iconOptions(theme, record.options));
|
|
524
|
+
for (const [type, listeners] of record.listeners) for (const listener of listeners) nextNode.addEventListener(type, listener);
|
|
525
|
+
record.node.replaceWith(nextNode);
|
|
526
|
+
record.node = nextNode;
|
|
527
|
+
};
|
|
528
|
+
const mount = (host: Element, iconId: string, iconOptions: VanillaIconOptions = {}): MoeiconsMount => {
|
|
529
|
+
const factory = factories[theme]?.[iconId] ?? factories[defaultTheme]?.[iconId];
|
|
530
|
+
if (!factory) throw new Error(\`unknown icon "\${iconId}"\`);
|
|
531
|
+
const record = { host, iconId, options: iconOptions, node: factory({ ...themeDefaults[theme], ...iconOptions }), listeners: new Map<string, Set<EventListener>>() };
|
|
532
|
+
host.appendChild(record.node);
|
|
533
|
+
mounts.add(record);
|
|
534
|
+
return {
|
|
535
|
+
get node() { return record.node; },
|
|
536
|
+
update(next) { if (!mounts.has(record)) return; record.options = next; render(record); },
|
|
537
|
+
on(type, listener) {
|
|
538
|
+
if (!mounts.has(record)) return () => {};
|
|
539
|
+
let listeners = record.listeners.get(type);
|
|
540
|
+
if (!listeners) { listeners = new Set(); record.listeners.set(type, listeners); }
|
|
541
|
+
listeners.add(listener);
|
|
542
|
+
record.node.addEventListener(type, listener);
|
|
543
|
+
return () => { record.listeners.get(type)?.delete(listener); record.node.removeEventListener(type, listener); };
|
|
544
|
+
},
|
|
545
|
+
unmount() { if (!mounts.has(record)) return; record.node.remove(); record.listeners.clear(); mounts.delete(record); },
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const api: MoeiconsRuntime = {
|
|
550
|
+
getTheme: () => theme,
|
|
551
|
+
setTheme(next) {
|
|
552
|
+
const resolved = resolveTheme(next);
|
|
553
|
+
if (resolved === theme) return;
|
|
554
|
+
theme = resolved;
|
|
555
|
+
for (const record of mounts) {
|
|
556
|
+
if (!record.node.isConnected) { mounts.delete(record); continue; }
|
|
557
|
+
render(record);
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
mount,
|
|
561
|
+
mountIcon(host, iconId, iconOptions = {}) {
|
|
562
|
+
return mount(host, iconId, iconOptions).node;
|
|
563
|
+
},
|
|
564
|
+
destroy() {
|
|
565
|
+
for (const mount of mounts) mount.node.remove();
|
|
566
|
+
mounts.clear();
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
return api;
|
|
570
|
+
}
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
402
573
|
function archiveManifest(archiveFiles) {
|
|
403
574
|
const entry = Object.entries(archiveFiles).find(([path]) => /(?:^|\/)assets\/manifest\.json$/.test(path));
|
|
404
575
|
if (!entry)
|
|
@@ -417,12 +588,15 @@ function archiveManifest(archiveFiles) {
|
|
|
417
588
|
function sha256Hex(bytes) {
|
|
418
589
|
return createHash("sha256").update(bytes).digest("hex");
|
|
419
590
|
}
|
|
420
|
-
function selectRawAssets(archiveFiles, themes,
|
|
591
|
+
function selectRawAssets(archiveFiles, themes, config, sourceCatalog) {
|
|
421
592
|
const manifest = archiveManifest(archiveFiles);
|
|
422
593
|
if (!manifest)
|
|
423
594
|
return { ok: false, errors: ["installed artifact assets manifest is missing or invalid"] };
|
|
424
595
|
const base = manifest.path.slice(0, manifest.path.lastIndexOf("/"));
|
|
425
|
-
const wanted = new Set(themes.
|
|
596
|
+
const wanted = new Set(themes.flatMap((theme) => config.icons.flatMap((icon) => {
|
|
597
|
+
const actual = effectiveThemeForIcon(icon, theme, themes, config, sourceCatalog);
|
|
598
|
+
return actual.kind === "svg" ? [`${actual.entry.styleGroup}/${icon}.svg`] : [];
|
|
599
|
+
})));
|
|
426
600
|
const assets = [];
|
|
427
601
|
const errors = [];
|
|
428
602
|
for (const path of wanted) {
|
|
@@ -459,6 +633,9 @@ export function planGeneratedFiles(config, outputDir, options = {}) {
|
|
|
459
633
|
}
|
|
460
634
|
errors.push(...reportProxyNameCollisions(config.icons));
|
|
461
635
|
errors.push(...reportLibraryExportNameCollisions(config.icons));
|
|
636
|
+
if (config.target === "vanilla") {
|
|
637
|
+
errors.push(...reportVanillaNamespaceCollisions(config));
|
|
638
|
+
}
|
|
462
639
|
for (const iconId of config.icons) {
|
|
463
640
|
const catalogIcon = findCatalogIcon(iconId, options.catalog ?? catalog);
|
|
464
641
|
if (!catalogIcon) {
|
|
@@ -466,9 +643,8 @@ export function planGeneratedFiles(config, outputDir, options = {}) {
|
|
|
466
643
|
continue;
|
|
467
644
|
}
|
|
468
645
|
for (const [theme, entry] of Object.entries(config.themes)) {
|
|
469
|
-
if (!
|
|
470
|
-
errors.push(`icon "${iconId}" is not available in style group "${entry.styleGroup}" for theme "${theme}"`);
|
|
471
|
-
}
|
|
646
|
+
if (!resolveIconTheme(config, options.catalog ?? catalog, theme, iconId))
|
|
647
|
+
errors.push(`icon "${iconId}" is not available in style group "${entry.styleGroup}" for theme "${theme}"; no selected variant satisfies missingIconPolicy`);
|
|
472
648
|
}
|
|
473
649
|
}
|
|
474
650
|
const resolved = resolveThemes(config, options.catalog ?? catalog);
|
|
@@ -489,38 +665,58 @@ export function planGeneratedFiles(config, outputDir, options = {}) {
|
|
|
489
665
|
if (options.archiveFiles === undefined) {
|
|
490
666
|
return { ok: false, errors: ["vanilla target requires an installed artifact"] };
|
|
491
667
|
}
|
|
492
|
-
const raw = selectRawAssets(options.archiveFiles, resolved.themes, config.
|
|
668
|
+
const raw = selectRawAssets(options.archiveFiles, resolved.themes, config, options.catalog ?? catalog);
|
|
493
669
|
if (!raw.ok)
|
|
494
670
|
return raw;
|
|
495
671
|
const rawByPath = new Map(raw.assets.map((file) => [file.path.replace(/^assets\//, ""), file.content]));
|
|
672
|
+
const archivePaths = new Set(Object.keys(options.archiveFiles));
|
|
496
673
|
const groups = new Set(resolved.themes.map((theme) => theme.entry.styleGroup));
|
|
497
|
-
files.push({ path: rel("types.ts"), content: "export interface VanillaIconOptions extends Record<string, string | number | undefined> { className?: string; strokeWidth?: number; }\n" });
|
|
674
|
+
files.push({ path: rel("types.ts"), content: "export interface VanillaIconOptions extends Record<string, string | number | undefined> { className?: string; strokeWidth?: number; }\nexport type Theme = " + Object.keys(config.themes).map((t) => JSON.stringify(t)).join(" | ") + ";\n" });
|
|
498
675
|
for (const styleGroup of groups) {
|
|
499
676
|
const exports = ["// Vanilla factory exports"];
|
|
500
|
-
for (const iconId of config.icons) {
|
|
677
|
+
for (const iconId of config.icons.filter((id) => resolved.themes.some((theme) => theme.entry.styleGroup === styleGroup && themeHasIcon(config, options.catalog ?? catalog, theme.theme, id)))) {
|
|
501
678
|
const name = toPascalCase(iconId);
|
|
502
679
|
const source = rawByPath.get(`${styleGroup}/${iconId}.svg`);
|
|
503
680
|
if (!source || typeof source === "string")
|
|
504
681
|
return { ok: false, errors: [`raw asset missing: ${styleGroup}/${iconId}.svg`] };
|
|
505
|
-
const
|
|
506
|
-
|
|
682
|
+
const kind = resolved.themes.find((theme) => theme.entry.styleGroup === styleGroup)?.group.type;
|
|
683
|
+
const strategy = kind === "outline" || kind === "solid" ? kind : "mixed";
|
|
684
|
+
const artifactPath = `.moeicons/artifact/vanilla/${styleGroup}/${name}.js`;
|
|
685
|
+
const archiveHasFactory = archivePaths.has(`vanilla/${styleGroup}/${name}.js`) ||
|
|
686
|
+
archivePaths.has(`${config.tier}/vanilla/${styleGroup}/${name}.js`);
|
|
687
|
+
const imported = posix.relative(posix.join(outputDir, styleGroup), artifactPath);
|
|
688
|
+
const specifier = imported.startsWith(".") ? imported : `./${imported}`;
|
|
689
|
+
files.push({ path: rel(`${styleGroup}/${name}.ts`), content: archiveHasFactory
|
|
690
|
+
? `${OWNER_HEADER}\nexport { create${name}, default } from ${JSON.stringify(specifier)};\n`
|
|
691
|
+
: vanillaFactorySource(name, decodeUtf8(source), strategy) });
|
|
507
692
|
exports.push(`export { default as ${toLibraryExportName(iconId)}, create${name} } from './${name}';`);
|
|
508
693
|
}
|
|
509
694
|
files.push({ path: rel(`${styleGroup}/index.ts`), content: `${exports.join("\n")}\n` });
|
|
510
695
|
}
|
|
511
|
-
files.push({
|
|
696
|
+
files.push({
|
|
697
|
+
path: rel("runtime.ts"),
|
|
698
|
+
content: vanillaRuntimeSource(config, resolved.themes, options.catalog ?? catalog),
|
|
699
|
+
});
|
|
700
|
+
const groupNamespaces = [...groups].map((group) => {
|
|
701
|
+
const ns = toProxyName(group);
|
|
702
|
+
return `export * as ${ns} from './${group}';`;
|
|
703
|
+
});
|
|
704
|
+
files.push({
|
|
705
|
+
path: rel("index.ts"),
|
|
706
|
+
content: `${groupNamespaces.join("\n")}\nexport { createMoeiconsRuntime } from './runtime';\nexport type { Theme, VanillaIconOptions } from './types';\n`,
|
|
707
|
+
});
|
|
512
708
|
}
|
|
513
709
|
else if (target === "vue") {
|
|
514
|
-
appendVueFiles(files, rel, config, resolved.themes);
|
|
710
|
+
appendVueFiles(files, rel, config, resolved.themes, options.catalog ?? catalog);
|
|
515
711
|
}
|
|
516
712
|
else {
|
|
517
|
-
appendReactFiles(files, rel, config, resolved.themes);
|
|
713
|
+
appendReactFiles(files, rel, config, resolved.themes, options.catalog ?? catalog);
|
|
518
714
|
}
|
|
519
715
|
if (target === "assets") {
|
|
520
716
|
if (options.archiveFiles === undefined) {
|
|
521
717
|
return { ok: false, errors: ["assets target requires an installed artifact"] };
|
|
522
718
|
}
|
|
523
|
-
const raw = selectRawAssets(options.archiveFiles, resolved.themes, config.
|
|
719
|
+
const raw = selectRawAssets(options.archiveFiles, resolved.themes, config, options.catalog ?? catalog);
|
|
524
720
|
if (!raw.ok)
|
|
525
721
|
return raw;
|
|
526
722
|
for (const asset of raw.assets)
|
|
@@ -540,7 +736,18 @@ export function planGeneratedFiles(config, outputDir, options = {}) {
|
|
|
540
736
|
],
|
|
541
737
|
};
|
|
542
738
|
}
|
|
543
|
-
|
|
739
|
+
// Exact catalog variants are the v2 evidence. Frozen v1 catalogs did not
|
|
740
|
+
// carry this field and therefore use only the explicit legacy parser. A
|
|
741
|
+
// mixed archive/catalog state is ambiguous and must not be guessed.
|
|
742
|
+
const mediaModes = new Set(bitmapThemes.map((theme) => theme.group.variants && theme.group.variants.length > 0 ? 2 : 1));
|
|
743
|
+
if (mediaModes.size !== 1) {
|
|
744
|
+
return { ok: false, errors: ["bitmap themes mix v1 and v2 catalog contracts"] };
|
|
745
|
+
}
|
|
746
|
+
const mediaContractVersion = [...mediaModes][0];
|
|
747
|
+
const selected = selectBitmapVariantAssets(options.archiveFiles, unique, config.icons, { mediaContractVersion, requestedPairs: resolved.themes.flatMap((theme) => config.icons.flatMap((iconId) => {
|
|
748
|
+
const actual = effectiveThemeForIcon(iconId, theme, resolved.themes, config, options.catalog ?? catalog);
|
|
749
|
+
return actual.variant ? [{ variantId: actual.variant.resourceVariantId, iconId }] : [];
|
|
750
|
+
})) });
|
|
544
751
|
if (!selected.ok)
|
|
545
752
|
return { ok: false, errors: [...selected.errors] };
|
|
546
753
|
for (const asset of selected.assets) {
|