coursecode 0.1.27 → 0.1.29
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/lib/authoring-api.js +214 -21
- package/lib/export-content.js +52 -0
- package/lib/mcp-prompts.js +72 -9
- package/lib/mcp-server.js +1 -1
- package/lib/stub-player/outline-mode.js +48 -0
- package/lib/stub-player/styles/_outline-mode.css +110 -0
- package/package.json +1 -1
package/lib/authoring-api.js
CHANGED
|
@@ -167,9 +167,8 @@ export function getAuthoringContext() {
|
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
169
|
* Dynamic CSS catalog — extracts structured class data from real CSS files via PostCSS.
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* With filterCategory: returns full detail for that category (all declarations).
|
|
170
|
+
* Token values (spacing, font sizes, etc.) are resolved from design-tokens.css so
|
|
171
|
+
* AI sees actual values like "1rem" instead of opaque "var(--space-4)".
|
|
173
172
|
*
|
|
174
173
|
* Category names are derived from file paths relative to framework/css/:
|
|
175
174
|
* utilities/borders.css → "utilities/borders"
|
|
@@ -177,8 +176,77 @@ export function getAuthoringContext() {
|
|
|
177
176
|
* components/hero.css → "components/hero"
|
|
178
177
|
*/
|
|
179
178
|
|
|
180
|
-
// Module-level
|
|
179
|
+
// Module-level caches — parsed once per process
|
|
181
180
|
let _cssCatalogCache = null;
|
|
181
|
+
let _tokenMapCache = null;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse design-tokens.css and resolve var() chains to final values.
|
|
185
|
+
* Returns a map of --variable-name → resolved-value.
|
|
186
|
+
*/
|
|
187
|
+
function buildTokenMap() {
|
|
188
|
+
if (_tokenMapCache) return _tokenMapCache;
|
|
189
|
+
|
|
190
|
+
const frameworkRoot = getFrameworkRoot();
|
|
191
|
+
const tokensFile = path.join(frameworkRoot, 'framework', 'css', 'design-tokens.css');
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(tokensFile)) {
|
|
194
|
+
_tokenMapCache = {};
|
|
195
|
+
return _tokenMapCache;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const source = fs.readFileSync(tokensFile, 'utf-8');
|
|
199
|
+
const root = postcss.parse(source);
|
|
200
|
+
const tokens = {};
|
|
201
|
+
|
|
202
|
+
// Extract all custom properties from :root blocks
|
|
203
|
+
root.walk(node => {
|
|
204
|
+
if (node.type === 'decl' && node.prop.startsWith('--')) {
|
|
205
|
+
tokens[node.prop] = node.value.trim();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Resolve var() chains iteratively (handles --space-4 → --space-4-base → 0.25rem)
|
|
210
|
+
for (let i = 0; i < 10; i++) {
|
|
211
|
+
let changed = false;
|
|
212
|
+
for (const [prop, value] of Object.entries(tokens)) {
|
|
213
|
+
const resolved = value.replace(/var\(\s*([^,)]+)\s*\)/g, (match, varName) => {
|
|
214
|
+
const trimmed = varName.trim();
|
|
215
|
+
if (tokens[trimmed] && !tokens[trimmed].includes('var(')) {
|
|
216
|
+
changed = true;
|
|
217
|
+
return tokens[trimmed];
|
|
218
|
+
}
|
|
219
|
+
return match;
|
|
220
|
+
});
|
|
221
|
+
tokens[prop] = resolved;
|
|
222
|
+
}
|
|
223
|
+
if (!changed) break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_tokenMapCache = tokens;
|
|
227
|
+
return _tokenMapCache;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Replace var() references in a CSS value with resolved token values.
|
|
232
|
+
* Only resolves structural tokens (spacing, sizes, radii). Color tokens are
|
|
233
|
+
* left as var() references since semantic names like var(--bg-elevated) are
|
|
234
|
+
* more useful to AI than theme-dependent hex values like #1e293b.
|
|
235
|
+
*/
|
|
236
|
+
function resolveVarRefs(value, tokenMap) {
|
|
237
|
+
return value.replace(/var\(\s*([^,)]+?)(?:\s*,\s*([^)]+))?\s*\)/g, (_match, varName, fallback) => {
|
|
238
|
+
const resolved = tokenMap[varName.trim()];
|
|
239
|
+
if (resolved && !resolved.includes('var(')) {
|
|
240
|
+
// Skip color values — semantic variable names are more useful
|
|
241
|
+
if (/^#|^rgba?\(|^hsla?\(|^color-mix\(|^oklch\(/.test(resolved)) {
|
|
242
|
+
return _match;
|
|
243
|
+
}
|
|
244
|
+
return resolved;
|
|
245
|
+
}
|
|
246
|
+
if (fallback) return fallback.trim();
|
|
247
|
+
return _match;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
182
250
|
|
|
183
251
|
function buildCssCatalog() {
|
|
184
252
|
if (_cssCatalogCache) return _cssCatalogCache;
|
|
@@ -203,6 +271,9 @@ function buildCssCatalog() {
|
|
|
203
271
|
// No course directory — framework-only mode
|
|
204
272
|
}
|
|
205
273
|
|
|
274
|
+
// Resolve design tokens so declarations show real values
|
|
275
|
+
const tokenMap = buildTokenMap();
|
|
276
|
+
|
|
206
277
|
const categories = {};
|
|
207
278
|
let totalClasses = 0;
|
|
208
279
|
|
|
@@ -218,7 +289,7 @@ function buildCssCatalog() {
|
|
|
218
289
|
const root = postcss.parse(source, { from: file });
|
|
219
290
|
const classes = {};
|
|
220
291
|
|
|
221
|
-
extractClassCatalog(root, classes);
|
|
292
|
+
extractClassCatalog(root, classes, tokenMap);
|
|
222
293
|
|
|
223
294
|
if (Object.keys(classes).length > 0) {
|
|
224
295
|
categories[category] = {
|
|
@@ -239,8 +310,9 @@ function buildCssCatalog() {
|
|
|
239
310
|
/**
|
|
240
311
|
* Extract class names with abbreviated declarations from PostCSS nodes.
|
|
241
312
|
* Walks the AST and builds { className: "shortDescription" } entries.
|
|
313
|
+
* Token values are resolved so AI sees "1rem" instead of "var(--space-4)".
|
|
242
314
|
*/
|
|
243
|
-
function extractClassCatalog(node, classes) {
|
|
315
|
+
function extractClassCatalog(node, classes, tokenMap) {
|
|
244
316
|
if (node.type === 'rule' && node.selector) {
|
|
245
317
|
// Only process simple class selectors (e.g., .foo, .foo-bar)
|
|
246
318
|
// Skip compound selectors, pseudo-classes, nested selectors
|
|
@@ -255,11 +327,12 @@ function extractClassCatalog(node, classes) {
|
|
|
255
327
|
const className = simpleClassMatch[1];
|
|
256
328
|
if (classes[className]) continue; // Already captured
|
|
257
329
|
|
|
258
|
-
// Build short description from declarations
|
|
330
|
+
// Build short description from declarations, resolving token values
|
|
259
331
|
const decls = [];
|
|
260
332
|
node.walk(child => {
|
|
261
333
|
if (child.type === 'decl') {
|
|
262
|
-
|
|
334
|
+
const resolved = resolveVarRefs(`${child.prop}: ${child.value}`, tokenMap);
|
|
335
|
+
decls.push(resolved);
|
|
263
336
|
}
|
|
264
337
|
});
|
|
265
338
|
|
|
@@ -276,34 +349,154 @@ function extractClassCatalog(node, classes) {
|
|
|
276
349
|
// Recurse into @media, @supports, etc.
|
|
277
350
|
if (node.nodes) {
|
|
278
351
|
for (const child of node.nodes) {
|
|
279
|
-
extractClassCatalog(child, classes);
|
|
352
|
+
extractClassCatalog(child, classes, tokenMap);
|
|
280
353
|
}
|
|
281
354
|
}
|
|
282
355
|
}
|
|
283
356
|
|
|
357
|
+
// Internal categories — framework-managed CSS that authors don't write manually.
|
|
358
|
+
// Filtered from TOC and search by default; always accessible via direct category drill-in.
|
|
359
|
+
const INTERNAL_CATEGORY_PREFIXES = ['interactions/'];
|
|
360
|
+
const INTERNAL_CATEGORIES = new Set([
|
|
361
|
+
'accessibility',
|
|
362
|
+
'components/assessments',
|
|
363
|
+
'components/audio-player',
|
|
364
|
+
'components/document-gallery',
|
|
365
|
+
'components/embed-frame',
|
|
366
|
+
'components/engagement',
|
|
367
|
+
'components/footer',
|
|
368
|
+
'components/notifications',
|
|
369
|
+
'components/sidebar',
|
|
370
|
+
'components/spinner',
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
function isInternalCategory(category) {
|
|
374
|
+
if (INTERNAL_CATEGORY_PREFIXES.some(p => category.startsWith(p))) return true;
|
|
375
|
+
return INTERNAL_CATEGORIES.has(category);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Brief descriptions for each author-facing category.
|
|
379
|
+
// Shown in TOC mode so AI can navigate without drilling into every category.
|
|
380
|
+
const CATEGORY_HINTS = {
|
|
381
|
+
'layout': 'Columns, splits, stacks, content widths (columns-*, split-*, content-*)',
|
|
382
|
+
'utilities/spacing': 'Margin (m-*), padding (p-*), gap (gap-*)',
|
|
383
|
+
'utilities/colors': 'Text (text-*) and background (bg-*) colors',
|
|
384
|
+
'utilities/typography': 'Font size, weight, line-height, alignment (text-sm, font-bold)',
|
|
385
|
+
'utilities/display': 'Display mode, overflow, position (d-flex, d-grid, d-none)',
|
|
386
|
+
'utilities/flexbox': 'Flex direction, alignment, wrapping, gap (flex-row, justify-center)',
|
|
387
|
+
'utilities/grid': 'CSS Grid columns, rows, spans',
|
|
388
|
+
'utilities/borders': 'Border width, radius, style (rounded-*, border-*)',
|
|
389
|
+
'utilities/animations': 'Transitions, entrance animations (fade-in, slide-up)',
|
|
390
|
+
'utilities/lists': 'List type, spacing, marker styles',
|
|
391
|
+
'utilities/visibility': 'Show/hide, opacity (visible, hidden, sr-only)',
|
|
392
|
+
'utilities/icons': 'Icon sizing, colors, alignment in text (icon-sm, icon-primary)',
|
|
393
|
+
'utilities/decorative': 'Dividers, gradients, shadows, overlays',
|
|
394
|
+
'utilities/tables': 'Table layout and cell utilities',
|
|
395
|
+
'utilities/container': 'Container width constraints',
|
|
396
|
+
'utilities/accessibility-utils': 'Screen reader, focus, skip-link helpers',
|
|
397
|
+
'components/cards': 'Card containers with headers, bodies, footers (card, card-*)',
|
|
398
|
+
'components/callouts': 'Info/warning/tip/danger callout boxes (callout, callout-*)',
|
|
399
|
+
'components/hero': 'Full-width hero banner sections',
|
|
400
|
+
'components/images': 'Image sizing, grids, rounded, shadow, captions (image-*)',
|
|
401
|
+
'components/tables': 'Table striping, borders, hover, compact variants',
|
|
402
|
+
'components/badges': 'Inline badge/label indicators (badge, badge-*)',
|
|
403
|
+
'components/buttons': 'Button variants and sizes (btn, btn-*)',
|
|
404
|
+
'components/tabs': 'Tab list and panel styling',
|
|
405
|
+
'components/accordions': 'Accordion panel structure and states',
|
|
406
|
+
'components/carousel': 'Carousel slide navigation',
|
|
407
|
+
'components/breadcrumbs': 'Breadcrumb path navigation',
|
|
408
|
+
'components/modals': 'Modal dialog overlays',
|
|
409
|
+
'components/tooltip': 'Hover tooltip styling',
|
|
410
|
+
'components/flip-cards': 'Front/back flip card containers',
|
|
411
|
+
'components/slide-header': 'Slide title and subtitle styling',
|
|
412
|
+
'components/forms': 'Form inputs, labels, validation',
|
|
413
|
+
'components/toggle': 'Toggle switch controls',
|
|
414
|
+
'components/checkbox-group': 'Grouped checkbox layouts',
|
|
415
|
+
'components/dropdown': 'Dropdown select menus',
|
|
416
|
+
'components/collapse': 'Collapsible content panels',
|
|
417
|
+
'components/lightbox': 'Fullscreen image viewer',
|
|
418
|
+
'components/video-player': 'Embedded video player',
|
|
419
|
+
};
|
|
420
|
+
|
|
284
421
|
/**
|
|
285
|
-
* Get CSS catalog —
|
|
286
|
-
*
|
|
422
|
+
* Get CSS catalog — three access modes:
|
|
423
|
+
* 1. No args: compact TOC (category names + class counts)
|
|
424
|
+
* 2. category: full class list with declarations for one category
|
|
425
|
+
* 3. search: find classes by name across all categories
|
|
426
|
+
*
|
|
427
|
+
* Internal categories (interactions, accessibility, app shell) are hidden by default.
|
|
428
|
+
* Set includeInternal: true to show them in TOC and search.
|
|
429
|
+
* Direct category access always works regardless of includeInternal.
|
|
430
|
+
*
|
|
431
|
+
* @param {Object} [options]
|
|
432
|
+
* @param {string} [options.category] - Return full detail for this category
|
|
433
|
+
* @param {string} [options.search] - Search class names (substring match)
|
|
434
|
+
* @param {boolean} [options.includeInternal] - Include framework-internal categories (default: false)
|
|
287
435
|
*/
|
|
288
|
-
export function getCssCatalog(
|
|
436
|
+
export function getCssCatalog({ category, search, includeInternal = false } = {}) {
|
|
289
437
|
const catalog = buildCssCatalog();
|
|
290
438
|
|
|
291
|
-
|
|
292
|
-
|
|
439
|
+
// Mode: Category detail — always works, even for internal categories
|
|
440
|
+
if (category) {
|
|
441
|
+
const cat = catalog.categories[category];
|
|
293
442
|
if (!cat) {
|
|
443
|
+
const available = Object.keys(catalog.categories)
|
|
444
|
+
.filter(c => includeInternal || !isInternalCategory(c))
|
|
445
|
+
.sort();
|
|
294
446
|
return {
|
|
295
|
-
error: `Unknown category: '${
|
|
296
|
-
available
|
|
447
|
+
error: `Unknown category: '${category}'`,
|
|
448
|
+
available
|
|
297
449
|
};
|
|
298
450
|
}
|
|
299
|
-
return { category
|
|
451
|
+
return { category, ...cat };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Mode: Search — case-insensitive substring match on class names
|
|
455
|
+
if (search) {
|
|
456
|
+
const query = search.toLowerCase();
|
|
457
|
+
const results = [];
|
|
458
|
+
for (const [cat, data] of Object.entries(catalog.categories)) {
|
|
459
|
+
if (!includeInternal && isInternalCategory(cat)) continue;
|
|
460
|
+
for (const [cls, declarations] of Object.entries(data.classes)) {
|
|
461
|
+
if (cls.toLowerCase().includes(query)) {
|
|
462
|
+
results.push({ class: cls, category: cat, declarations });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const capped = results.length > 50;
|
|
467
|
+
return {
|
|
468
|
+
query: search,
|
|
469
|
+
results: results.slice(0, 50),
|
|
470
|
+
count: results.length,
|
|
471
|
+
capped,
|
|
472
|
+
message: capped
|
|
473
|
+
? `Showing 50 of ${results.length} matches. Narrow your search or use 'category' to browse.`
|
|
474
|
+
: `${results.length} classes matching '${search}'.`
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Mode: TOC — category names, class counts, and hints
|
|
479
|
+
const categories = {};
|
|
480
|
+
let totalClasses = 0;
|
|
481
|
+
let filteredClasses = 0;
|
|
482
|
+
for (const [cat, data] of Object.entries(catalog.categories)) {
|
|
483
|
+
const count = Object.keys(data.classes).length;
|
|
484
|
+
if (!includeInternal && isInternalCategory(cat)) {
|
|
485
|
+
filteredClasses += count;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const entry = { count };
|
|
489
|
+
if (CATEGORY_HINTS[cat]) entry.hint = CATEGORY_HINTS[cat];
|
|
490
|
+
categories[cat] = entry;
|
|
491
|
+
totalClasses += count;
|
|
300
492
|
}
|
|
301
493
|
|
|
302
494
|
return {
|
|
303
|
-
categories
|
|
304
|
-
totalClasses
|
|
305
|
-
categoryCount: Object.keys(
|
|
306
|
-
message: `${
|
|
495
|
+
categories,
|
|
496
|
+
totalClasses,
|
|
497
|
+
categoryCount: Object.keys(categories).length,
|
|
498
|
+
message: `${totalClasses} CSS classes across ${Object.keys(categories).length} categories. Use 'category' for full class list or 'search' to find classes by name.`
|
|
499
|
+
+ (filteredClasses > 0 ? ` (${filteredClasses} internal classes hidden — use includeInternal: true to show)` : '')
|
|
307
500
|
};
|
|
308
501
|
}
|
|
309
502
|
|
package/lib/export-content.js
CHANGED
|
@@ -1139,6 +1139,10 @@ export async function getContentExport(options = {}) {
|
|
|
1139
1139
|
const fullCoursePath = validateProject(coursePath, true);
|
|
1140
1140
|
if (!fullCoursePath) return null;
|
|
1141
1141
|
|
|
1142
|
+
// Size guard threshold — prevent unbounded context consumption.
|
|
1143
|
+
// Only applies to full-course exports (no slides filter).
|
|
1144
|
+
const MAX_UNFILTERED_SIZE = 40 * 1024; // 40KB
|
|
1145
|
+
|
|
1142
1146
|
try {
|
|
1143
1147
|
const config = await loadCourseConfig(fullCoursePath);
|
|
1144
1148
|
|
|
@@ -1186,6 +1190,11 @@ export async function getContentExport(options = {}) {
|
|
|
1186
1190
|
md += formatBranding(config);
|
|
1187
1191
|
md += formatFeatures(config);
|
|
1188
1192
|
|
|
1193
|
+
// Size guard: if full-course export exceeds threshold, return summary instead
|
|
1194
|
+
if (!slides && md.length > MAX_UNFILTERED_SIZE) {
|
|
1195
|
+
return buildExportSummary(config, structure, fullCoursePath, md.length, exportOptions);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1189
1198
|
return md;
|
|
1190
1199
|
} catch (error) {
|
|
1191
1200
|
console.error('Failed to generate content export:', error.message);
|
|
@@ -1193,6 +1202,49 @@ export async function getContentExport(options = {}) {
|
|
|
1193
1202
|
}
|
|
1194
1203
|
}
|
|
1195
1204
|
|
|
1205
|
+
/**
|
|
1206
|
+
* Build a compact summary when full export exceeds size threshold.
|
|
1207
|
+
* Returns structure overview + per-slide sizes so AI can scope targeted exports.
|
|
1208
|
+
*/
|
|
1209
|
+
function buildExportSummary(config, structure, coursePath, totalSize, exportOptions) {
|
|
1210
|
+
const meta = config.metadata || {};
|
|
1211
|
+
let summary = `# ${meta.title || 'Untitled Course'} — Export Summary\n\n`;
|
|
1212
|
+
summary += `> Full export is ${(totalSize / 1024).toFixed(0)}KB — too large for context. Use the \`slides\` parameter to export specific slides.\n\n`;
|
|
1213
|
+
|
|
1214
|
+
summary += '## Course Structure\n\n';
|
|
1215
|
+
summary += generateStructureOverview(structure);
|
|
1216
|
+
summary += '\n\n';
|
|
1217
|
+
|
|
1218
|
+
// Per-slide size estimates
|
|
1219
|
+
summary += '## Slide Sizes\n\n';
|
|
1220
|
+
summary += '| Slide ID | Title | Size |\n';
|
|
1221
|
+
summary += '|----------|-------|------|\n';
|
|
1222
|
+
|
|
1223
|
+
function measureItems(items) {
|
|
1224
|
+
for (const item of items) {
|
|
1225
|
+
if (item.type === 'section') {
|
|
1226
|
+
measureItems(item.children || []);
|
|
1227
|
+
} else {
|
|
1228
|
+
let content = '';
|
|
1229
|
+
if (item.type === 'slide') {
|
|
1230
|
+
content = formatSlide(item, coursePath, exportOptions);
|
|
1231
|
+
} else if (item.type === 'assessment') {
|
|
1232
|
+
content = formatAssessment(item, coursePath, exportOptions);
|
|
1233
|
+
}
|
|
1234
|
+
const sizeKB = (content.length / 1024).toFixed(1);
|
|
1235
|
+
const title = item.title || item.menu?.label || item.id;
|
|
1236
|
+
summary += `| \`${item.id}\` | ${title} | ${sizeKB}KB |\n`;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
measureItems(structure);
|
|
1242
|
+
summary += `\n**Total:** ${(totalSize / 1024).toFixed(0)}KB across all slides\n`;
|
|
1243
|
+
summary += '\nUse `slides: "slide-id-1,slide-id-2"` to export specific slides, or `interactionsOnly: true` for just Q&A.\n';
|
|
1244
|
+
|
|
1245
|
+
return summary;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1196
1248
|
/**
|
|
1197
1249
|
* Generate JSON output
|
|
1198
1250
|
*/
|
package/lib/mcp-prompts.js
CHANGED
|
@@ -39,6 +39,10 @@ Requires preview server to be running.`,
|
|
|
39
39
|
type: 'object',
|
|
40
40
|
properties: {},
|
|
41
41
|
required: []
|
|
42
|
+
},
|
|
43
|
+
annotations: {
|
|
44
|
+
readOnlyHint: true,
|
|
45
|
+
idempotentHint: true
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
{
|
|
@@ -71,6 +75,9 @@ Requires preview server to be running.`,
|
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
required: ['slideId']
|
|
78
|
+
},
|
|
79
|
+
annotations: {
|
|
80
|
+
idempotentHint: true
|
|
74
81
|
}
|
|
75
82
|
},
|
|
76
83
|
{
|
|
@@ -102,6 +109,9 @@ Requires preview server to be running.`,
|
|
|
102
109
|
}
|
|
103
110
|
},
|
|
104
111
|
required: ['interactionId', 'response']
|
|
112
|
+
},
|
|
113
|
+
annotations: {
|
|
114
|
+
destructiveHint: false
|
|
105
115
|
}
|
|
106
116
|
},
|
|
107
117
|
{
|
|
@@ -115,6 +125,10 @@ Requires preview server to be running.`,
|
|
|
115
125
|
type: 'object',
|
|
116
126
|
properties: {},
|
|
117
127
|
required: []
|
|
128
|
+
},
|
|
129
|
+
annotations: {
|
|
130
|
+
destructiveHint: true,
|
|
131
|
+
idempotentHint: true
|
|
118
132
|
}
|
|
119
133
|
},
|
|
120
134
|
{
|
|
@@ -153,6 +167,10 @@ Requires preview server to be running.`,
|
|
|
153
167
|
}
|
|
154
168
|
},
|
|
155
169
|
required: []
|
|
170
|
+
},
|
|
171
|
+
annotations: {
|
|
172
|
+
readOnlyHint: true,
|
|
173
|
+
idempotentHint: true
|
|
156
174
|
}
|
|
157
175
|
},
|
|
158
176
|
{
|
|
@@ -188,9 +206,11 @@ Requires preview server to be running.`,
|
|
|
188
206
|
}
|
|
189
207
|
},
|
|
190
208
|
required: []
|
|
209
|
+
},
|
|
210
|
+
annotations: {
|
|
211
|
+
idempotentHint: true
|
|
191
212
|
}
|
|
192
213
|
},
|
|
193
|
-
// --- Workflow & Build Tools ---
|
|
194
214
|
{
|
|
195
215
|
name: 'coursecode_workflow_status',
|
|
196
216
|
description: `Detect the current authoring stage and get stage-specific instructions.
|
|
@@ -215,6 +235,10 @@ Call this after completing a major milestone to get updated guidance for the nex
|
|
|
215
235
|
type: 'object',
|
|
216
236
|
properties: {},
|
|
217
237
|
required: []
|
|
238
|
+
},
|
|
239
|
+
annotations: {
|
|
240
|
+
readOnlyHint: true,
|
|
241
|
+
idempotentHint: true
|
|
218
242
|
}
|
|
219
243
|
},
|
|
220
244
|
{
|
|
@@ -240,27 +264,45 @@ Use in Stage 5 when the course is ready for export.`,
|
|
|
240
264
|
}
|
|
241
265
|
},
|
|
242
266
|
required: []
|
|
267
|
+
},
|
|
268
|
+
annotations: {
|
|
269
|
+
idempotentHint: true
|
|
243
270
|
}
|
|
244
271
|
},
|
|
245
|
-
// --- Catalog & Validation Tools (filesystem, no preview needed) ---
|
|
246
272
|
{
|
|
247
273
|
name: 'coursecode_css_catalog',
|
|
248
|
-
description: `
|
|
274
|
+
description: `Browse or search CSS classes available for slide authoring.
|
|
249
275
|
|
|
250
|
-
|
|
251
|
-
|
|
276
|
+
Three modes:
|
|
277
|
+
- No args: compact table of contents (category names + class counts)
|
|
278
|
+
- category: full class list with abbreviated declarations for one category
|
|
279
|
+
- search: find classes by name across all categories
|
|
252
280
|
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
Author-facing categories (layout, utilities, common components) shown by default.
|
|
282
|
+
Framework-internal categories (interactions, accessibility, app shell) hidden — set includeInternal: true to include them.
|
|
283
|
+
|
|
284
|
+
Use to discover available CSS classes before authoring slides. Lint catches invalid classes with fix suggestions.`,
|
|
255
285
|
inputSchema: {
|
|
256
286
|
type: 'object',
|
|
257
287
|
properties: {
|
|
258
288
|
category: {
|
|
259
289
|
type: 'string',
|
|
260
|
-
description: '
|
|
290
|
+
description: 'Get full class details for this category (e.g., "utilities/spacing", "layout", "components/cards")'
|
|
291
|
+
},
|
|
292
|
+
search: {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: 'Search class names (e.g., "gap", "card", "text-"). Returns matching classes with declarations.'
|
|
295
|
+
},
|
|
296
|
+
includeInternal: {
|
|
297
|
+
type: 'boolean',
|
|
298
|
+
description: 'Include framework-internal categories (interactions, accessibility, app shell). Default: false.'
|
|
261
299
|
}
|
|
262
300
|
},
|
|
263
301
|
required: []
|
|
302
|
+
},
|
|
303
|
+
annotations: {
|
|
304
|
+
readOnlyHint: true,
|
|
305
|
+
idempotentHint: true
|
|
264
306
|
}
|
|
265
307
|
},
|
|
266
308
|
{
|
|
@@ -281,6 +323,10 @@ Use to discover available components before authoring slides.`,
|
|
|
281
323
|
}
|
|
282
324
|
},
|
|
283
325
|
required: []
|
|
326
|
+
},
|
|
327
|
+
annotations: {
|
|
328
|
+
readOnlyHint: true,
|
|
329
|
+
idempotentHint: true
|
|
284
330
|
}
|
|
285
331
|
},
|
|
286
332
|
{
|
|
@@ -301,9 +347,12 @@ Use to discover available interactions before creating assessments.`,
|
|
|
301
347
|
}
|
|
302
348
|
},
|
|
303
349
|
required: []
|
|
350
|
+
},
|
|
351
|
+
annotations: {
|
|
352
|
+
readOnlyHint: true,
|
|
353
|
+
idempotentHint: true
|
|
304
354
|
}
|
|
305
355
|
},
|
|
306
|
-
|
|
307
356
|
{
|
|
308
357
|
name: 'coursecode_lint',
|
|
309
358
|
description: `Run the course linter and get structured results.
|
|
@@ -329,6 +378,10 @@ Use AFTER making changes to validate the course.`,
|
|
|
329
378
|
type: 'object',
|
|
330
379
|
properties: {},
|
|
331
380
|
required: []
|
|
381
|
+
},
|
|
382
|
+
annotations: {
|
|
383
|
+
readOnlyHint: true,
|
|
384
|
+
idempotentHint: true
|
|
332
385
|
}
|
|
333
386
|
},
|
|
334
387
|
{
|
|
@@ -348,6 +401,10 @@ Use to discover available icons before authoring slides or configuring menus.`,
|
|
|
348
401
|
}
|
|
349
402
|
},
|
|
350
403
|
required: []
|
|
404
|
+
},
|
|
405
|
+
annotations: {
|
|
406
|
+
readOnlyHint: true,
|
|
407
|
+
idempotentHint: true
|
|
351
408
|
}
|
|
352
409
|
},
|
|
353
410
|
{
|
|
@@ -368,6 +425,8 @@ Filtering options keep output manageable:
|
|
|
368
425
|
- excludeInteractions: content only, no Q&A
|
|
369
426
|
- format: 'md' (default) or 'json' for structured data
|
|
370
427
|
|
|
428
|
+
For large courses (>40KB), full exports automatically return a summary with per-slide sizes instead. Use the slides parameter to export specific content.
|
|
429
|
+
|
|
371
430
|
Does not require preview server.`,
|
|
372
431
|
inputSchema: {
|
|
373
432
|
type: 'object',
|
|
@@ -403,6 +462,10 @@ Does not require preview server.`,
|
|
|
403
462
|
}
|
|
404
463
|
},
|
|
405
464
|
required: []
|
|
465
|
+
},
|
|
466
|
+
annotations: {
|
|
467
|
+
readOnlyHint: true,
|
|
468
|
+
idempotentHint: true
|
|
406
469
|
}
|
|
407
470
|
},
|
|
408
471
|
];
|
package/lib/mcp-server.js
CHANGED
|
@@ -242,7 +242,7 @@ export async function startMcpServer(options = {}) {
|
|
|
242
242
|
|
|
243
243
|
// === Catalog & validation tools (filesystem, no preview needed) ===
|
|
244
244
|
case 'coursecode_css_catalog':
|
|
245
|
-
result = getCssCatalog(args
|
|
245
|
+
result = getCssCatalog(args || {});
|
|
246
246
|
break;
|
|
247
247
|
|
|
248
248
|
case 'coursecode_component_catalog':
|
|
@@ -41,6 +41,7 @@ export function createOutlineModeHandlers(context) {
|
|
|
41
41
|
let isVisible = false;
|
|
42
42
|
let courseLoaded = false;
|
|
43
43
|
let viewingStage = null; // Which stage the dashboard is showing
|
|
44
|
+
const calloutDismissed = localStorage.getItem('coursecode-walkthroughCalloutDismissed') === 'true';
|
|
44
45
|
|
|
45
46
|
async function checkStage() {
|
|
46
47
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -133,6 +134,9 @@ export function createOutlineModeHandlers(context) {
|
|
|
133
134
|
// Stage stepper
|
|
134
135
|
parts.push(renderStepper(detectedStage, viewingStage));
|
|
135
136
|
|
|
137
|
+
// Walkthrough callout (dismissable, shown below stepper)
|
|
138
|
+
parts.push(renderWalkthroughCallout());
|
|
139
|
+
|
|
136
140
|
// Stage header — contextual button text
|
|
137
141
|
const hasSlides = stageData.checklist?.hasSlides;
|
|
138
142
|
let skipLabel;
|
|
@@ -511,6 +515,39 @@ export function createOutlineModeHandlers(context) {
|
|
|
511
515
|
`;
|
|
512
516
|
}
|
|
513
517
|
|
|
518
|
+
// ── Walkthrough Callout ───────────────────────────────────
|
|
519
|
+
|
|
520
|
+
function renderWalkthroughCallout() {
|
|
521
|
+
if (calloutDismissed) return '';
|
|
522
|
+
|
|
523
|
+
const hasSlides = stageData?.checklist?.hasSlides;
|
|
524
|
+
let skipLabel;
|
|
525
|
+
if (courseLoaded) {
|
|
526
|
+
skipLabel = '← Back to Course';
|
|
527
|
+
} else if (hasSlides) {
|
|
528
|
+
skipLabel = 'View Course ▸';
|
|
529
|
+
} else {
|
|
530
|
+
skipLabel = 'Skip to Course ▸';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return `
|
|
534
|
+
<div class="outline-walkthrough-callout" id="outline-walkthrough-callout">
|
|
535
|
+
<div class="outline-callout-content">
|
|
536
|
+
<div class="outline-callout-icon">💡</div>
|
|
537
|
+
<div class="outline-callout-text">
|
|
538
|
+
<strong>This is an optional walkthrough.</strong>
|
|
539
|
+
It guides you through building a course step-by-step, but you can skip it at any time.
|
|
540
|
+
Close with the <span class="outline-callout-kbd">✕</span> in the top-right corner, or return anytime from the course preview toolbar.
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="outline-callout-actions">
|
|
544
|
+
<button id="outline-callout-skip-btn" class="outline-callout-skip-btn">${skipLabel}</button>
|
|
545
|
+
<button id="outline-callout-dismiss-btn" class="outline-callout-dismiss-btn">Got it</button>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
|
|
514
551
|
// ── Shared Components ────────────────────────────────────
|
|
515
552
|
|
|
516
553
|
function renderChecklist() {
|
|
@@ -587,6 +624,17 @@ export function createOutlineModeHandlers(context) {
|
|
|
587
624
|
// Skip / Back button
|
|
588
625
|
document.getElementById('stub-player-skip-outline-btn')?.addEventListener('click', dismissDashboard);
|
|
589
626
|
|
|
627
|
+
// Walkthrough callout handlers
|
|
628
|
+
document.getElementById('outline-callout-skip-btn')?.addEventListener('click', dismissDashboard);
|
|
629
|
+
document.getElementById('outline-callout-dismiss-btn')?.addEventListener('click', () => {
|
|
630
|
+
localStorage.setItem('coursecode-walkthroughCalloutDismissed', 'true');
|
|
631
|
+
const callout = document.getElementById('outline-walkthrough-callout');
|
|
632
|
+
if (callout) {
|
|
633
|
+
callout.classList.add('dismissing');
|
|
634
|
+
callout.addEventListener('animationend', () => callout.remove(), { once: true });
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
590
638
|
// Stepper clicks
|
|
591
639
|
outlineContent.querySelectorAll('.stepper-step').forEach(btn => {
|
|
592
640
|
btn.addEventListener('click', () => {
|
|
@@ -147,6 +147,116 @@
|
|
|
147
147
|
margin-top: 20px;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/* ── Walkthrough Callout ────────────────── */
|
|
151
|
+
|
|
152
|
+
.outline-walkthrough-callout {
|
|
153
|
+
margin-bottom: 24px;
|
|
154
|
+
padding: 16px 20px;
|
|
155
|
+
background: rgba(74, 111, 165, 0.1);
|
|
156
|
+
border: 1px solid rgba(74, 111, 165, 0.25);
|
|
157
|
+
border-left: 3px solid var(--color-info);
|
|
158
|
+
border-radius: 8px;
|
|
159
|
+
animation: callout-slide-in 0.3s ease-out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@keyframes callout-slide-in {
|
|
163
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
164
|
+
to { opacity: 1; transform: translateY(0); }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.outline-walkthrough-callout.dismissing {
|
|
168
|
+
animation: callout-slide-out 0.25s ease-in forwards;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@keyframes callout-slide-out {
|
|
172
|
+
from { opacity: 1; transform: translateY(0); max-height: 200px; margin-bottom: 24px; }
|
|
173
|
+
to { opacity: 0; transform: translateY(-8px); max-height: 0; margin-bottom: 0; padding: 0 20px; overflow: hidden; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.outline-callout-content {
|
|
177
|
+
display: flex;
|
|
178
|
+
gap: 12px;
|
|
179
|
+
align-items: flex-start;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.outline-callout-icon {
|
|
183
|
+
font-size: 20px;
|
|
184
|
+
line-height: 1;
|
|
185
|
+
flex-shrink: 0;
|
|
186
|
+
margin-top: 1px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.outline-callout-text {
|
|
190
|
+
font-size: 13px;
|
|
191
|
+
line-height: 1.6;
|
|
192
|
+
color: var(--color-gray-400);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.outline-callout-text strong {
|
|
196
|
+
color: var(--color-white);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.outline-callout-kbd {
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
width: 18px;
|
|
204
|
+
height: 18px;
|
|
205
|
+
background: rgba(0, 0, 0, 0.3);
|
|
206
|
+
border: 1px solid var(--color-primary-panel);
|
|
207
|
+
border-radius: 4px;
|
|
208
|
+
font-size: 10px;
|
|
209
|
+
color: var(--color-gray-400);
|
|
210
|
+
vertical-align: middle;
|
|
211
|
+
line-height: 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.outline-callout-actions {
|
|
215
|
+
display: flex;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
margin-top: 14px;
|
|
218
|
+
padding-left: 32px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.outline-callout-skip-btn {
|
|
222
|
+
background: var(--color-info);
|
|
223
|
+
color: var(--color-white);
|
|
224
|
+
border: none;
|
|
225
|
+
padding: 7px 16px;
|
|
226
|
+
border-radius: 6px;
|
|
227
|
+
font-size: 12px;
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition: background 0.15s, transform 0.1s;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.outline-callout-skip-btn:hover {
|
|
234
|
+
background: color-mix(in srgb, var(--color-info) 80%, var(--color-white));
|
|
235
|
+
transform: translateY(-1px);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.outline-callout-skip-btn:active {
|
|
239
|
+
transform: translateY(0);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.outline-callout-dismiss-btn {
|
|
243
|
+
background: none;
|
|
244
|
+
color: var(--color-gray-600);
|
|
245
|
+
border: 1px solid var(--color-primary-panel);
|
|
246
|
+
padding: 7px 14px;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
font-size: 12px;
|
|
249
|
+
font-weight: 500;
|
|
250
|
+
cursor: pointer;
|
|
251
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.outline-callout-dismiss-btn:hover {
|
|
255
|
+
background: var(--color-primary-panel);
|
|
256
|
+
color: var(--color-gray-200);
|
|
257
|
+
border-color: var(--color-info);
|
|
258
|
+
}
|
|
259
|
+
|
|
150
260
|
/* ── Stage Header ───────────────────────── */
|
|
151
261
|
|
|
152
262
|
.outline-stage-header {
|