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,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"description": "The closed registry of components a slide JSX file may compose. The build-time linter rejects any PascalCase JSX tag that is not in this file. Layouts are the only allowed top-level slide component; primitives may appear inside body content of freeform layouts (SlideContent*Blank, SlideQuote*Blank).",
|
|
4
|
+
"layouts": [
|
|
5
|
+
"SlideCover",
|
|
6
|
+
"SlideAgendaBlack",
|
|
7
|
+
"SlideAgendaBlue",
|
|
8
|
+
"SlideChapterBlack",
|
|
9
|
+
"SlideChapterBlue",
|
|
10
|
+
"SlideClosing",
|
|
11
|
+
"SlideContentDarkBlank",
|
|
12
|
+
"SlideContentLightBlank",
|
|
13
|
+
"SlideQuoteBlack",
|
|
14
|
+
"SlideQuoteBlackBlank",
|
|
15
|
+
"SlideQuoteBlue",
|
|
16
|
+
"SlideQuoteBlueBlank"
|
|
17
|
+
],
|
|
18
|
+
"primitives": []
|
|
19
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Discover which skill a deck was authored against, and return the paths the
|
|
2
|
+
// rest of the local preview pipeline (build.mjs, lint-slides.mjs) needs to
|
|
3
|
+
// load layouts, tokens, primitives and assets from.
|
|
4
|
+
//
|
|
5
|
+
// Post-connect, the corporate design (css, layouts, assets) is no longer
|
|
6
|
+
// shipped inside the skill — it lives in the slidemind add-in and is
|
|
7
|
+
// materialized per-CD into a local bundle cache. resolveSkill now resolves
|
|
8
|
+
// those design roots from (in order): the deck-local override, the materialized
|
|
9
|
+
// bundle (keyed by the deck's .bundle-ref.json), then any legacy in-package
|
|
10
|
+
// skill files (back-compat for decks authored before the connect migration).
|
|
11
|
+
//
|
|
12
|
+
// The canonical .pptx render happens SERVER-SIDE in the add-in; these paths
|
|
13
|
+
// only feed the local HTML/editor preview.
|
|
14
|
+
//
|
|
15
|
+
// Lookup order for a deck's skill name:
|
|
16
|
+
// 1. `decks/<slug>/plan.md` frontmatter — `skill:` field.
|
|
17
|
+
// 2. Default: `create-slides`.
|
|
18
|
+
//
|
|
19
|
+
// If a `skills/<skill>/skill.json` manifest exists, optional fields it can
|
|
20
|
+
// declare: `mode`, `templates`, `defaultTemplate`, `designSystem.css`.
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = path.dirname(__filename);
|
|
30
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
31
|
+
|
|
32
|
+
const DEFAULT_SKILL = "create-slides";
|
|
33
|
+
|
|
34
|
+
// Shipped fallback layout-contract registry for the LOCAL HTML preview.
|
|
35
|
+
// The add-in derives layout contracts from the bundle's layouts directly, so
|
|
36
|
+
// this is only consulted by the local preview when neither the deck nor the
|
|
37
|
+
// materialized bundle carries its own primitives.json.
|
|
38
|
+
const SHIPPED_PRIMITIVES = path.join(__dirname, "primitives.json");
|
|
39
|
+
|
|
40
|
+
function firstExisting(candidates) {
|
|
41
|
+
for (const c of candidates) {
|
|
42
|
+
if (c && fs.existsSync(c)) return c;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// First existing candidate, or `fallback` when none exist. Encodes the
|
|
48
|
+
// deck-override → bundle → skill precedence in one place so every resolved path
|
|
49
|
+
// shares the same lookup rule.
|
|
50
|
+
function firstExistingOr(candidates, fallback) {
|
|
51
|
+
return firstExisting(candidates) ?? fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveSkillForDeck(deckDir) {
|
|
55
|
+
const planMeta = readPlanFrontmatter(deckDir);
|
|
56
|
+
const skillName = planMeta.skill || DEFAULT_SKILL;
|
|
57
|
+
const bundleDir = deckDir ? resolveBundleDirForDeck(deckDir) : null;
|
|
58
|
+
return resolveSkill(skillName, planMeta, { deckDir, bundleDir });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveSkill(skillName, planMeta = {}, opts = {}) {
|
|
62
|
+
const skillDir = path.join(PACKAGE_ROOT, "skills", skillName);
|
|
63
|
+
const manifest = fs.existsSync(skillDir) ? readManifest(skillDir) : {};
|
|
64
|
+
const cssName = manifest.designSystem?.css || "corporate-design.css";
|
|
65
|
+
|
|
66
|
+
// Design roots in precedence order: materialized bundle → (legacy) in-package
|
|
67
|
+
// skill dir. Deck-local overrides are layered on top per-asset below. A root
|
|
68
|
+
// only counts if it actually carries design files — otherwise the now
|
|
69
|
+
// brand-stripped skill dir would mask the "materialize the bundle first"
|
|
70
|
+
// guidance for a deck that hasn't downloaded one yet.
|
|
71
|
+
const bundleDir =
|
|
72
|
+
opts.bundleDir && fs.existsSync(opts.bundleDir) ? opts.bundleDir : null;
|
|
73
|
+
const designRoots = [
|
|
74
|
+
bundleDir,
|
|
75
|
+
fs.existsSync(skillDir) ? skillDir : null,
|
|
76
|
+
].filter(
|
|
77
|
+
(r) =>
|
|
78
|
+
r &&
|
|
79
|
+
(fs.existsSync(path.join(r, cssName)) ||
|
|
80
|
+
fs.existsSync(path.join(r, "layouts"))),
|
|
81
|
+
);
|
|
82
|
+
if (designRoots.length === 0) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`No design source for skill "${skillName}". The corporate design now lives ` +
|
|
85
|
+
`in the slidemind add-in — run 'slidemind download-bundle <deck>' to ` +
|
|
86
|
+
`materialize it, then retry.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const deckLayouts = opts.deckDir ? path.join(opts.deckDir, "layouts") : null;
|
|
91
|
+
const deckAssets = opts.deckDir ? path.join(opts.deckDir, "assets") : null;
|
|
92
|
+
const deckPrimitives = opts.deckDir
|
|
93
|
+
? path.join(opts.deckDir, "primitives.json")
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
const cssFile = firstExistingOr(
|
|
97
|
+
designRoots.map((r) => path.join(r, cssName)),
|
|
98
|
+
path.join(designRoots[0], cssName),
|
|
99
|
+
);
|
|
100
|
+
const layoutsDir = firstExistingOr(
|
|
101
|
+
[deckLayouts, ...designRoots.map((r) => path.join(r, "layouts"))],
|
|
102
|
+
path.join(designRoots[0], "layouts"),
|
|
103
|
+
);
|
|
104
|
+
const layoutsCssFile = firstExistingOr(
|
|
105
|
+
designRoots.map((r) => path.join(r, "layouts", "layouts.css")),
|
|
106
|
+
path.join(designRoots[0], "layouts", "layouts.css"),
|
|
107
|
+
);
|
|
108
|
+
const assetsDir = firstExistingOr(
|
|
109
|
+
[deckAssets, ...designRoots.map((r) => path.join(r, "assets"))],
|
|
110
|
+
path.join(designRoots[0], "assets"),
|
|
111
|
+
);
|
|
112
|
+
const primitivesPath = firstExistingOr(
|
|
113
|
+
[
|
|
114
|
+
deckPrimitives,
|
|
115
|
+
...designRoots.map((r) => path.join(r, "primitives.json")),
|
|
116
|
+
SHIPPED_PRIMITIVES,
|
|
117
|
+
],
|
|
118
|
+
SHIPPED_PRIMITIVES,
|
|
119
|
+
);
|
|
120
|
+
// One truth: brand paths mirror the resolved design roots (the bundle IS the
|
|
121
|
+
// brand source; there is no separate brand skill anymore).
|
|
122
|
+
const brand = {
|
|
123
|
+
skillDir: designRoots[0],
|
|
124
|
+
assetsDir,
|
|
125
|
+
cssFile,
|
|
126
|
+
layoutsDir,
|
|
127
|
+
layoutsCssFile,
|
|
128
|
+
primitivesPath,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// The PPTX slide-master template only fed the now-archived local exporter;
|
|
132
|
+
// resolve it only if a manifest still declares one and the file exists.
|
|
133
|
+
const mode = manifest.mode || "compose";
|
|
134
|
+
let templatePath = null;
|
|
135
|
+
let templateKey = null;
|
|
136
|
+
if (manifest.templates) {
|
|
137
|
+
templateKey =
|
|
138
|
+
planMeta.template ||
|
|
139
|
+
manifest.defaultTemplate ||
|
|
140
|
+
Object.keys(manifest.templates)[0];
|
|
141
|
+
const rel = manifest.templates[templateKey];
|
|
142
|
+
const candidate =
|
|
143
|
+
rel && fs.existsSync(skillDir) ? path.join(skillDir, rel) : null;
|
|
144
|
+
if (candidate && fs.existsSync(candidate)) templatePath = candidate;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
skillName,
|
|
149
|
+
skillDir,
|
|
150
|
+
manifest,
|
|
151
|
+
mode,
|
|
152
|
+
cssFile,
|
|
153
|
+
layoutsDir,
|
|
154
|
+
assetsDir,
|
|
155
|
+
primitivesPath,
|
|
156
|
+
brand,
|
|
157
|
+
templateKey,
|
|
158
|
+
templatePath,
|
|
159
|
+
locale: planMeta.locale || null,
|
|
160
|
+
packageRoot: PACKAGE_ROOT,
|
|
161
|
+
bundleDir,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// plan.md frontmatter
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function readPlanFrontmatter(deckDir) {
|
|
170
|
+
if (!deckDir) return {};
|
|
171
|
+
const file = path.join(deckDir, "plan.md");
|
|
172
|
+
if (!fs.existsSync(file)) return {};
|
|
173
|
+
const text = fs.readFileSync(file, "utf8");
|
|
174
|
+
if (!text.startsWith("---")) return {};
|
|
175
|
+
const end = text.indexOf("\n---", 3);
|
|
176
|
+
if (end < 0) return {};
|
|
177
|
+
const body = text.slice(3, end);
|
|
178
|
+
const out = {};
|
|
179
|
+
for (const line of body.split("\n")) {
|
|
180
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.+?)\s*$/);
|
|
181
|
+
if (m) out[m[1]] = stripQuotes(m[2]);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stripQuotes(s) {
|
|
187
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
188
|
+
return s.slice(1, -1);
|
|
189
|
+
}
|
|
190
|
+
return s;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// skill.json manifest
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function readManifest(skillDir) {
|
|
198
|
+
const file = path.join(skillDir, "skill.json");
|
|
199
|
+
if (!fs.existsSync(file)) return {};
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
202
|
+
} catch (err) {
|
|
203
|
+
throw new Error(`Invalid skill.json in ${skillDir}: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Runtime applier — consumes decks/<slug>/overrides.json and mutates the
|
|
2
|
+
// live slide DOM to reflect styles, positions, hidden chrome, and
|
|
3
|
+
// user-added free text elements.
|
|
4
|
+
//
|
|
5
|
+
// The overlay loads first (at the end of <body>); this file loads in <head>
|
|
6
|
+
// so its global API is ready by the time overlay.js wires callbacks into it.
|
|
7
|
+
|
|
8
|
+
(() => {
|
|
9
|
+
const DECK_ROOT_SEL = "#stage";
|
|
10
|
+
|
|
11
|
+
// Shape: { version: 1, slides: { "<slideName>": { styles, positions, hidden, freeElements } } }
|
|
12
|
+
let overrides = { version: 1, slides: {} };
|
|
13
|
+
let currentSlide = null;
|
|
14
|
+
let navObserver = null;
|
|
15
|
+
|
|
16
|
+
// We observe the stage so overrides re-apply after every React reconcile.
|
|
17
|
+
let observer = null;
|
|
18
|
+
// Drag/resize gestures pause the observer so optimistic inline-style writes
|
|
19
|
+
// aren't clobbered mid-gesture by an override re-apply triggered by the
|
|
20
|
+
// mutations the gesture itself causes. Counted so nested pauses are safe.
|
|
21
|
+
let observerPaused = 0;
|
|
22
|
+
|
|
23
|
+
function ensureObserver() {
|
|
24
|
+
if (observer) return;
|
|
25
|
+
const stage = document.querySelector(DECK_ROOT_SEL);
|
|
26
|
+
if (!stage) return;
|
|
27
|
+
observer = new MutationObserver(() => {
|
|
28
|
+
if (observerPaused > 0) return;
|
|
29
|
+
if (currentSlide) applySlide(currentSlide);
|
|
30
|
+
});
|
|
31
|
+
observer.observe(stage, { childList: true, subtree: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pauseObserver() {
|
|
35
|
+
observerPaused++;
|
|
36
|
+
}
|
|
37
|
+
function resumeObserver() {
|
|
38
|
+
observerPaused = Math.max(0, observerPaused - 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function registerOverrides(nextOverrides) {
|
|
42
|
+
overrides = nextOverrides || { version: 1, slides: {} };
|
|
43
|
+
if (currentSlide) applySlide(currentSlide);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setCurrentSlide(slideName) {
|
|
47
|
+
currentSlide = slideName;
|
|
48
|
+
ensureObserver();
|
|
49
|
+
applySlide(slideName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function watchDeckNav() {
|
|
53
|
+
if (navObserver) return;
|
|
54
|
+
const nav = document.getElementById("nav");
|
|
55
|
+
if (!nav) return;
|
|
56
|
+
const sync = () => {
|
|
57
|
+
const text = nav.textContent || "";
|
|
58
|
+
const match = text.match(/^\d+\s*\/\s*\d+\s+·\s+(\S+)/);
|
|
59
|
+
if (match) setCurrentSlide(match[1]);
|
|
60
|
+
};
|
|
61
|
+
sync();
|
|
62
|
+
navObserver = new MutationObserver(sync);
|
|
63
|
+
navObserver.observe(nav, { childList: true, characterData: true, subtree: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function applySlide(slideName, rootArg) {
|
|
67
|
+
const root = rootArg || document.querySelector(DECK_ROOT_SEL);
|
|
68
|
+
if (!root) return;
|
|
69
|
+
const slideEl = root.querySelector(".slide") || root;
|
|
70
|
+
if (!slideEl) return;
|
|
71
|
+
const o = overrides.slides ? overrides.slides[slideName] : null;
|
|
72
|
+
// Strip any free elements that don't belong to this slide (cross-slide
|
|
73
|
+
// leftovers from a previous render). Keep only those in the current set.
|
|
74
|
+
const wantedFreeIds = new Set(
|
|
75
|
+
(o && o.freeElements ? o.freeElements.map((f) => f.id) : []),
|
|
76
|
+
);
|
|
77
|
+
for (const fe of root.querySelectorAll("[data-free-id]")) {
|
|
78
|
+
if (!wantedFreeIds.has(fe.getAttribute("data-free-id"))) {
|
|
79
|
+
fe.remove();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Clear ghost inline styles from elements that no longer have an
|
|
84
|
+
// override. Without this, an undo/clear leaves the element pinned at the
|
|
85
|
+
// old absolute position because we only ever *set* properties.
|
|
86
|
+
for (const el of root.querySelectorAll("[data-edit-id]")) {
|
|
87
|
+
if (el.hasAttribute("data-free")) continue;
|
|
88
|
+
const id = el.getAttribute("data-edit-id");
|
|
89
|
+
const hasPos = !!(o && o.positions && o.positions[id]);
|
|
90
|
+
if (!hasPos) {
|
|
91
|
+
el.style.removeProperty("position");
|
|
92
|
+
el.style.removeProperty("left");
|
|
93
|
+
el.style.removeProperty("top");
|
|
94
|
+
el.style.removeProperty("width");
|
|
95
|
+
el.style.removeProperty("height");
|
|
96
|
+
el.style.removeProperty("z-index");
|
|
97
|
+
}
|
|
98
|
+
const hasStyle = !!(o && o.styles && o.styles[id]);
|
|
99
|
+
if (!hasStyle) {
|
|
100
|
+
el.style.removeProperty("color");
|
|
101
|
+
el.style.removeProperty("font-size");
|
|
102
|
+
el.style.removeProperty("font-weight");
|
|
103
|
+
}
|
|
104
|
+
const hidden = !!(o && o.hidden && o.hidden[id]);
|
|
105
|
+
if (!hidden && el.style.display === "none") {
|
|
106
|
+
el.style.removeProperty("display");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!o) return;
|
|
111
|
+
|
|
112
|
+
// styles — write what's set, clear what isn't (idempotent).
|
|
113
|
+
if (o.styles) {
|
|
114
|
+
for (const editId of Object.keys(o.styles)) {
|
|
115
|
+
const el = findByEditId(root, editId);
|
|
116
|
+
if (!el) continue;
|
|
117
|
+
const s = o.styles[editId];
|
|
118
|
+
if (s.colorToken) el.style.setProperty("color", `var(${s.colorToken})`);
|
|
119
|
+
else el.style.removeProperty("color");
|
|
120
|
+
if (s.fontSizeToken)
|
|
121
|
+
el.style.setProperty("font-size", `var(${s.fontSizeToken})`);
|
|
122
|
+
else el.style.removeProperty("font-size");
|
|
123
|
+
if (s.fontWeight != null)
|
|
124
|
+
el.style.setProperty("font-weight", String(s.fontWeight));
|
|
125
|
+
else el.style.removeProperty("font-weight");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// positions — same idempotency rule. Clear any field that's missing so a
|
|
130
|
+
// partial override (e.g. zIndex-only) can't ghost stale left/top.
|
|
131
|
+
if (o.positions) {
|
|
132
|
+
for (const editId of Object.keys(o.positions)) {
|
|
133
|
+
const el = findByEditId(root, editId);
|
|
134
|
+
if (!el) continue;
|
|
135
|
+
const p = o.positions[editId];
|
|
136
|
+
// We assume any override target is drawn on the absolute-positioned
|
|
137
|
+
// .slide canvas; set position so our coords take effect even if
|
|
138
|
+
// the element was laid out by flex/grid inside a Mode B slide.
|
|
139
|
+
el.style.position = "absolute";
|
|
140
|
+
if (p.left != null) el.style.left = p.left + "px";
|
|
141
|
+
else el.style.removeProperty("left");
|
|
142
|
+
if (p.top != null) el.style.top = p.top + "px";
|
|
143
|
+
else el.style.removeProperty("top");
|
|
144
|
+
if (p.width != null) el.style.width = p.width + "px";
|
|
145
|
+
else el.style.removeProperty("width");
|
|
146
|
+
if (p.height != null) el.style.height = p.height + "px";
|
|
147
|
+
else el.style.removeProperty("height");
|
|
148
|
+
if (p.zIndex != null) el.style.zIndex = String(p.zIndex);
|
|
149
|
+
else el.style.removeProperty("z-index");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// hidden chrome (logos, page marks, etc. that we don't want on this slide)
|
|
154
|
+
if (o.hidden) {
|
|
155
|
+
for (const editId of Object.keys(o.hidden)) {
|
|
156
|
+
const el = findByEditId(root, editId);
|
|
157
|
+
if (!el) continue;
|
|
158
|
+
if (o.hidden[editId]) el.style.display = "none";
|
|
159
|
+
else el.style.removeProperty("display");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// free elements
|
|
164
|
+
if (o.freeElements && o.freeElements.length) {
|
|
165
|
+
for (const fe of o.freeElements) {
|
|
166
|
+
const wantImage = fe.kind === "image";
|
|
167
|
+
let el = root.querySelector(`[data-free-id="${cssEscape(fe.id)}"]`);
|
|
168
|
+
// If the kind switched (e.g. text -> image) the existing node has the
|
|
169
|
+
// wrong tagName; drop it so we re-create cleanly.
|
|
170
|
+
if (el) {
|
|
171
|
+
const isImg = el.tagName === "IMG";
|
|
172
|
+
if (wantImage !== isImg) {
|
|
173
|
+
el.remove();
|
|
174
|
+
el = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!el) {
|
|
178
|
+
el = document.createElement(wantImage ? "img" : "div");
|
|
179
|
+
el.setAttribute("data-free-id", fe.id);
|
|
180
|
+
el.setAttribute("data-edit-id", fe.id);
|
|
181
|
+
el.setAttribute("data-free", "1");
|
|
182
|
+
if (wantImage) {
|
|
183
|
+
el.setAttribute("data-free-kind", "image");
|
|
184
|
+
el.setAttribute("draggable", "false");
|
|
185
|
+
el.setAttribute("alt", "");
|
|
186
|
+
}
|
|
187
|
+
slideEl.appendChild(el);
|
|
188
|
+
} else if (el.parentNode !== slideEl) {
|
|
189
|
+
// re-parent if React re-created the slide element
|
|
190
|
+
slideEl.appendChild(el);
|
|
191
|
+
}
|
|
192
|
+
el.style.position = "absolute";
|
|
193
|
+
el.style.left = (fe.x ?? 100) + "px";
|
|
194
|
+
el.style.top = (fe.y ?? 100) + "px";
|
|
195
|
+
if (fe.width != null) el.style.width = fe.width + "px";
|
|
196
|
+
if (fe.height != null) el.style.height = fe.height + "px";
|
|
197
|
+
el.style.zIndex = String(fe.zIndex ?? 4);
|
|
198
|
+
el.style.pointerEvents = "auto";
|
|
199
|
+
if (wantImage) {
|
|
200
|
+
// Mode B image free element. PPTX export does NOT support these
|
|
201
|
+
// (build-pptx.py has no body-builder for arbitrary uploads); warn so
|
|
202
|
+
// the user knows the deck won't round-trip 1:1 to PowerPoint.
|
|
203
|
+
if (!el.__warnedImagePptx) {
|
|
204
|
+
console.warn(
|
|
205
|
+
"[editor] image free-element will NOT export to .pptx:",
|
|
206
|
+
{ id: fe.id, src: fe.src },
|
|
207
|
+
);
|
|
208
|
+
el.__warnedImagePptx = true;
|
|
209
|
+
}
|
|
210
|
+
el.style.objectFit = "contain";
|
|
211
|
+
// Reset any text-mode bleed-through.
|
|
212
|
+
el.style.color = "";
|
|
213
|
+
el.style.fontSize = "";
|
|
214
|
+
el.style.fontWeight = "";
|
|
215
|
+
el.style.lineHeight = "";
|
|
216
|
+
el.style.fontFamily = "";
|
|
217
|
+
el.style.whiteSpace = "";
|
|
218
|
+
if (fe.src && el.getAttribute("src") !== fe.src) {
|
|
219
|
+
el.setAttribute("src", fe.src);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
el.style.color = fe.colorToken
|
|
223
|
+
? `var(${fe.colorToken})`
|
|
224
|
+
: "var(--fg-2)";
|
|
225
|
+
el.style.fontSize = fe.fontSizeToken
|
|
226
|
+
? `var(${fe.fontSizeToken})`
|
|
227
|
+
: "var(--fs-body)";
|
|
228
|
+
el.style.fontWeight = fe.fontWeight ? String(fe.fontWeight) : "400";
|
|
229
|
+
el.style.lineHeight = "var(--lh-body, 1.45)";
|
|
230
|
+
el.style.fontFamily = "var(--font-sans)";
|
|
231
|
+
el.style.whiteSpace = "pre-line";
|
|
232
|
+
// Only write textContent if the user isn't actively editing this
|
|
233
|
+
// element (overlay sets contenteditable when editing).
|
|
234
|
+
if (!el.hasAttribute("contenteditable")) {
|
|
235
|
+
el.textContent = fe.text || "";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findByEditId(root, editId) {
|
|
243
|
+
// Prefer the first visible match. CSS.escape handles slashes and colons
|
|
244
|
+
// in our id scheme.
|
|
245
|
+
return root.querySelector(`[data-edit-id="${cssEscape(editId)}"]`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function cssEscape(s) {
|
|
249
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
250
|
+
return CSS.escape(s);
|
|
251
|
+
}
|
|
252
|
+
return String(s).replace(/["\\]/g, "\\$&");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Public API for overlay.js
|
|
256
|
+
window.SlideEditor = window.SlideEditor || {};
|
|
257
|
+
window.SlideEditor.registerOverrides = registerOverrides;
|
|
258
|
+
window.SlideEditor.setCurrentSlide = setCurrentSlide;
|
|
259
|
+
window.SlideEditor.getOverrides = () => overrides;
|
|
260
|
+
window.SlideEditor.applySlide = applySlide;
|
|
261
|
+
window.SlideEditor.applyOverridesTo = (root, slideName) =>
|
|
262
|
+
applySlide(slideName, root);
|
|
263
|
+
window.SlideEditor.findByEditId = findByEditId;
|
|
264
|
+
window.SlideEditor.pauseObserver = pauseObserver;
|
|
265
|
+
window.SlideEditor.resumeObserver = resumeObserver;
|
|
266
|
+
|
|
267
|
+
if (window.__SLIDE_EDITOR_OVERRIDES__) {
|
|
268
|
+
registerOverrides(window.__SLIDE_EDITOR_OVERRIDES__);
|
|
269
|
+
}
|
|
270
|
+
if (document.readyState === "loading") {
|
|
271
|
+
document.addEventListener("DOMContentLoaded", watchDeckNav);
|
|
272
|
+
} else {
|
|
273
|
+
watchDeckNav();
|
|
274
|
+
}
|
|
275
|
+
})();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Brand truecolor helpers for the installer UI.
|
|
2
|
+
//
|
|
3
|
+
// Tokens lifted directly from skills/create-slides/corporate-design.css
|
|
4
|
+
// so the CLI palette matches the deck output. We bypass clack's own
|
|
5
|
+
// theming (which is locked to util.styleText named colors at link time)
|
|
6
|
+
// by injecting pre-colored glyphs through clack's `log.message({ symbol })`
|
|
7
|
+
// hook instead.
|
|
8
|
+
|
|
9
|
+
const FORCE_COLOR =
|
|
10
|
+
process.env.FORCE_COLOR === "1" || process.env.FORCE_COLOR === "true";
|
|
11
|
+
const NO_COLOR =
|
|
12
|
+
!!process.env.NO_COLOR ||
|
|
13
|
+
(!FORCE_COLOR && (!process.stdout.isTTY || process.env.TERM === "dumb"));
|
|
14
|
+
|
|
15
|
+
const tc = (r, g, b) => (s) =>
|
|
16
|
+
NO_COLOR ? String(s) : `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
|
|
17
|
+
|
|
18
|
+
const fmt = (open, close) => (s) =>
|
|
19
|
+
NO_COLOR ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`;
|
|
20
|
+
|
|
21
|
+
// Brand palette (corporate-design.css → :root)
|
|
22
|
+
export const brand = {
|
|
23
|
+
techBlue: tc(0, 0, 255), // #0000FF primary
|
|
24
|
+
techBlue80: tc(51, 51, 255), // #3333FF
|
|
25
|
+
techBlue40: tc(153, 153, 255), // #9999FF
|
|
26
|
+
techBlue20: tc(204, 204, 255), // #CCCCFF
|
|
27
|
+
black: tc(0, 0, 0), // #000000
|
|
28
|
+
ink: tc(40, 52, 64), // #283440 body text / dk2
|
|
29
|
+
slate: tc(108, 125, 140), // #6C7D8C secondary surfaces
|
|
30
|
+
gray: tc(182, 189, 204), // #B6BDCC tertiary
|
|
31
|
+
fog: tc(235, 240, 242), // #EBF0F2
|
|
32
|
+
magenta: tc(254, 13, 108), // #FE0D6C traffic-light / underline
|
|
33
|
+
green: tc(0, 200, 0), // #00C800 emphasis only
|
|
34
|
+
bold: fmt(1, 22),
|
|
35
|
+
dim: fmt(2, 22),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const ANIMATE = !NO_COLOR && process.stdout.isTTY;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Install slidemind into Claude Code.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code auto-discovers user-level skills at
|
|
4
|
+
// `~/.claude/skills/<name>/SKILL.md`. The skill itself owns the user-facing
|
|
5
|
+
// plan / create / edit workflow, so we no longer ship separate slash commands
|
|
6
|
+
// — one skill, one source of truth.
|
|
7
|
+
//
|
|
8
|
+
// Layout after install:
|
|
9
|
+
// ~/.claude/skills/slidemind-create-slides/SKILL.md + assets, layouts, icons, references, …
|
|
10
|
+
//
|
|
11
|
+
// We also actively unregister any prior `commands/slidemind/` directory and
|
|
12
|
+
// any `mcpServers.slidemind` entry from a past install so re-running converges.
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
expandHome,
|
|
18
|
+
installDir,
|
|
19
|
+
isForce,
|
|
20
|
+
isLinkMode,
|
|
21
|
+
listSkills,
|
|
22
|
+
rmIfExists,
|
|
23
|
+
ui,
|
|
24
|
+
} from "./lib.mjs";
|
|
25
|
+
|
|
26
|
+
const NAMESPACE = "slidemind";
|
|
27
|
+
|
|
28
|
+
function claudeRoot() {
|
|
29
|
+
return expandHome("~/.claude");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function claudeUserConfigFile() {
|
|
33
|
+
// User-scope MCP servers live in ~/.claude.json (NOT ~/.claude/settings.json).
|
|
34
|
+
// The file is large and contains unrelated state, so we read-modify-write
|
|
35
|
+
// exactly the mcpServers.slidemind entry.
|
|
36
|
+
return expandHome("~/.claude.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function legacyCommandsDir() {
|
|
40
|
+
return path.join(claudeRoot(), "commands", NAMESPACE);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function skillTarget(skillName) {
|
|
44
|
+
return path.join(claudeRoot(), "skills", `${NAMESPACE}-${skillName}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadClaudeConfig() {
|
|
48
|
+
const file = claudeUserConfigFile();
|
|
49
|
+
if (!fs.existsSync(file)) return {};
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) || {};
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Claude config at ${file} is not valid JSON; refusing to overwrite.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveClaudeConfig(cfg) {
|
|
60
|
+
const file = claudeUserConfigFile();
|
|
61
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
62
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function unregisterMcpServer() {
|
|
66
|
+
const file = claudeUserConfigFile();
|
|
67
|
+
if (!fs.existsSync(file)) return;
|
|
68
|
+
const cfg = loadClaudeConfig();
|
|
69
|
+
if (cfg.mcpServers && cfg.mcpServers[NAMESPACE]) {
|
|
70
|
+
delete cfg.mcpServers[NAMESPACE];
|
|
71
|
+
if (Object.keys(cfg.mcpServers).length === 0) delete cfg.mcpServers;
|
|
72
|
+
saveClaudeConfig(cfg);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function registerMcpServer(packageRoot) {
|
|
77
|
+
const mcpServer = path.join(packageRoot, "src", "mcp", "server.mjs");
|
|
78
|
+
const cfg = loadClaudeConfig();
|
|
79
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
80
|
+
cfg.mcpServers[NAMESPACE] = {
|
|
81
|
+
type: "stdio",
|
|
82
|
+
command: "node",
|
|
83
|
+
args: [mcpServer],
|
|
84
|
+
};
|
|
85
|
+
saveClaudeConfig(cfg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function install({ packageRoot, flags }) {
|
|
89
|
+
const link = isLinkMode(flags);
|
|
90
|
+
const force = isForce(flags);
|
|
91
|
+
|
|
92
|
+
await ui.section("Claude Code", claudeRoot());
|
|
93
|
+
|
|
94
|
+
// Drop the legacy ~/.claude/commands/slidemind/ directory if a prior install
|
|
95
|
+
// created it — the workflow now lives inside the skill.
|
|
96
|
+
if (fs.existsSync(legacyCommandsDir())) {
|
|
97
|
+
rmIfExists(legacyCommandsDir());
|
|
98
|
+
ui.step({ status: "remove", label: "legacy", value: legacyCommandsDir() });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const skill of listSkills(packageRoot)) {
|
|
102
|
+
const dest = skillTarget(skill.name);
|
|
103
|
+
await ui.spin(
|
|
104
|
+
{
|
|
105
|
+
label: "skill",
|
|
106
|
+
value: dest,
|
|
107
|
+
note: link ? "linking" : "copying",
|
|
108
|
+
finalNote: link ? "linked" : "copied",
|
|
109
|
+
},
|
|
110
|
+
() => installDir({ src: skill.path, dest, link, force }),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Register the slidemind MCP server so agents can list/push into open
|
|
115
|
+
// PowerPoint windows via slidemind_list_open_presentations / slidemind_push_deck.
|
|
116
|
+
registerMcpServer(packageRoot);
|
|
117
|
+
ui.step({ status: "ok", label: "mcp server", value: claudeUserConfigFile() });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function uninstall() {
|
|
121
|
+
await ui.section("Claude Code", claudeRoot());
|
|
122
|
+
|
|
123
|
+
if (fs.existsSync(legacyCommandsDir())) {
|
|
124
|
+
rmIfExists(legacyCommandsDir());
|
|
125
|
+
ui.step({ status: "remove", label: "legacy", value: legacyCommandsDir() });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const skillsRoot = path.join(claudeRoot(), "skills");
|
|
129
|
+
if (fs.existsSync(skillsRoot)) {
|
|
130
|
+
for (const entry of fs.readdirSync(skillsRoot)) {
|
|
131
|
+
if (entry.startsWith(`${NAMESPACE}-`)) {
|
|
132
|
+
const target = path.join(skillsRoot, entry);
|
|
133
|
+
rmIfExists(target);
|
|
134
|
+
ui.step({ status: "remove", label: "skill", value: target });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
unregisterMcpServer();
|
|
140
|
+
ui.step({ status: "info", label: "mcp server", value: "unregistered" });
|
|
141
|
+
}
|