coursecode 0.1.6 → 0.1.7

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.
@@ -127,16 +127,29 @@ export function createOutlineModeHandlers(context) {
127
127
 
128
128
  const parts = [];
129
129
 
130
+ // Close button (pinned top-left)
131
+ parts.push('<button id="outline-close-btn" class="outline-close-btn" title="Close dashboard">×</button>');
132
+
130
133
  // Stage stepper
131
134
  parts.push(renderStepper(detectedStage, viewingStage));
132
135
 
133
- // Stage header
136
+ // Stage header — contextual button text
137
+ const hasSlides = stageData.checklist?.hasSlides;
138
+ let skipLabel;
139
+ if (courseLoaded) {
140
+ skipLabel = '← Back to Course';
141
+ } else if (hasSlides) {
142
+ skipLabel = 'View Course ▸';
143
+ } else {
144
+ skipLabel = 'Skip to Course ▸';
145
+ }
146
+
134
147
  parts.push(`
135
148
  <div class="outline-stage-header">
136
149
  <h1>${stage.title}</h1>
137
150
  <p class="outline-stage-desc">${stage.desc}</p>
138
151
  <button id="stub-player-skip-outline-btn" class="outline-skip-btn">
139
- ${courseLoaded ? '← Back to Course' : 'Skip to Course ▸'}
152
+ ${skipLabel}
140
153
  </button>
141
154
  </div>
142
155
  `);
@@ -154,13 +167,32 @@ export function createOutlineModeHandlers(context) {
154
167
  if (s.num < detected) classes.push('completed');
155
168
  if (s.num === detected) classes.push('current');
156
169
  if (s.num === viewing) classes.push('viewing');
170
+
171
+ let dotContent;
172
+ if (s.num < detected) {
173
+ dotContent = '✓';
174
+ } else if (s.num === detected && s.num === viewing) {
175
+ dotContent = s.num;
176
+ } else {
177
+ dotContent = s.num;
178
+ }
179
+
157
180
  return `<button class="stepper-step ${classes.join(' ')}" data-stage="${s.num}">
158
- <span class="stepper-dot">${s.num < detected ? '✓' : s.num}</span>
181
+ <span class="stepper-dot">${dotContent}</span>
159
182
  <span class="stepper-label">${s.label}</span>
160
183
  </button>`;
161
184
  });
162
185
 
163
- return `<div class="outline-stepper">${steps.join('<span class="stepper-line"></span>')}</div>`;
186
+ // Color connector lines between completed steps
187
+ const connectors = [];
188
+ for (let i = 0; i < STAGES.length - 1; i++) {
189
+ const filled = STAGES[i].num < detected;
190
+ connectors.push(`<span class="stepper-line${filled ? ' stepper-line-filled' : ''}"></span>`);
191
+ }
192
+
193
+ // Interleave steps and connectors
194
+ const html = steps.map((step, i) => step + (connectors[i] || '')).join('');
195
+ return `<div class="outline-stepper">${html}</div>`;
164
196
  }
165
197
 
166
198
  async function renderStageContent(stageNum) {
@@ -273,6 +305,31 @@ export function createOutlineModeHandlers(context) {
273
305
  }
274
306
  } catch { /* ignore */ }
275
307
 
