@lemonppt/renderer 0.1.7 → 0.2.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 (59) hide show
  1. package/assets/fonts/Anton/Anton-Regular.ttf +0 -0
  2. package/assets/fonts/Anton/OFL.txt +93 -0
  3. package/assets/fonts/Archivo/Archivo-Italic[wdth,wght].ttf +0 -0
  4. package/assets/fonts/Archivo/Archivo[wdth,wght].ttf +0 -0
  5. package/assets/fonts/Archivo/OFL.txt +93 -0
  6. package/assets/fonts/Caveat/Caveat[wght].ttf +0 -0
  7. package/assets/fonts/Caveat/OFL.txt +93 -0
  8. package/assets/fonts/IBMPlexSans/IBMPlexSans-Italic[wdth,wght].ttf +0 -0
  9. package/assets/fonts/IBMPlexSans/IBMPlexSans[wdth,wght].ttf +0 -0
  10. package/assets/fonts/IBMPlexSans/OFL.txt +93 -0
  11. package/assets/fonts/Inter/Inter-Italic[opsz,wght].ttf +0 -0
  12. package/assets/fonts/Inter/Inter[opsz,wght].ttf +0 -0
  13. package/assets/fonts/Inter/OFL.txt +93 -0
  14. package/assets/fonts/JetBrainsMono/JetBrainsMono-Italic[wght].ttf +0 -0
  15. package/assets/fonts/JetBrainsMono/JetBrainsMono[wght].ttf +0 -0
  16. package/assets/fonts/JetBrainsMono/OFL.txt +93 -0
  17. package/assets/fonts/LICENSE.md +20 -0
  18. package/assets/fonts/Newsreader/Newsreader-Italic[opsz,wght].ttf +0 -0
  19. package/assets/fonts/Newsreader/Newsreader[opsz,wght].ttf +0 -0
  20. package/assets/fonts/Newsreader/OFL.txt +93 -0
  21. package/assets/fonts/NotoSansSC/NotoSansSC[wght].ttf +0 -0
  22. package/assets/fonts/NotoSansSC/OFL.txt +93 -0
  23. package/assets/fonts/NotoSerifSC/NotoSerifSC[wght].ttf +0 -0
  24. package/assets/fonts/NotoSerifSC/OFL.txt +94 -0
  25. package/assets/fonts/SpaceGrotesk/OFL.txt +93 -0
  26. package/assets/fonts/SpaceGrotesk/SpaceGrotesk[wght].ttf +0 -0
  27. package/assets/fonts/SpaceMono/OFL.txt +93 -0
  28. package/assets/fonts/SpaceMono/SpaceMono-Bold.ttf +0 -0
  29. package/assets/fonts/SpaceMono/SpaceMono-BoldItalic.ttf +0 -0
  30. package/assets/fonts/SpaceMono/SpaceMono-Italic.ttf +0 -0
  31. package/assets/fonts/SpaceMono/SpaceMono-Regular.ttf +0 -0
  32. package/assets/fonts/fonts.css +154 -0
  33. package/dist/client/client-render.js +5472 -0
  34. package/dist/client/theme-echarts.js +82 -0
  35. package/dist/client-render.d.ts +45 -0
  36. package/dist/client-render.d.ts.map +1 -0
  37. package/dist/client-render.js +75 -0
  38. package/dist/client-render.js.map +1 -0
  39. package/dist/editor-script.d.ts +1 -1
  40. package/dist/editor-script.d.ts.map +1 -1
  41. package/dist/editor-script.js +2758 -280
  42. package/dist/editor-script.js.map +1 -1
  43. package/dist/export-pdf.d.ts +5 -0
  44. package/dist/export-pdf.d.ts.map +1 -1
  45. package/dist/export-pdf.js +21 -1
  46. package/dist/export-pdf.js.map +1 -1
  47. package/dist/export-pptx.d.ts +26 -3
  48. package/dist/export-pptx.d.ts.map +1 -1
  49. package/dist/export-pptx.js +20500 -1103
  50. package/dist/export-pptx.js.map +1 -1
  51. package/dist/render.d.ts.map +1 -1
  52. package/dist/render.js +1161 -59
  53. package/dist/render.js.map +1 -1
  54. package/dist/theme-echarts.d.ts +9 -0
  55. package/dist/theme-echarts.d.ts.map +1 -0
  56. package/dist/theme-echarts.js +14 -0
  57. package/dist/theme-echarts.js.map +1 -0
  58. package/package.json +8 -6
  59. package/LICENSE +0 -661
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { normalizeDeckGoal } from '@lemonppt/core';
3
- import { renderSlide } from '@lemonppt/templates';
3
+ import { renderSlide, listLayouts, getLayoutSchema, generateThemeCssVariablesWithDark, generateTheme02CssVariablesWithSchemes, generateTheme03CssVariablesWithSchemes, generateTheme04CssVariablesWithTonesAndAppearance, generateTheme05CssVariablesWithSchemesAndAppearance, generateTheme06CssVariablesWithSchemesAndAppearance, } from '@lemonppt/templates';
4
4
  import ReactDOMServer from 'react-dom/server';
5
5
  import { editorScript } from './editor-script.js';
