@lemonppt/renderer 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lemonforme
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ export declare const editorScript = "\n(function () {\n const STORAGE_KEY = 'lemonppt:editor:' + (window.__lemonPPT_goal?.randomSeed || window.__lemonPPT_goal?.title || 'default');\n const MAX_HISTORY = 50;\n\n let goal = window.__lemonPPT_goal;\n if (!goal) return;\n\n // \u4F18\u5148\u4ECE localStorage \u6062\u590D\n try {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n const parsed = JSON.parse(saved);\n if (parsed && parsed.slides) {\n goal = parsed;\n window.__lemonPPT_goal = goal;\n syncDomFromGoal();\n }\n }\n } catch (err) {\n console.warn('\u81EA\u52A8\u6062\u590D\u5931\u8D25', err);\n }\n\n const history = [deepClone(goal)];\n let historyIndex = 0;\n\n function deepClone(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n\n function recordHistory() {\n // \u622A\u65AD redo \u5206\u652F\n if (historyIndex < history.length - 1) {\n history.splice(historyIndex + 1);\n }\n history.push(deepClone(goal));\n if (history.length > MAX_HISTORY) {\n history.shift();\n } else {\n historyIndex++;\n }\n updateUndoRedoButtons();\n }\n\n function undo() {\n if (historyIndex <= 0) return;\n historyIndex--;\n restoreGoal(history[historyIndex]);\n }\n\n function redo() {\n if (historyIndex >= history.length - 1) return;\n historyIndex++;\n restoreGoal(history[historyIndex]);\n }\n\n function restoreGoal(newGoal) {\n goal = newGoal;\n window.__lemonPPT_goal = goal;\n Object.assign(window.__lemonPPT_goal, newGoal);\n syncDomFromGoal();\n autoSave();\n updateUndoRedoButtons();\n }\n\n function autoSave() {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(goal));\n } catch (err) {\n console.warn('\u81EA\u52A8\u4FDD\u5B58\u5931\u8D25', err);\n }\n }\n\n function syncDomFromGoal() {\n // \u6587\u672C\n document.querySelectorAll('[data-lp-editable=\"true\"]').forEach((el) => {\n const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));\n const prop = el.getAttribute('data-lp-prop');\n if (Number.isNaN(slideIdx) || !prop) return;\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n const value = getProp(slide.props, prop);\n el.textContent = value == null ? '' : String(value);\n });\n\n // \u56FE\u7247\n document.querySelectorAll('[data-lp-editable-image=\"true\"]').forEach((img) => {\n const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));\n const prop = img.getAttribute('data-lp-prop') || 'image';\n if (Number.isNaN(slideIdx)) return;\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n const value = getProp(slide.props, prop);\n if (value) {\n img.setAttribute('src', String(value));\n } else {\n img.remove();\n }\n });\n }\n\n function getProp(obj, path) {\n const keys = path.split('.');\n let target = obj;\n for (const k of keys) {\n if (target == null || typeof target !== 'object') return undefined;\n target = target[k];\n }\n return target;\n }\n\n function setProp(obj, path, value) {\n const keys = path.split('.');\n let target = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n if (!(k in target)) target[k] = [];\n target = target[k];\n }\n target[keys[keys.length - 1]] = value;\n }\n\n // \u56FE\u7247\u7F16\u8F91\u5F39\u7A97\n function openImageEditor(img, slideIdx, prop) {\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n\n const overlay = document.createElement('div');\n overlay.className = 'lp-image-editor-overlay';\n overlay.innerHTML = `\n <div class=\"lp-image-editor\">\n <h3>\u7F16\u8F91\u56FE\u7247</h3>\n <label>\n <span>\u56FE\u7247 URL</span>\n <input type=\"text\" class=\"lp-image-url\" placeholder=\"https://...\" value=\"${escapeHtml(img.getAttribute('src') || '')}\">\n </label>\n <div class=\"lp-image-or\">\u6216</div>\n <label class=\"lp-image-file-label\">\n <span>\u4E0A\u4F20\u672C\u5730\u56FE\u7247</span>\n <input type=\"file\" class=\"lp-image-file\" accept=\"image/*\">\n </label>\n <div class=\"lp-image-preview-wrap\">\n <img class=\"lp-image-preview\" src=\"${escapeHtml(img.getAttribute('src') || '')}\" alt=\"\">\n </div>\n <div class=\"lp-image-actions\">\n <button class=\"lp-image-delete lp-image-btn-secondary\">\u5220\u9664\u56FE\u7247</button>\n <div class=\"lp-image-actions-right\">\n <button class=\"lp-image-cancel lp-image-btn-secondary\">\u53D6\u6D88</button>\n <button class=\"lp-image-confirm\">\u786E\u8BA4</button>\n </div>\n </div>\n </div>\n `;\n\n document.body.appendChild(overlay);\n\n const urlInput = overlay.querySelector('.lp-image-url');\n const fileInput = overlay.querySelector('.lp-image-file');\n const preview = overlay.querySelector('.lp-image-preview');\n const confirmBtn = overlay.querySelector('.lp-image-confirm');\n const cancelBtn = overlay.querySelector('.lp-image-cancel');\n const deleteBtn = overlay.querySelector('.lp-image-delete');\n\n let pendingValue = img.getAttribute('src') || '';\n\n urlInput.addEventListener('input', () => {\n pendingValue = urlInput.value;\n preview.src = pendingValue;\n });\n\n fileInput.addEventListener('change', () => {\n const file = fileInput.files[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = (e) => {\n pendingValue = e.target.result;\n urlInput.value = '';\n preview.src = pendingValue;\n };\n reader.readAsDataURL(file);\n });\n\n function close() {\n overlay.remove();\n }\n\n function apply() {\n recordHistory();\n if (pendingValue) {\n img.setAttribute('src', pendingValue);\n setProp(slide.props, prop, pendingValue);\n } else {\n img.remove();\n setProp(slide.props, prop, undefined);\n }\n autoSave();\n close();\n }\n\n function remove() {\n recordHistory();\n img.remove();\n setProp(slide.props, prop, undefined);\n autoSave();\n close();\n }\n\n confirmBtn.addEventListener('click', apply);\n cancelBtn.addEventListener('click', close);\n deleteBtn.addEventListener('click', remove);\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) close();\n });\n }\n\n function escapeHtml(str) {\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n const editableEls = document.querySelectorAll('[data-lp-editable=\"true\"]');\n\n editableEls.forEach((el) => {\n el.setAttribute('contenteditable', 'true');\n\n el.addEventListener('focus', () => {\n recordHistory();\n });\n\n el.addEventListener('blur', () => {\n const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));\n const prop = el.getAttribute('data-lp-prop');\n if (Number.isNaN(slideIdx) || !prop) return;\n\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n\n const value = el.textContent || '';\n setProp(slide.props, prop, value);\n autoSave();\n });\n\n el.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n el.blur();\n }\n });\n });\n\n // \u56FE\u7247\u6362\u56FE\n const imageEls = document.querySelectorAll('[data-lp-editable-image=\"true\"]');\n imageEls.forEach((img) => {\n img.addEventListener('click', () => {\n const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));\n const prop = img.getAttribute('data-lp-prop') || 'image';\n if (Number.isNaN(slideIdx)) return;\n openImageEditor(img, slideIdx, prop);\n });\n });\n\n const downloadBtn = document.getElementById('lp-download-goal');\n if (downloadBtn) {\n downloadBtn.addEventListener('click', () => {\n const blob = new Blob([JSON.stringify(goal, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'goal.json';\n a.click();\n URL.revokeObjectURL(url);\n });\n }\n\n const exportPptxBtn = document.getElementById('lp-export-pptx');\n if (exportPptxBtn) {\n exportPptxBtn.addEventListener('click', async () => {\n exportPptxBtn.textContent = '\u5BFC\u51FA\u4E2D...';\n exportPptxBtn.disabled = true;\n try {\n const res = await fetch('/api/export/pptx', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u5BFC\u51FA\u5931\u8D25: ' + res.status);\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.pptx';\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n exportPptxBtn.textContent = '\u5BFC\u51FA PPTX';\n exportPptxBtn.disabled = false;\n }\n });\n }\n\n const exportPdfBtn = document.getElementById('lp-export-pdf');\n if (exportPdfBtn) {\n exportPdfBtn.addEventListener('click', async () => {\n exportPdfBtn.textContent = '\u5BFC\u51FA\u4E2D...';\n exportPdfBtn.disabled = true;\n try {\n const res = await fetch('/api/export/pdf', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u5BFC\u51FA\u5931\u8D25: ' + res.status);\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.pdf';\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n exportPdfBtn.textContent = '\u5BFC\u51FA PDF';\n exportPdfBtn.disabled = false;\n }\n });\n }\n\n // \u64A4\u9500 / \u91CD\u505A\n const undoBtn = document.getElementById('lp-undo');\n const redoBtn = document.getElementById('lp-redo');\n\n function updateUndoRedoButtons() {\n if (undoBtn) undoBtn.disabled = historyIndex <= 0;\n if (redoBtn) redoBtn.disabled = historyIndex >= history.length - 1;\n }\n\n if (undoBtn) undoBtn.addEventListener('click', undo);\n if (redoBtn) redoBtn.addEventListener('click', redo);\n\n // \u4E3B\u9898\u5207\u6362\n const themeSwitcher = document.getElementById('lp-theme-switcher');\n if (themeSwitcher) {\n themeSwitcher.value = goal.theme || 'minimal';\n themeSwitcher.addEventListener('change', () => {\n const newTheme = themeSwitcher.value;\n if (newTheme === goal.theme) return;\n goal.theme = newTheme;\n autoSave();\n window.location.href = '/editor?theme=' + encodeURIComponent(newTheme);\n });\n }\n\n document.addEventListener('keydown', (e) => {\n const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n const mod = isMac ? e.metaKey : e.ctrlKey;\n if (mod && e.key.toLowerCase() === 'z') {\n e.preventDefault();\n if (e.shiftKey) redo();\n else undo();\n }\n if (mod && e.key.toLowerCase() === 'y') {\n e.preventDefault();\n redo();\n }\n });\n\n updateUndoRedoButtons();\n\n // \u7FFB\u9875\u811A\u672C\n const slides = document.querySelectorAll('.lp-slide-wrapper');\n const thumbnails = document.querySelectorAll('.lp-thumbnail');\n const prevBtn = document.getElementById('lp-prev');\n const nextBtn = document.getElementById('lp-next');\n const currentLabel = document.getElementById('lp-current');\n let current = 0;\n\n function updateClasses() {\n slides.forEach((slide, index) => {\n slide.classList.remove('active', 'prev');\n if (index === current) slide.classList.add('active');\n else if (index < current) slide.classList.add('prev');\n });\n thumbnails.forEach((thumb, index) => {\n thumb.classList.toggle('active', index === current);\n });\n const activeThumb = thumbnails[current];\n if (activeThumb) activeThumb.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n if (currentLabel) currentLabel.textContent = String(current + 1);\n if (prevBtn) prevBtn.disabled = current === 0;\n if (nextBtn) nextBtn.disabled = current === slides.length - 1;\n }\n\n function goTo(index) {\n if (index < 0 || index >= slides.length) return;\n current = index;\n updateClasses();\n if (typeof selectSlide === 'function') selectSlide(current);\n }\n\n if (prevBtn) prevBtn.addEventListener('click', () => goTo(current - 1));\n if (nextBtn) nextBtn.addEventListener('click', () => goTo(current + 1));\n thumbnails.forEach((thumb) => {\n thumb.addEventListener('click', () => goTo(Number(thumb.dataset.index)));\n });\n function isEditingTarget(target) {\n if (!target || !target.closest) return false;\n return target.closest('[contenteditable=\"true\"]') || target.closest('.lp-image-editor');\n }\n\n document.addEventListener('keydown', (e) => {\n if (isEditingTarget(e.target)) return;\n if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') goTo(current + 1);\n if (e.key === 'ArrowLeft' || e.key === 'PageUp') goTo(current - 1);\n if (e.key === 'Home') goTo(0);\n if (e.key === 'End') goTo(slides.length - 1);\n });\n\n updateClasses();\n\n // \u7F29\u653E\u63A7\u5236\n const stage = document.querySelector('.lp-editor-stage');\n const scaler = document.querySelector('.lp-editor-stage-scaler');\n const zoomSlider = document.getElementById('lp-zoom-slider');\n const zoomValue = document.getElementById('lp-zoom-value');\n const zoomOutBtn = document.getElementById('lp-zoom-out');\n const zoomInBtn = document.getElementById('lp-zoom-in');\n const zoomFitBtn = document.getElementById('lp-zoom-fit');\n let userZoom = null;\n\n function fitScale() {\n if (!stage || !scaler) return 1;\n return Math.min(stage.clientWidth / 1280, stage.clientHeight / 720) * 0.92;\n }\n\n function updateScale() {\n if (!stage || !scaler) return;\n const scale = userZoom == null ? fitScale() : userZoom;\n scaler.style.transform = 'scale(' + Math.max(scale, 0.35) + ')';\n if (zoomValue) zoomValue.textContent = Math.round(scale * 100) + '%';\n if (zoomSlider && userZoom != null) zoomSlider.value = String(Math.round(scale * 100));\n }\n\n if (zoomSlider) {\n zoomSlider.addEventListener('input', () => {\n userZoom = Number(zoomSlider.value) / 100;\n updateScale();\n });\n }\n if (zoomOutBtn) {\n zoomOutBtn.addEventListener('click', () => {\n userZoom = (userZoom == null ? fitScale() : userZoom) - 0.1;\n if (userZoom < 0.35) userZoom = 0.35;\n updateScale();\n });\n }\n if (zoomInBtn) {\n zoomInBtn.addEventListener('click', () => {\n userZoom = (userZoom == null ? fitScale() : userZoom) + 0.1;\n if (userZoom > 1.5) userZoom = 1.5;\n updateScale();\n });\n }\n if (zoomFitBtn) {\n zoomFitBtn.addEventListener('click', () => {\n userZoom = null;\n updateScale();\n });\n }\n window.addEventListener('resize', updateScale);\n updateScale();\n\n // \u64AD\u653E\uFF1A\u5148\u6E32\u67D3\u9759\u6001\u7248\u672C\u518D\u6253\u5F00\n const playBtn = document.getElementById('lp-play');\n if (playBtn) {\n playBtn.addEventListener('click', async () => {\n playBtn.textContent = '\u51C6\u5907\u4E2D...';\n playBtn.disabled = true;\n try {\n const res = await fetch('/api/render', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u6E32\u67D3\u5931\u8D25: ' + res.status);\n window.open('/deck/index.html', '_blank');\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n playBtn.textContent = '\u25B6 \u64AD\u653E';\n playBtn.disabled = false;\n }\n });\n }\n\n // \u5185\u5BB9\u7ED3\u6784\u53D8\u66F4\u540E\u91CD\u65B0\u6E32\u67D3\u5F53\u524D\u7F16\u8F91\u5668\uFF08\u4EC5\u7528\u4E8E\u6570\u7EC4\u589E\u5220\u7B49\u6A21\u677F\u5185\u5BB9\u8C03\u6574\uFF09\n function saveCurrentForReload() {\n try {\n localStorage.setItem('lemonppt:editor:currentSlide', String(current));\n } catch (e) {}\n }\n\n async function reloadEditor() {\n saveCurrentForReload();\n try {\n const res = await fetch('/api/render-editor', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u91CD\u65B0\u6E32\u67D3\u5931\u8D25: ' + res.status);\n window.location.reload();\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n }\n }\n\n // \u5143\u7D20\u9009\u4E2D\u4E0E\u53F3\u4FA7\u5C5E\u6027\u9762\u677F\n let selectedEl = null;\n let selectedSlideIdx = 0;\n const propertyContent = document.getElementById('lp-property-content');\n\n const FIELD_LABELS = {\n kicker: '\u6807\u7B7E',\n title: '\u6807\u9898',\n subtitle: '\u526F\u6807\u9898',\n date: '\u65E5\u671F',\n quote: '\u5F15\u7528',\n author: '\u4F5C\u8005',\n role: '\u804C\u4F4D',\n company: '\u516C\u53F8',\n value: '\u6570\u503C',\n unit: '\u5355\u4F4D',\n label: '\u6307\u6807\u540D',\n description: '\u8BF4\u660E',\n change: '\u53D8\u5316',\n imageUrl: '\u56FE\u7247',\n url: '\u56FE\u7247',\n logoUrl: 'Logo',\n avatarUrl: '\u5934\u50CF',\n caption: '\u8BF4\u660E',\n q: '\u95EE\u9898',\n a: '\u56DE\u7B54',\n question: '\u95EE\u9898',\n answer: '\u56DE\u7B54',\n name: '\u540D\u79F0',\n price: '\u4EF7\u683C',\n period: '\u5468\u671F',\n cta: '\u6309\u94AE\u6587\u6848',\n type: '\u56FE\u8868\u7C7B\u578B',\n status: '\u72B6\u6001',\n bio: '\u7B80\u4ECB',\n };\n\n function getFieldLabel(path) {\n const key = String(path).split('.').pop() || '';\n return FIELD_LABELS[key] || key;\n }\n\n function inferFieldType(path, value) {\n const key = String(path).split('.').pop() || '';\n if (/image|url|logo|avatar/i.test(key) && typeof value === 'string') return 'image';\n if (typeof value === 'boolean') return 'boolean';\n if (typeof value === 'number') return 'number';\n if (key === 'type' && ['bar', 'line', 'pie'].includes(String(value))) return 'select';\n if (key === 'status') return 'select';\n if (['description', 'bio', 'quote', 'answer', 'a', 'subtitle'].includes(key)) return 'textarea';\n return 'text';\n }\n\n function getSelectOptions(path) {\n const key = String(path).split('.').pop() || '';\n if (key === 'type') return [{ value: 'bar', label: '\u67F1\u72B6' }, { value: 'line', label: '\u6298\u7EBF' }, { value: 'pie', label: '\u997C\u56FE' }];\n if (key === 'status') return [{ value: '\u5DF2\u5B8C\u6210', label: '\u5DF2\u5B8C\u6210' }, { value: '\u8FDB\u884C\u4E2D', label: '\u8FDB\u884C\u4E2D' }, { value: '\u89C4\u5212\u4E2D', label: '\u89C4\u5212\u4E2D' }];\n return [];\n }\n\n function clearSelection() {\n if (selectedEl) {\n selectedEl.classList.remove('lp-selected');\n selectedEl = null;\n }\n selectedSlideIdx = current;\n renderSlidePanel();\n }\n\n function setField(path, value) {\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n recordHistory();\n setProp(slide.props, path, value);\n syncDomFromGoal();\n autoSave();\n }\n\n function createEl(tag, className, parent) {\n const el = document.createElement(tag);\n if (className) el.className = className;\n if (parent) parent.appendChild(el);\n return el;\n }\n\n function createTextField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const input = createEl('input', 'lp-property-input', wrap);\n input.type = 'text';\n input.value = value == null ? '' : String(value);\n input.addEventListener('input', () => onChange(input.value));\n return wrap;\n }\n\n function createTextareaField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const textarea = createEl('textarea', 'lp-property-textarea', wrap);\n textarea.value = value == null ? '' : String(value);\n textarea.addEventListener('input', () => onChange(textarea.value));\n return wrap;\n }\n\n function createNumberField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const input = createEl('input', 'lp-property-input', wrap);\n input.type = 'number';\n input.value = value == null ? '' : String(value);\n input.addEventListener('input', () => onChange(Number(input.value)));\n return wrap;\n }\n\n function createToggleField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n const labelEl = createEl('label', 'lp-property-toggle', wrap);\n const text = createEl('span', '', labelEl);\n text.textContent = label;\n const input = createEl('input', '', labelEl);\n input.type = 'checkbox';\n input.checked = !!value;\n const track = createEl('div', 'lp-property-toggle-track', labelEl);\n createEl('div', 'lp-property-toggle-thumb', track);\n input.addEventListener('change', () => onChange(input.checked));\n return wrap;\n }\n\n function createSelectField(label, value, options, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const segmented = createEl('div', 'lp-property-segmented', wrap);\n options.forEach((opt) => {\n const btn = createEl('button', '', segmented);\n btn.textContent = opt.label;\n btn.type = 'button';\n if (opt.value === value) btn.classList.add('active');\n btn.addEventListener('click', () => {\n Array.from(segmented.children).forEach((b) => b.classList.remove('active'));\n btn.classList.add('active');\n onChange(opt.value);\n });\n });\n return wrap;\n }\n\n function createImageField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const urlInput = createEl('input', 'lp-property-input', wrap);\n urlInput.type = 'text';\n urlInput.value = value == null ? '' : String(value);\n urlInput.placeholder = 'https://...';\n urlInput.addEventListener('input', () => onChange(urlInput.value));\n\n const fileWrap = createEl('div', 'lp-property-field');\n fileWrap.style.marginTop = '8px';\n const fileInput = createEl('input', 'lp-property-input', fileWrap);\n fileInput.type = 'file';\n fileInput.accept = 'image/*';\n fileInput.addEventListener('change', () => {\n const file = fileInput.files[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = (e) => {\n const dataUrl = e.target.result;\n urlInput.value = dataUrl;\n onChange(dataUrl);\n };\n reader.readAsDataURL(file);\n });\n wrap.appendChild(fileWrap);\n return wrap;\n }\n\n function createFieldControl(path, value) {\n const type = inferFieldType(path, value);\n const label = getFieldLabel(path);\n if (type === 'image') return createImageField(label, value, (v) => setField(path, v || undefined));\n if (type === 'textarea') return createTextareaField(label, value, (v) => setField(path, v || undefined));\n if (type === 'number') return createNumberField(label, value, (v) => setField(path, v));\n if (type === 'boolean') return createToggleField(label, value, (v) => setField(path, v));\n if (type === 'select') return createSelectField(label, value, getSelectOptions(path), (v) => setField(path, v));\n return createTextField(label, value, (v) => setField(path, v || undefined));\n }\n\n function inferEmptyItem(array) {\n if (!array.length) return {};\n const sample = array[0];\n if (typeof sample === 'string') return '';\n if (typeof sample === 'object' && sample !== null) {\n const item = {};\n Object.keys(sample).forEach((k) => {\n const v = sample[k];\n item[k] = typeof v === 'boolean' ? false : (typeof v === 'number' ? 0 : '');\n });\n return item;\n }\n return '';\n }\n\n function createArraySection(path, array, parent) {\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(path) + ' (' + array.length + ')';\n\n const list = createEl('div', 'lp-property-array', section);\n array.forEach((item, index) => {\n const itemWrap = createEl('div', 'lp-property-array-item', list);\n const header = createEl('div', 'lp-property-array-header', itemWrap);\n header.textContent = '\u7B2C ' + (index + 1) + ' \u9879';\n const removeBtn = createEl('button', 'lp-property-btn lp-property-btn-sm lp-property-btn-danger', header);\n removeBtn.textContent = '\u5220\u9664';\n removeBtn.type = 'button';\n removeBtn.addEventListener('click', () => {\n recordHistory();\n const arr = getProp(goal.slides[selectedSlideIdx].props, path);\n if (Array.isArray(arr)) {\n arr.splice(index, 1);\n autoSave();\n reloadEditor();\n }\n });\n\n if (typeof item === 'string') {\n const textarea = createEl('textarea', 'lp-property-textarea', itemWrap);\n textarea.value = item;\n textarea.addEventListener('input', () => {\n setField(path + '.' + index, textarea.value);\n });\n } else if (typeof item === 'object' && item !== null) {\n Object.keys(item).forEach((key) => {\n itemWrap.appendChild(createFieldControl(path + '.' + index + '.' + key, item[key]));\n });\n }\n });\n\n const addBtn = createEl('button', 'lp-property-btn lp-property-btn-primary', section);\n addBtn.textContent = '\uFF0B \u6DFB\u52A0\u4E00\u9879';\n addBtn.type = 'button';\n addBtn.addEventListener('click', () => {\n recordHistory();\n const arr = getProp(goal.slides[selectedSlideIdx].props, path);\n if (Array.isArray(arr)) {\n arr.push(inferEmptyItem(arr));\n autoSave();\n reloadEditor();\n }\n });\n }\n\n function renderSlideFields(props, parent) {\n Object.keys(props).forEach((key) => {\n if (key === '_style') return;\n const value = props[key];\n if (Array.isArray(value)) {\n createArraySection(key, value, parent);\n } else if (value !== null && typeof value === 'object') {\n // \u5D4C\u5957\u5BF9\u8C61\u76F4\u63A5\u5C55\u5F00\uFF08\u76EE\u524D\u8F83\u5C11\uFF09\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(key);\n Object.keys(value).forEach((subKey) => {\n section.appendChild(createFieldControl(key + '.' + subKey, value[subKey]));\n });\n } else {\n parent.appendChild(createFieldControl(key, value));\n }\n });\n }\n\n function renderSlidePanel() {\n if (!propertyContent) return;\n propertyContent.innerHTML = '';\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n\n const info = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', info).textContent = '\u5E7B\u706F\u7247 ' + (selectedSlideIdx + 1);\n const layoutLabel = createEl('div', 'lp-property-help', info);\n layoutLabel.textContent = '\u7248\u5F0F\uFF1A' + slide.layout;\n\n if (selectedEl) {\n const prop = selectedEl.getAttribute('data-lp-prop');\n const quickSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', quickSection).textContent = '\u5F53\u524D\u9009\u4E2D';\n const fieldValue = getProp(slide.props, prop);\n quickSection.appendChild(createFieldControl(prop, fieldValue));\n }\n\n const fieldsSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', fieldsSection).textContent = '\u5185\u5BB9\u5C5E\u6027';\n renderSlideFields(slide.props, fieldsSection);\n }\n\n function selectEl(el) {\n if (selectedEl === el) return;\n if (selectedEl) selectedEl.classList.remove('lp-selected');\n selectedEl = el;\n selectedEl.classList.add('lp-selected');\n selectedSlideIdx = Number(el.getAttribute('data-lp-slide-idx')) || current;\n renderSlidePanel();\n // \u6EDA\u52A8\u5230\u5F53\u524D\u9009\u4E2D\u5B57\u6BB5\n const path = el.getAttribute('data-lp-prop');\n if (path && propertyContent) {\n const label = propertyContent.querySelector('.lp-property-label');\n // \u7B80\u5355\u9AD8\u4EAE\uFF1A\u6682\u65F6\u4E0D\u505A\u81EA\u52A8\u6EDA\u52A8\uFF0C\u907F\u514D\u590D\u6742\u5EA6\n }\n }\n\n function selectSlide(index) {\n if (selectedEl) {\n selectedEl.classList.remove('lp-selected');\n selectedEl = null;\n }\n selectedSlideIdx = index;\n renderSlidePanel();\n }\n\n document.addEventListener('focusin', (e) => {\n const el = e.target.closest && e.target.closest('[data-lp-editable=\"true\"], [data-lp-editable-image=\"true\"]');\n if (el) selectEl(el);\n });\n\n document.addEventListener('click', (e) => {\n const editableEl = e.target.closest && e.target.closest('[data-lp-editable=\"true\"], [data-lp-editable-image=\"true\"]');\n if (editableEl) {\n selectEl(editableEl);\n return;\n }\n const wrapper = e.target.closest && e.target.closest('.lp-slide-wrapper');\n if (wrapper) {\n selectSlide(Number(wrapper.getAttribute('data-slide-index')));\n return;\n }\n if (e.target.closest && !e.target.closest('.lp-editor-right-panel')) {\n clearSelection();\n }\n });\n\n // \u6062\u590D\u4E0A\u6B21\u505C\u7559\u7684\u5E7B\u706F\u7247\uFF08\u7ED3\u6784\u53D8\u66F4\u540E\u91CD\u8F7D\u7528\uFF09\n const savedCurrent = localStorage.getItem('lemonppt:editor:currentSlide');\n if (savedCurrent) {\n const savedIndex = Number(savedCurrent);\n if (!Number.isNaN(savedIndex) && savedIndex >= 0 && savedIndex < slides.length) {\n goTo(savedIndex);\n }\n localStorage.removeItem('lemonppt:editor:currentSlide');\n }\n\n // \u9ED8\u8BA4\u9009\u4E2D\u5F53\u524D\u5E7B\u706F\u7247\n selectSlide(current);\n})();\n";
2
+ //# sourceMappingURL=editor-script.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor-script.d.ts","sourceRoot":"","sources":["../src/editor-script.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,2w9BA02BxB,CAAC"}