slides-grab 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/AGENTS.md +80 -0
  2. package/LICENSE +21 -0
  3. package/PROGRESS.md +39 -0
  4. package/README.md +120 -0
  5. package/SETUP.md +51 -0
  6. package/bin/ppt-agent.js +204 -0
  7. package/convert.cjs +184 -0
  8. package/package.json +51 -0
  9. package/prd.json +135 -0
  10. package/prd.md +104 -0
  11. package/scripts/editor-server.js +779 -0
  12. package/scripts/html2pdf.js +217 -0
  13. package/scripts/validate-slides.js +416 -0
  14. package/skills/ppt-design-skill/SKILL.md +38 -0
  15. package/skills/ppt-plan-skill/SKILL.md +37 -0
  16. package/skills/ppt-pptx-skill/SKILL.md +37 -0
  17. package/skills/ppt-presentation-skill/SKILL.md +57 -0
  18. package/src/editor/codex-edit.js +213 -0
  19. package/src/editor/editor.html +1733 -0
  20. package/src/editor/js/editor-bbox.js +332 -0
  21. package/src/editor/js/editor-chat.js +56 -0
  22. package/src/editor/js/editor-direct-edit.js +110 -0
  23. package/src/editor/js/editor-dom.js +55 -0
  24. package/src/editor/js/editor-init.js +284 -0
  25. package/src/editor/js/editor-navigation.js +54 -0
  26. package/src/editor/js/editor-select.js +264 -0
  27. package/src/editor/js/editor-send.js +157 -0
  28. package/src/editor/js/editor-sse.js +163 -0
  29. package/src/editor/js/editor-state.js +32 -0
  30. package/src/editor/js/editor-utils.js +167 -0
  31. package/src/editor/screenshot.js +73 -0
  32. package/src/resolve.js +159 -0
  33. package/templates/chart.html +121 -0
  34. package/templates/closing.html +54 -0
  35. package/templates/content.html +50 -0
  36. package/templates/contents.html +60 -0
  37. package/templates/cover.html +64 -0
  38. package/templates/custom/.gitkeep +0 -0
  39. package/templates/custom/README.md +7 -0
  40. package/templates/diagram.html +98 -0
  41. package/templates/quote.html +31 -0
  42. package/templates/section-divider.html +43 -0
  43. package/templates/split-layout.html +41 -0
  44. package/templates/statistics.html +55 -0
  45. package/templates/team.html +49 -0
  46. package/templates/timeline.html +59 -0
  47. package/themes/corporate.css +8 -0
  48. package/themes/executive.css +10 -0
  49. package/themes/modern-dark.css +9 -0
  50. package/themes/sage.css +9 -0
  51. package/themes/warm.css +8 -0
