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.
- package/bin/cli.js +1 -0
- package/framework/css/02-layout.css +8 -1
- package/framework/css/components/audio-player.css +4 -3
- package/framework/css/components/buttons.css +7 -6
- package/framework/css/components/footer.css +5 -2
- package/framework/css/components/hero.css +31 -4
- package/framework/css/framework.css +2 -1
- package/framework/css/layouts/article.css +1 -1
- package/framework/css/responsive-structure.css +210 -0
- package/framework/css/responsive.css +15 -170
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +8 -15
- package/framework/docs/DATA_MODEL.md +1 -1
- package/framework/docs/FRAMEWORK_GUIDE.md +31 -10
- package/framework/docs/USER_GUIDE.md +3 -1
- package/framework/js/utilities/icons.js +4 -2
- package/lib/cloud.js +4 -0
- package/lib/headless-browser.js +1 -1
- package/lib/mcp-prompts.js +22 -22
- package/lib/mcp-server.js +1 -1
- package/lib/preview-routes-api.js +59 -1
- package/lib/stub-player/outline-mode.js +187 -13
- package/lib/stub-player/styles/_header-bar.css +18 -0
- package/lib/stub-player/styles/_outline-mode.css +257 -20
- package/package.json +10 -1
- package/template/course/slides/example-course-structure.js +1 -1
- package/template/course/slides/example-finishing.js +4 -4
- package/template/course/slides/example-preview-tour.js +1 -1
- package/template/course/slides/example-ui-showcase.js +1 -1
- package/template/course/slides/example-welcome.js +4 -4
- package/template/course/slides/example-workflow.js +1 -1
|
@@ -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
|
-
${
|
|
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">${
|
|
181
|
+
<span class="stepper-dot">${dotContent}</span>
|
|
159
182
|
<span class="stepper-label">${s.label}</span>
|
|
160
183
|
</button>`;
|
|
161
184
|
});
|
|
162
185
|
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
471
|
-
|
|
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.
|
|
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
|
|
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/
|