coursecode 0.1.47 → 0.1.49
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/README.md +10 -0
- package/lib/create.js +70 -8
- package/lib/import.js +2 -2
- package/lib/stub-player/app-viewer.js +72 -24
- package/lib/stub-player/header-bar.js +3 -1
- package/lib/stub-player/styles/_header-bar.css +9 -0
- package/lib/stub-player.d.ts +2 -0
- package/lib/stub-player.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,16 @@ CourseCode creates real project files you can inspect, version, and edit directl
|
|
|
6
6
|
|
|
7
7
|
Bring your own PDFs, Word docs, or PowerPoints, use AI to accelerate authoring, and deploy to any LMS format without vendor lock-in or subscriptions.
|
|
8
8
|
|
|
9
|
+
## View the demo
|
|
10
|
+
|
|
11
|
+
- [View the live CourseCode demo course](https://preview.coursecodecloud.com/preview/coursecode-demo/nTfJU2qvp23P0mguxrrGFZr4FlXfM03N)
|
|
12
|
+
|
|
13
|
+
## Explore the ecosystem
|
|
14
|
+
|
|
15
|
+
- [Build SCORM courses with the framework](https://coursecodeframework.com/scorm/)
|
|
16
|
+
- [Use CourseCode Desktop if you prefer a GUI](https://coursecodedesktop.com/scorm/)
|
|
17
|
+
- [Use CourseCode Cloud for hosted delivery](https://coursecodecloud.com)
|
|
18
|
+
|
|
9
19
|
## Features
|
|
10
20
|
|
|
11
21
|
- **MCP integration**: Works with Claude Code, Codex, Cursor, CourseCode Desktop, and any MCP-capable AI tool — previews, screenshots, linting, and testing without manual file sharing
|
package/lib/create.js
CHANGED
|
@@ -10,6 +10,44 @@ import { spawn } from 'child_process';
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
12
12
|
|
|
13
|
+
export function toProjectDirectoryName(name) {
|
|
14
|
+
return String(name || '')
|
|
15
|
+
.trim()
|
|
16
|
+
.normalize('NFKD')
|
|
17
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/['’]/g, '')
|
|
20
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
21
|
+
.replace(/^_+|_+$/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function escapeSingleQuotedValue(value) {
|
|
25
|
+
return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function stampCourseTitle(configContent, title) {
|
|
29
|
+
const titleValue = escapeSingleQuotedValue(title);
|
|
30
|
+
let nextContent = configContent.replace(
|
|
31
|
+
/^(\s*)title\s*:\s*['"`][^'"`]*['"`](\s*,?)/m,
|
|
32
|
+
`$1title: '${titleValue}'$2`
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
nextContent = nextContent.replace(
|
|
36
|
+
/^(\s*)courseTitle\s*:\s*['"`][^'"`]*['"`](\s*,?)/m,
|
|
37
|
+
`$1courseTitle: '${titleValue}'$2`
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return nextContent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeCourseTitle(projectDir, title) {
|
|
44
|
+
const configPath = path.join(projectDir, 'course', 'course-config.js');
|
|
45
|
+
if (!fs.existsSync(configPath)) return;
|
|
46
|
+
|
|
47
|
+
const current = fs.readFileSync(configPath, 'utf-8');
|
|
48
|
+
fs.writeFileSync(configPath, stampCourseTitle(current, title), 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
/**
|
|
14
52
|
* Copy directory recursively
|
|
15
53
|
*/
|
|
@@ -85,15 +123,31 @@ function gitInit(cwd) {
|
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
export async function create(name, options = {}) {
|
|
88
|
-
const
|
|
126
|
+
const displayName = String(name || '').trim();
|
|
127
|
+
const directoryName = toProjectDirectoryName(displayName);
|
|
128
|
+
|
|
129
|
+
if (!displayName) {
|
|
130
|
+
console.error('\n❌ Course name is required.\n');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!directoryName) {
|
|
135
|
+
console.error('\n❌ Course name must include letters or numbers.\n');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const targetDir = path.resolve(process.cwd(), directoryName);
|
|
89
140
|
|
|
90
141
|
// Check if directory already exists
|
|
91
142
|
if (fs.existsSync(targetDir)) {
|
|
92
|
-
console.error(`\n❌ Directory "${
|
|
143
|
+
console.error(`\n❌ Directory "${directoryName}" already exists.\n`);
|
|
93
144
|
process.exit(1);
|
|
94
145
|
}
|
|
95
146
|
|
|
96
|
-
console.log(`\n🚀 Creating CourseCode project: ${
|
|
147
|
+
console.log(`\n🚀 Creating CourseCode project: ${displayName}\n`);
|
|
148
|
+
if (directoryName !== displayName) {
|
|
149
|
+
console.log(` Project folder: ${directoryName}`);
|
|
150
|
+
}
|
|
97
151
|
|
|
98
152
|
// Create project directory
|
|
99
153
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -138,7 +192,7 @@ export async function create(name, options = {}) {
|
|
|
138
192
|
// Read and customize package.json
|
|
139
193
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
140
194
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
141
|
-
pkg.name =
|
|
195
|
+
pkg.name = directoryName;
|
|
142
196
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
143
197
|
|
|
144
198
|
// Create .coursecoderc.json to track framework version
|
|
@@ -161,6 +215,8 @@ export async function create(name, options = {}) {
|
|
|
161
215
|
clean({ basePath: targetDir });
|
|
162
216
|
}
|
|
163
217
|
|
|
218
|
+
writeCourseTitle(targetDir, displayName);
|
|
219
|
+
|
|
164
220
|
// Install dependencies
|
|
165
221
|
if (options.install !== false) {
|
|
166
222
|
console.log('\n Installing dependencies...\n');
|
|
@@ -184,7 +240,7 @@ export async function create(name, options = {}) {
|
|
|
184
240
|
// Print success message
|
|
185
241
|
if (options.blank) {
|
|
186
242
|
console.log(`
|
|
187
|
-
✅ CourseCode project "${
|
|
243
|
+
✅ CourseCode project "${displayName}" created (blank starter)!
|
|
188
244
|
|
|
189
245
|
Course files:
|
|
190
246
|
- course/course-config.js - Course metadata & structure (minimal)
|
|
@@ -199,7 +255,7 @@ export async function create(name, options = {}) {
|
|
|
199
255
|
`);
|
|
200
256
|
} else {
|
|
201
257
|
console.log(`
|
|
202
|
-
✅ CourseCode project "${
|
|
258
|
+
✅ CourseCode project "${displayName}" created successfully!
|
|
203
259
|
|
|
204
260
|
Course files:
|
|
205
261
|
- course/course-config.js - Course metadata & structure
|
|
@@ -228,9 +284,15 @@ export async function create(name, options = {}) {
|
|
|
228
284
|
|
|
229
285
|
child.on('error', () => {
|
|
230
286
|
console.warn(' ⚠️ Failed to start dev server. Run manually:');
|
|
231
|
-
console.log(` cd ${
|
|
287
|
+
console.log(` cd ${directoryName} && coursecode dev\n`);
|
|
232
288
|
});
|
|
233
289
|
} else {
|
|
234
|
-
console.log(`\n To start developing:\n\n cd ${
|
|
290
|
+
console.log(`\n To start developing:\n\n cd ${directoryName}\n coursecode dev\n`);
|
|
235
291
|
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
displayName,
|
|
295
|
+
directoryName,
|
|
296
|
+
targetDir
|
|
297
|
+
};
|
|
236
298
|
}
|
package/lib/import.js
CHANGED
|
@@ -179,9 +179,9 @@ export async function importPresentation(source, options = {}) {
|
|
|
179
179
|
|
|
180
180
|
// Create blank project (no example slides — they'd just be deleted)
|
|
181
181
|
console.log(' ⏳ Creating course project...\n');
|
|
182
|
-
await create(name, { blank: true, install: options.install });
|
|
182
|
+
const createdProject = await create(name, { blank: true, install: options.install });
|
|
183
183
|
|
|
184
|
-
const targetDir = path.resolve(process.cwd(), name);
|
|
184
|
+
const targetDir = createdProject?.targetDir || path.resolve(process.cwd(), name);
|
|
185
185
|
const courseDir = path.join(targetDir, 'course');
|
|
186
186
|
|
|
187
187
|
// Import presentation in-place
|
|
@@ -20,6 +20,9 @@ import { createLoginHandlers } from './login-screen.js';
|
|
|
20
20
|
const config = window.STUB_CONFIG || {};
|
|
21
21
|
const LAUNCH_URL = config.launchUrl || '/';
|
|
22
22
|
const START_SLIDE = config.startSlide || null;
|
|
23
|
+
const QUERY = new URLSearchParams(window.location.search);
|
|
24
|
+
const SHOW_HEADER = resolveShowHeader();
|
|
25
|
+
const INITIAL_SKIP_GATING = resolveInitialSkipGating();
|
|
23
26
|
|
|
24
27
|
// State
|
|
25
28
|
let isInitialized = false;
|
|
@@ -44,27 +47,38 @@ function init() {
|
|
|
44
47
|
? createContentViewerHandlers({ initialContent: config.courseContent || null })
|
|
45
48
|
: null;
|
|
46
49
|
|
|
50
|
+
document.body.classList.toggle('stub-player-header-hidden', !SHOW_HEADER);
|
|
51
|
+
|
|
47
52
|
// Header Bar — viewer mode: Review + More menu (skip gating, reset)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
contentPanel
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
if (SHOW_HEADER) {
|
|
54
|
+
createHeaderBarHandlers({
|
|
55
|
+
onToggle: () => {},
|
|
56
|
+
onContent: () => {
|
|
57
|
+
if (contentPanel) {
|
|
58
|
+
contentPanel.classList.toggle('visible');
|
|
59
|
+
if (contentPanel.classList.contains('visible') && !contentLoaded && contentHandlers) {
|
|
60
|
+
contentHandlers.loadContent();
|
|
61
|
+
contentLoaded = true;
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
64
|
+
},
|
|
65
|
+
onReset: () => doReset(),
|
|
66
|
+
onSkipGating: (enabled) => {
|
|
67
|
+
if (enabled) {
|
|
68
|
+
loadCourse();
|
|
69
|
+
} else {
|
|
70
|
+
doReset();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
initialSkipGating: INITIAL_SKIP_GATING
|
|
74
|
+
});
|
|
75
|
+
} else if (INITIAL_SKIP_GATING !== null) {
|
|
76
|
+
try {
|
|
77
|
+
localStorage.setItem('coursecode-skipGating', INITIAL_SKIP_GATING ? 'true' : 'false');
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore storage failures
|
|
66
80
|
}
|
|
67
|
-
}
|
|
81
|
+
}
|
|
68
82
|
|
|
69
83
|
// Load course if no login screen active
|
|
70
84
|
if (!document.getElementById('stub-player-login-screen')?.classList.contains('visible')) {
|
|
@@ -75,13 +89,39 @@ function init() {
|
|
|
75
89
|
setupOutsideClickListener(contentPanel);
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function resolveShowHeader() {
|
|
93
|
+
const previewHeader = QUERY.get('previewHeader');
|
|
94
|
+
if (previewHeader === 'hidden') return false;
|
|
95
|
+
if (previewHeader === 'visible') return true;
|
|
96
|
+
|
|
97
|
+
const hideHeader = resolveBooleanQuery('hideHeader');
|
|
98
|
+
if (hideHeader !== null) return !hideHeader;
|
|
99
|
+
|
|
100
|
+
return config.showHeader !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveInitialSkipGating() {
|
|
104
|
+
const querySkipGating = resolveBooleanQuery('skipGating');
|
|
105
|
+
if (querySkipGating !== null) return querySkipGating;
|
|
106
|
+
return typeof config.skipGating === 'boolean' ? config.skipGating : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveBooleanQuery(name) {
|
|
110
|
+
const value = QUERY.get(name);
|
|
111
|
+
if (value === null) return null;
|
|
112
|
+
const normalized = value.toLowerCase();
|
|
113
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
114
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
// =============================================================================
|
|
79
119
|
// COURSE LOADING & NAVIGATION
|
|
80
120
|
// =============================================================================
|
|
81
121
|
|
|
82
122
|
function loadCourse() {
|
|
83
123
|
const frame = document.getElementById('stub-player-course-frame');
|
|
84
|
-
const skipGating =
|
|
124
|
+
const skipGating = resolveSkipGating();
|
|
85
125
|
let url = LAUNCH_URL;
|
|
86
126
|
if (skipGating) url += (url.includes('?') ? '&' : '?') + 'skipGating=true';
|
|
87
127
|
frame.src = url;
|
|
@@ -99,7 +139,7 @@ function handleFrameLoad() {
|
|
|
99
139
|
win.cmi5 = window.cmi5;
|
|
100
140
|
win.lti = window.lti;
|
|
101
141
|
|
|
102
|
-
if (
|
|
142
|
+
if (resolveSkipGating()) {
|
|
103
143
|
win.__SCORM_PREVIEW_SKIP_GATING = true;
|
|
104
144
|
}
|
|
105
145
|
|
|
@@ -115,6 +155,11 @@ function handleFrameLoad() {
|
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
|
|
158
|
+
function resolveSkipGating() {
|
|
159
|
+
if (INITIAL_SKIP_GATING !== null) return INITIAL_SKIP_GATING;
|
|
160
|
+
return document.getElementById('stub-player-skip-gating')?.checked === true;
|
|
161
|
+
}
|
|
162
|
+
|
|
118
163
|
function navigateToSlide(contentWindow, slideIdOrIndex) {
|
|
119
164
|
const maxAttempts = 50;
|
|
120
165
|
let attempts = 0;
|
|
@@ -158,10 +203,13 @@ function resolveSlideId(contentWindow, slideIdOrIndex) {
|
|
|
158
203
|
// =============================================================================
|
|
159
204
|
|
|
160
205
|
function doReset() {
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
// Viewer previews share an origin between the stub player and course iframe.
|
|
207
|
+
// Match the local /__reset route by clearing course/framework progress too.
|
|
208
|
+
let skipGating = null;
|
|
209
|
+
try { skipGating = localStorage.getItem('coursecode-skipGating'); } catch { /* ignore */ }
|
|
210
|
+
try { localStorage.clear(); } catch { /* ignore */ }
|
|
211
|
+
if (skipGating !== null) {
|
|
212
|
+
try { localStorage.setItem('coursecode-skipGating', skipGating); } catch { /* ignore */ }
|
|
165
213
|
}
|
|
166
214
|
window.location.reload();
|
|
167
215
|
}
|
|
@@ -102,9 +102,10 @@ export function generateHeaderBar({ isLive, hasContent }) {
|
|
|
102
102
|
* @param {Function} callbacks.onReset - () => void
|
|
103
103
|
* @param {Function} callbacks.onSkipGating - (enabled) => void
|
|
104
104
|
* @param {Function} callbacks.onStatus - () => void
|
|
105
|
+
* @param {boolean|null} callbacks.initialSkipGating - query/config override for skip-gating state
|
|
105
106
|
*/
|
|
106
107
|
export function createHeaderBarHandlers(callbacks) {
|
|
107
|
-
const { onToggle, onDebug, onConfig, onContent, onInteract, onCatalog, onEdit, onReset, onSkipGating, onStatus } = callbacks;
|
|
108
|
+
const { onToggle, onDebug, onConfig, onContent, onInteract, onCatalog, onEdit, onReset, onSkipGating, onStatus, initialSkipGating = null } = callbacks;
|
|
108
109
|
const closeMoreMenu = () => {
|
|
109
110
|
document.getElementById('stub-player-more-menu')?.classList.remove('visible');
|
|
110
111
|
};
|
|
@@ -211,6 +212,7 @@ export function createHeaderBarHandlers(callbacks) {
|
|
|
211
212
|
if (skipGatingCheckbox) {
|
|
212
213
|
// Restore persisted state (defaults to true / skip on)
|
|
213
214
|
const persistedSkip = (() => {
|
|
215
|
+
if (initialSkipGating !== null) return initialSkipGating;
|
|
214
216
|
try {
|
|
215
217
|
const stored = localStorage.getItem(SKIP_GATING_STORAGE_KEY);
|
|
216
218
|
return stored !== 'false';
|
|
@@ -333,6 +333,15 @@
|
|
|
333
333
|
height: 100%;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
body.stub-player-header-hidden #stub-player-header {
|
|
337
|
+
display: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
body.stub-player-header-hidden #stub-player-course-frame {
|
|
341
|
+
top: 0;
|
|
342
|
+
height: 100%;
|
|
343
|
+
}
|
|
344
|
+
|
|
336
345
|
/* Responsive: icon-only mode on narrow viewports */
|
|
337
346
|
@media (max-width: 820px) {
|
|
338
347
|
#stub-player-header .btn-label {
|
package/lib/stub-player.d.ts
CHANGED
package/lib/stub-player.js
CHANGED
|
@@ -53,12 +53,15 @@ export { escapeHtml };
|
|
|
53
53
|
* @param {boolean} [config.liveReload] - True to enable live reload via SSE
|
|
54
54
|
* @param {string} [config.courseContent] - Markdown/HTML content for the content viewer
|
|
55
55
|
* @param {string|number} [config.startSlide] - Slide ID or index to navigate to on load
|
|
56
|
+
* @param {boolean} [config.showHeader] - False to hide the preview player header
|
|
57
|
+
* @param {boolean} [config.skipGating] - True to bypass navigation locks
|
|
56
58
|
* @returns {string} - Complete HTML for the player page
|
|
57
59
|
*/
|
|
58
60
|
export function generateStubPlayer(config) {
|
|
59
|
-
const { title, launchUrl, storageKey, passwordHash, isLive, liveReload, courseContent, startSlide, isDesktop, moduleBasePath = '/__stub-player' } = config;
|
|
61
|
+
const { title, launchUrl, storageKey, passwordHash, isLive, liveReload, courseContent, startSlide, isDesktop, showHeader = true, skipGating = null, moduleBasePath = '/__stub-player' } = config;
|
|
60
62
|
const hasPassword = !!passwordHash;
|
|
61
63
|
const hasContent = !!courseContent;
|
|
64
|
+
const headerVisible = showHeader !== false;
|
|
62
65
|
|
|
63
66
|
const stubPlayerStyles = loadStyles(isLive);
|
|
64
67
|
|
|
@@ -73,7 +76,7 @@ export function generateStubPlayer(config) {
|
|
|
73
76
|
${stubPlayerStyles}
|
|
74
77
|
</style>
|
|
75
78
|
</head>
|
|
76
|
-
<body>
|
|
79
|
+
<body class="${headerVisible ? '' : 'stub-player-header-hidden'}">
|
|
77
80
|
${hasPassword ? generateLoginScreen({ title: escapeHtml(title) }) : ''}
|
|
78
81
|
|
|
79
82
|
<iframe id="stub-player-course-frame" name="stub-player-course-frame"></iframe>
|
|
@@ -109,6 +112,8 @@ ${stubPlayerStyles}
|
|
|
109
112
|
liveReload: ${liveReload || false},
|
|
110
113
|
startSlide: ${startSlide !== undefined ? JSON.stringify(startSlide) : 'null'},
|
|
111
114
|
courseContent: ${hasContent ? JSON.stringify(courseContent) : 'null'},
|
|
115
|
+
showHeader: ${headerVisible},
|
|
116
|
+
skipGating: ${typeof skipGating === 'boolean' ? skipGating : 'null'},
|
|
112
117
|
isDesktop: ${isDesktop || false},
|
|
113
118
|
isCI: ${!!process.env.CI}
|
|
114
119
|
};
|