@@ -0,0 +1,284 @@
1
+ // editor-init.js — Entry point: imports, event bindings, init()
2
+
3
+ import { state, TOOL_MODE_DRAW, TOOL_MODE_SELECT } from './editor-state.js';
4
+ import {
5
+ btnPrev, btnNext, slideIframe, drawLayer, promptInput, modelSelect,
6
+ btnSend, btnClearBboxes, slideCounter,
7
+ toggleBold, toggleItalic, toggleUnderline, toggleStrike,
8
+ alignLeft, alignCenter, alignRight,
9
+ popoverTextInput, popoverApplyText, popoverTextColorInput, popoverBgColorInput,
10
+ popoverSizeInput, popoverApplySize, toolModeDrawBtn, toolModeSelectBtn,
11
+ } from './editor-dom.js';
12
+ import {
13
+ currentSlideFile, getSlideState, normalizeModelName, setStatus,
14
+ saveSelectedModel, loadModelOptions, clamp,
15
+ } from './editor-utils.js';
16
+ import { renderChatMessages } from './editor-chat.js';
17
+ import {
18
+ onBboxChange, renderBboxes, scaleSlide, startDrawing, moveDrawing, endDrawing,
19
+ clearBboxesForCurrentSlide, initBboxLayerEvents, getXPath,
20
+ } from './editor-bbox.js';
21
+ import {
22
+ setToolMode, updateToolModeUI, renderObjectSelection, updateObjectEditorControls,
23
+ getSelectedObjectElement, setSelectedObjectXPath, updateHoveredObjectFromPointer,
24
+ clearHoveredObject, getSelectableTargetAt, readSelectedObjectStyleState,
25
+ } from './editor-select.js';
26
+ import {
27
+ mutateSelectedObject, applyTextDecorationToken,
28
+ } from './editor-direct-edit.js';
29
+ import { updateSendState, applyChanges } from './editor-send.js';
30
+ import { goToSlide } from './editor-navigation.js';
31
+ import { connectSSE, loadRunsInitial } from './editor-sse.js';
32
+
33
+ // Late-binding: connect bbox changes to updateSendState
34
+ onBboxChange(updateSendState);
35
+
36
+ // Bbox layer events
37
+ initBboxLayerEvents();
38
+
39
+ // Navigation
40
+ btnPrev.addEventListener('click', () => { void goToSlide(state.currentIndex - 1); });
41
+ btnNext.addEventListener('click', () => { void goToSlide(state.currentIndex + 1); });
42
+
43
+ // Tool modes
44
+ toolModeDrawBtn.addEventListener('click', () => setToolMode(TOOL_MODE_DRAW));
45
+ toolModeSelectBtn.addEventListener('click', () => setToolMode(TOOL_MODE_SELECT));
46
+
47
+ // Clear bboxes
48
+ btnClearBboxes.addEventListener('click', clearBboxesForCurrentSlide);
49
+
50
+ // Drawing
51
+ drawLayer.addEventListener('mousedown', startDrawing);
52
+ drawLayer.addEventListener('mousemove', (event) => {
53
+ if (state.toolMode !== TOOL_MODE_SELECT) return;
54
+ updateHoveredObjectFromPointer(event.clientX, event.clientY);
55
+ });
56
+ drawLayer.addEventListener('mouseleave', clearHoveredObject);
57
+ drawLayer.addEventListener('click', (event) => {
58
+ if (state.toolMode !== TOOL_MODE_SELECT) return;
59
+ const target = getSelectableTargetAt(event.clientX, event.clientY);
60
+ if (!target) {
61
+ setSelectedObjectXPath('', 'No selectable object at this point.');
62
+ return;
63
+ }
64
+
65
+ const xpath = getXPath(target);
66
+ setSelectedObjectXPath(xpath, `Object selected on ${currentSlideFile()}.`);
67
+ });
68
+ window.addEventListener('mousemove', moveDrawing);
69
+ window.addEventListener('mouseup', endDrawing);
70
+
71
+ // Send
72
+ btnSend.addEventListener('click', applyChanges);
73
+
74
+ // Model select
75
+ modelSelect.addEventListener('change', () => {
76
+ const nextModel = normalizeModelName(modelSelect.value);
77
+ if (!state.availableModels.includes(nextModel)) {
78
+ modelSelect.value = state.selectedModel;
79
+ return;
80
+ }
81
+
82
+ const slide = currentSlideFile();
83
+ if (slide) {
84
+ const ss = getSlideState(slide);
85
+ ss.model = nextModel;
86
+ }
87
+ state.selectedModel = nextModel;
88
+ state.defaultModel = nextModel;
89
+ saveSelectedModel(state.selectedModel);
90
+ updateSendState();
91
+ setStatus(`Model selected: ${state.selectedModel}`);
92
+ });
93
+
94
+ // Prompt input
95
+ promptInput.addEventListener('input', () => {
96
+ const slide = currentSlideFile();
97
+ if (slide) {
98
+ const ss = getSlideState(slide);
99
+ ss.prompt = promptInput.value;
100
+ }
101
+ updateSendState();
102
+ });
103
+
104
+ // Text editing
105
+ popoverApplyText.addEventListener('click', () => {
106
+ if (popoverApplyText.disabled) return;
107
+ mutateSelectedObject((el) => {
108
+ const escaped = popoverTextInput.value
109
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
110
+ el.innerHTML = escaped.replace(/\n/g, '<br>');
111
+ }, 'Object text updated and saved.', { delay: 120 });
112
+ });
113
+
114
+ popoverApplySize.addEventListener('click', () => {
115
+ if (popoverApplySize.disabled) return;
116
+ const size = clamp(Number.parseInt(popoverSizeInput.value || '24', 10) || 24, 8, 180);
117
+ mutateSelectedObject((el) => {
118
+ el.style.fontSize = `${size}px`;
119
+ }, 'Object font size updated and saved.');
120
+ });
121
+
122
+ popoverTextInput.addEventListener('keydown', (event) => {
123
+ if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
124
+ event.preventDefault();
125
+ event.stopPropagation();
126
+ popoverApplyText.click();
127
+ }
128
+ });
129
+
130
+ popoverSizeInput.addEventListener('keydown', (event) => {
131
+ if (event.key === 'Enter') {
132
+ event.preventDefault();
133
+ event.stopPropagation();
134
+ popoverApplySize.click();
135
+ }
136
+ });
137
+
138
+ popoverTextColorInput.addEventListener('input', () => {
139
+ if (popoverTextColorInput.disabled) return;
140
+ mutateSelectedObject((el) => {
141
+ el.style.color = popoverTextColorInput.value;
142
+ }, 'Text color updated.', { delay: 300 });
143
+ });
144
+
145
+ popoverBgColorInput.addEventListener('input', () => {
146
+ if (popoverBgColorInput.disabled) return;
147
+ mutateSelectedObject((el) => {
148
+ el.style.backgroundColor = popoverBgColorInput.value;
149
+ }, 'Background color updated.', { delay: 300 });
150
+ });
151
+
152
+ // Style toggles
153
+ toggleBold.addEventListener('click', () => {
154
+ mutateSelectedObject((el) => {
155
+ const nextBold = !readSelectedObjectStyleState(el).bold;
156
+ el.style.fontWeight = nextBold ? '700' : '400';
157
+ }, 'Object font weight updated and saved.');
158
+ });
159
+
160
+ toggleItalic.addEventListener('click', () => {
161
+ mutateSelectedObject((el) => {
162
+ const nextItalic = !readSelectedObjectStyleState(el).italic;
163
+ el.style.fontStyle = nextItalic ? 'italic' : 'normal';
164
+ }, 'Object font style updated and saved.');
165
+ });
166
+
167
+ toggleUnderline.addEventListener('click', () => {
168
+ mutateSelectedObject((el) => {
169
+ const nextUnderline = !readSelectedObjectStyleState(el).underline;
170
+ applyTextDecorationToken(el, 'underline', nextUnderline);
171
+ }, 'Object underline updated and saved.');
172
+ });
173
+
174
+ toggleStrike.addEventListener('click', () => {
175
+ mutateSelectedObject((el) => {
176
+ const nextStrike = !readSelectedObjectStyleState(el).strike;
177
+ applyTextDecorationToken(el, 'line-through', nextStrike);
178
+ }, 'Object strikethrough updated and saved.');
179
+ });
180
+
181
+ // Alignment
182
+ alignLeft.addEventListener('click', () => {
183
+ mutateSelectedObject((el) => {
184
+ el.style.textAlign = 'left';
185
+ }, 'Object alignment updated and saved.');
186
+ });
187
+
188
+ alignCenter.addEventListener('click', () => {
189
+ mutateSelectedObject((el) => {
190
+ el.style.textAlign = 'center';
191
+ }, 'Object alignment updated and saved.');
192
+ });
193
+
194
+ alignRight.addEventListener('click', () => {
195
+ mutateSelectedObject((el) => {
196
+ el.style.textAlign = 'right';
197
+ }, 'Object alignment updated and saved.');
198
+ });
199
+
200
+ // Global keyboard
201
+ document.addEventListener('keydown', (event) => {
202
+ const inPromptField = document.activeElement === promptInput;
203
+
204
+ if (state.toolMode === TOOL_MODE_SELECT && (event.ctrlKey || event.metaKey) && !inPromptField) {
205
+ const key = event.key.toLowerCase();
206
+ if (key === 'b') { event.preventDefault(); if (!toggleBold.disabled) toggleBold.click(); return; }
207
+ if (key === 'i') { event.preventDefault(); if (!toggleItalic.disabled) toggleItalic.click(); return; }
208
+ if (key === 'u') { event.preventDefault(); if (!toggleUnderline.disabled) toggleUnderline.click(); return; }
209
+ }
210
+
211
+ if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
212
+ event.preventDefault();
213
+ applyChanges();
214
+ return;
215
+ }
216
+
217
+ if (event.key === 'Escape') {
218
+ if (document.activeElement) document.activeElement.blur();
219
+ return;
220
+ }
221
+
222
+ if (inPromptField) return;
223
+
224
+ if (event.key === 'ArrowLeft') {
225
+ event.preventDefault();
226
+ void goToSlide(state.currentIndex - 1);
227
+ } else if (event.key === 'ArrowRight') {
228
+ event.preventDefault();
229
+ void goToSlide(state.currentIndex + 1);
230
+ }
231
+ });
232
+
233
+ // Resize
234
+ window.addEventListener('resize', scaleSlide);
235
+
236
+ // Iframe load
237
+ slideIframe.addEventListener('load', () => {
238
+ const slide = currentSlideFile();
239
+ if (slide) {
240
+ const ss = getSlideState(slide);
241
+ if (ss.selectedObjectXPath && !getSelectedObjectElement(slide)) {
242
+ ss.selectedObjectXPath = '';
243
+ }
244
+ }
245
+ state.hoveredObjectXPath = '';
246
+ renderBboxes();
247
+ renderObjectSelection();
248
+ updateObjectEditorControls();
249
+ updateSendState();
250
+ });
251
+
252
+ // Init
253
+ async function init() {
254
+ setStatus('Loading slide list...');
255
+
256
+ try {
257
+ const res = await fetch('/api/slides');
258
+ if (!res.ok) {
259
+ throw new Error(`Failed to fetch slide list: ${res.status}`);
260
+ }
261
+
262
+ state.slides = await res.json();
263
+
264
+ if (state.slides.length === 0) {
265
+ setStatus('No slides found.');
266
+ slideCounter.textContent = '0 / 0';
267
+ return;
268
+ }
269
+
270
+ await loadModelOptions();
271
+ updateToolModeUI();
272
+ await goToSlide(0);
273
+ scaleSlide();
274
+ await loadRunsInitial();
275
+ connectSSE();
276
+
277
+ setStatus(`Ready. Model: ${state.selectedModel}. Draw red pending bboxes, run Codex, then review green bboxes.`);
278
+ } catch (error) {
279
+ setStatus(`Error loading slides: ${error.message}`);
280
+ console.error('Init error:', error);
281
+ }
282
+ }
283
+
284
+ init();
@@ -0,0 +1,54 @@
1
+ // editor-navigation.js — Slide navigation (goToSlide, persistDraft)
2
+
3
+ import { state } from './editor-state.js';
4
+ import {
5
+ slideIframe, slideCounter, btnPrev, btnNext, sessionFileChip,
6
+ promptInput, modelSelect,
7
+ } from './editor-dom.js';
8
+ import { currentSlideFile, getSlideState, normalizeModelName, setStatus } from './editor-utils.js';
9
+ import { renderChatMessages } from './editor-chat.js';
10
+ import { renderBboxes, scaleSlide } from './editor-bbox.js';
11
+ import { renderObjectSelection, updateObjectEditorControls } from './editor-select.js';
12
+ import { flushDirectSaveForSlide } from './editor-direct-edit.js';
13
+ import { updateSendState } from './editor-send.js';
14
+
15
+ export function persistCurrentSlideDraft() {
16
+ const slide = currentSlideFile();
17
+ if (!slide) return;
18
+ const ss = getSlideState(slide);
19
+ ss.prompt = promptInput.value;
20
+ ss.model = normalizeModelName(modelSelect.value) || ss.model || state.defaultModel;
21
+ }
22
+
23
+ export async function goToSlide(index) {
24
+ if (index < 0 || index >= state.slides.length) return;
25
+
26
+ const previousSlide = currentSlideFile();
27
+ persistCurrentSlideDraft();
28
+ if (previousSlide) {
29
+ await flushDirectSaveForSlide(previousSlide);
30
+ }
31
+
32
+ state.currentIndex = index;
33
+ const slide = currentSlideFile();
34
+ slideIframe.src = `/slides/${slide}`;
35
+ if (sessionFileChip) sessionFileChip.textContent = slide;
36
+ slideCounter.textContent = `${state.currentIndex + 1} / ${state.slides.length}`;
37
+ btnPrev.disabled = state.currentIndex === 0;
38
+ btnNext.disabled = state.currentIndex === state.slides.length - 1;
39
+
40
+ const ss = getSlideState(slide);
41
+ if (!state.availableModels.includes(normalizeModelName(ss.model))) {
42
+ ss.model = state.defaultModel;
43
+ }
44
+ state.selectedModel = ss.model;
45
+ modelSelect.value = state.selectedModel;
46
+ promptInput.value = ss.prompt || '';
47
+ state.hoveredObjectXPath = '';
48
+ renderChatMessages();
49
+ renderBboxes();
50
+ renderObjectSelection();
51
+ updateObjectEditorControls();
52
+ updateSendState();
53
+ setStatus(`Loaded ${slide}`);
54
+ }
@@ -0,0 +1,264 @@
1
+ // editor-select.js — Object selection, hover, tool mode UI
2
+
3
+ import { state, TOOL_MODE_DRAW, TOOL_MODE_SELECT, SLIDE_W, SLIDE_H, NON_SELECTABLE_TAGS, DIRECT_TEXT_TAGS } from './editor-state.js';
4
+ import {
5
+ slideIframe, slidePanel, drawBox, toolModeDrawBtn, toolModeSelectBtn,
6
+ bboxToolbar, selectToolbar, editorHint, objectSelectedBox, objectHoverBox,
7
+ selectedObjectMini, miniTag, miniText, selectEmptyHint,
8
+ toggleBold, toggleItalic, toggleUnderline, toggleStrike,
9
+ alignLeft, alignCenter, alignRight,
10
+ popoverTextInput, popoverApplyText, popoverTextColorInput, popoverBgColorInput,
11
+ popoverSizeInput, popoverApplySize,
12
+ } from './editor-dom.js';
13
+ import {
14
+ currentSlideFile, getSlideState, setStatus, clamp,
15
+ normalizeHexColor, parsePixelValue, isBoldFontWeight,
16
+ } from './editor-utils.js';
17
+ import { renderBboxes, scaleSlide, clientToSlidePoint, getXPath } from './editor-bbox.js';
18
+
19
+ function isElementNode(node) {
20
+ return Boolean(node) && node.nodeType === Node.ELEMENT_NODE;
21
+ }
22
+
23
+ export function resolveXPath(doc, xpath) {
24
+ if (!doc || typeof xpath !== 'string' || xpath.trim() === '') return null;
25
+ try {
26
+ return doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export function getSelectedObjectElement(slide = currentSlideFile()) {
33
+ if (!slide) return null;
34
+ const ss = getSlideState(slide);
35
+ const xpath = ss.selectedObjectXPath;
36
+ if (!xpath) return null;
37
+
38
+ const doc = slideIframe.contentDocument;
39
+ const el = resolveXPath(doc, xpath);
40
+ return isElementNode(el) ? el : null;
41
+ }
42
+
43
+ export function isSelectableElement(el) {
44
+ if (!isElementNode(el)) return false;
45
+ const tag = el.tagName.toLowerCase();
46
+ if (NON_SELECTABLE_TAGS.has(tag)) return false;
47
+ const rect = el.getBoundingClientRect();
48
+ return rect.width > 1 && rect.height > 1;
49
+ }
50
+
51
+ export function isTextEditableElement(el) {
52
+ if (!isElementNode(el)) return false;
53
+ const tag = el.tagName.toLowerCase();
54
+ if (!DIRECT_TEXT_TAGS.has(tag)) return false;
55
+ const INLINE_TAGS = new Set(['BR','B','STRONG','I','EM','U','S','SPAN','A','SMALL','SUB','SUP','MARK','CODE']);
56
+ return Array.from(el.children).every(c => INLINE_TAGS.has(c.tagName));
57
+ }
58
+
59
+ export function getSelectableTargetAt(clientX, clientY) {
60
+ const doc = slideIframe.contentDocument;
61
+ if (!doc) return null;
62
+
63
+ const point = clientToSlidePoint(clientX, clientY);
64
+ let node = doc.elementFromPoint(point.x, point.y);
65
+ while (node && !isSelectableElement(node)) {
66
+ node = node.parentElement;
67
+ }
68
+ return isElementNode(node) ? node : null;
69
+ }
70
+
71
+ export function elementToSlideRect(el) {
72
+ if (!isElementNode(el)) return null;
73
+ const rect = el.getBoundingClientRect();
74
+ if (!rect.width || !rect.height) return null;
75
+ return {
76
+ x: clamp(Math.round(rect.left), 0, SLIDE_W),
77
+ y: clamp(Math.round(rect.top), 0, SLIDE_H),
78
+ width: Math.max(1, Math.round(rect.width)),
79
+ height: Math.max(1, Math.round(rect.height)),
80
+ };
81
+ }
82
+
83
+ export function applyOverlayRect(node, rect) {
84
+ if (!node || !rect) {
85
+ if (node) node.style.display = 'none';
86
+ return;
87
+ }
88
+
89
+ node.style.display = 'block';
90
+ node.style.left = `${rect.x}px`;
91
+ node.style.top = `${rect.y}px`;
92
+ node.style.width = `${rect.width}px`;
93
+ node.style.height = `${rect.height}px`;
94
+ }
95
+
96
+ export function readSelectedObjectStyleState(el) {
97
+ const frameWindow = slideIframe.contentWindow;
98
+ const styles = frameWindow?.getComputedStyle ? frameWindow.getComputedStyle(el) : null;
99
+ const textDecorationLine = styles?.textDecorationLine || '';
100
+ return {
101
+ textEditable: isTextEditableElement(el),
102
+ textValue: (el.innerHTML || '').replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, '').trim(),
103
+ textColor: normalizeHexColor(styles?.color, '#111111'),
104
+ backgroundColor: normalizeHexColor(styles?.backgroundColor, '#ffffff'),
105
+ fontSize: parsePixelValue(styles?.fontSize, 24),
106
+ bold: isBoldFontWeight(styles?.fontWeight),
107
+ italic: styles?.fontStyle === 'italic',
108
+ underline: /\bunderline\b/.test(textDecorationLine),
109
+ strike: /\bline-through\b/.test(textDecorationLine),
110
+ textAlign: styles?.textAlign || 'left',
111
+ };
112
+ }
113
+
114
+ function setToggleActive(button, active) {
115
+ if (!button) return;
116
+ button.classList.toggle('active', Boolean(active));
117
+ button.setAttribute('aria-pressed', active ? 'true' : 'false');
118
+ }
119
+
120
+ function setControlEnabled(button, enabled) {
121
+ if (!button) return;
122
+ button.disabled = !enabled;
123
+ button.setAttribute('aria-disabled', enabled ? 'false' : 'true');
124
+ }
125
+
126
+ export function getSelectedObjectCapabilities(el) {
127
+ const textEditable = isTextEditableElement(el);
128
+ return {
129
+ textEditable,
130
+ textColorEditable: textEditable,
131
+ backgroundEditable: isElementNode(el),
132
+ sizeEditable: textEditable,
133
+ emphasisEditable: textEditable,
134
+ alignEditable: textEditable,
135
+ };
136
+ }
137
+
138
+ function syncInlineInputs(snapshot) {
139
+ if (!document.activeElement || !document.activeElement.closest?.('#select-toolbar')) {
140
+ popoverTextInput.value = snapshot?.textValue || '';
141
+ popoverSizeInput.value = String(snapshot?.fontSize || 24);
142
+ }
143
+ popoverTextColorInput.value = snapshot?.textColor || '#111111';
144
+ popoverBgColorInput.value = snapshot?.backgroundColor || '#ffffff';
145
+ }
146
+
147
+ export function updateObjectEditorControls() {
148
+ const selected = state.toolMode === TOOL_MODE_SELECT ? getSelectedObjectElement() : null;
149
+ const snapshot = selected ? readSelectedObjectStyleState(selected) : null;
150
+ const capabilities = getSelectedObjectCapabilities(selected);
151
+
152
+ if (bboxToolbar) {
153
+ bboxToolbar.hidden = state.toolMode !== TOOL_MODE_DRAW;
154
+ }
155
+ if (selectToolbar) {
156
+ selectToolbar.hidden = state.toolMode !== TOOL_MODE_SELECT;
157
+ }
158
+
159
+ if (selectedObjectMini && selectEmptyHint) {
160
+ if (selected) {
161
+ const tag = selected.tagName.toLowerCase();
162
+ const fullText = (selected.textContent || '').trim();
163
+ const preview = fullText.slice(0, 24);
164
+ miniTag.textContent = `<${tag}>`;
165
+ miniText.textContent = preview ? ` ${preview}${fullText.length > 24 ? '\u2026' : ''}` : '';
166
+ selectedObjectMini.style.display = '';
167
+ selectEmptyHint.style.display = 'none';
168
+ } else {
169
+ selectedObjectMini.style.display = 'none';
170
+ selectEmptyHint.style.display = state.toolMode === TOOL_MODE_SELECT ? '' : 'none';
171
+ }
172
+ }
173
+
174
+ popoverTextInput.disabled = !capabilities.textEditable;
175
+ popoverApplyText.disabled = !capabilities.textEditable;
176
+ popoverTextColorInput.disabled = !capabilities.textColorEditable;
177
+ popoverBgColorInput.disabled = !capabilities.backgroundEditable;
178
+ popoverSizeInput.disabled = !capabilities.sizeEditable;
179
+ popoverApplySize.disabled = !capabilities.sizeEditable;
180
+
181
+ setControlEnabled(toggleBold, capabilities.emphasisEditable);
182
+ setControlEnabled(toggleItalic, capabilities.emphasisEditable);
183
+ setControlEnabled(toggleUnderline, capabilities.emphasisEditable);
184
+ setControlEnabled(toggleStrike, capabilities.emphasisEditable);
185
+ setControlEnabled(alignLeft, capabilities.alignEditable);
186
+ setControlEnabled(alignCenter, capabilities.alignEditable);
187
+ setControlEnabled(alignRight, capabilities.alignEditable);
188
+
189
+ setToggleActive(toggleBold, capabilities.emphasisEditable && snapshot?.bold);
190
+ setToggleActive(toggleItalic, capabilities.emphasisEditable && snapshot?.italic);
191
+ setToggleActive(toggleUnderline, capabilities.emphasisEditable && snapshot?.underline);
192
+ setToggleActive(toggleStrike, capabilities.emphasisEditable && snapshot?.strike);
193
+ setToggleActive(alignLeft, capabilities.alignEditable && (snapshot?.textAlign === 'left' || snapshot?.textAlign === 'start'));
194
+ setToggleActive(alignCenter, capabilities.alignEditable && snapshot?.textAlign === 'center');
195
+ setToggleActive(alignRight, capabilities.alignEditable && (snapshot?.textAlign === 'right' || snapshot?.textAlign === 'end'));
196
+
197
+ syncInlineInputs(snapshot);
198
+ }
199
+
200
+ export function renderObjectSelection() {
201
+ const selectedEl = state.toolMode === TOOL_MODE_SELECT ? getSelectedObjectElement() : null;
202
+ const hoveredEl = state.toolMode === TOOL_MODE_SELECT
203
+ ? resolveXPath(slideIframe.contentDocument, state.hoveredObjectXPath)
204
+ : null;
205
+
206
+ const selectedRect = selectedEl ? elementToSlideRect(selectedEl) : null;
207
+ const hoveredRect = hoveredEl && hoveredEl !== selectedEl ? elementToSlideRect(hoveredEl) : null;
208
+ applyOverlayRect(objectSelectedBox, selectedRect);
209
+ applyOverlayRect(objectHoverBox, hoveredRect);
210
+ }
211
+
212
+ export function updateToolModeUI() {
213
+ const isDraw = state.toolMode === TOOL_MODE_DRAW;
214
+ slidePanel.classList.toggle('mode-draw', isDraw);
215
+ slidePanel.classList.toggle('mode-select', !isDraw);
216
+ toolModeDrawBtn.classList.toggle('active', isDraw);
217
+ toolModeSelectBtn.classList.toggle('active', !isDraw);
218
+ toolModeDrawBtn.setAttribute('aria-pressed', isDraw ? 'true' : 'false');
219
+ toolModeSelectBtn.setAttribute('aria-pressed', !isDraw ? 'true' : 'false');
220
+ editorHint.textContent = isDraw
221
+ ? 'Drag on the slide to add red bboxes. Cmd/Ctrl+Enter to run.'
222
+ : 'Click an object to edit. \u2318B bold \u00b7 \u2318I italic \u00b7 \u2318U underline';
223
+ renderBboxes();
224
+ renderObjectSelection();
225
+ updateObjectEditorControls();
226
+ scaleSlide();
227
+ }
228
+
229
+ export function setToolMode(mode) {
230
+ state.toolMode = mode === TOOL_MODE_SELECT ? TOOL_MODE_SELECT : TOOL_MODE_DRAW;
231
+ state.drawing = false;
232
+ state.drawStart = null;
233
+ drawBox.style.display = 'none';
234
+ if (state.toolMode !== TOOL_MODE_SELECT) {
235
+ state.hoveredObjectXPath = '';
236
+ }
237
+ updateToolModeUI();
238
+ setStatus(state.toolMode === TOOL_MODE_SELECT ? 'Select mode enabled.' : 'BBox draw mode enabled.');
239
+ }
240
+
241
+ export function setSelectedObjectXPath(xpath, statusMessage = 'Object selected.') {
242
+ const slide = currentSlideFile();
243
+ if (!slide) return;
244
+ const ss = getSlideState(slide);
245
+ ss.selectedObjectXPath = xpath || '';
246
+ state.hoveredObjectXPath = xpath || '';
247
+ renderObjectSelection();
248
+ updateObjectEditorControls();
249
+ if (statusMessage) {
250
+ setStatus(statusMessage);
251
+ }
252
+ }
253
+
254
+ export function updateHoveredObjectFromPointer(clientX, clientY) {
255
+ if (state.toolMode !== TOOL_MODE_SELECT) return;
256
+ const target = getSelectableTargetAt(clientX, clientY);
257
+ state.hoveredObjectXPath = target ? getXPath(target) : '';
258
+ renderObjectSelection();
259
+ }
260
+
261
+ export function clearHoveredObject() {
262
+ state.hoveredObjectXPath = '';
263
+ renderObjectSelection();
264
+ }