308
+ // Fetch converted reference files for context
309
+ let refsHtml = '';
310
+ try {
311
+ const refsRes = await fetch('/__refs');
312
+ if (refsRes.ok) {
313
+ const refs = await refsRes.json();
314
+ const converted = refs.converted || [];
315
+ if (converted.length > 0) {
316
+ const items = converted.map(f => `
317
+ <a class="outline-ref-link" href="/__stub-player/ref-preview?file=${encodeURIComponent(f)}" target="_blank" title="Preview ${f}">
318
+ <span class="outline-ref-link-icon">📄</span>
319
+ <span class="outline-ref-link-name">${f}</span>
320
+ <span class="outline-ref-link-arrow">↗</span>
321
+ </a>
322
+ `).join('');
323
+ refsHtml = `
324
+ <div class="outline-card">
325
+ <h2>📚 Converted Reference Materials</h2>
326
+ <p style="font-size:12px;color:var(--color-gray-600);margin:0 0 12px;">Use these as context when writing your outline.</p>
327
+ <div class="outline-ref-links">${items}</div>
328
+ </div>`;
329
+ }
330
+ }
331
+ } catch { /* ignore */ }
332
+
276
333
  return `
277
334
  <div class="outline-card">
278
335
  <h2>📋 What to do</h2>
@@ -282,6 +339,7 @@ export function createOutlineModeHandlers(context) {
282
339
  <li>Define modules, lessons, and learning objectives</li>
283
340
  </ol>
284
341
  </div>
342
+ ${refsHtml}
285
343
  ${outlineHtml || '<div class=\'outline-card outline-card-empty\'><h2>📝 Course Outline</h2><p>No outline found yet. Create <code>COURSE_OUTLINE.md</code> to continue.</p></div>'}
286
344
  ${renderChecklist()}
287
345
  `;
@@ -296,7 +354,10 @@ export function createOutlineModeHandlers(context) {
296
354
  if (res.ok) {
297
355
  const config = await res.json();
298
356
  const slideCount = config.slideCount || 0;
299
- const assessmentCount = (config.slideIds || []).filter(s => s.type === 'assessment').length;
357
+ const slideIds = config.slideIds || [];
358
+ const assessmentCount = slideIds.filter(s => s.type === 'assessment').length;
359
+
360
+ // Stats row
300
361
  configHtml = `
301
362
  <div class="outline-card">
302
363
  <h2>📊 Course Structure</h2>
@@ -306,6 +367,26 @@ export function createOutlineModeHandlers(context) {
306
367
  <div class="outline-stat"><span class="outline-stat-num">${config.objectives?.length || 0}</span><span class="outline-stat-label">Objectives</span></div>
307
368
  </div>
308
369
  </div>`;
370
+
371
+ // Slide list
372
+ if (slideIds.length > 0) {
373
+ const slideItems = slideIds.map(s => {
374
+ const icon = s.type === 'assessment' ? '📝' : '📄';
375
+ const badge = s.type === 'assessment' ? '<span class="outline-slide-badge assessment">Assessment</span>' : '';
376
+ return `
377
+ <div class="outline-slide-item">
378
+ <span class="outline-slide-icon">${icon}</span>
379
+ <span class="outline-slide-id">${s.id}</span>
380
+ ${s.title && s.title !== s.id ? `<span class="outline-slide-title">${escapeHtml(s.title)}</span>` : ''}
381
+ ${badge}
382
+ </div>`;
383
+ }).join('');
384
+ configHtml += `
385
+ <div class="outline-card">
386
+ <h2>🗂️ Slide List</h2>
387
+ <div class="outline-slide-list">${slideItems}</div>
388
+ </div>`;
389
+ }
309
390
  }
310
391
  } catch { /* ignore */ }
311
392
 
