lumina-slides 8.9.4 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LUMINA_LLM_EXAMPLES.json +234 -0
  2. package/README.md +18 -18
  3. package/dist/lumina-slides.js +13207 -12659
  4. package/dist/lumina-slides.umd.cjs +215 -215
  5. package/dist/style.css +1 -1
  6. package/package.json +5 -4
  7. package/src/App.vue +16 -0
  8. package/src/animation/index.ts +11 -0
  9. package/src/animation/registry.ts +126 -0
  10. package/src/animation/stagger.ts +95 -0
  11. package/src/animation/types.ts +53 -0
  12. package/src/components/LandingPage.vue +229 -0
  13. package/src/components/LuminaDeck.vue +224 -0
  14. package/src/components/LuminaSpeakerNotes.vue +701 -0
  15. package/src/components/base/BaseSlide.vue +122 -0
  16. package/src/components/base/LuminaElement.vue +67 -0
  17. package/src/components/base/VideoPlayer.vue +204 -0
  18. package/src/components/layouts/LayoutAuto.vue +71 -0
  19. package/src/components/layouts/LayoutChart.vue +287 -0
  20. package/src/components/layouts/LayoutCustom.vue +92 -0
  21. package/src/components/layouts/LayoutDiagram.vue +253 -0
  22. package/src/components/layouts/LayoutFeatures.vue +121 -0
  23. package/src/components/layouts/LayoutFlex.vue +172 -0
  24. package/src/components/layouts/LayoutFree.vue +62 -0
  25. package/src/components/layouts/LayoutHalf.vue +127 -0
  26. package/src/components/layouts/LayoutStatement.vue +74 -0
  27. package/src/components/layouts/LayoutSteps.vue +106 -0
  28. package/src/components/layouts/LayoutTimeline.vue +104 -0
  29. package/src/components/layouts/LayoutVideo.vue +41 -0
  30. package/src/components/parts/FlexBullets.vue +45 -0
  31. package/src/components/parts/FlexButton.vue +132 -0
  32. package/src/components/parts/FlexImage.vue +54 -0
  33. package/src/components/parts/FlexOrdered.vue +44 -0
  34. package/src/components/parts/FlexSpacer.vue +13 -0
  35. package/src/components/parts/FlexStepper.vue +59 -0
  36. package/src/components/parts/FlexText.vue +29 -0
  37. package/src/components/parts/FlexTimeline.vue +67 -0
  38. package/src/components/parts/FlexTitle.vue +39 -0
  39. package/src/components/parts/LuminaBackground.vue +100 -0
  40. package/src/components/site/LivePreview.vue +101 -0
  41. package/src/components/site/SiteApi.vue +301 -0
  42. package/src/components/site/SiteDashboard.vue +604 -0
  43. package/src/components/site/SiteDocs.vue +3267 -0
  44. package/src/components/site/SiteExamples.vue +65 -0
  45. package/src/components/site/SiteFooter.vue +6 -0
  46. package/src/components/site/SiteHome.vue +362 -0
  47. package/src/components/site/SiteNavBar.vue +122 -0
  48. package/src/components/site/SitePlayground.vue +389 -0
  49. package/src/components/site/SitePromptBuilder.vue +266 -0
  50. package/src/components/site/SiteUserMenu.vue +90 -0
  51. package/src/components/studio/ActionEditor.vue +108 -0
  52. package/src/components/studio/ArrayEditor.vue +124 -0
  53. package/src/components/studio/CollapsibleSection.vue +33 -0
  54. package/src/components/studio/ColorField.vue +22 -0
  55. package/src/components/studio/EditorCanvas.vue +326 -0
  56. package/src/components/studio/EditorLayoutFeatures.vue +18 -0
  57. package/src/components/studio/EditorLayoutFixed.vue +46 -0
  58. package/src/components/studio/EditorLayoutFlex.vue +133 -0
  59. package/src/components/studio/EditorLayoutHalf.vue +18 -0
  60. package/src/components/studio/EditorLayoutStatement.vue +18 -0
  61. package/src/components/studio/EditorLayoutSteps.vue +18 -0
  62. package/src/components/studio/EditorLayoutTimeline.vue +18 -0
  63. package/src/components/studio/EditorNode.vue +89 -0
  64. package/src/components/studio/FieldEditor.vue +133 -0
  65. package/src/components/studio/IconPicker.vue +109 -0
  66. package/src/components/studio/LayerItem.vue +117 -0
  67. package/src/components/studio/LuminaStudio.vue +30 -0
  68. package/src/components/studio/SaveSuccessModal.vue +138 -0
  69. package/src/components/studio/SlideNavigator.vue +373 -0
  70. package/src/components/studio/SliderField.vue +44 -0
  71. package/src/components/studio/StudioInspector.vue +595 -0
  72. package/src/components/studio/StudioJsonEditor.vue +191 -0
  73. package/src/components/studio/StudioLayers.vue +145 -0
  74. package/src/components/studio/StudioSettings.vue +514 -0
  75. package/src/components/studio/StudioSidebar.vue +29 -0
  76. package/src/components/studio/StudioToolbar.vue +222 -0
  77. package/src/components/studio/fieldLabels.ts +224 -0
  78. package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
  79. package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
  80. package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
  81. package/src/composables/useAuth.ts +87 -0
  82. package/src/composables/useEditor.ts +224 -0
  83. package/src/composables/useElementState.ts +81 -0
  84. package/src/composables/useFlexLayout.ts +122 -0
  85. package/src/composables/useKeyboard.ts +45 -0
  86. package/src/composables/useLumina.ts +32 -0
  87. package/src/composables/useStudio.ts +87 -0
  88. package/src/composables/useSwipeNav.ts +53 -0
  89. package/src/composables/useTransition.ts +373 -0
  90. package/src/core/Lumina.ts +819 -0
  91. package/src/core/animationConfig.ts +251 -0
  92. package/src/core/compression.ts +34 -0
  93. package/src/core/elementController.ts +170 -0
  94. package/src/core/elementId.ts +27 -0
  95. package/src/core/elementResolver.ts +207 -0
  96. package/src/core/events.ts +53 -0
  97. package/src/core/fonts.ts +100 -0
  98. package/src/core/presets.ts +231 -0
  99. package/src/core/prompts.ts +272 -0
  100. package/src/core/schema.ts +478 -0
  101. package/src/core/speaker-channel.ts +250 -0
  102. package/src/core/store.ts +461 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1611 -0
  105. package/src/directives/vStudio.ts +45 -0
  106. package/src/index.ts +175 -0
  107. package/src/main.ts +17 -0
  108. package/src/router/index.ts +92 -0
  109. package/src/style/main.css +462 -0
  110. package/src/utils/deep.ts +127 -0
  111. package/src/utils/firebase.ts +184 -0
  112. package/src/utils/streaming.ts +134 -0
  113. package/src/views/DashboardView.vue +32 -0
  114. package/src/views/DeckView.vue +289 -0
  115. package/src/views/HomeView.vue +17 -0
  116. package/src/views/SiteLayout.vue +21 -0
  117. package/src/views/StudioView.vue +61 -0
  118. package/src/vite-env.d.ts +6 -0
  119. package/IMPLEMENTATION.md +0 -418
