slidemind 0.0.27
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/.claude-plugin/plugin.json +23 -0
- package/CLAUDE.md +137 -0
- package/README.md +100 -0
- package/bin/slidemind.mjs +603 -0
- package/package.json +62 -0
- package/skills/create-slides/SKILL.md +108 -0
- package/src/bridge/build-and-push.mjs +232 -0
- package/src/bridge/bundle.mjs +373 -0
- package/src/bridge/client.mjs +338 -0
- package/src/builders/build.mjs +724 -0
- package/src/builders/lint-slides.mjs +796 -0
- package/src/builders/primitives.json +19 -0
- package/src/builders/resolve-skill.mjs +205 -0
- package/src/builders/runtime-applier.js +275 -0
- package/src/installers/brand-theme.mjs +38 -0
- package/src/installers/claude.mjs +141 -0
- package/src/installers/codex.mjs +229 -0
- package/src/installers/lib.mjs +435 -0
- package/src/installers/opencode.mjs +181 -0
- package/src/installers/tui.mjs +125 -0
- package/src/mcp/recolor.mjs +111 -0
- package/src/mcp/server.mjs +476 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Install slidemind into opencode.
|
|
2
|
+
//
|
|
3
|
+
// opencode auto-discovers user-level skills at
|
|
4
|
+
// `~/.config/opencode/skills/<name>/SKILL.md` (parity with Claude Code's
|
|
5
|
+
// `~/.claude/skills/` layout). We copy each skill there with the
|
|
6
|
+
// `slidemind-` namespace prefix so opencode loads it via SKILL.md
|
|
7
|
+
// frontmatter, exactly like the other harnesses.
|
|
8
|
+
//
|
|
9
|
+
// Layout after install:
|
|
10
|
+
// ~/.config/opencode/skills/slidemind-create-slides/SKILL.md + assets
|
|
11
|
+
//
|
|
12
|
+
// Earlier versions modelled the skill as an opencode "agent" (because
|
|
13
|
+
// opencode didn't have native skills at the time) and stashed the
|
|
14
|
+
// payload at `~/.config/opencode/slidemind/`. We actively remove both
|
|
15
|
+
// on every install so upgraders converge to the correct layout.
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import {
|
|
20
|
+
expandHome,
|
|
21
|
+
installDir,
|
|
22
|
+
isForce,
|
|
23
|
+
isLinkMode,
|
|
24
|
+
listSkills,
|
|
25
|
+
rmIfExists,
|
|
26
|
+
ui,
|
|
27
|
+
} from "./lib.mjs";
|
|
28
|
+
|
|
29
|
+
const NAMESPACE = "slidemind";
|
|
30
|
+
|
|
31
|
+
function opencodeRoot() {
|
|
32
|
+
return expandHome("~/.config/opencode");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function skillsRoot() {
|
|
36
|
+
return path.join(opencodeRoot(), "skills");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function skillTarget(skillName) {
|
|
40
|
+
return path.join(skillsRoot(), `${NAMESPACE}-${skillName}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Pre-skills layout. Removed on every install/uninstall.
|
|
44
|
+
function legacySkillsRoot() {
|
|
45
|
+
return path.join(opencodeRoot(), NAMESPACE);
|
|
46
|
+
}
|
|
47
|
+
function legacyAgentDir() {
|
|
48
|
+
return path.join(opencodeRoot(), "agent");
|
|
49
|
+
}
|
|
50
|
+
function legacyCommandDir() {
|
|
51
|
+
return path.join(opencodeRoot(), "command");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function opencodeConfigFile() {
|
|
55
|
+
return path.join(opencodeRoot(), "opencode.json");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadOpencodeConfig() {
|
|
59
|
+
const file = opencodeConfigFile();
|
|
60
|
+
if (!fs.existsSync(file)) return {};
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) || {};
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`opencode config at ${file} is not valid JSON; refusing to overwrite.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function saveOpencodeConfig(cfg) {
|
|
69
|
+
const file = opencodeConfigFile();
|
|
70
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
71
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function unregisterMcpServer() {
|
|
75
|
+
const file = opencodeConfigFile();
|
|
76
|
+
if (!fs.existsSync(file)) return;
|
|
77
|
+
const cfg = loadOpencodeConfig();
|
|
78
|
+
if (cfg.mcp && cfg.mcp[NAMESPACE]) {
|
|
79
|
+
delete cfg.mcp[NAMESPACE];
|
|
80
|
+
if (Object.keys(cfg.mcp).length === 0) delete cfg.mcp;
|
|
81
|
+
saveOpencodeConfig(cfg);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function registerMcpServer(packageRoot) {
|
|
86
|
+
const mcpServer = path.join(packageRoot, "src", "mcp", "server.mjs");
|
|
87
|
+
const cfg = loadOpencodeConfig();
|
|
88
|
+
if (!cfg["$schema"]) cfg["$schema"] = "https://opencode.ai/config.json";
|
|
89
|
+
cfg.mcp = cfg.mcp || {};
|
|
90
|
+
cfg.mcp[NAMESPACE] = {
|
|
91
|
+
type: "local",
|
|
92
|
+
command: ["node", mcpServer],
|
|
93
|
+
enabled: true,
|
|
94
|
+
};
|
|
95
|
+
saveOpencodeConfig(cfg);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removeLegacyArtifacts() {
|
|
99
|
+
// Old `~/.config/opencode/slidemind/` payload directory.
|
|
100
|
+
if (fs.existsSync(legacySkillsRoot())) {
|
|
101
|
+
rmIfExists(legacySkillsRoot());
|
|
102
|
+
ui.step({ status: "remove", label: "legacy", value: legacySkillsRoot() });
|
|
103
|
+
}
|
|
104
|
+
// Old `~/.config/opencode/agent/slidemind-*.md` agent shims.
|
|
105
|
+
const agentDir = legacyAgentDir();
|
|
106
|
+
if (fs.existsSync(agentDir)) {
|
|
107
|
+
for (const entry of fs.readdirSync(agentDir)) {
|
|
108
|
+
if (entry.startsWith(`${NAMESPACE}-`) && entry.endsWith(".md")) {
|
|
109
|
+
const target = path.join(agentDir, entry);
|
|
110
|
+
rmIfExists(target);
|
|
111
|
+
ui.step({ status: "remove", label: "legacy agent", value: target });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Old `~/.config/opencode/command/slidemind-*.md` command files.
|
|
116
|
+
const commandDir = legacyCommandDir();
|
|
117
|
+
if (fs.existsSync(commandDir)) {
|
|
118
|
+
for (const entry of fs.readdirSync(commandDir)) {
|
|
119
|
+
if (entry.startsWith(`${NAMESPACE}-`) && entry.endsWith(".md")) {
|
|
120
|
+
const target = path.join(commandDir, entry);
|
|
121
|
+
rmIfExists(target);
|
|
122
|
+
ui.step({ status: "remove", label: "legacy command", value: target });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function install({ packageRoot, flags }) {
|
|
129
|
+
const link = isLinkMode(flags);
|
|
130
|
+
const force = isForce(flags);
|
|
131
|
+
|
|
132
|
+
await ui.section("opencode", opencodeRoot());
|
|
133
|
+
|
|
134
|
+
// Drop pre-skills layout (agent shims, command files, the old
|
|
135
|
+
// ~/.config/opencode/slidemind/ payload) — opencode now scans
|
|
136
|
+
// ~/.config/opencode/skills/ natively.
|
|
137
|
+
removeLegacyArtifacts();
|
|
138
|
+
|
|
139
|
+
// Skill files: copy/symlink each skill into
|
|
140
|
+
// ~/.config/opencode/skills/slidemind-<name>/ so opencode auto-discovers
|
|
141
|
+
// them via SKILL.md frontmatter.
|
|
142
|
+
fs.mkdirSync(skillsRoot(), { recursive: true });
|
|
143
|
+
for (const skill of listSkills(packageRoot)) {
|
|
144
|
+
const dest = skillTarget(skill.name);
|
|
145
|
+
await ui.spin(
|
|
146
|
+
{
|
|
147
|
+
label: "skill",
|
|
148
|
+
value: dest,
|
|
149
|
+
note: link ? "linking" : "copying",
|
|
150
|
+
finalNote: link ? "linked" : "copied",
|
|
151
|
+
},
|
|
152
|
+
() => installDir({ src: skill.path, dest, link, force }),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Register the slidemind MCP server so agents can list/push into open
|
|
157
|
+
// PowerPoint windows via slidemind_list_open_presentations / slidemind_push_deck.
|
|
158
|
+
registerMcpServer(packageRoot);
|
|
159
|
+
ui.step({ status: "ok", label: "mcp server", value: opencodeConfigFile() });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function uninstall() {
|
|
163
|
+
await ui.section("opencode", opencodeRoot());
|
|
164
|
+
|
|
165
|
+
removeLegacyArtifacts();
|
|
166
|
+
|
|
167
|
+
// Remove our installed skills (only the ones we own — leave other
|
|
168
|
+
// entries in ~/.config/opencode/skills/ alone).
|
|
169
|
+
if (fs.existsSync(skillsRoot())) {
|
|
170
|
+
for (const entry of fs.readdirSync(skillsRoot())) {
|
|
171
|
+
if (entry.startsWith(`${NAMESPACE}-`)) {
|
|
172
|
+
const target = path.join(skillsRoot(), entry);
|
|
173
|
+
rmIfExists(target);
|
|
174
|
+
ui.step({ status: "remove", label: "skill", value: target });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
unregisterMcpServer();
|
|
180
|
+
ui.step({ status: "info", label: "mcp server", value: "unregistered" });
|
|
181
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Minimal clack-style TUI primitives, but with full colour control over
|
|
2
|
+
// every glyph. Replaces @clack/prompts where we need to paint the rail
|
|
3
|
+
// itself (gradient) and animate text mid-line (harness shimmer) — both
|
|
4
|
+
// of which clack hides behind util.styleText named-colour calls.
|
|
5
|
+
//
|
|
6
|
+
// API mirrors the slice of clack we used:
|
|
7
|
+
// intro(text)
|
|
8
|
+
// message(text, { symbol })
|
|
9
|
+
// outro(text)
|
|
10
|
+
// Plus internal row tracking so a top→bottom gradient can be applied to
|
|
11
|
+
// the rail glyphs.
|
|
12
|
+
|
|
13
|
+
import { brand } from "./brand-theme.mjs";
|
|
14
|
+
|
|
15
|
+
const FORCE_COLOR =
|
|
16
|
+
process.env.FORCE_COLOR === "1" || process.env.FORCE_COLOR === "true";
|
|
17
|
+
const NO_COLOR =
|
|
18
|
+
!!process.env.NO_COLOR ||
|
|
19
|
+
(!FORCE_COLOR && (!process.stdout.isTTY || process.env.TERM === "dumb"));
|
|
20
|
+
|
|
21
|
+
const FG_RESET = "\x1b[39m";
|
|
22
|
+
const FADE_ROWS = 28; // rail finishes fading after this many emitted rows
|
|
23
|
+
|
|
24
|
+
let _row = 0;
|
|
25
|
+
|
|
26
|
+
// Linear interpolate between #9999FF (techBlue40) and #6C7D8C (slate).
|
|
27
|
+
// Top of the rail is bright tech-blue, fading to brand slate as the
|
|
28
|
+
// install progresses — feels like the deck's tech-blue gradation card.
|
|
29
|
+
function railEscape(row) {
|
|
30
|
+
if (NO_COLOR) return "";
|
|
31
|
+
const t = Math.min(row / FADE_ROWS, 1);
|
|
32
|
+
const r = Math.round(153 + (108 - 153) * t);
|
|
33
|
+
const g = Math.round(153 + (125 - 153) * t);
|
|
34
|
+
const b = Math.round(255 + (140 - 255) * t);
|
|
35
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function paint(row, glyph) {
|
|
39
|
+
return NO_COLOR ? glyph : railEscape(row) + glyph + FG_RESET;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function rail(row = _row) {
|
|
43
|
+
return paint(row, "│");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getRow() {
|
|
47
|
+
return _row;
|
|
48
|
+
}
|
|
49
|
+
export function bumpRow(n = 1) {
|
|
50
|
+
_row += n;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function intro(text) {
|
|
54
|
+
_row = 0;
|
|
55
|
+
process.stdout.write("\n" + paint(_row, "┌") + " " + text + "\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Each message starts with its own gradient-coloured rail spacer, then
|
|
59
|
+
// the glyph + text. The caller supplies `symbol` for non-rail glyphs
|
|
60
|
+
// (◆ ● ▲ ■ etc.); when omitted we fall back to a continuation rail.
|
|
61
|
+
export function message(text, { symbol } = {}) {
|
|
62
|
+
_row += 1;
|
|
63
|
+
process.stdout.write(paint(_row, "│") + "\n");
|
|
64
|
+
_row += 1;
|
|
65
|
+
const sym = symbol ?? paint(_row, "│");
|
|
66
|
+
process.stdout.write(sym + " " + text + "\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function outro(text) {
|
|
70
|
+
_row += 1;
|
|
71
|
+
process.stdout.write(paint(_row, "│") + "\n");
|
|
72
|
+
_row += 1;
|
|
73
|
+
process.stdout.write(paint(_row, "└") + " " + text + "\n\n");
|
|
74
|
+
_row = 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reserve the next two rows for a spin step: emits the rail spacer,
|
|
78
|
+
// returns the row index the spinner is written on. Callers should
|
|
79
|
+
// `process.stdout.write(...)` the spinner content themselves (without a
|
|
80
|
+
// trailing `\n`) so they can overwrite it via `\r`. When the operation
|
|
81
|
+
// completes they call `commitSpinRow()` after writing the final line.
|
|
82
|
+
export function reserveSpinRows() {
|
|
83
|
+
_row += 1;
|
|
84
|
+
process.stdout.write(paint(_row, "│") + "\n");
|
|
85
|
+
_row += 1;
|
|
86
|
+
return _row;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ANIMATE = !NO_COLOR && process.stdout.isTTY;
|
|
90
|
+
|
|
91
|
+
// Sweep a brightness "highlight" left-to-right across `text`, settling
|
|
92
|
+
// on `restColor(text)` at the end. The caller has already positioned
|
|
93
|
+
// the cursor where the text should appear; this helper writes only the
|
|
94
|
+
// text region (no rail, no symbol) and uses `\r\x1b[<col>G` to reset to
|
|
95
|
+
// that column for each redraw.
|
|
96
|
+
export async function shimmer(text, { startCol = 1, restColor }) {
|
|
97
|
+
const final = brand.bold(restColor(text));
|
|
98
|
+
if (!ANIMATE) {
|
|
99
|
+
process.stdout.write(final);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const len = text.length;
|
|
103
|
+
const sweepRange = len + 6; // start the wave just before the text
|
|
104
|
+
const frames = 14;
|
|
105
|
+
const frameMs = 45;
|
|
106
|
+
|
|
107
|
+
for (let f = 0; f <= frames; f++) {
|
|
108
|
+
const pos = (f / frames) * sweepRange - 3;
|
|
109
|
+
let line = "";
|
|
110
|
+
for (let i = 0; i < len; i++) {
|
|
111
|
+
const dist = i - pos;
|
|
112
|
+
const a = Math.abs(dist);
|
|
113
|
+
let painter;
|
|
114
|
+
if (a <= 1) painter = brand.techBlue20; // peak brightness
|
|
115
|
+
else if (a <= 2.5) painter = brand.techBlue40;
|
|
116
|
+
else if (a <= 4) painter = brand.slate;
|
|
117
|
+
else painter = (s) => brand.dim(brand.slate(s));
|
|
118
|
+
line += brand.bold(painter(text[i]));
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(`\x1b[${startCol}G\x1b[K` + line);
|
|
121
|
+
if (f < frames) await new Promise((r) => setTimeout(r, frameMs));
|
|
122
|
+
}
|
|
123
|
+
// Final settle.
|
|
124
|
+
process.stdout.write(`\x1b[${startCol}G\x1b[K` + final);
|
|
125
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Describe how an engine result can be recoloured, so the agent never has to
|
|
2
|
+
// open the SVG to find out (the exact step that was wasting time: "let me
|
|
3
|
+
// inspect two SVGs to confirm how recolouring works").
|
|
4
|
+
//
|
|
5
|
+
// The slidemind_download_result tool already has the chosen result's bytes in
|
|
6
|
+
// hand (the bridge returns `svgMarkup` for SVG results), so we can classify it
|
|
7
|
+
// once and report the answer instead of leaving the agent to guess.
|
|
8
|
+
//
|
|
9
|
+
// mode:
|
|
10
|
+
// "currentColor" — paints with fill/stroke="currentColor". A CSS `color`
|
|
11
|
+
// on the <img> tints it live in BOTH the HTML preview
|
|
12
|
+
// (which inlines the SVG, see src/builders/build.mjs) and
|
|
13
|
+
// the .pptx render (which substitutes currentColor). You
|
|
14
|
+
// can also bake a fixed colour in with icon_color.
|
|
15
|
+
// "mono" — a single hard-coded paint colour (or the SVG default,
|
|
16
|
+
// black) and no currentColor. CSS `color` will NOT touch
|
|
17
|
+
// it — recolour by re-downloading with icon_color, which
|
|
18
|
+
// makes the bridge rewrite the fill.
|
|
19
|
+
// "multicolor" — several distinct paint colours. Leave it as-is; passing
|
|
20
|
+
// icon_color would flatten a deliberately multi-tone glyph.
|
|
21
|
+
// "raster" — PNG/JPEG/etc. Colour is not applicable.
|
|
22
|
+
|
|
23
|
+
function isSvg(mimeType, svgMarkup) {
|
|
24
|
+
if (typeof svgMarkup === "string" && /<svg[\s>]/i.test(svgMarkup)) return true;
|
|
25
|
+
return /svg/i.test(String(mimeType || ""));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Collect the distinct, concrete paint colours an SVG declares — via fill="…"
|
|
29
|
+
// / stroke="…" attributes and `fill:`/`stroke:` style properties. `none`,
|
|
30
|
+
// `transparent`, and `currentColor` are not concrete colours and are excluded.
|
|
31
|
+
function distinctPaints(svgMarkup) {
|
|
32
|
+
const colors = new Set();
|
|
33
|
+
const re =
|
|
34
|
+
/(?:fill|stroke)\s*[:=]\s*["']?\s*(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+)/g;
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = re.exec(svgMarkup)) !== null) {
|
|
37
|
+
const raw = m[1].trim().toLowerCase();
|
|
38
|
+
if (raw === "none" || raw === "transparent" || raw === "currentcolor") {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Bare keyword that isn't a real colour name (e.g. "url" from url(#grad))
|
|
42
|
+
// would over-count; only keep hex/rgb/hsl and a small set of plain names.
|
|
43
|
+
if (/^#|^rgb|^hsl/.test(raw) || NAMED_COLORS.has(raw)) {
|
|
44
|
+
colors.add(raw);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return colors;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const NAMED_COLORS = new Set([
|
|
51
|
+
"black", "white", "red", "green", "blue", "yellow", "orange", "purple",
|
|
52
|
+
"pink", "gray", "grey", "cyan", "magenta", "brown", "navy", "teal",
|
|
53
|
+
"silver", "gold", "maroon", "olive", "lime", "aqua", "fuchsia",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
function hasCurrentColor(svgMarkup) {
|
|
57
|
+
return /(?:fill|stroke)\s*[:=]\s*["']?\s*currentColor/i.test(svgMarkup);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build the descriptor attached to a download result. `appliedColor` is the
|
|
61
|
+
// hex the caller asked the bridge to bake in (null if none) — when present the
|
|
62
|
+
// returned bytes are already coloured.
|
|
63
|
+
export function describeRecolor(svgMarkup, mimeType, appliedColor = null) {
|
|
64
|
+
const applied = Boolean(appliedColor) && isSvg(mimeType, svgMarkup);
|
|
65
|
+
|
|
66
|
+
if (!isSvg(mimeType, svgMarkup)) {
|
|
67
|
+
return {
|
|
68
|
+
recolorable: false,
|
|
69
|
+
mode: "raster",
|
|
70
|
+
applied: false,
|
|
71
|
+
color: null,
|
|
72
|
+
note: "Raster image — recolouring not applicable.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const markup = typeof svgMarkup === "string" ? svgMarkup : "";
|
|
77
|
+
let mode;
|
|
78
|
+
if (hasCurrentColor(markup)) {
|
|
79
|
+
mode = "currentColor";
|
|
80
|
+
} else {
|
|
81
|
+
const paints = distinctPaints(markup);
|
|
82
|
+
mode = paints.size >= 2 ? "multicolor" : "mono";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const recolorable = mode === "currentColor" || mode === "mono";
|
|
86
|
+
|
|
87
|
+
let note;
|
|
88
|
+
if (applied) {
|
|
89
|
+
note =
|
|
90
|
+
`Saved already coloured ${appliedColor} — reference it directly; ` +
|
|
91
|
+
`the colour is baked into the file (correct in preview and render).`;
|
|
92
|
+
} else if (mode === "currentColor") {
|
|
93
|
+
note =
|
|
94
|
+
"Tintable: set CSS `color` on the <img> (honoured in both the HTML " +
|
|
95
|
+
"preview and the .pptx render), or pass icon_color to bake a fixed colour in.";
|
|
96
|
+
} else if (mode === "mono") {
|
|
97
|
+
note =
|
|
98
|
+
"Single-colour glyph: CSS `color` won't tint it — re-download with " +
|
|
99
|
+
"icon_color (a hex resolved from a corporate-design.css token) to recolour.";
|
|
100
|
+
} else {
|
|
101
|
+
note = "Multi-colour glyph: leave as-is; icon_color would flatten it.";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
recolorable,
|
|
106
|
+
mode,
|
|
107
|
+
applied,
|
|
108
|
+
color: applied ? appliedColor : null,
|
|
109
|
+
note,
|
|
110
|
+
};
|
|
111
|
+
}
|