@@ -392,15 +473,38 @@ export function createOutlineModeHandlers(context) {
392
473
  // ── Stage 5: Export Ready ────────────────────────────────
393
474
 
394
475
  async function renderStage5() {
476
+ const commands = [
477
+ { cmd: 'coursecode build', label: 'cmi5 (default)' },
478
+ { cmd: 'coursecode build --format scorm2004', label: 'SCORM 2004' },
479
+ { cmd: 'coursecode build --format scorm1.2', label: 'SCORM 1.2' },
480
+ { cmd: 'coursecode build --format lti', label: 'LTI 1.3' },
481
+ { cmd: 'coursecode deploy', label: 'Deploy to Cloud' }
482
+ ];
483
+
484
+ const cmdRows = commands.map(c => `
485
+ <div class="outline-cmd">
486
+ <code>${c.cmd}</code>
487
+ <span class="outline-cmd-label">${c.label}</span>
488
+ <button class="outline-copy-btn" data-cmd="${c.cmd}" title="Copy to clipboard">📋</button>
489
+ </div>
490
+ `).join('');
491
+
395
492
  return `
396
493
  <div class="outline-card outline-card-success">
397
494
  <h2>🎉 Ready for Export</h2>
398
495
  <p>Your course is complete and passing all checks. Export it for your LMS:</p>
496
+ <div class="outline-build-actions">
497
+ <button class="outline-build-btn" id="outline-build-btn">▶ Build Now (cmi5)</button>
498
+ <select class="outline-build-format" id="outline-build-format">
499
+ <option value="cmi5">cmi5</option>
500
+ <option value="scorm2004">SCORM 2004</option>
501
+ <option value="scorm1.2">SCORM 1.2</option>
502
+ <option value="lti">LTI 1.3</option>
503
+ </select>
504
+ </div>
505
+ <div id="outline-build-status" class="outline-build-status" style="display:none;"></div>
399
506
  <div class="outline-export-cmds">
400
- <div class="outline-cmd"><code>coursecode build</code><span>cmi5 (default)</span></div>
401
- <div class="outline-cmd"><code>coursecode build --format scorm2004</code><span>SCORM 2004</span></div>
402
- <div class="outline-cmd"><code>coursecode build --format scorm1.2</code><span>SCORM 1.2</span></div>
403
- <div class="outline-cmd"><code>coursecode build --format lti</code><span>LTI 1.3</span></div>
507
+ ${cmdRows}
404
508
  </div>
405
509
  </div>
406
510
  ${renderChecklist()}
@@ -467,8 +571,8 @@ export function createOutlineModeHandlers(context) {
467
571
  // ── Event Handlers ───────────────────────────────────────
468
572
 
469
573
  function attachHandlers() {
470
- // Skip / Back button
471
- document.getElementById('stub-player-skip-outline-btn')?.addEventListener('click', () => {
574
+ // Close button (pinned ×)
575
+ const dismissDashboard = () => {
472
576
  if (stageData && stageData.stageNumber < 3) {
473
577
  localStorage.setItem('coursecode-skipOutline', 'true');
474
578
  }
@@ -477,7 +581,11 @@ export function createOutlineModeHandlers(context) {
477
581
  context.loadCourse();
478
582
  courseLoaded = true;
479
583
  }
480
- });
584
+ };
585
+ document.getElementById('outline-close-btn')?.addEventListener('click', dismissDashboard);
586
+
587
+ // Skip / Back button
588
+ document.getElementById('stub-player-skip-outline-btn')?.addEventListener('click', dismissDashboard);
481
589
 
482
590
  // Stepper clicks
483
591
  outlineContent.querySelectorAll('.stepper-step').forEach(btn => {
@@ -536,7 +644,6 @@ export function createOutlineModeHandlers(context) {
536
644
  setupRefsDropzone(dropzone);
537
645
  }
538
646
 
539
-
540
647
  // Branch option: Convert PowerPoint to Course
541
648
  document.getElementById('branch-convert')?.addEventListener('click', () => {
542
649
  showConvertSubView();
@@ -552,6 +659,73 @@ export function createOutlineModeHandlers(context) {
552
659
  if (convertZone) {
553
660
  setupConvertDropzone(convertZone);
554
661
  }
662
+
663
+ // Copy-to-clipboard buttons
664
+ outlineContent.querySelectorAll('.outline-copy-btn').forEach(btn => {
665
+ btn.addEventListener('click', async () => {
666
+ const cmd = btn.dataset.cmd;
667
+ try {
668
+ await navigator.clipboard.writeText(cmd);
669
+ btn.textContent = '✓';
670
+ setTimeout(() => { btn.textContent = '📋'; }, 1500);
671
+ } catch {
672
+ btn.textContent = '✗';
673
+ setTimeout(() => { btn.textContent = '📋'; }, 1500);
674
+ }
675
+ });
676
+ });
677
+
678
+ // Build Now button
679
+ const buildBtn = document.getElementById('outline-build-btn');
680
+ const buildFormat = document.getElementById('outline-build-format');
681
+ const buildStatus = document.getElementById('outline-build-status');
682
+ if (buildBtn) {
683
+ buildBtn.addEventListener('click', async () => {
684
+ const format = buildFormat?.value || 'cmi5';
685
+ buildBtn.disabled = true;
686
+ buildBtn.textContent = `Building (${format})…`;
687
+ if (buildStatus) {
688
+ buildStatus.style.display = 'block';
689
+ buildStatus.textContent = 'Build in progress…';
690
+ buildStatus.className = 'outline-build-status building';
691
+ }
692
+
693
+ try {
694
+ const res = await fetch(`/__build?format=${format}`, { method: 'POST' });
695
+ const result = await res.json();
696
+ if (result.success) {
697
+ buildBtn.textContent = '✅ Build Complete';
698
+ if (buildStatus) {
699
+ buildStatus.textContent = `Built ${format} in ${result.duration}. Output: dist/`;
700
+ buildStatus.className = 'outline-build-status success';
701
+ }
702
+ } else {
703
+ buildBtn.textContent = '❌ Build Failed';
704
+ if (buildStatus) {
705
+ buildStatus.textContent = result.error || result.errors?.join(', ') || 'Build failed';
706
+ buildStatus.className = 'outline-build-status error';
707
+ }
708
+ }
709
+ } catch (err) {
710
+ buildBtn.textContent = '❌ Build Failed';
711
+ if (buildStatus) {
712
+ buildStatus.textContent = err.message;
713
+ buildStatus.className = 'outline-build-status error';
714
+ }
715
+ }
716
+
717
+ setTimeout(() => {
718
+ buildBtn.disabled = false;
719
+ buildBtn.textContent = '▶ Build Now (cmi5)';
720
+ }, 3000);
721
+ });
722
+
723
+ // Sync button label with format dropdown
724
+ buildFormat?.addEventListener('change', () => {
725
+ const f = buildFormat.value;
726
+ buildBtn.textContent = `▶ Build Now (${f})`;
727
+ });
728
+ }
555
729
  }
556
730
 
557
731
 
@@ -126,6 +126,13 @@
126
126
  font-size: 11px;
127
127
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
128
128
  letter-spacing: 0.3px;
129
+ display: inline-block;
130
+ max-width: 24ch;
131
+ min-width: 0;
132
+ white-space: nowrap;
133
+ overflow: hidden;
134
+ text-overflow: ellipsis;
135
+ vertical-align: middle;
129
136
  }
130
137
 
131
138
  #stub-player-header .slide-id-badge:empty {
@@ -340,4 +347,15 @@
340
347
  #stub-player-header button {
341
348
  padding: 5px 7px;
342
349
  }
350
+ #stub-player-header .slide-id-badge {
351
+ max-width: 14ch;
352
+ font-size: 10px;
353
+ padding: 2px 6px 2px 8px;
354
+ }
355
+ }
356
+
357
+ @media (max-width: 560px) {
358
+ #stub-player-header .slide-id-badge {
359
+ display: none;
360
+ }
343
361
  }
@@ -21,6 +21,35 @@
21
21
  max-width: 720px;
22
22
  margin: 0 auto;
23
23
  padding: 32px 24px 60px;
24
+ position: relative;
25
+ }
26
+
27
+ /* ── Close button (pinned top-left) ──────── */
28
+
29
+ .outline-close-btn {
30
+ position: sticky;
31
+ top: 12px;
32
+ float: right;
33
+ margin-right: -8px;
34
+ margin-top: -12px;
35
+ width: 32px;
36
+ height: 32px;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ background: none;
41
+ border: none;
42
+ border-radius: 50%;
43
+ color: var(--color-gray-750);
44
+ font-size: 24px;
45
+ line-height: 1;
46
+ cursor: pointer;
47
+ z-index: 20;
48
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
49
+ }
50
+
51
+ .outline-close-btn:hover {
52
+ color: var(--color-white);
24
53
  }
25
54
 
26
55
  /* Loading */
@@ -334,26 +363,7 @@
334
363
  display: flex;
335
364
  flex-direction: column;
336
365
  gap: 6px;
337
- }
338
-
339
- .outline-cmd {
340
- display: flex;
341
- justify-content: space-between;
342
- align-items: center;
343
- padding: 10px 12px;
344
- background: rgba(0, 0, 0, 0.2);
345
- border-radius: 6px;
346
- border: 1px solid var(--color-primary-panel);
347
- }
348
-
349
- .outline-cmd code {
350
- font-size: 12px;
351
- color: var(--color-accent);
352
- }
353
-
354
- .outline-cmd span {
355
- font-size: 11px;
356
- color: var(--color-gray-750);
366
+ margin-top: 12px;
357
367
  }
358
368
 
359
369
  /* ── Checklist ───────────────────────────── */
@@ -750,3 +760,230 @@
750
760
  border-radius: 6px;
751
761
  border-left: 3px solid var(--color-warning);
752
762
  }
763
+
764
+ /* ── Filled stepper connector lines ──────── */
765
+
766
+ .stepper-line-filled {
767
+ background: var(--color-success);
768
+ }
769
+
770
+ /* ── Reference file preview links (Stage 2) ── */
771
+
772
+ .outline-ref-links {
773
+ display: flex;
774
+ flex-direction: column;
775
+ gap: 2px;
776
+ }
777
+
778
+ .outline-ref-link {
779
+ display: flex;
780
+ align-items: center;
781
+ gap: 10px;
782
+ padding: 8px 12px;
783
+ border-radius: 6px;
784
+ text-decoration: none;
785
+ color: var(--color-gray-200);
786
+ transition: background 0.15s;
787
+ }
788
+
789
+ .outline-ref-link:hover {
790
+ background: rgba(74, 111, 165, 0.12);
791
+ }
792
+
793
+ .outline-ref-link-icon {
794
+ font-size: 14px;
795
+ flex-shrink: 0;
796
+ }
797
+
798
+ .outline-ref-link-name {
799
+ font-size: 13px;
800
+ flex: 1;
801
+ }
802
+
803
+ .outline-ref-link-arrow {
804
+ font-size: 12px;
805
+ color: var(--color-gray-750);
806
+ opacity: 0;
807
+ transition: opacity 0.15s;
808
+ }
809
+
810
+ .outline-ref-link:hover .outline-ref-link-arrow {
811
+ opacity: 1;
812
+ }
813
+
814
+ /* ── Slide list (Stage 3) ──────────────────── */
815
+
816
+ .outline-slide-list {
817
+ display: flex;
818
+ flex-direction: column;
819
+ gap: 2px;
820
+ max-height: 400px;
821
+ overflow-y: auto;
822
+ mask-image: linear-gradient(to bottom, black calc(100% - 24px), transparent 100%);
823
+ -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 24px), transparent 100%);
824
+ }
825
+
826
+ .outline-slide-item {
827
+ display: flex;
828
+ align-items: center;
829
+ gap: 10px;
830
+ padding: 6px 10px;
831
+ border-radius: 5px;
832
+ transition: background 0.15s;
833
+ }
834
+
835
+ .outline-slide-item:hover {
836
+ background: rgba(74, 111, 165, 0.08);
837
+ }
838
+
839
+ .outline-slide-icon {
840
+ font-size: 13px;
841
+ flex-shrink: 0;
842
+ }
843
+
844
+ .outline-slide-id {
845
+ font-size: 12px;
846
+ font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
847
+ color: var(--color-accent);
848
+ }
849
+
850
+ .outline-slide-title {
851
+ font-size: 12px;
852
+ color: var(--color-gray-600);
853
+ flex: 1;
854
+ overflow: hidden;
855
+ text-overflow: ellipsis;
856
+ white-space: nowrap;
857
+ }
858
+
859
+ .outline-slide-badge {
860
+ font-size: 10px;
861
+ font-weight: 600;
862
+ padding: 2px 8px;
863
+ border-radius: 4px;
864
+ white-space: nowrap;
865
+ }
866
+
867
+ .outline-slide-badge.assessment {
868
+ background: rgba(138, 163, 219, 0.15);
869
+ color: var(--color-info-soft);
870
+ }
871
+
872
+ /* ── Copy button (Stage 5 commands) ─────────── */
873
+
874
+ .outline-cmd {
875
+ display: flex;
876
+ justify-content: space-between;
877
+ align-items: center;
878
+ padding: 10px 12px;
879
+ background: rgba(0, 0, 0, 0.2);
880
+ border-radius: 6px;
881
+ border: 1px solid var(--color-primary-panel);
882
+ gap: 8px;
883
+ }
884
+
885
+ .outline-cmd code {
886
+ font-size: 12px;
887
+ color: var(--color-accent);
888
+ flex: 1;
889
+ }
890
+
891
+ .outline-cmd-label {
892
+ font-size: 11px;
893
+ color: var(--color-gray-750);
894
+ white-space: nowrap;
895
+ }
896
+
897
+ .outline-copy-btn {
898
+ background: none;
899
+ border: 1px solid var(--color-primary-panel);
900
+ color: var(--color-gray-600);
901
+ padding: 4px 8px;
902
+ border-radius: 4px;
903
+ font-size: 12px;
904
+ cursor: pointer;
905
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
906
+ flex-shrink: 0;
907
+ }
908
+
909
+ .outline-copy-btn:hover {
910
+ background: var(--color-primary-panel);
911
+ border-color: var(--color-info);
912
+ color: var(--color-white);
913
+ }
914
+
915
+ /* ── Build actions (Stage 5) ───────────────── */
916
+
917
+ .outline-build-actions {
918
+ display: flex;
919
+ gap: 8px;
920
+ margin: 16px 0 12px;
921
+ }
922
+
923
+ .outline-build-btn {
924
+ background: var(--color-success);
925
+ color: var(--color-white);
926
+ border: none;
927
+ padding: 10px 20px;
928
+ border-radius: 6px;
929
+ font-size: 13px;
930
+ font-weight: 600;
931
+ cursor: pointer;
932
+ transition: background 0.15s, transform 0.1s, opacity 0.15s;
933
+ }
934
+
935
+ .outline-build-btn:hover:not(:disabled) {
936
+ background: color-mix(in srgb, var(--color-success) 80%, var(--color-black));
937
+ transform: translateY(-1px);
938
+ }
939
+
940
+ .outline-build-btn:active:not(:disabled) {
941
+ transform: translateY(0);
942
+ }
943
+
944
+ .outline-build-btn:disabled {
945
+ opacity: 0.7;
946
+ cursor: default;
947
+ }
948
+
949
+ .outline-build-format {
950
+ background: var(--color-primary-panel);
951
+ color: var(--color-gray-200);
952
+ border: 1px solid rgba(74, 111, 165, 0.3);
953
+ padding: 8px 12px;
954
+ border-radius: 6px;
955
+ font-size: 12px;
956
+ cursor: pointer;
957
+ appearance: auto;
958
+ }
959
+
960
+ .outline-build-format:focus {
961
+ outline: 2px solid var(--color-info);
962
+ outline-offset: -1px;
963
+ }
964
+
965
+ .outline-build-status {
966
+ padding: 10px 14px;
967
+ border-radius: 6px;
968
+ font-size: 12px;
969
+ margin-bottom: 12px;
970
+ }
971
+
972
+ .outline-build-status.building {
973
+ background: rgba(241, 135, 1, 0.1);
974
+ border: 1px solid rgba(241, 135, 1, 0.3);
975
+ color: var(--color-warning);
976
+ animation: pulse-convert 1.5s ease-in-out infinite;
977
+ }
978
+
979
+ .outline-build-status.success {
980
+ background: rgba(29, 118, 72, 0.1);
981
+ border: 1px solid rgba(29, 118, 72, 0.3);
982
+ color: var(--color-success-soft);
983
+ }
984
+
985
+ .outline-build-status.error {
986
+ background: rgba(220, 38, 38, 0.1);
987
+ border: 1px solid rgba(220, 38, 38, 0.3);
988
+ color: var(--color-danger);
989
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,10 @@
44
44
  "preview:cmi5": "LMS_FORMAT=cmi5 node lib/preview-server.js --framework-dev",
45
45
  "preview:lti": "LMS_FORMAT=lti node lib/preview-server.js --framework-dev",
46
46
  "lint": "eslint .",
47
+ "lint:responsive": "node scripts/check-responsive-css-ownership.mjs",
48
+ "lint:responsive:structure": "node scripts/check-responsive-structure-scoping.mjs",
47
49
  "lint:fix": "eslint . --fix",
50
+ "smoke:responsive": "node scripts/responsive-visual-smoke.mjs",
48
51
  "build": "npx vite build --config vite.framework-dev.config.js",
49
52
  "build:dev": "npx vite build --config vite.framework-dev.config.js",
50
53
  "build:scorm2004": "LMS_FORMAT=scorm2004 npx vite build --config vite.framework-dev.config.js",
@@ -57,8 +60,13 @@
57
60
  "package:cmi5": "npm run lint && LMS_FORMAT=cmi5 PACKAGE=true npx vite build --config vite.framework-dev.config.js",
58
61
  "package:lti": "npm run lint && LMS_FORMAT=lti PACKAGE=true npx vite build --config vite.framework-dev.config.js",
59
62
  "test": "vitest run --config tests/vitest.config.js",
63
+ "test:e2e": "vitest run --config tests/vitest.e2e.config.js",
64
+ "test:all": "npm test && npm run test:e2e",
60
65
  "test:cloud": "vitest run --config tests/vitest.cloud.config.js",
61
66
  "test:watch": "vitest --config tests/vitest.config.js",
67
+ "test:coverage": "vitest run --config tests/vitest.config.js --coverage",
68
+ "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run build",
69
+ "prerelease:check:full": "npm run prerelease:check && npm run smoke:responsive -- --profile=expanded",
62
70
  "prepublishOnly": "echo '📦 Preparing coursecode for npm...'"
63
71
  },
64
72
  "repository": {
@@ -104,6 +112,7 @@
104
112
  },
105
113
  "devDependencies": {
106
114
  "@vitejs/plugin-legacy": "^7.2.1",
115
+ "@vitest/coverage-v8": "^4.0.18",
107
116
  "@xapi/cmi5": "^1.4.0",
108
117
  "acorn": "^8.15.0",
109
118
  "archiver": "^7.0.1",
@@ -21,7 +21,7 @@ export const slide = {
21
21
  <h2 class="text-lg font-bold border-bottom pb-2 mb-4">${iconManager.getIcon('folder')} Project Structure</h2>
22
22
  <div class="cols-2 gap-6">
23
23
  <div>
24
- <pre class="bg-gray-100 p-4 rounded text-sm"><code>my-course/
24
+ <pre class="bg-gray-100 p-4 rounded overflow-x-auto" style="max-width: 100%; font-size: clamp(0.72rem, 2vw, 0.875rem); line-height: 1.35;"><code>my-course/
25
25
  ├── course/ ← <strong>Your content</strong>
26
26
  │ ├── course-config.js
27
27
  │ ├── slides/