@@ -0,0 +1,701 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LUMINA SPEAKER NOTES
4
+ *
5
+ * Standalone component for the speaker notes popup window.
6
+ * Provides bidirectional sync with the main presentation.
7
+ *
8
+ * Features:
9
+ * - Current slide notes with markdown rendering
10
+ * - Next slide preview
11
+ * - Navigation controls (synced with main window)
12
+ * - Presentation timer with start/pause/reset
13
+ * - Connection status indicator
14
+ */
15
+ import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
16
+ import { SpeakerChannel } from '../core/speaker-channel';
17
+ import type { SpeakerSyncPayload } from '../core/types';
18
+
19
+ // --- Props ---
20
+ const props = defineProps<{
21
+ channelId?: string;
22
+ }>();
23
+
24
+ // --- State ---
25
+ const currentIndex = ref(0);
26
+ const totalSlides = ref(0);
27
+ const currentNotes = ref('');
28
+ const nextSlidePreview = ref<{ title?: string; type?: string } | null>(null);
29
+ const isConnected = ref(false);
30
+ const channel = ref<SpeakerChannel | null>(null);
31
+
32
+ // Timer state
33
+ const timerSeconds = ref(0);
34
+ const timerRunning = ref(false);
35
+ const timerInterval = ref<ReturnType<typeof setInterval> | null>(null);
36
+
37
+ // --- Computed ---
38
+ const slideIndicator = computed(() => {
39
+ if (totalSlides.value === 0) return 'No slides loaded';
40
+ return `Slide ${currentIndex.value + 1} / ${totalSlides.value}`;
41
+ });
42
+
43
+ const formattedTime = computed(() => {
44
+ const hours = Math.floor(timerSeconds.value / 3600);
45
+ const minutes = Math.floor((timerSeconds.value % 3600) / 60);
46
+ const seconds = timerSeconds.value % 60;
47
+
48
+ if (hours > 0) {
49
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
50
+ }
51
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
52
+ });
53
+
54
+ const hasNext = computed(() => currentIndex.value < totalSlides.value - 1);
55
+ const hasPrev = computed(() => currentIndex.value > 0);
56
+
57
+ // --- Markdown Rendering ---
58
+ function renderMarkdown(text: string): string {
59
+ if (!text) return '';
60
+
61
+ return text
62
+ // Bold: **text** or __text__
63
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
64
+ .replace(/__(.*?)__/g, '<strong>$1</strong>')
65
+ // Italic: *text* or _text_
66
+ .replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, '<em>$1</em>')
67
+ .replace(/(?<!_)_(?!_)(.*?)(?<!_)_(?!_)/g, '<em>$1</em>')
68
+ // Line breaks
69
+ .replace(/\n/g, '<br>')
70
+ // Unordered lists: - item or * item
71
+ .replace(/^[-*]\s+(.*)$/gm, '<li>$1</li>')
72
+ .replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
73
+ // Clean up multiple consecutive ul tags
74
+ .replace(/<\/ul>\s*<ul>/g, '');
75
+ }
76
+
77
+ // --- Channel Communication ---
78
+ function initChannel() {
79
+ // Get channel ID from props or URL params or use default
80
+ const urlParams = new URLSearchParams(window.location.search);
81
+ const channelId = props.channelId || urlParams.get('channel') || 'lumina-speaker-default';
82
+
83
+ // Force new instance to ensure we have a distinct BoradcastChannel object
84
+ // capable of receiving messages from the main window (even in same context)
85
+ channel.value = SpeakerChannel.getInstance(channelId, true);
86
+
87
+ // Subscribe to connection changes
88
+ channel.value.onConnectionChange((connected) => {
89
+ isConnected.value = connected;
90
+ });
91
+
92
+ // Subscribe to state updates from main window
93
+ channel.value.subscribe(handleMessage);
94
+
95
+ // Send ping to establish connection with retries
96
+ // This ensures we get the initial state even if the main window takes a moment to set up
97
+ let retryCount = 0;
98
+ const maxRetries = 10;
99
+ const retryInterval = setInterval(() => {
100
+ if (totalSlides.value > 0 || retryCount >= maxRetries) {
101
+ clearInterval(retryInterval);
102
+ return;
103
+ }
104
+ channel.value?.ping();
105
+ retryCount++;
106
+ }, 300);
107
+
108
+ // Initial ping
109
+ channel.value.ping();
110
+ }
111
+
112
+ function handleMessage(payload: SpeakerSyncPayload) {
113
+ switch (payload.action) {
114
+ case 'state':
115
+ if (payload.index !== undefined) currentIndex.value = payload.index;
116
+ if (payload.totalSlides !== undefined) totalSlides.value = payload.totalSlides;
117
+ if (payload.currentNotes !== undefined) currentNotes.value = payload.currentNotes;
118
+ if (payload.nextSlidePreview) nextSlidePreview.value = payload.nextSlidePreview;
119
+ break;
120
+ case 'goto':
121
+ if (payload.index !== undefined) currentIndex.value = payload.index;
122
+ break;
123
+ }
124
+ }
125
+
126
+ // --- Navigation (send to main window) ---
127
+ function goNext() {
128
+ if (!hasNext.value) return;
129
+ channel.value?.send({ action: 'next' });
130
+ }
131
+
132
+ function goPrev() {
133
+ if (!hasPrev.value) return;
134
+ channel.value?.send({ action: 'prev' });
135
+ }
136
+
137
+ function goTo(index: number) {
138
+ channel.value?.send({ action: 'goto', index });
139
+ }
140
+
141
+ // --- Timer Controls ---
142
+ function startTimer() {
143
+ if (timerRunning.value) return;
144
+ timerRunning.value = true;
145
+ timerInterval.value = setInterval(() => {
146
+ timerSeconds.value++;
147
+ }, 1000);
148
+ }
149
+
150
+ function pauseTimer() {
151
+ timerRunning.value = false;
152
+ if (timerInterval.value) {
153
+ clearInterval(timerInterval.value);
154
+ timerInterval.value = null;
155
+ }
156
+ }
157
+
158
+ function toggleTimer() {
159
+ if (timerRunning.value) {
160
+ pauseTimer();
161
+ } else {
162
+ startTimer();
163
+ }
164
+ }
165
+
166
+ function resetTimer() {
167
+ pauseTimer();
168
+ timerSeconds.value = 0;
169
+ }
170
+
171
+ // --- Keyboard Shortcuts ---
172
+ function handleKeydown(e: KeyboardEvent) {
173
+ switch (e.key) {
174
+ case 'ArrowRight':
175
+ case ' ':
176
+ case 'Enter':
177
+ e.preventDefault();
178
+ goNext();
179
+ break;
180
+ case 'ArrowLeft':
181
+ case 'Backspace':
182
+ e.preventDefault();
183
+ goPrev();
184
+ break;
185
+ case 't':
186
+ case 'T':
187
+ e.preventDefault();
188
+ toggleTimer();
189
+ break;
190
+ case 'r':
191
+ case 'R':
192
+ if (e.ctrlKey || e.metaKey) return; // Allow browser refresh
193
+ e.preventDefault();
194
+ resetTimer();
195
+ break;
196
+ }
197
+ }
198
+
199
+ // --- Lifecycle ---
200
+ onMounted(() => {
201
+ initChannel();
202
+ window.addEventListener('keydown', handleKeydown);
203
+
204
+ // Notify close on window unload
205
+ window.addEventListener('beforeunload', () => {
206
+ channel.value?.notifyClose();
207
+ });
208
+ });
209
+
210
+ onUnmounted(() => {
211
+ pauseTimer();
212
+ window.removeEventListener('keydown', handleKeydown);
213
+ channel.value?.destroy();
214
+ });
215
+ </script>
216
+
217
+ <template>
218
+ <div class="speaker-notes">
219
+ <!-- Header -->
220
+ <header class="sn-header">
221
+ <div class="sn-title">
222
+ <svg class="sn-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
223
+ <path d="M12 2L2 7l10 5 10-5-10-5z" />
224
+ <path d="M2 17l10 5 10-5" />
225
+ <path d="M2 12l10 5 10-5" />
226
+ </svg>
227
+ <span>Lumina Speaker Notes</span>
228
+ </div>
229
+ <div class="sn-status">
230
+ <span class="sn-indicator" :class="{ connected: isConnected }"></span>
231
+ <span class="sn-slide-count">{{ slideIndicator }}</span>
232
+ </div>
233
+ </header>
234
+
235
+ <!-- Main Content -->
236
+ <main class="sn-content">
237
+ <!-- Current Notes -->
238
+ <section class="sn-notes-section">
239
+ <h2 class="sn-section-title">Notes</h2>
240
+ <div v-if="currentNotes" class="sn-notes-content" v-html="renderMarkdown(currentNotes)"></div>
241
+ <div v-else class="sn-notes-empty">
242
+ <p>No notes for this slide</p>
243
+ </div>
244
+ </section>
245
+
246
+ <!-- Next Slide Preview (Removed) -->
247
+ </main>
248
+
249
+ <!-- Footer Controls -->
250
+ <footer class="sn-footer">
251
+ <div class="sn-nav-controls">
252
+ <button class="sn-btn sn-btn-nav" :disabled="!hasPrev" @click="goPrev"
253
+ title="Previous (← or Backspace)">
254
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
255
+ <polyline points="15 18 9 12 15 6" />
256
+ </svg>
257
+ <span>Prev</span>
258
+ </button>
259
+
260
+ <div class="sn-timer" @click="toggleTimer" title="Click to start/pause (T)">
261
+ <span class="sn-timer-icon" :class="{ running: timerRunning }">
262
+ {{ timerRunning ? '⏸' : '▶' }}
263
+ </span>
264
+ <span class="sn-timer-value">{{ formattedTime }}</span>
265
+ <button class="sn-timer-reset" @click.stop="resetTimer" title="Reset timer (R)">↻</button>
266
+ </div>
267
+
268
+ <button class="sn-btn sn-btn-nav sn-btn-primary" :disabled="!hasNext" @click="goNext"
269
+ title="Next (→, Space, or Enter)">
270
+ <span>Next</span>
271
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
272
+ <polyline points="9 18 15 12 9 6" />
273
+ </svg>
274
+ </button>
275
+ </div>
276
+ </footer>
277
+ </div>
278
+ </template>
279
+
280
+ <style scoped>
281
+ /* --- Variables --- */
282
+ :root {
283
+ --sn-bg: #0f0f1a;
284
+ --sn-surface: #1a1a2e;
285
+ --sn-surface-alt: #252540;
286
+ --sn-text: #e4e4e7;
287
+ --sn-text-muted: #71717a;
288
+ --sn-accent: #6366f1;
289
+ --sn-accent-soft: rgba(99, 102, 241, 0.15);
290
+ --sn-success: #22c55e;
291
+ --sn-border: rgba(255, 255, 255, 0.08);
292
+ --sn-radius: 12px;
293
+ }
294
+
295
+ .speaker-notes {
296
+ width: 100%;
297
+ min-height: 100vh;
298
+ min-height: 100dvh;
299
+ background: var(--sn-bg);
300
+ color: var(--sn-text);
301
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
302
+ display: flex;
303
+ flex-direction: column;
304
+ box-sizing: border-box;
305
+ padding-left: env(safe-area-inset-left);
306
+ padding-right: env(safe-area-inset-right);
307
+ }
308
+
309
+ /* --- Header --- */
310
+ .sn-header {
311
+ display: flex;
312
+ justify-content: space-between;
313
+ align-items: center;
314
+ padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px 12px;
315
+ background: var(--sn-surface);
316
+ border-bottom: 1px solid var(--sn-border);
317
+ flex-shrink: 0;
318
+ }
319
+
320
+ .sn-title {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 8px;
324
+ font-size: 12px;
325
+ font-weight: 600;
326
+ min-width: 0;
327
+ }
328
+
329
+ .sn-title span {
330
+ overflow: hidden;
331
+ text-overflow: ellipsis;
332
+ white-space: nowrap;
333
+ }
334
+
335
+ .sn-logo {
336
+ width: 16px;
337
+ height: 16px;
338
+ color: var(--sn-accent);
339
+ }
340
+
341
+ .sn-status {
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 8px;
345
+ }
346
+
347
+ .sn-indicator {
348
+ width: 6px;
349
+ height: 6px;
350
+ border-radius: 50%;
351
+ background: var(--sn-text-muted);
352
+ transition: background 0.3s ease;
353
+ }
354
+
355
+ .sn-indicator.connected {
356
+ background: var(--sn-success);
357
+ box-shadow: 0 0 8px var(--sn-success);
358
+ }
359
+
360
+ .sn-slide-count {
361
+ font-size: 11px;
362
+ color: var(--sn-text-muted);
363
+ font-variant-numeric: tabular-nums;
364
+ flex-shrink: 0;
365
+ }
366
+
367
+ /* --- Content --- */
368
+ .sn-content {
369
+ flex: 1;
370
+ padding: 16px;
371
+ display: flex;
372
+ flex-direction: column;
373
+ gap: 16px;
374
+ overflow-y: auto;
375
+ overflow-x: hidden;
376
+ -webkit-overflow-scrolling: touch;
377
+ min-height: 0;
378
+ }
379
+
380
+ .sn-section-title {
381
+ font-size: 10px;
382
+ font-weight: 700;
383
+ text-transform: uppercase;
384
+ letter-spacing: 0.05em;
385
+ color: var(--sn-text-muted);
386
+ margin-bottom: 6px;
387
+ }
388
+
389
+ /* --- Notes Section --- */
390
+ .sn-notes-section {
391
+ flex: 1;
392
+ }
393
+
394
+ .sn-notes-content {
395
+ background: var(--sn-surface);
396
+ border-radius: var(--sn-radius);
397
+ padding: 16px;
398
+ font-size: 14px;
399
+ line-height: 1.55;
400
+ min-height: 100px;
401
+ word-break: break-word;
402
+ overflow-wrap: break-word;
403
+ }
404
+
405
+ .sn-notes-content :deep(strong) {
406
+ color: var(--sn-accent);
407
+ font-weight: 600;
408
+ }
409
+
410
+ .sn-notes-content :deep(em) {
411
+ font-style: italic;
412
+ color: var(--sn-text);
413
+ }
414
+
415
+ .sn-notes-content :deep(ul) {
416
+ margin: 8px 0;
417
+ padding-left: 20px;
418
+ }
419
+
420
+ .sn-notes-content :deep(li) {
421
+ margin: 4px 0;
422
+ }
423
+
424
+ .sn-notes-empty {
425
+ background: var(--sn-surface);
426
+ border-radius: var(--sn-radius);
427
+ padding: 24px 16px;
428
+ text-align: center;
429
+ color: var(--sn-text-muted);
430
+ font-size: 13px;
431
+ min-height: 100px;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ }
436
+
437
+ /* --- Preview Section (Removed) --- */
438
+
439
+ /* --- Footer Controls --- */
440
+ .sn-footer {
441
+ padding: 8px 12px;
442
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
443
+ background: var(--sn-surface);
444
+ border-top: 1px solid var(--sn-border);
445
+ flex-shrink: 0;
446
+ }
447
+
448
+ .sn-nav-controls {
449
+ display: flex;
450
+ justify-content: space-between;
451
+ align-items: center;
452
+ gap: 8px;
453
+ }
454
+
455
+ .sn-btn {
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: center;
459
+ gap: 6px;
460
+ padding: 6px 12px;
461
+ border: none;
462
+ border-radius: 6px;
463
+ font-size: 12px;
464
+ font-weight: 500;
465
+ cursor: pointer;
466
+ transition: all 0.2s ease;
467
+ background: var(--sn-surface-alt);
468
+ color: var(--sn-text);
469
+ }
470
+
471
+ .sn-btn svg {
472
+ width: 14px;
473
+ height: 14px;
474
+ }
475
+
476
+ .sn-btn:hover:not(:disabled) {
477
+ background: var(--sn-accent-soft);
478
+ color: var(--sn-accent);
479
+ }
480
+
481
+ .sn-btn:disabled {
482
+ opacity: 0.4;
483
+ cursor: not-allowed;
484
+ }
485
+
486
+ .sn-btn-primary {
487
+ background: var(--sn-accent);
488
+ color: white;
489
+ }
490
+
491
+ .sn-btn-primary:hover:not(:disabled) {
492
+ background: #5558e3;
493
+ color: white;
494
+ }
495
+
496
+ /* --- Timer --- */
497
+ .sn-timer {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 8px;
501
+ padding: 6px 12px;
502
+ background: var(--sn-surface-alt);
503
+ border-radius: 6px;
504
+ cursor: pointer;
505
+ transition: background 0.2s ease;
506
+ user-select: none;
507
+ }
508
+
509
+ .sn-timer:hover {
510
+ background: var(--sn-accent-soft);
511
+ }
512
+
513
+ .sn-timer-icon {
514
+ font-size: 10px;
515
+ width: 14px;
516
+ text-align: center;
517
+ }
518
+
519
+ .sn-timer-icon.running {
520
+ color: var(--sn-success);
521
+ }
522
+
523
+ .sn-timer-value {
524
+ font-size: 14px;
525
+ font-weight: 600;
526
+ font-variant-numeric: tabular-nums;
527
+ letter-spacing: 0.02em;
528
+ min-width: 50px;
529
+ }
530
+
531
+ .sn-timer-reset {
532
+ background: none;
533
+ border: none;
534
+ color: var(--sn-text-muted);
535
+ font-size: 14px;
536
+ cursor: pointer;
537
+ padding: 2px;
538
+ border-radius: 4px;
539
+ transition: color 0.2s ease;
540
+ }
541
+
542
+ .sn-timer-reset:hover {
543
+ color: var(--sn-text);
544
+ }
545
+
546
+ /* --- Responsive: tablets and large phones --- */
547
+ @media (max-width: 600px) {
548
+ .sn-header {
549
+ padding: calc(8px + env(safe-area-inset-top, 0px)) 10px 8px 10px;
550
+ }
551
+
552
+ .sn-title {
553
+ font-size: 11px;
554
+ }
555
+
556
+ .sn-content {
557
+ padding: 12px 10px;
558
+ }
559
+
560
+ .sn-notes-content {
561
+ font-size: 13px;
562
+ padding: 12px;
563
+ min-height: 80px;
564
+ }
565
+
566
+ .sn-notes-empty {
567
+ padding: 16px 12px;
568
+ min-height: 80px;
569
+ }
570
+
571
+ .sn-footer {
572
+ padding: 8px 10px;
573
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
574
+ }
575
+
576
+ .sn-btn {
577
+ min-height: 44px;
578
+ min-width: 44px;
579
+ padding: 10px 12px;
580
+ }
581
+
582
+ .sn-btn span {
583
+ display: none;
584
+ }
585
+
586
+ .sn-nav-controls {
587
+ gap: 6px;
588
+ }
589
+
590
+ .sn-timer {
591
+ min-height: 44px;
592
+ padding: 10px 12px;
593
+ }
594
+
595
+ .sn-timer-value {
596
+ font-size: 13px;
597
+ min-width: 44px;
598
+ }
599
+
600
+ .sn-timer-reset {
601
+ min-width: 36px;
602
+ min-height: 36px;
603
+ display: flex;
604
+ align-items: center;
605
+ justify-content: center;
606
+ }
607
+ }
608
+
609
+ /* --- Responsive: small phones --- */
610
+ @media (max-width: 480px) {
611
+ .sn-header {
612
+ padding: calc(6px + env(safe-area-inset-top, 0px)) 8px 6px 8px;
613
+ }
614
+
615
+ .sn-title {
616
+ font-size: 10px;
617
+ gap: 6px;
618
+ }
619
+
620
+ .sn-logo {
621
+ width: 14px;
622
+ height: 14px;
623
+ }
624
+
625
+ .sn-slide-count {
626
+ font-size: 10px;
627
+ }
628
+
629
+ .sn-content {
630
+ padding: 10px 8px;
631
+ }
632
+
633
+ .sn-section-title {
634
+ font-size: 9px;
635
+ }
636
+
637
+ .sn-notes-content {
638
+ font-size: 13px;
639
+ padding: 10px 12px;
640
+ min-height: 72px;
641
+ }
642
+
643
+ .sn-notes-empty {
644
+ padding: 14px 10px;
645
+ font-size: 12px;
646
+ }
647
+
648
+ .sn-footer {
649
+ padding: 6px 8px;
650
+ padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
651
+ }
652
+
653
+ .sn-btn {
654
+ min-height: 44px;
655
+ min-width: 40px;
656
+ padding: 10px;
657
+ }
658
+
659
+ .sn-btn svg {
660
+ width: 16px;
661
+ height: 16px;
662
+ }
663
+
664
+ .sn-nav-controls {
665
+ gap: 4px;
666
+ }
667
+
668
+ .sn-timer {
669
+ padding: 8px 10px;
670
+ gap: 6px;
671
+ }
672
+
673
+ .sn-timer-value {
674
+ font-size: 12px;
675
+ min-width: 40px;
676
+ }
677
+
678
+ .sn-timer-reset {
679
+ font-size: 12px;
680
+ min-width: 32px;
681
+ min-height: 32px;
682
+ }
683
+ }
684
+
685
+ /* --- Responsive: very small phones (320px) --- */
686
+ @media (max-width: 360px) {
687
+ .sn-title span {
688
+ white-space: normal;
689
+ line-height: 1.2;
690
+ }
691
+
692
+ .sn-notes-content {
693
+ font-size: 12px;
694
+ padding: 10px;
695
+ }
696
+
697
+ .sn-notes-content :deep(ul) {
698
+ padding-left: 16px;
699
+ }
700
+ }
701
+ </style>