6
6
  export function renderDeck(goal, options = {}) {
@@ -10,13 +10,13 @@ export function renderDeck(goal, options = {}) {
10
10
  const slideElements = goal.slides.map((slide, index) => {
11
11
  const element = renderSlide(slide, { slideIdx: index, editable, theme: goal.theme });
12
12
  const stateClass = index === 0 ? 'active' : '';
13
- return (_jsx("div", { className: `lp-slide-wrapper ${stateClass}`, "data-slide-index": index, "data-layout": slide.layout, style: {
13
+ return (_jsx("div", { className: `lp-slide-wrapper ${stateClass}`, "data-slide-index": index, "data-layout": slide.layout, "data-lp-transition": String(slide.props.transition || 'none'), style: {
14
14
  width: `${width}px`,
15
15
  height: `${height}px`,
16
16
  boxSizing: 'border-box',
17
17
  }, children: element ?? (_jsxs("div", { className: "lp-error", children: ["\u672A\u627E\u5230\u7248\u5F0F: ", slide.layout] })) }, index));
18
18
  });
19
- const slidesMarkup = ReactDOMServer.renderToStaticMarkup(_jsx("div", { className: "lp-deck", "data-theme": goal.theme, "data-editable": editable ? 'true' : undefined, style: {
19
+ const slidesMarkup = ReactDOMServer.renderToStaticMarkup(_jsx("div", { className: "lp-deck", "data-theme": goal.theme, "data-lp-transition": "none", "data-editable": editable ? 'true' : undefined, style: {
20
20
  width: `${width}px`,
21
21
  height: `${height}px`,
22
22
  }, children: slideElements }));
@@ -30,9 +30,10 @@ export function renderDeck(goal, options = {}) {
30
30
  });
31
31
  const navMarkup = buildNavMarkup(slideCount);
32
32
  const scriptMarkup = editable ? buildEditorScriptMarkup(goal) : buildScriptMarkup();
33
- const editorBarMarkup = editable ? buildEditorBarMarkup() : '';
33
+ const editorBarMarkup = editable ? buildEditorBarMarkup(goal) : '';
34
34
  const leftPanelMarkup = editable ? buildLeftPanelMarkup(goal, slideHtmls, width, height) : '';
35
35
  const rightPanelMarkup = editable ? buildRightPanelMarkup() : '';
36
+ const addSlideModalMarkup = editable ? buildAddSlideModalMarkup(goal) : '';
36
37
  const bodyClass = editable ? 'lp-editor-body' : '';
37
38
  const bodyMarkup = editable
38
39
  ? `<div class="lp-editor-root">
@@ -54,19 +55,39 @@ ${leftPanelMarkup}
54
55
  </main>
55
56
  ${rightPanelMarkup}
56
57
  </div>
58
+ ${addSlideModalMarkup}
57
59
  ${scriptMarkup}
58
60
  </div>`
59
61
  : `${slidesMarkup}${navMarkup}${scriptMarkup}`;
62
+ const theme = goal.theme || 'theme01';
63
+ const colorScheme = goal.colorScheme || (theme === 'theme02' || theme === 'theme03' ? 'scheme-a' : theme === 'theme04' ? 'green' : theme === 'theme05' ? 'coral' : theme === 'theme06' ? 'volt' : 'light');
64
+ const appearance = goal.appearance || (theme === 'theme03' || theme === 'theme04' || theme === 'theme05' || theme === 'theme06' ? 'dark' : undefined);
65
+ const appearanceAttr = appearance ? ` data-appearance="${appearance}"` : '';
66
+ const themeCssVars = theme === 'theme01'
67
+ ? generateThemeCssVariablesWithDark()
68
+ : theme === 'theme02'
69
+ ? generateTheme02CssVariablesWithSchemes()
70
+ : theme === 'theme03'
71
+ ? generateTheme03CssVariablesWithSchemes()
72
+ : theme === 'theme04'
73
+ ? generateTheme04CssVariablesWithTonesAndAppearance()
74
+ : theme === 'theme05'
75
+ ? generateTheme05CssVariablesWithSchemesAndAppearance()
76
+ : theme === 'theme06'
77
+ ? generateTheme06CssVariablesWithSchemesAndAppearance()
78
+ : '';
60
79
  const html = `<!DOCTYPE html>
61
- <html lang="${goal.language ?? 'zh'}">
80
+ <html lang="${goal.language ?? 'zh'}" data-theme="${colorScheme}"${appearanceAttr}>
62
81
  <head>
63
82
  <meta charset="UTF-8">
64
83
  <meta name="viewport" content="width=${width}, initial-scale=1.0">
65
84
  <title>${escapeHtml(goal.title)}</title>
66
85
  <link rel="stylesheet" href="./assets/fonts/fonts.css">
67
- <link rel="stylesheet" href="./assets/${goal.theme || 'base'}.css">
86
+ <link rel="stylesheet" href="./assets/${theme}.css">
87
+ <script src="./assets/jquery.min.js"></script>
68
88
  <style>
69
89
  * { margin: 0; padding: 0; box-sizing: border-box; }
90
+ ${themeCssVars}
70
91
  html, body {
71
92
  width: 100%;
72
93
  height: 100%;
@@ -110,6 +131,8 @@ ${scriptMarkup}
110
131
  left: auto !important;
111
132
  transform: none !important;
112
133
  width: 100% !important;
134
+ height: 64px !important;
135
+ box-sizing: border-box !important;
113
136
  border-radius: 0 !important;
114
137
  border-bottom: 1px solid #333 !important;
115
138
  background: #1e1e1e !important;
@@ -118,20 +141,149 @@ ${scriptMarkup}
118
141
  display: flex !important;
119
142
  align-items: center !important;
120
143
  gap: 12px !important;
144
+ z-index: 100;
121
145
  }
122
146
  .lp-editor-root .lp-editor-title {
123
147
  margin-right: auto;
124
148
  }
149
+ .lp-editor-root .lp-appearance-switcher {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ background: #2a2a2a;
153
+ border: 1px solid #444;
154
+ border-radius: 999px;
155
+ padding: 3px;
156
+ gap: 3px;
157
+ }
158
+ .lp-editor-root .lp-appearance-btn {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 5px;
162
+ background: transparent;
163
+ color: #aaa;
164
+ border: none;
165
+ border-radius: 999px;
166
+ padding: 5px 10px;
167
+ font-size: 13px;
168
+ cursor: pointer;
169
+ transition: background 0.15s, color 0.15s;
170
+ }
171
+ .lp-editor-root .lp-appearance-btn:hover {
172
+ color: #e5e5e5;
173
+ }
174
+ .lp-editor-root .lp-appearance-active {
175
+ background: #3a3a3a;
176
+ color: #fff;
177
+ font-weight: 500;
178
+ }
125
179
  .lp-editor-root .lp-editor-btn {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ gap: 6px;
126
183
  background: #2a2a2a;
127
184
  color: #e5e5e5;
128
185
  border: 1px solid #444;
186
+ border-radius: 8px;
187
+ padding: 7px 12px;
188
+ font-size: 13px;
189
+ line-height: 1.4;
190
+ cursor: pointer;
191
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
129
192
  }
130
193
  .lp-editor-root .lp-editor-btn:hover:not(:disabled) {
131
194
  background: #333;
195
+ border-color: #555;
132
196
  }
133
197
  .lp-editor-root .lp-editor-btn:disabled {
134
198
  opacity: 0.5;
199
+ cursor: not-allowed;
200
+ }
201
+ .lp-editor-root .lp-editor-btn-primary {
202
+ background: #34d399 !important;
203
+ border-color: #34d399 !important;
204
+ color: #111 !important;
205
+ font-weight: 600;
206
+ }
207
+ .lp-editor-root .lp-editor-btn-primary:hover:not(:disabled) {
208
+ background: #10b981 !important;
209
+ border-color: #10b981 !important;
210
+ }
211
+ .lp-editor-root .lp-editor-select {
212
+ background: #2a2a2a;
213
+ color: #e5e5e5;
214
+ border: 1px solid #444;
215
+ border-radius: 6px;
216
+ padding: 6px 10px;
217
+ font-size: 13px;
218
+ outline: none;
219
+ max-width: 120px;
220
+ }
221
+ .lp-editor-root .lp-editor-select:focus {
222
+ border-color: #5b8cff;
223
+ }
224
+ .lp-editor-export {
225
+ position: relative;
226
+ }
227
+ .lp-editor-export-toggle {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: 6px;
231
+ }
232
+ .lp-editor-caret {
233
+ font-size: 10px;
234
+ line-height: 1;
235
+ transform: translateY(1px);
236
+ }
237
+ .lp-editor-export-menu {
238
+ position: absolute;
239
+ top: calc(100% + 6px);
240
+ right: 0;
241
+ min-width: 220px;
242
+ background: #2a2a2a;
243
+ border: 1px solid #444;
244
+ border-radius: 8px;
245
+ padding: 6px;
246
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
247
+ z-index: 1000;
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 2px;
251
+ }
252
+ .lp-editor-export-menu[hidden] {
253
+ display: none !important;
254
+ }
255
+ .lp-editor-export-item {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 10px;
259
+ width: 100%;
260
+ padding: 8px 10px;
261
+ background: transparent;
262
+ border: none;
263
+ border-radius: 6px;
264
+ color: #e5e5e5;
265
+ font-size: 14px;
266
+ cursor: pointer;
267
+ text-align: left;
268
+ }
269
+ .lp-editor-export-item:hover {
270
+ background: #3a3a3a;
271
+ }
272
+ .lp-editor-export-icon {
273
+ width: 20px;
274
+ text-align: center;
275
+ color: #aaa;
276
+ font-size: 14px;
277
+ }
278
+ .lp-editor-export-label {
279
+ flex: 1;
280
+ }
281
+ .lp-editor-export-tag {
282
+ font-size: 11px;
283
+ color: #999;
284
+ background: #3a3a3a;
285
+ padding: 2px 6px;
286
+ border-radius: 4px;
135
287
  }
136
288
  .lp-editor-workspace {
137
289
  display: flex;
@@ -139,15 +291,16 @@ ${scriptMarkup}
139
291
  min-height: 0;
140
292
  }
141
293
  .lp-editor-left-panel {
142
- width: 240px;
294
+ width: 180px;
143
295
  flex: none;
144
296
  background: #1e1e1e;
145
297
  border-right: 1px solid #333;
146
298
  overflow-y: auto;
147
299
  padding: 12px;
300
+ position: relative;
148
301
  }
149
302
  .lp-editor-right-panel {
150
- width: 280px;
303
+ width: 400px;
151
304
  flex: none;
152
305
  background: #1e1e1e;
153
306
  border-left: 1px solid #333;
@@ -161,6 +314,10 @@ ${scriptMarkup}
161
314
  justify-content: center;
162
315
  overflow: hidden;
163
316
  position: relative;
317
+ background-color: #1a1a1a;
318
+ background-image: radial-gradient(circle, rgba(255,255,255,0.12) 1.5px, transparent 1.5px);
319
+ background-size: 28px 28px;
320
+ background-position: center center;
164
321
  }
165
322
  .lp-editor-stage-scaler {
166
323
  transform-origin: center center;
@@ -187,24 +344,37 @@ ${scriptMarkup}
187
344
  border: 2px solid transparent;
188
345
  border-radius: 6px;
189
346
  background: #2a2a2a;
190
- cursor: pointer;
347
+ cursor: grab;
191
348
  display: flex;
192
349
  flex-direction: column;
193
350
  justify-content: center;
194
351
  padding: 10px;
195
352
  text-align: left;
196
353
  color: #bbb;
197
- transition: border-color 0.15s, background 0.15s;
354
+ transition: border-color 0.15s, background 0.15s, opacity 0.15s;
198
355
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
356
+ position: relative;
357
+ user-select: none;
358
+ -webkit-user-select: none;
359
+ outline: none;
360
+ content-visibility: auto;
361
+ contain-intrinsic-size: auto 180px;
199
362
  }
200
363
  .lp-thumbnail:hover {
201
364
  background: #333;
202
365
  }
366
+ .lp-thumbnail:focus-visible {
367
+ border-color: #5b8cff;
368
+ }
203
369
  .lp-thumbnail.active {
204
370
  border-color: #3b82f6;
205
371
  background: #26354f;
206
372
  color: #fff;
207
373
  }
374
+ .lp-thumbnail.dragging,
375
+ .lp-thumbnail:active {
376
+ cursor: grabbing;
377
+ }
208
378
  .lp-thumbnail-index {
209
379
  font-size: 11px;
210
380
  color: #888;
@@ -225,6 +395,119 @@ ${scriptMarkup}
225
395
  color: #666;
226
396
  margin-top: 4px;
227
397
  }
398
+ .lp-thumbnail-drag-handle {
399
+ position: absolute;
400
+ top: 6px;
401
+ left: 6px;
402
+ width: 20px;
403
+ height: 24px;
404
+ display: flex;
405
+ flex-direction: column;
406
+ align-items: center;
407
+ justify-content: center;
408
+ gap: 2px;
409
+ opacity: 0.45;
410
+ z-index: 4;
411
+ color: #888;
412
+ transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
413
+ line-height: 1;
414
+ font-size: 10px;
415
+ user-select: none;
416
+ -webkit-user-select: none;
417
+ pointer-events: none;
418
+ border-radius: 4px;
419
+ padding: 2px;
420
+ }
421
+ .lp-thumbnail:hover .lp-thumbnail-drag-handle,
422
+ .lp-thumbnail-drag-handle:hover {
423
+ opacity: 0.9;
424
+ color: #bbb;
425
+ background: rgba(255, 255, 255, 0.08);
426
+ }
427
+ .lp-thumbnail.dragging .lp-thumbnail-drag-handle {
428
+ opacity: 1;
429
+ color: #fff;
430
+ background: rgba(255, 255, 255, 0.15);
431
+ }
432
+ .lp-thumbnail.dragging {
433
+ opacity: 0.45;
434
+ border: 2px dashed #3b82f6;
435
+ background: #1f2937;
436
+ }
437
+ .lp-thumbnail.drag-over {
438
+ border-color: #10b981;
439
+ background: #1f3d32;
440
+ }
441
+ .lp-thumbnail-drop-indicator {
442
+ position: absolute;
443
+ left: 0;
444
+ right: 0;
445
+ height: 3px;
446
+ background: #3b82f6;
447
+ border-radius: 2px;
448
+ pointer-events: none;
449
+ z-index: 10;
450
+ opacity: 0;
451
+ transition: opacity 0.1s ease;
452
+ box-shadow: 0 0 8px rgba(59, 130, 246, 0.6);
453
+ }
454
+ .lp-thumbnail-drop-indicator.visible {
455
+ opacity: 1;
456
+ }
457
+ .lp-editor-thumbnails {
458
+ position: relative;
459
+ }
460
+ .lp-delete-confirm-toast {
461
+ position: fixed;
462
+ z-index: 1000;
463
+ width: 180px;
464
+ background: #262626;
465
+ border: 1px solid #444;
466
+ border-radius: 8px;
467
+ padding: 12px;
468
+ box-shadow: 0 8px 24px rgba(0,0,0,0.35);
469
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
470
+ transition: opacity 0.15s ease, transform 0.15s ease;
471
+ margin: 0;
472
+ }
473
+ .lp-delete-confirm-toast[hidden] {
474
+ display: none;
475
+ }
476
+ .lp-delete-confirm-text {
477
+ font-size: 13px;
478
+ color: #e5e5e5;
479
+ line-height: 1.4;
480
+ margin-bottom: 10px;
481
+ }
482
+ .lp-delete-confirm-actions {
483
+ display: flex;
484
+ gap: 8px;
485
+ justify-content: flex-end;
486
+ }
487
+ .lp-delete-confirm-cancel,
488
+ .lp-delete-confirm-confirm {
489
+ padding: 5px 12px;
490
+ border-radius: 6px;
491
+ font-size: 13px;
492
+ cursor: pointer;
493
+ border: 1px solid #555;
494
+ background: #333;
495
+ color: #e5e5e5;
496
+ transition: background 0.15s, border-color 0.15s;
497
+ }
498
+ .lp-delete-confirm-cancel:hover {
499
+ background: #3a3a3a;
500
+ border-color: #666;
501
+ }
502
+ .lp-delete-confirm-confirm {
503
+ background: #dc2626;
504
+ border-color: #dc2626;
505
+ color: #fff;
506
+ }
507
+ .lp-delete-confirm-confirm:hover {
508
+ background: #b91c1c;
509
+ border-color: #b91c1c;
510
+ }
228
511
  .lp-property-header {
229
512
  font-size: 14px;
230
513
  font-weight: 600;
@@ -266,8 +549,9 @@ ${scriptMarkup}
266
549
  resize: vertical;
267
550
  }
268
551
  .lp-selected {
269
- outline: 2px solid #3b82f6 !important;
552
+ outline: 2px solid var(--lp-accent, #3b82f6) !important;
270
553
  outline-offset: 2px !important;
554
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--lp-accent, #3b82f6) 20%, transparent);
271
555
  }
272
556
  .lp-editor-root .lp-nav,
273
557
  .lp-editor-root .lp-page-counter,
@@ -280,6 +564,8 @@ ${scriptMarkup}
280
564
  padding: 0;
281
565
  justify-content: flex-end;
282
566
  background: var(--lp-bg, #222);
567
+ content-visibility: auto;
568
+ contain-intrinsic-size: auto 120px;
283
569
  }
284
570
  .lp-thumbnail-render {
285
571
  position: absolute;
@@ -332,6 +618,33 @@ ${scriptMarkup}
332
618
  .lp-thumbnail.active .lp-thumbnail-scrim {
333
619
  background: linear-gradient(to bottom, rgba(59,130,246,0.15) 0%, rgba(59,130,246,0.45) 100%);
334
620
  }
621
+ .lp-thumbnail-delete {
622
+ position: absolute;
623
+ top: 4px;
624
+ right: 4px;
625
+ z-index: 3;
626
+ width: 20px;
627
+ height: 20px;
628
+ display: flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ border-radius: 4px;
632
+ background: rgba(239, 68, 68, 0.85);
633
+ color: #fff;
634
+ font-size: 14px;
635
+ line-height: 1;
636
+ cursor: pointer;
637
+ opacity: 0;
638
+ transition: opacity 0.15s, background 0.15s;
639
+ pointer-events: auto;
640
+ }
641
+ .lp-thumbnail:hover .lp-thumbnail-delete,
642
+ .lp-thumbnail:focus-within .lp-thumbnail-delete {
643
+ opacity: 1;
644
+ }
645
+ .lp-thumbnail-delete:hover {
646
+ background: rgba(220, 38, 38, 1);
647
+ }
335
648
  .lp-editor-zoom-bar {
336
649
  position: absolute;
337
650
  bottom: 16px;
@@ -378,6 +691,15 @@ ${scriptMarkup}
378
691
  text-transform: uppercase;
379
692
  letter-spacing: 0.05em;
380
693
  }
694
+ .lp-property-section-header {
695
+ display: flex;
696
+ align-items: center;
697
+ justify-content: space-between;
698
+ margin-bottom: 10px;
699
+ }
700
+ .lp-property-section-header .lp-property-section-title {
701
+ margin-bottom: 0;
702
+ }
381
703
  .lp-property-help {
382
704
  font-size: 12px;
383
705
  color: #888;
@@ -417,28 +739,30 @@ ${scriptMarkup}
417
739
  cursor: pointer;
418
740
  }
419
741
  .lp-property-toggle-track {
420
- width: 38px;
421
- height: 20px;
742
+ width: 46px;
743
+ height: 24px;
422
744
  background: #444;
423
- border-radius: 10px;
745
+ border-radius: 12px;
424
746
  position: relative;
425
- transition: background 0.2s;
747
+ transition: background 0.25s ease;
748
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.25);
426
749
  }
427
750
  .lp-property-toggle input:checked + .lp-property-toggle-track {
428
- background: #3b82f6;
751
+ background: #34d399;
429
752
  }
430
753
  .lp-property-toggle-thumb {
431
754
  position: absolute;
432
755
  top: 2px;
433
756
  left: 2px;
434
- width: 16px;
435
- height: 16px;
757
+ width: 20px;
758
+ height: 20px;
436
759
  background: #fff;
437
760
  border-radius: 50%;
438
- transition: transform 0.2s;
761
+ transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
762
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
439
763
  }
440
764
  .lp-property-toggle input:checked + .lp-property-toggle-track .lp-property-toggle-thumb {
441
- transform: translateX(18px);
765
+ transform: translateX(22px);
442
766
  }
443
767
  .lp-property-toggle input {
444
768
  position: absolute;
@@ -488,24 +812,193 @@ ${scriptMarkup}
488
812
  .lp-property-range {
489
813
  width: 100%;
490
814
  }
815
+ .lp-property-slider-wrap {
816
+ margin-bottom: 16px;
817
+ }
818
+ .lp-property-slider-label {
819
+ font-size: 12px;
820
+ color: #bbb;
821
+ }
822
+ .lp-property-slider-value {
823
+ font-size: 13px;
824
+ font-weight: 700;
825
+ color: #fff;
826
+ min-width: 26px;
827
+ text-align: center;
828
+ padding: 2px 8px;
829
+ background: linear-gradient(135deg, rgba(91,155,213,0.9), rgba(139,92,246,0.9));
830
+ border-radius: 999px;
831
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
832
+ }
833
+ .lp-property-slider-ruler {
834
+ position: relative;
835
+ padding: 18px 18px 10px;
836
+ background: rgba(255, 255, 255, 0.04);
837
+ border: 1px solid rgba(255, 255, 255, 0.08);
838
+ border-radius: 14px;
839
+ cursor: pointer;
840
+ transition: background 0.2s ease, border-color 0.2s ease;
841
+ }
842
+ .lp-property-slider-ruler:hover {
843
+ background: rgba(255, 255, 255, 0.07);
844
+ border-color: rgba(255, 255, 255, 0.14);
845
+ }
846
+ .lp-property-slider-ruler::before {
847
+ content: '';
848
+ position: absolute;
849
+ top: 22px;
850
+ left: 18px;
851
+ right: 18px;
852
+ height: 2px;
853
+ background: rgba(255, 255, 255, 0.12);
854
+ border-radius: 1px;
855
+ pointer-events: none;
856
+ }
857
+ .lp-property-slider {
858
+ width: 100%;
859
+ -webkit-appearance: none;
860
+ appearance: none;
861
+ height: 8px;
862
+ background: rgba(255, 255, 255, 0.14);
863
+ border-radius: 4px;
864
+ outline: none;
865
+ display: block;
866
+ margin: 0;
867
+ cursor: pointer;
868
+ transition: box-shadow 0.2s ease;
869
+ }
870
+ .lp-property-slider:hover {
871
+ box-shadow: 0 0 0 4px rgba(91, 155, 213, 0.12);
872
+ }
873
+ .lp-property-slider::-webkit-slider-thumb {
874
+ -webkit-appearance: none;
875
+ appearance: none;
876
+ width: 24px;
877
+ height: 24px;
878
+ background: #fff;
879
+ border-radius: 50%;
880
+ cursor: grab;
881
+ border: 4px solid #5b9bd5;
882
+ box-shadow: 0 0 0 4px rgba(91, 155, 213, 0.2), 0 4px 12px rgba(0, 0, 0, 0.35);
883
+ margin-top: -8px;
884
+ transition: transform 0.15s ease, box-shadow 0.2s ease;
885
+ }
886
+ .lp-property-slider::-webkit-slider-thumb:hover {
887
+ transform: scale(1.12);
888
+ box-shadow: 0 0 0 6px rgba(91, 155, 213, 0.28), 0 6px 16px rgba(0, 0, 0, 0.4);
889
+ }
890
+ .lp-property-slider:active::-webkit-slider-thumb {
891
+ cursor: grabbing;
892
+ transform: scale(1.05);
893
+ border-color: #8b5cf6;
894
+ }
895
+ .lp-property-slider::-webkit-slider-runnable-track {
896
+ height: 8px;
897
+ border-radius: 4px;
898
+ }
899
+ .lp-property-slider::-moz-range-thumb {
900
+ width: 24px;
901
+ height: 24px;
902
+ background: #fff;
903
+ border-radius: 50%;
904
+ cursor: grab;
905
+ border: 4px solid #5b9bd5;
906
+ box-shadow: 0 0 0 4px rgba(91, 155, 213, 0.2), 0 4px 12px rgba(0, 0, 0, 0.35);
907
+ transition: transform 0.15s ease, box-shadow 0.2s ease;
908
+ }
909
+ .lp-property-slider::-moz-range-thumb:hover {
910
+ transform: scale(1.12);
911
+ box-shadow: 0 0 0 6px rgba(91, 155, 213, 0.28), 0 6px 16px rgba(0, 0, 0, 0.4);
912
+ }
913
+ .lp-property-slider:active::-moz-range-thumb {
914
+ cursor: grabbing;
915
+ transform: scale(1.05);
916
+ border-color: #8b5cf6;
917
+ }
918
+ .lp-property-slider::-moz-range-track {
919
+ height: 8px;
920
+ border-radius: 4px;
921
+ }
922
+ .lp-property-slider-scale {
923
+ position: relative;
924
+ height: 34px;
925
+ margin-top: 10px;
926
+ margin-left: 0;
927
+ margin-right: 0;
928
+ }
929
+ .lp-property-slider-tick {
930
+ position: absolute;
931
+ top: 0;
932
+ transform: translateX(-50%);
933
+ display: flex;
934
+ flex-direction: column;
935
+ align-items: center;
936
+ gap: 5px;
937
+ cursor: pointer;
938
+ padding: 0 6px;
939
+ transition: transform 0.15s ease;
940
+ min-width: 20px;
941
+ }
942
+ .lp-property-slider-tick:hover {
943
+ transform: translateX(-50%) scale(1.15);
944
+ }
945
+ .lp-property-slider-tick::before {
946
+ content: '';
947
+ width: 2px;
948
+ height: 12px;
949
+ background: rgba(255, 255, 255, 0.65);
950
+ border-radius: 1px;
951
+ transition: background 0.2s ease, height 0.15s ease;
952
+ }
953
+ .lp-property-slider-tick:hover::before {
954
+ background: #fff;
955
+ height: 16px;
956
+ }
957
+ .lp-property-slider-tick-label {
958
+ font-size: 12px;
959
+ font-weight: 700;
960
+ color: rgba(255, 255, 255, 0.85);
961
+ line-height: 1;
962
+ transition: color 0.2s ease;
963
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
964
+ white-space: nowrap;
965
+ }
966
+ .lp-property-slider-tick:hover .lp-property-slider-tick-label {
967
+ color: #fff;
968
+ }
491
969
  .lp-property-array {
492
970
  display: flex;
493
971
  flex-direction: column;
494
- gap: 10px;
972
+ gap: 8px;
495
973
  }
496
974
  .lp-property-array-item {
497
975
  background: #252525;
498
- border: 1px solid #383838;
976
+ border-bottom: 1px solid #383838;
499
977
  border-radius: 8px;
500
- padding: 10px;
978
+ padding: 10px 10px 20px 10px;
501
979
  }
502
- .lp-property-array-header {
980
+ .lp-property-array-item-header {
503
981
  display: flex;
504
982
  align-items: center;
505
983
  justify-content: space-between;
984
+ font-family: var(--lp-font-mono, ui-monospace, monospace);
985
+ font-size: 11px;
986
+ font-weight: 700;
987
+ color: var(--lp-accent, #888);
506
988
  margin-bottom: 8px;
989
+ letter-spacing: 0.04em;
990
+ }
991
+ .lp-property-array-header {
992
+ display: flex;
993
+ align-items: center;
994
+ justify-content: space-between;
507
995
  font-size: 12px;
508
996
  color: #aaa;
997
+ line-height: 1;
998
+ }
999
+ .lp-property-array-header .lp-property-btn-sm {
1000
+ width: auto;
1001
+ margin-left: auto;
509
1002
  }
510
1003
  .lp-property-btn {
511
1004
  width: 100%;
@@ -520,20 +1013,34 @@ ${scriptMarkup}
520
1013
  .lp-property-btn:hover {
521
1014
  background: #333;
522
1015
  }
1016
+ .lp-property-btn:disabled {
1017
+ opacity: 0.5;
1018
+ cursor: not-allowed;
1019
+ }
523
1020
  .lp-property-btn-primary {
524
1021
  background: #3b82f6;
525
1022
  border-color: #3b82f6;
526
1023
  color: #fff;
527
1024
  }
528
- .lp-property-btn-primary:hover {
1025
+ .lp-property-btn-primary:hover:not(:disabled) {
529
1026
  background: #2563eb;
530
1027
  }
531
1028
  .lp-property-btn-danger {
532
- border-color: #7f1d1d;
533
- color: #fca5a5;
1029
+ border-color: transparent;
1030
+ background: transparent;
1031
+ color: #ef4444;
1032
+ width: 22px;
1033
+ height: 22px;
1034
+ padding: 0;
1035
+ display: inline-flex;
1036
+ align-items: center;
1037
+ justify-content: center;
1038
+ border-radius: 4px;
1039
+ font-size: 16px;
1040
+ line-height: 1;
534
1041
  }
535
- .lp-property-btn-danger:hover {
536
- background: #450a0a;
1042
+ .lp-property-btn-danger:hover:not(:disabled) {
1043
+ background: rgba(239, 68, 68, 0.12);
537
1044
  }
538
1045
  .lp-property-btn-sm {
539
1046
  padding: 4px 8px;
@@ -546,6 +1053,61 @@ ${scriptMarkup}
546
1053
  text-align: center;
547
1054
  padding: 20px 0;
548
1055
  }
1056
+ /* 可编辑图片占位区:固定区域、点击直接上传、深色主题可见 */
1057
+ .lp-editable-image-placeholder {
1058
+ width: 100%;
1059
+ height: 100%;
1060
+ min-height: 120px;
1061
+ display: flex;
1062
+ flex-direction: column;
1063
+ align-items: center;
1064
+ justify-content: center;
1065
+ gap: 8px;
1066
+ background: color-mix(in srgb, var(--lp-ink, #111) 6%, transparent);
1067
+ border: 2px dashed color-mix(in srgb, var(--lp-ink, #111) 22%, transparent);
1068
+ border-radius: 12px;
1069
+ color: var(--lp-ink3, #6b7280);
1070
+ cursor: pointer;
1071
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
1072
+ user-select: none;
1073
+ }
1074
+ .lp-editable-image-placeholder:hover {
1075
+ background: color-mix(in srgb, var(--lp-accent, #3b82f6) 10%, transparent);
1076
+ border-color: var(--lp-accent, #3b82f6);
1077
+ color: var(--lp-accent, #3b82f6);
1078
+ }
1079
+ .lp-editable-image-placeholder-icon {
1080
+ width: 36px;
1081
+ height: 36px;
1082
+ display: flex;
1083
+ align-items: center;
1084
+ justify-content: center;
1085
+ border-radius: 50%;
1086
+ background: color-mix(in srgb, var(--lp-ink, #111) 10%, transparent);
1087
+ font-size: 20px;
1088
+ line-height: 1;
1089
+ transition: background 0.15s, transform 0.15s;
1090
+ }
1091
+ .lp-editable-image-placeholder:hover .lp-editable-image-placeholder-icon {
1092
+ background: color-mix(in srgb, var(--lp-accent, #3b82f6) 16%, transparent);
1093
+ transform: scale(1.08);
1094
+ }
1095
+ .lp-editable-image-placeholder-text {
1096
+ font-size: 13px;
1097
+ font-weight: 500;
1098
+ }
1099
+ /* 画布空编辑元素占位提示:新增数组项或清空标题时仍可定位和编辑 */
1100
+ [data-lp-editable="true"]:empty::before {
1101
+ content: '点击编辑';
1102
+ opacity: 0.35;
1103
+ pointer-events: none;
1104
+ }
1105
+ [data-lp-editable="true"]:empty {
1106
+ min-width: 3em;
1107
+ outline: 1px dashed color-mix(in srgb, var(--lp-accent, #3b82f6) 40%, transparent);
1108
+ outline-offset: 2px;
1109
+ border-radius: 2px;
1110
+ }
549
1111
  @media print {
550
1112
  html, body {
551
1113
  display: block;
@@ -594,16 +1156,288 @@ ${scriptMarkup}
594
1156
  break-after: auto;
595
1157
  }
596
1158
  }
1159
+
1160
+ /* ---- 编辑器演示模式 ---- */
1161
+ .lp-editor-presentation-mode .lp-editor-bar,
1162
+ .lp-editor-presentation-mode .lp-editor-left-panel,
1163
+ .lp-editor-presentation-mode .lp-editor-right-panel,
1164
+ .lp-editor-presentation-mode .lp-editor-zoom-bar,
1165
+ .lp-editor-presentation-mode .lp-editor-page-counter {
1166
+ display: none !important;
1167
+ }
1168
+ .lp-editor-presentation-mode .lp-editor-workspace {
1169
+ display: block;
1170
+ }
1171
+ .lp-editor-presentation-mode .lp-editor-stage {
1172
+ position: fixed;
1173
+ inset: 0;
1174
+ z-index: 1000;
1175
+ background: #000;
1176
+ display: flex;
1177
+ align-items: center;
1178
+ justify-content: center;
1179
+ overflow: hidden;
1180
+ }
1181
+ .lp-editor-presentation-mode .lp-editor-stage-scaler {
1182
+ transform: scale(var(--lp-presentation-scale, 1)) !important;
1183
+ width: ${width}px !important;
1184
+ height: ${height}px !important;
1185
+ max-width: none;
1186
+ max-height: none;
1187
+ display: flex;
1188
+ align-items: center;
1189
+ justify-content: center;
1190
+ }
1191
+ .lp-editor-presentation-mode .lp-deck {
1192
+ width: ${width}px !important;
1193
+ height: ${height}px !important;
1194
+ border-radius: 0;
1195
+ }
1196
+ .lp-editor-root:fullscreen {
1197
+ background: #000;
1198
+ }
1199
+ .lp-editor-root:-webkit-full-screen {
1200
+ background: #000;
1201
+ }
1202
+ .lp-editor-presentation-exit {
1203
+ position: fixed;
1204
+ top: 16px;
1205
+ right: 16px;
1206
+ z-index: 1001;
1207
+ background: rgba(255, 255, 255, 0.12);
1208
+ color: #fff;
1209
+ border: 1px solid rgba(255, 255, 255, 0.2);
1210
+ border-radius: 6px;
1211
+ padding: 8px 12px;
1212
+ font-size: 13px;
1213
+ cursor: pointer;
1214
+ opacity: 0;
1215
+ transition: opacity 200ms ease;
1216
+ }
1217
+ .lp-editor-presentation-mode:hover .lp-editor-presentation-exit,
1218
+ .lp-editor-presentation-exit:focus {
1219
+ opacity: 1;
1220
+ }
1221
+
1222
+ /* 添加幻灯片弹窗 */
1223
+ .lp-add-slide-modal {
1224
+ position: fixed;
1225
+ inset: 0;
1226
+ z-index: 2000;
1227
+ display: flex;
1228
+ align-items: center;
1229
+ justify-content: center;
1230
+ }
1231
+ .lp-add-slide-modal[hidden] {
1232
+ display: none;
1233
+ }
1234
+ .lp-add-slide-modal-overlay {
1235
+ position: absolute;
1236
+ inset: 0;
1237
+ background: rgba(0, 0, 0, 0.72);
1238
+ backdrop-filter: blur(4px);
1239
+ }
1240
+ .lp-add-slide-modal-content {
1241
+ position: relative;
1242
+ width: min(1080px, calc(100vw - 48px));
1243
+ max-height: calc(100vh - 64px);
1244
+ background: #1e1e1e;
1245
+ border-radius: 16px;
1246
+ padding: 28px 32px 32px;
1247
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
1248
+ display: flex;
1249
+ flex-direction: column;
1250
+ color: #fff;
1251
+ }
1252
+ .lp-add-slide-modal-close {
1253
+ position: absolute;
1254
+ top: 16px;
1255
+ right: 16px;
1256
+ width: 32px;
1257
+ height: 32px;
1258
+ border: none;
1259
+ background: transparent;
1260
+ color: rgba(255, 255, 255, 0.6);
1261
+ font-size: 26px;
1262
+ line-height: 1;
1263
+ cursor: pointer;
1264
+ border-radius: 6px;
1265
+ display: flex;
1266
+ align-items: center;
1267
+ justify-content: center;
1268
+ transition: background 150ms ease, color 150ms ease;
1269
+ }
1270
+ .lp-add-slide-modal-close:hover {
1271
+ background: rgba(255, 255, 255, 0.1);
1272
+ color: #fff;
1273
+ }
1274
+ .lp-add-slide-modal-title {
1275
+ font-size: 18px;
1276
+ font-weight: 500;
1277
+ margin: 0 0 24px;
1278
+ color: #fff;
1279
+ }
1280
+ .lp-add-slide-grid {
1281
+ display: grid;
1282
+ grid-template-columns: repeat(6, minmax(0, 1fr));
1283
+ gap: 14px;
1284
+ overflow-y: auto;
1285
+ overflow-x: hidden;
1286
+ padding-right: 4px;
1287
+ margin-bottom: 28px;
1288
+ }
1289
+ .lp-add-slide-option {
1290
+ display: flex;
1291
+ flex-direction: column;
1292
+ align-items: center;
1293
+ gap: 10px;
1294
+ padding: 16px 8px;
1295
+ background: #2a2a2a;
1296
+ border: 2px solid transparent;
1297
+ border-radius: 12px;
1298
+ cursor: pointer;
1299
+ transition: background 150ms ease, border-color 150ms ease, transform 120ms ease;
1300
+ }
1301
+ .lp-add-slide-option:hover {
1302
+ background: #333;
1303
+ }
1304
+ .lp-add-slide-option.selected {
1305
+ border-color: #34d399;
1306
+ background: #243a30;
1307
+ }
1308
+ .lp-add-slide-icon {
1309
+ width: 44px;
1310
+ height: 44px;
1311
+ border-radius: 8px;
1312
+ background: #3f3f3f;
1313
+ display: flex;
1314
+ align-items: center;
1315
+ justify-content: center;
1316
+ color: rgba(255, 255, 255, 0.75);
1317
+ transition: background 150ms ease, color 150ms ease;
1318
+ }
1319
+ .lp-add-slide-option:hover .lp-add-slide-icon {
1320
+ background: #4a4a4a;
1321
+ color: #fff;
1322
+ }
1323
+ .lp-add-slide-option.selected .lp-add-slide-icon {
1324
+ background: #34d399;
1325
+ color: #1e1e1e;
1326
+ }
1327
+ .lp-add-slide-icon svg {
1328
+ width: 22px;
1329
+ height: 22px;
1330
+ }
1331
+ .lp-add-slide-label {
1332
+ font-size: 13px;
1333
+ color: rgba(255, 255, 255, 0.85);
1334
+ text-align: center;
1335
+ line-height: 1.25;
1336
+ white-space: nowrap;
1337
+ overflow: hidden;
1338
+ text-overflow: ellipsis;
1339
+ max-width: 100%;
1340
+ }
1341
+ .lp-add-slide-option.selected .lp-add-slide-label {
1342
+ color: #34d399;
1343
+ }
1344
+ .lp-add-slide-modal-btn {
1345
+ align-self: center;
1346
+ padding: 12px 32px;
1347
+ border-radius: 999px;
1348
+ border: none;
1349
+ font-size: 15px;
1350
+ font-weight: 500;
1351
+ cursor: pointer;
1352
+ background: #34d399;
1353
+ color: #111;
1354
+ transition: background 150ms ease, opacity 150ms ease, transform 120ms ease;
1355
+ }
1356
+ .lp-add-slide-modal-btn:hover:not(:disabled) {
1357
+ background: #2cb886;
1358
+ }
1359
+ .lp-add-slide-modal-btn:disabled {
1360
+ background: #4a4a4a;
1361
+ color: rgba(255, 255, 255, 0.4);
1362
+ cursor: not-allowed;
1363
+ }
1364
+ @media (max-width: 860px) {
1365
+ .lp-add-slide-grid {
1366
+ grid-template-columns: repeat(4, 1fr);
1367
+ }
1368
+ }
1369
+ @media (max-width: 560px) {
1370
+ .lp-add-slide-grid {
1371
+ grid-template-columns: repeat(3, 1fr);
1372
+ }
1373
+ .lp-add-slide-modal-content {
1374
+ padding: 24px 20px 28px;
1375
+ }
1376
+ }
1377
+
1378
+ /* jQuery 自定义滚动条样式(基于 WebKit + Firefox 标准属性) */
1379
+ .lp-editor-left-panel.lp-custom-scrollbar::-webkit-scrollbar,
1380
+ .lp-editor-right-panel.lp-custom-scrollbar::-webkit-scrollbar,
1381
+ .lp-add-slide-grid.lp-custom-scrollbar::-webkit-scrollbar {
1382
+ width: 8px;
1383
+ height: 8px;
1384
+ }
1385
+ .lp-editor-left-panel.lp-custom-scrollbar::-webkit-scrollbar-track,
1386
+ .lp-editor-right-panel.lp-custom-scrollbar::-webkit-scrollbar-track,
1387
+ .lp-add-slide-grid.lp-custom-scrollbar::-webkit-scrollbar-track {
1388
+ background: rgba(255, 255, 255, 0.1);
1389
+ border-radius: 4px;
1390
+ }
1391
+ .lp-editor-left-panel.lp-custom-scrollbar::-webkit-scrollbar-thumb,
1392
+ .lp-editor-right-panel.lp-custom-scrollbar::-webkit-scrollbar-thumb,
1393
+ .lp-add-slide-grid.lp-custom-scrollbar::-webkit-scrollbar-thumb {
1394
+ background: rgba(255, 255, 255, 0.35);
1395
+ border-radius: 4px;
1396
+ }
1397
+ .lp-editor-left-panel.lp-custom-scrollbar::-webkit-scrollbar-thumb:hover,
1398
+ .lp-editor-right-panel.lp-custom-scrollbar::-webkit-scrollbar-thumb:hover,
1399
+ .lp-add-slide-grid.lp-custom-scrollbar::-webkit-scrollbar-thumb:hover {
1400
+ background: rgba(255, 255, 255, 0.55);
1401
+ }
1402
+ .lp-editor-left-panel.lp-custom-scrollbar,
1403
+ .lp-editor-right-panel.lp-custom-scrollbar,
1404
+ .lp-add-slide-grid.lp-custom-scrollbar {
1405
+ scrollbar-width: thin;
1406
+ scrollbar-color: rgba(255, 255, 255, 0.35) rgba(255, 255, 255, 0.1);
1407
+ }
1408
+
1409
+ .lp-filmstrip-v1-track.lp-custom-scrollbar::-webkit-scrollbar {
1410
+ width: 8px;
1411
+ height: 8px;
1412
+ }
1413
+ .lp-filmstrip-v1-track.lp-custom-scrollbar::-webkit-scrollbar-track {
1414
+ background: rgba(0, 0, 0, 0.08);
1415
+ border-radius: 4px;
1416
+ }
1417
+ .lp-filmstrip-v1-track.lp-custom-scrollbar::-webkit-scrollbar-thumb {
1418
+ background: rgba(0, 0, 0, 0.35);
1419
+ border-radius: 4px;
1420
+ }
1421
+ .lp-filmstrip-v1-track.lp-custom-scrollbar::-webkit-scrollbar-thumb:hover {
1422
+ background: rgba(0, 0, 0, 0.55);
1423
+ }
1424
+ .lp-filmstrip-v1-track.lp-custom-scrollbar {
1425
+ scrollbar-width: thin;
1426
+ scrollbar-color: rgba(0, 0, 0, 0.35) rgba(0, 0, 0, 0.08);
1427
+ }
597
1428
  </style>
598
1429
  </head>
599
1430
  <body class="${bodyClass}">
600
1431
  ${bodyMarkup}
601
1432
  </body>
602
1433
  </html>`;
603
- const theme = goal.theme || 'base';
604
1434
  return {
605
1435
  html,
606
- assets: ['./assets/fonts/fonts.css', `./assets/${theme}.css`],
1436
+ assets: [
1437
+ './assets/fonts/fonts.css',
1438
+ `./assets/${theme}.css`,
1439
+ './assets/jquery.min.js',
1440
+ ],
607
1441
  };
608
1442
  }
609
1443
  function buildNavMarkup(slideCount) {
@@ -618,33 +1452,212 @@ function buildNavMarkup(slideCount) {
618
1452
  <div class="lp-page-counter"><span id="lp-current">1</span> / ${slideCount}</div>
619
1453
  <div class="lp-hint">← → 翻页</div>`;
620
1454
  }
621
- function buildEditorBarMarkup() {
1455
+ function buildEditorBarMarkup(goal) {
1456
+ const theme = goal.theme || 'theme01';
1457
+ const isTheme02 = theme === 'theme02';
1458
+ const isTheme03 = theme === 'theme03';
1459
+ const isTheme04 = theme === 'theme04';
1460
+ const isTheme05 = theme === 'theme05';
1461
+ const isTheme06 = theme === 'theme06';
1462
+ const colorScheme = goal.colorScheme || (isTheme02 || isTheme03 ? 'scheme-a' : isTheme04 ? 'green' : isTheme05 ? 'coral' : isTheme06 ? 'volt' : 'light');
1463
+ const appearance = goal.appearance || ((isTheme03 || isTheme04 || isTheme05 || isTheme06) ? 'dark' : undefined);
1464
+ let appearanceButtons = '';
1465
+ if (isTheme02) {
1466
+ appearanceButtons = `<button class="lp-appearance-btn ${colorScheme === 'scheme-a' ? 'lp-appearance-active' : ''}" data-appearance="scheme-a" title="电光青绿" aria-pressed="${colorScheme === 'scheme-a'}">
1467
+ 电光青绿
1468
+ </button>
1469
+ <button class="lp-appearance-btn ${colorScheme === 'scheme-b' ? 'lp-appearance-active' : ''}" data-appearance="scheme-b" title="靛蓝珊瑚" aria-pressed="${colorScheme === 'scheme-b'}">
1470
+ 靛蓝珊瑚
1471
+ </button>`;
1472
+ }
1473
+ else if (isTheme03) {
1474
+ appearanceButtons = `<button class="lp-appearance-btn ${appearance === 'light' ? 'lp-appearance-active' : ''}" data-appearance="light" title="浅色" aria-pressed="${appearance === 'light'}">
1475
+ 浅色
1476
+ </button>
1477
+ <button class="lp-appearance-btn ${appearance === 'dark' ? 'lp-appearance-active' : ''}" data-appearance="dark" title="深色" aria-pressed="${appearance === 'dark'}">
1478
+ 深色
1479
+ </button>`;
1480
+ }
1481
+ else if (isTheme04) {
1482
+ appearanceButtons = `<button class="lp-appearance-btn ${colorScheme === 'green' ? 'lp-appearance-active' : ''}" data-theme="green" title="糖果绿" aria-pressed="${colorScheme === 'green'}">
1483
+ 糖果绿
1484
+ </button>
1485
+ <button class="lp-appearance-btn ${colorScheme === 'yellow' ? 'lp-appearance-active' : ''}" data-theme="yellow" title="柠檬黄" aria-pressed="${colorScheme === 'yellow'}">
1486
+ 柠檬黄
1487
+ </button>
1488
+ <button class="lp-appearance-btn ${colorScheme === 'blue' ? 'lp-appearance-active' : ''}" data-theme="blue" title="冰晶蓝" aria-pressed="${colorScheme === 'blue'}">
1489
+ 冰晶蓝
1490
+ </button>
1491
+ <button class="lp-appearance-btn ${colorScheme === 'pink' ? 'lp-appearance-active' : ''}" data-theme="pink" title="糖果粉" aria-pressed="${colorScheme === 'pink'}">
1492
+ 糖果粉
1493
+ </button>`;
1494
+ }
1495
+ else if (isTheme05) {
1496
+ const scheme = String(colorScheme);
1497
+ appearanceButtons = `<button class="lp-appearance-btn ${scheme === 'coral' ? 'lp-appearance-active' : ''}" data-theme="coral" title="珊瑚红" aria-pressed="${scheme === 'coral'}">
1498
+ 珊瑚红
1499
+ </button>
1500
+ <button class="lp-appearance-btn ${scheme === 'amber' ? 'lp-appearance-active' : ''}" data-theme="amber" title="琥珀黄" aria-pressed="${scheme === 'amber'}">
1501
+ 琥珀黄
1502
+ </button>
1503
+ <button class="lp-appearance-btn ${scheme === 'teal' ? 'lp-appearance-active' : ''}" data-theme="teal" title="青绿" aria-pressed="${scheme === 'teal'}">
1504
+ 青绿
1505
+ </button>
1506
+ <button class="lp-appearance-btn ${scheme === 'indigo' ? 'lp-appearance-active' : ''}" data-theme="indigo" title="靛蓝" aria-pressed="${scheme === 'indigo'}">
1507
+ 靛蓝
1508
+ </button>
1509
+ <button class="lp-appearance-btn ${scheme === 'violet' ? 'lp-appearance-active' : ''}" data-theme="violet" title="紫罗兰" aria-pressed="${scheme === 'violet'}">
1510
+ 紫罗兰
1511
+ </button>`;
1512
+ }
1513
+ else if (isTheme06) {
1514
+ const scheme = String(colorScheme);
1515
+ appearanceButtons = `<button class="lp-appearance-btn ${scheme === 'volt' ? 'lp-appearance-active' : ''}" data-theme="volt" title="电光青柠" aria-pressed="${scheme === 'volt'}">
1516
+ 电光青柠
1517
+ </button>
1518
+ <button class="lp-appearance-btn ${scheme === 'magma' ? 'lp-appearance-active' : ''}" data-theme="magma" title="熔岩珊瑚" aria-pressed="${scheme === 'magma'}">
1519
+ 熔岩珊瑚
1520
+ </button>
1521
+ <button class="lp-appearance-btn ${scheme === 'nebula' ? 'lp-appearance-active' : ''}" data-theme="nebula" title="星云蓝紫" aria-pressed="${scheme === 'nebula'}">
1522
+ 星云蓝紫
1523
+ </button>
1524
+ <button class="lp-appearance-btn ${scheme === 'nova' ? 'lp-appearance-active' : ''}" data-theme="nova" title="新星金黄" aria-pressed="${scheme === 'nova'}">
1525
+ 新星金黄
1526
+ </button>`;
1527
+ }
1528
+ else {
1529
+ appearanceButtons = `<button class="lp-appearance-btn ${colorScheme === 'light' ? 'lp-appearance-active' : ''}" data-appearance="light" title="浅色" aria-pressed="${colorScheme === 'light'}">
1530
+ 浅色
1531
+ </button>
1532
+ <button class="lp-appearance-btn ${colorScheme === 'dark' ? 'lp-appearance-active' : ''}" data-appearance="dark" title="深色" aria-pressed="${colorScheme === 'dark'}">
1533
+ 深色
1534
+ </button>`;
1535
+ }
1536
+ let lightDarkButtons = '';
1537
+ if (isTheme04 || isTheme05 || isTheme06) {
1538
+ lightDarkButtons = `<div class="lp-appearance-switcher lp-appearance-switcher--lightdark" role="group" aria-label="深浅模式">
1539
+ <button class="lp-appearance-btn ${appearance === 'light' ? 'lp-appearance-active' : ''}" data-appearance="light" title="浅色" aria-pressed="${appearance === 'light'}">
1540
+ 浅色
1541
+ </button>
1542
+ <button class="lp-appearance-btn ${appearance === 'dark' ? 'lp-appearance-active' : ''}" data-appearance="dark" title="深色" aria-pressed="${appearance === 'dark'}">
1543
+ 深色
1544
+ </button>
1545
+ </div>`;
1546
+ }
622
1547
  return `<div class="lp-editor-bar">
623
1548
  <span class="lp-editor-title">lemonPPT 编辑器</span>
624
- <label class="lp-theme-switcher-label">
625
- 主题
626
- <select id="lp-theme-switcher">
627
- <option value="base">极简白</option>
628
- <option value="dark-tech">深色科技</option>
629
- <option value="warm-business">暖色商务</option>
630
- </select>
631
- </label>
632
- <button class="lp-editor-btn" id="lp-play" title="播放演示">▶ 播放</button>
1549
+ <div class="lp-appearance-switcher" role="group" aria-label="外观">
1550
+ ${appearanceButtons}
1551
+ </div>
1552
+ ${lightDarkButtons}
1553
+ <button class="lp-editor-btn" id="lp-add-slide" title="添加新幻灯片">+ 添加幻灯片</button>
1554
+ <button class="lp-editor-btn lp-editor-btn-primary" id="lp-play" title="播放演示">▶ 播放</button>
633
1555
  <button class="lp-editor-btn" id="lp-undo" title="撤销 (Ctrl+Z)">↩ 撤销</button>
634
1556
  <button class="lp-editor-btn" id="lp-redo" title="重做 (Ctrl+Y)">↪ 重做</button>
635
- <button class="lp-editor-btn" id="lp-download-goal" title="下载 goal.json">下载 goal.json</button>
636
- <button class="lp-editor-btn" id="lp-export-pptx" title="导出 PPTX">导出 PPTX</button>
637
- <button class="lp-editor-btn" id="lp-export-pdf" title="导出 PDF">导出 PDF</button>
1557
+ <div class="lp-editor-export" id="lp-editor-export">
1558
+ <button class="lp-editor-btn lp-editor-export-toggle" id="lp-export-toggle" type="button" aria-haspopup="true" aria-expanded="false">
1559
+ <span>⬇</span> 导出 <span class="lp-editor-caret">⌄</span>
1560
+ </button>
1561
+ <div class="lp-editor-export-menu" id="lp-export-menu" hidden>
1562
+ <button class="lp-editor-export-item" id="lp-export-html" type="button">
1563
+ <span class="lp-editor-export-icon">&lt;/&gt;</span>
1564
+ <span class="lp-editor-export-label">HTML</span>
1565
+ <span class="lp-editor-export-tag">离线包</span>
1566
+ </button>
1567
+ <button class="lp-editor-export-item" id="lp-export-pdf" type="button">
1568
+ <span class="lp-editor-export-icon">🗎</span>
1569
+ <span class="lp-editor-export-label">PDF</span>
1570
+ <span class="lp-editor-export-tag">下载</span>
1571
+ </button>
1572
+ <button class="lp-editor-export-item" id="lp-export-pptx" type="button">
1573
+ <span class="lp-editor-export-icon">🖵</span>
1574
+ <span class="lp-editor-export-label">PPTX</span>
1575
+ <span class="lp-editor-export-tag">可编辑</span>
1576
+ </button>
1577
+ </div>
1578
+ </div>
1579
+ </div>`;
1580
+ }
1581
+ const LAYOUT_ICONS = {
1582
+ cover: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg>',
1583
+ tableOfContents: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>',
1584
+ content: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h10M4 18h16"/></svg>',
1585
+ metric: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>',
1586
+ quote: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 10c0-3 2-5 5-5m-5 5v5h5m6-5c0-3 2-5 5-5m-5 5v5h5"/></svg>',
1587
+ closing: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12h16M12 5l7 7-7 7"/></svg>',
1588
+ feature: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6-4.8-6 4.8 2.4-7.2-6-4.8h7.6z"/></svg>',
1589
+ process: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>',
1590
+ chapter: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6M9 13h6"/></svg>',
1591
+ comparison: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h7v12H4zM13 6h7v12h-7z"/></svg>',
1592
+ stats: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4v16h16"/></svg>',
1593
+ timeline: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M4 12h4m8 0h4"/></svg>',
1594
+ testimonial: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/></svg>',
1595
+ roadmap: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h10M4 18h16"/></svg>',
1596
+ chart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19v-6m5 6V9m5 10V5m5 14v-8"/></svg>',
1597
+ bento: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="18" height="8" rx="1"/></svg>',
1598
+ gallery: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 15l-5-5-5 6-3-3-5 5"/></svg>',
1599
+ table: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 10h18M3 14h18M3 18h18"/></svg>',
1600
+ tags: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h10M4 17h14"/></svg>',
1601
+ filmstrip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 6v12M18 6v12"/></svg>',
1602
+ faq: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 16v-1a3 3 0 0 1 3-3h0a3 3 0 0 0 3-3h0a3 3 0 0 0-3-3h0a3 3 0 0 0-3 3v1"/><circle cx="12" cy="17" r=".5" fill="currentColor"/></svg>',
1603
+ team: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/></svg>',
1604
+ pricing: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>',
1605
+ partners: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><circle cx="17" cy="7" r="4"/></svg>',
1606
+ image: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 15l-5-5-5 6-3-3-5 5"/></svg>',
1607
+ swot: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h8v8H4zM12 4h8v8h-8zM4 12h8v8H4zM12 12h8v8h-8z"/></svg>',
1608
+ pest: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4zM4 9h16M4 15h16M9 4v16"/></svg>',
1609
+ };
1610
+ function getLayoutIcon(role) {
1611
+ return LAYOUT_ICONS[role] ?? LAYOUT_ICONS.content;
1612
+ }
1613
+ function buildAddSlideModalMarkup(goal) {
1614
+ const theme = goal.theme || 'theme01';
1615
+ const layouts = listLayouts()
1616
+ .filter((l) => l.theme === theme)
1617
+ .sort((a, b) => a.role.localeCompare(b.role) || a.displayName.localeCompare(b.displayName));
1618
+ const options = layouts.map((layout) => {
1619
+ const icon = getLayoutIcon(layout.role);
1620
+ const label = (layout.displayName || layout.id).replace(/^Theme\s+\d+\s*/, '');
1621
+ return `<div class="lp-add-slide-option" data-layout="${escapeHtml(layout.id)}" data-role="${escapeHtml(layout.role)}">
1622
+ <div class="lp-add-slide-icon">${icon}</div>
1623
+ <span class="lp-add-slide-label">${escapeHtml(label)}</span>
1624
+ </div>`;
1625
+ }).join('\n');
1626
+ return `<div id="lp-add-slide-modal" class="lp-add-slide-modal" hidden>
1627
+ <div class="lp-add-slide-modal-overlay" data-close-modal></div>
1628
+ <div class="lp-add-slide-modal-content">
1629
+ <button class="lp-add-slide-modal-close" data-close-modal aria-label="关闭">&times;</button>
1630
+ <h3 class="lp-add-slide-modal-title">请选择你想添加的幻灯片版式</h3>
1631
+ <div class="lp-add-slide-grid" data-scrollbar>
1632
+ ${options}
1633
+ </div>
1634
+ <button class="lp-add-slide-modal-btn" id="lp-add-slide-confirm" disabled>添加幻灯片</button>
1635
+ </div>
638
1636
  </div>`;
639
1637
  }
640
1638
  function buildLeftPanelMarkup(goal, slideHtmls, slideWidth, slideHeight) {
641
- const thumbInnerWidth = 216;
1639
+ const thumbInnerWidth = 156;
642
1640
  const scale = thumbInnerWidth / slideWidth;
1641
+ const showDragHandle = goal.slides.length > 1;
1642
+ const dragHandleHtml = showDragHandle
1643
+ ? '<span class="lp-thumbnail-drag-handle" data-lp-action="drag-handle" aria-hidden="true">' +
1644
+ '<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor" aria-hidden="true">' +
1645
+ '<circle cx="2.5" cy="2.5" r="1.5"/>' +
1646
+ '<circle cx="9.5" cy="2.5" r="1.5"/>' +
1647
+ '<circle cx="2.5" cy="8" r="1.5"/>' +
1648
+ '<circle cx="9.5" cy="8" r="1.5"/>' +
1649
+ '<circle cx="2.5" cy="13.5" r="1.5"/>' +
1650
+ '<circle cx="9.5" cy="13.5" r="1.5"/>' +
1651
+ '</svg>' +
1652
+ '</span>'
1653
+ : '';
643
1654
  const buttons = goal.slides.map((slide, index) => {
644
1655
  const label = getSlideLabel(slide);
645
1656
  const activeClass = index === 0 ? 'active' : '';
646
1657
  const slideHtml = slideHtmls[index] || '';
647
- return `<button class="lp-thumbnail ${activeClass}" data-index="${index}" type="button">
1658
+ const draggableAttr = goal.slides.length > 1 ? 'draggable="true"' : '';
1659
+ return `<div class="lp-thumbnail ${activeClass}" data-index="${index}" role="button" tabindex="0" ${draggableAttr} aria-label="幻灯片 ${index + 1},拖动可调整顺序">
1660
+ ${dragHandleHtml}
648
1661
  <div class="lp-thumbnail-render">
649
1662
  <div class="lp-thumbnail-scaler" style="width:${slideWidth}px;height:${slideHeight}px;transform:scale(${scale});transform-origin:top left;">
650
1663
  ${slideHtml}
@@ -656,9 +1669,17 @@ function buildLeftPanelMarkup(goal, slideHtmls, slideWidth, slideHeight) {
656
1669
  <div class="lp-thumbnail-title">${escapeHtml(label)}</div>
657
1670
  <div class="lp-thumbnail-layout">${escapeHtml(slide.layout)}</div>
658
1671
  </div>
659
- </button>`;
1672
+ <span class="lp-thumbnail-delete" data-lp-action="delete-slide" data-index="${index}" title="删除幻灯片" aria-label="删除幻灯片">×</span>
1673
+ </div>`;
660
1674
  }).join('');
661
- return `<aside class="lp-editor-left-panel" aria-label="幻灯片缩略图">
1675
+ return `<aside class="lp-editor-left-panel" data-scrollbar aria-label="幻灯片缩略图">
1676
+ <div class="lp-delete-confirm-toast" id="lp-delete-confirm-toast" hidden>
1677
+ <div class="lp-delete-confirm-text" id="lp-delete-confirm-text">确定删除该幻灯片?</div>
1678
+ <div class="lp-delete-confirm-actions">
1679
+ <button class="lp-delete-confirm-cancel" id="lp-delete-confirm-cancel" type="button">取消</button>
1680
+ <button class="lp-delete-confirm-confirm" id="lp-delete-confirm-confirm" type="button">删除</button>
1681
+ </div>
1682
+ </div>
662
1683
  <div class="lp-editor-thumbnails">
663
1684
  ${buttons}
664
1685
  </div>
@@ -685,7 +1706,7 @@ function getSlideLabel(slide) {
685
1706
  return slide.layout;
686
1707
  }
687
1708
  function buildRightPanelMarkup() {
688
- return `<aside class="lp-editor-right-panel" aria-label="属性面板">
1709
+ return `<aside class="lp-editor-right-panel" data-scrollbar aria-label="属性面板">
689
1710
  <div class="lp-property-header">属性</div>
690
1711
  <div id="lp-property-content">
691
1712
  <div class="lp-property-empty">点击幻灯片中的文本或图片以编辑属性</div>
@@ -700,26 +1721,86 @@ function buildScriptMarkup() {
700
1721
  const prevBtn = document.getElementById('lp-prev');
701
1722
  const nextBtn = document.getElementById('lp-next');
702
1723
  const currentLabel = document.getElementById('lp-current');
1724
+ const deck = document.querySelector('.lp-deck');
703
1725
  let current = 0;
1726
+ let isTransitioning = false;
1727
+ let cleanupTimer = null;
1728
+
1729
+ function updateUI() {
1730
+ dots.forEach((dot, index) => {
1731
+ dot.classList.toggle('active', index === current);
1732
+ });
1733
+ if (currentLabel) currentLabel.textContent = String(current + 1);
1734
+ if (prevBtn) prevBtn.disabled = current === 0 || isTransitioning;
1735
+ if (nextBtn) nextBtn.disabled = current === slides.length - 1 || isTransitioning;
1736
+ }
1737
+
1738
+ function resetTransitionState() {
1739
+ slides.forEach((slide) => slide.classList.remove('enter', 'leave'));
1740
+ if (deck) {
1741
+ deck.setAttribute('data-lp-transition', 'none');
1742
+ deck.removeAttribute('data-lp-direction');
1743
+ }
1744
+ isTransitioning = false;
1745
+ updateClasses();
1746
+ }
704
1747
 
705
1748
  function updateClasses() {
706
1749
  slides.forEach((slide, index) => {
707
- slide.classList.remove('active', 'prev');
1750
+ slide.classList.remove('active', 'prev', 'enter', 'leave');
708
1751
  if (index === current) slide.classList.add('active');
709
1752
  else if (index < current) slide.classList.add('prev');
710
1753
  });
711
- dots.forEach((dot, index) => {
712
- dot.classList.toggle('active', index === current);
713
- });
714
- if (currentLabel) currentLabel.textContent = String(current + 1);
715
- if (prevBtn) prevBtn.disabled = current === 0;
716
- if (nextBtn) nextBtn.disabled = current === slides.length - 1;
1754
+ updateUI();
717
1755
  }
718
1756
 
719
1757
  function goTo(index) {
720
- if (index < 0 || index >= slides.length) return;
1758
+ if (index === current || index < 0 || index >= slides.length || isTransitioning) return;
1759
+ const leaving = slides[current];
1760
+ const entering = slides[index];
1761
+ const transition = entering.dataset.lpTransition || 'none';
1762
+ const direction = index > current ? 'next' : 'prev';
1763
+
1764
+ if (transition === 'none') {
1765
+ current = index;
1766
+ if (deck) {
1767
+ deck.setAttribute('data-lp-transition', 'none');
1768
+ deck.removeAttribute('data-lp-direction');
1769
+ }
1770
+ leaving.classList.remove('active');
1771
+ entering.classList.add('active');
1772
+ updateClasses();
1773
+ return;
1774
+ }
1775
+
1776
+ isTransitioning = true;
721
1777
  current = index;
722
- updateClasses();
1778
+ updateUI();
1779
+
1780
+ if (deck) {
1781
+ deck.setAttribute('data-lp-transition', transition);
1782
+ deck.setAttribute('data-lp-direction', direction);
1783
+ }
1784
+
1785
+ leaving.classList.add('leave');
1786
+ entering.classList.add('enter');
1787
+ // 先禁用 transition,让 leave/enter 的初始状态直接落位,
1788
+ // 避免从上一状态的 translateX 插值;随后恢复 transition 再切换 active。
1789
+ leaving.style.transition = 'none';
1790
+ entering.style.transition = 'none';
1791
+ void entering.offsetWidth;
1792
+ leaving.style.transition = '';
1793
+ entering.style.transition = '';
1794
+ leaving.classList.remove('active');
1795
+ entering.classList.add('active');
1796
+
1797
+ function finish() {
1798
+ entering.removeEventListener('transitionend', finish);
1799
+ clearTimeout(cleanupTimer);
1800
+ resetTransitionState();
1801
+ }
1802
+ entering.addEventListener('transitionend', finish);
1803
+ cleanupTimer = setTimeout(finish, 600);
723
1804
  }
724
1805
 
725
1806
  if (prevBtn) prevBtn.addEventListener('click', () => goTo(current - 1));
@@ -728,6 +1809,7 @@ function buildScriptMarkup() {
728
1809
  dot.addEventListener('click', () => goTo(Number(dot.dataset.index)));
729
1810
  });
730
1811
  document.addEventListener('keydown', (e) => {
1812
+ if (isTransitioning) return;
731
1813
  if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') goTo(current + 1);
732
1814
  if (e.key === 'ArrowLeft' || e.key === 'PageUp') goTo(current - 1);
733
1815
  if (e.key === 'Home') goTo(0);
@@ -736,12 +1818,32 @@ function buildScriptMarkup() {
736
1818
 
737
1819
  updateClasses();
738
1820
  })();
1821
+ </script>
1822
+ <script src="./assets/client-render.js"></script>
1823
+ <script src="./assets/theme-echarts.js"></script>
1824
+ <script>
1825
+ (function () {
1826
+ if (typeof window.__lemonPPT_initECharts === 'function') {
1827
+ window.__lemonPPT_initECharts();
1828
+ }
1829
+ })();
739
1830
  </script>`;
740
1831
  }
741
1832
  function buildEditorScriptMarkup(goal) {
742
1833
  const goalJson = JSON.stringify(goal, null, 2).replace(/</g, '\\u003c');
743
- return `<script>
1834
+ const schemas = {};
1835
+ listLayouts().forEach((layout) => {
1836
+ const schema = getLayoutSchema(layout.id);
1837
+ if (schema) {
1838
+ schemas[layout.id] = schema;
1839
+ }
1840
+ });
1841
+ const schemasJson = JSON.stringify(schemas).replace(/</g, '\\u003c');
1842
+ return `<script src="./assets/client-render.js"></script>
1843
+ <script src="./assets/theme-echarts.js"></script>
1844
+ <script>
744
1845
  window.__lemonPPT_goal = ${goalJson};
1846
+ window.__lemonPPT_layoutSchemas = ${schemasJson};
745
1847
  ${editorScript}
746
1848
  </script>`;
747
1849
  }