@curio-sd/e-module-builder 0.4.0 → 0.5.1
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 +57 -15
- package/bin/cli.js +3 -0
- package/build-pdf.mjs +454 -0
- package/build.mjs +126 -73
- package/package.json +3 -1
- package/src/css/main.css +124 -50
- package/src/js/checklist.js +1 -1
- package/src/js/exercises/exercise-shared.js +1 -1
- package/src/js/exercises/external-exercise.js +3 -4
- package/src/js/exercises/hub.js +5 -1
- package/src/js/exercises/load-exercise.js +3 -0
- package/src/js/exercises/theory-panel.js +95 -0
- package/src/js/home.js +1 -1
- package/src/js/nav.js +63 -8
- package/templates/index.html +17 -9
- package/templates/pages/inleveropdracht.html +32 -0
- package/templates/pages/meetmoment-praktijk.html +35 -0
- package/templates/pages/meetmoment-theorie.html +29 -0
- package/templates/pages/{toets.html → meetmoment.html} +11 -4
- package/templates/pages/oefening.html +29 -0
- package/templates/pages/oefeningen.html +46 -38
- package/templates/pages/theorie.html +6 -1
- package/templates/pages/toets-praktijk.html +0 -25
- package/templates/pages/toets-theorie.html +0 -25
package/build.mjs
CHANGED
|
@@ -24,6 +24,12 @@ const marked = new Marked(
|
|
|
24
24
|
})
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
const SECTION_RE = /^([a-zA-Z]+)(\d+)$/
|
|
28
|
+
|
|
29
|
+
function sectionLabel(prefix, num) {
|
|
30
|
+
return `${prefix.charAt(0).toUpperCase()}${prefix.slice(1)} ${num}`
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
28
34
|
|
|
29
35
|
function rewriteAssetPaths(html, basePath) {
|
|
@@ -108,8 +114,8 @@ const mod = moduleMd.data
|
|
|
108
114
|
|
|
109
115
|
const weekDirs = fs
|
|
110
116
|
.readdirSync(CONTENT)
|
|
111
|
-
.filter(d =>
|
|
112
|
-
.sort((a, b) => parseInt(a.
|
|
117
|
+
.filter(d => SECTION_RE.test(d) && fs.statSync(path.join(CONTENT, d)).isDirectory())
|
|
118
|
+
.sort((a, b) => parseInt(a.match(SECTION_RE)[2]) - parseInt(b.match(SECTION_RE)[2]))
|
|
113
119
|
|
|
114
120
|
const weekCount = mod.weeks > 0 ? mod.weeks : weekDirs.length
|
|
115
121
|
const activeWeeks = weekDirs.slice(0, weekCount)
|
|
@@ -119,7 +125,8 @@ const activeWeeks = weekDirs.slice(0, weekCount)
|
|
|
119
125
|
const weeksData = []
|
|
120
126
|
|
|
121
127
|
for (const weekDir of activeWeeks) {
|
|
122
|
-
const
|
|
128
|
+
const [, sectionPrefix, sectionNumStr] = weekDir.match(SECTION_RE)
|
|
129
|
+
const weekNum = parseInt(sectionNumStr)
|
|
123
130
|
const dir = path.join(CONTENT, weekDir)
|
|
124
131
|
|
|
125
132
|
// theory.md → src/data/theory-weekN.json
|
|
@@ -133,14 +140,14 @@ for (const weekDir of activeWeeks) {
|
|
|
133
140
|
}
|
|
134
141
|
writeJson(SRC_DATA, `theory-week${weekNum}.json`, theoryOut)
|
|
135
142
|
|
|
136
|
-
// quiz.md → src/data/
|
|
143
|
+
// quiz.md → src/data/meetmoment-quiz-weekN.json
|
|
137
144
|
const quizMd = readMd(path.join(dir, 'quiz.md'))
|
|
138
145
|
const quizOut = {
|
|
139
146
|
title: quizMd.data.title,
|
|
140
147
|
passScore: quizMd.data.passScore ?? 70,
|
|
141
148
|
questions: quizMd.data.questions ?? [],
|
|
142
149
|
}
|
|
143
|
-
writeJson(SRC_DATA, `
|
|
150
|
+
writeJson(SRC_DATA, `meetmoment-quiz-week${weekNum}.json`, quizOut)
|
|
144
151
|
|
|
145
152
|
// exercises/ subfolder → src/data/exercises/weekN.json
|
|
146
153
|
const exDir = path.join(dir, 'exercises')
|
|
@@ -149,9 +156,16 @@ for (const weekDir of activeWeeks) {
|
|
|
149
156
|
.filter(f => f.endsWith('.md') && f !== '_meta.md')
|
|
150
157
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
|
151
158
|
const exercises = exerciseFiles.map(f => {
|
|
152
|
-
const ex = readMd(path.join(exDir, f))
|
|
159
|
+
const { data: ex, content } = readMd(path.join(exDir, f))
|
|
153
160
|
if (!ex.type || ex.type === 'text') {
|
|
154
|
-
|
|
161
|
+
const src = content?.trim() ? content : (ex.description ?? '')
|
|
162
|
+
ex.descriptionHtml = rewriteAssetPaths(marked.parse(src), `week${weekNum}/exercises`)
|
|
163
|
+
}
|
|
164
|
+
ex.descriptionInlineHtml = marked.parseInline(ex.description ?? '')
|
|
165
|
+
if (ex.type === 'external') {
|
|
166
|
+
if (ex.task) ex.taskHtml = marked.parseInline(ex.task)
|
|
167
|
+
if (ex.hint) ex.hintHtml = marked.parse(ex.hint)
|
|
168
|
+
if (ex.solution) ex.solutionHtml = marked.parse(ex.solution)
|
|
155
169
|
}
|
|
156
170
|
return ex
|
|
157
171
|
})
|
|
@@ -175,27 +189,73 @@ for (const weekDir of activeWeeks) {
|
|
|
175
189
|
criteria: hwMd.data.criteria ?? [],
|
|
176
190
|
maxPoints: hwMd.data.maxPoints ?? 0,
|
|
177
191
|
tips: hwMd.data.tips ?? [],
|
|
192
|
+
...(hwMd.data.linked_theory ? { linked_theory: hwMd.data.linked_theory } : {}),
|
|
178
193
|
}
|
|
179
194
|
writeJson(SRC_DATA, `inleveropdracht-week${weekNum}.json`, hwOut)
|
|
180
195
|
|
|
181
196
|
weeksData.push({
|
|
182
197
|
week: weekNum,
|
|
198
|
+
dirName: weekDir,
|
|
199
|
+
prefix: sectionPrefix,
|
|
183
200
|
title: theoryMd.data.title,
|
|
184
|
-
summary: theoryMd.data.summary ?? '',
|
|
201
|
+
summary: marked.parseInline(theoryMd.data.summary ?? ''),
|
|
185
202
|
goal: theoryMd.data.goal,
|
|
186
203
|
leeruitkomsten: theoryMd.data.leeruitkomsten ?? [],
|
|
187
204
|
color: theoryMd.data.accent,
|
|
188
205
|
pages: [
|
|
189
|
-
{ key: 'theorie', href: `/pages
|
|
190
|
-
{ key: 'oefeningen', href: `/pages
|
|
191
|
-
{ key: '
|
|
192
|
-
{ key: 'oefening', href: `/pages
|
|
193
|
-
{ key: 'inleveropdracht', href: `/pages
|
|
206
|
+
{ key: 'theorie', href: `/pages/${weekDir}-theorie.html`, label: 'Theorie' },
|
|
207
|
+
{ key: 'oefeningen', href: `/pages/${weekDir}-oefeningen.html`, label: 'Oefeningen' },
|
|
208
|
+
{ key: 'meetmoment', href: `/pages/${weekDir}-meetmoment.html`, label: 'Meetmoment' },
|
|
209
|
+
{ key: 'oefening', href: `/pages/${weekDir}-oefening.html`, label: 'Oefening' },
|
|
210
|
+
{ key: 'inleveropdracht', href: `/pages/${weekDir}-inleveropdracht.html`, label: 'Inleveropdracht' },
|
|
194
211
|
],
|
|
195
212
|
})
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
// ─── 4. manifest
|
|
215
|
+
// ─── 4. assessment data (parsed early so navLabel is available for manifest) ───────
|
|
216
|
+
|
|
217
|
+
const ASSESSMENTS_DIR = path.join(CONTENT, 'assessments')
|
|
218
|
+
|
|
219
|
+
function buildAssessmentData(filePath, fallbackTitle, fallbackNavLabel, fallbackDescription) {
|
|
220
|
+
if (fs.existsSync(filePath)) {
|
|
221
|
+
const md = readMd(filePath)
|
|
222
|
+
return {
|
|
223
|
+
title: md.data.title ?? fallbackTitle,
|
|
224
|
+
navLabel: md.data.navLabel ?? fallbackNavLabel,
|
|
225
|
+
description: marked.parseInline(md.data.description ?? fallbackDescription),
|
|
226
|
+
passScore: md.data.passScore ?? 70,
|
|
227
|
+
questions: md.data.questions ?? [],
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { title: fallbackTitle, navLabel: fallbackNavLabel, description: marked.parseInline(fallbackDescription), passScore: 70, questions: [] }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildPracticalAssessmentData(filePath, fallbackTitle, fallbackNavLabel) {
|
|
234
|
+
if (fs.existsSync(filePath)) {
|
|
235
|
+
const md = readMd(filePath)
|
|
236
|
+
return {
|
|
237
|
+
title: md.data.title ?? fallbackTitle,
|
|
238
|
+
navLabel: md.data.navLabel ?? fallbackNavLabel,
|
|
239
|
+
description: marked.parseInline(md.data.description ?? ''),
|
|
240
|
+
html: marked.parse(md.content ?? ''),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { title: fallbackTitle, navLabel: fallbackNavLabel, description: '', html: '' }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const theoryAssessmentData = buildAssessmentData(
|
|
247
|
+
path.join(ASSESSMENTS_DIR, 'theory-assessment.md'),
|
|
248
|
+
`Meetmoment theorie — ${mod.name}`,
|
|
249
|
+
'Meetmoment Theorie',
|
|
250
|
+
'Meerkeuzevragen over de module. Minimaal 70% om te slagen.'
|
|
251
|
+
)
|
|
252
|
+
const practicalAssessmentData = buildPracticalAssessmentData(
|
|
253
|
+
path.join(ASSESSMENTS_DIR, 'practical-assessment.md'),
|
|
254
|
+
`Meetmoment praktijk — ${mod.name}`,
|
|
255
|
+
'Meetmoment Praktijk'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// ─── 5. manifest.json ────────────────────────────────────────────────────────
|
|
199
259
|
|
|
200
260
|
const manifest = {
|
|
201
261
|
module: {
|
|
@@ -204,7 +264,7 @@ const manifest = {
|
|
|
204
264
|
youtube: mod.youtube ?? null,
|
|
205
265
|
weeks: weekCount,
|
|
206
266
|
language: mod.language ?? 'nl',
|
|
207
|
-
description: mod.description ?? '',
|
|
267
|
+
description: marked.parseInline(mod.description ?? ''),
|
|
208
268
|
logoAlt: mod.logoAlt ?? mod.name,
|
|
209
269
|
exerciseMode: mod.exerciseMode ?? 'external',
|
|
210
270
|
},
|
|
@@ -212,34 +272,37 @@ const manifest = {
|
|
|
212
272
|
nav: {
|
|
213
273
|
home: { href: '/index.html', label: 'Home' },
|
|
214
274
|
weeks: weeksData.map(wk => ({
|
|
215
|
-
label:
|
|
275
|
+
label: sectionLabel(wk.prefix, wk.week),
|
|
216
276
|
title: wk.title,
|
|
217
277
|
children: [
|
|
218
|
-
{ href: `/pages
|
|
219
|
-
{ href: `/pages
|
|
220
|
-
{ href: `/pages
|
|
221
|
-
{ href: `/pages
|
|
278
|
+
{ href: `/pages/${wk.dirName}-theorie.html`, label: 'Theorie' },
|
|
279
|
+
{ href: `/pages/${wk.dirName}-oefeningen.html`, label: 'Oefeningen' },
|
|
280
|
+
{ href: `/pages/${wk.dirName}-meetmoment.html`, label: 'Quiz' },
|
|
281
|
+
{ href: `/pages/${wk.dirName}-inleveropdracht.html`, label: 'Inleveropdracht' },
|
|
222
282
|
],
|
|
223
283
|
})),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
284
|
+
assessmentSection: {
|
|
285
|
+
label: mod.assessmentSectionLabel ?? 'Afronding',
|
|
286
|
+
children: [
|
|
287
|
+
{ href: '/pages/checklist.html', label: 'Checklist' },
|
|
288
|
+
{ href: '/pages/meetmoment-theorie.html', label: theoryAssessmentData.navLabel },
|
|
289
|
+
{ href: '/pages/meetmoment-praktijk.html', label: practicalAssessmentData.navLabel },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
229
292
|
},
|
|
230
293
|
pages: {
|
|
231
294
|
static: [
|
|
232
295
|
'index.html',
|
|
233
296
|
'pages/checklist.html',
|
|
234
|
-
'pages/
|
|
235
|
-
'pages/
|
|
297
|
+
'pages/meetmoment-theorie.html',
|
|
298
|
+
'pages/meetmoment-praktijk.html',
|
|
236
299
|
],
|
|
237
300
|
week: weeksData.flatMap(wk => [
|
|
238
|
-
`pages
|
|
239
|
-
`pages
|
|
240
|
-
`pages
|
|
241
|
-
`pages
|
|
242
|
-
`pages
|
|
301
|
+
`pages/${wk.dirName}-theorie.html`,
|
|
302
|
+
`pages/${wk.dirName}-oefeningen.html`,
|
|
303
|
+
`pages/${wk.dirName}-meetmoment.html`,
|
|
304
|
+
`pages/${wk.dirName}-oefening.html`,
|
|
305
|
+
`pages/${wk.dirName}-inleveropdracht.html`,
|
|
243
306
|
]),
|
|
244
307
|
},
|
|
245
308
|
content: {
|
|
@@ -253,12 +316,13 @@ writeJson(SRC_DATA, 'manifest.json', manifest)
|
|
|
253
316
|
// ─── 5. checklist.json ───────────────────────────────────────────────────────
|
|
254
317
|
|
|
255
318
|
const checklistGroups = weeksData.map(wk => ({
|
|
256
|
-
id:
|
|
257
|
-
title:
|
|
319
|
+
id: wk.dirName,
|
|
320
|
+
title: `${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
258
321
|
color: wk.color,
|
|
259
322
|
items: (wk.leeruitkomsten ?? []).map((text, i) => ({
|
|
260
|
-
id:
|
|
323
|
+
id: `${wk.dirName}-item-${i}`,
|
|
261
324
|
text,
|
|
325
|
+
textHtml: marked.parseInline(text),
|
|
262
326
|
})),
|
|
263
327
|
}))
|
|
264
328
|
|
|
@@ -267,37 +331,16 @@ if (mod.algemeen?.length) {
|
|
|
267
331
|
id: 'algemeen',
|
|
268
332
|
title: 'Algemeen',
|
|
269
333
|
color: 'slate',
|
|
270
|
-
items: mod.algemeen.map((text, i) => ({ id: `algemeen-item-${i}`, text })),
|
|
334
|
+
items: mod.algemeen.map((text, i) => ({ id: `algemeen-item-${i}`, text, textHtml: marked.parseInline(text) })),
|
|
271
335
|
})
|
|
272
336
|
}
|
|
273
337
|
|
|
274
338
|
writeJson(SRC_DATA, 'checklist.json', { groups: checklistGroups })
|
|
275
339
|
|
|
276
|
-
// ─── 5b.
|
|
340
|
+
// ─── 5b. write assessment data JSON files ──────────────────────────────────────────
|
|
277
341
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const md = readMd(filePath)
|
|
281
|
-
return {
|
|
282
|
-
title: md.data.title ?? fallbackTitle,
|
|
283
|
-
passScore: md.data.passScore ?? 70,
|
|
284
|
-
questions: md.data.questions ?? [],
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return { title: fallbackTitle, passScore: 70, questions: [] }
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const EXAMS_DIR = path.join(CONTENT, 'exams')
|
|
291
|
-
writeJson(
|
|
292
|
-
SRC_DATA,
|
|
293
|
-
'toets-theorie.json',
|
|
294
|
-
buildExamData(path.join(EXAMS_DIR, 'theory-exam.md'), `Eindtoets theorie — ${mod.name}`)
|
|
295
|
-
)
|
|
296
|
-
writeJson(
|
|
297
|
-
SRC_DATA,
|
|
298
|
-
'toets-praktijk.json',
|
|
299
|
-
buildExamData(path.join(EXAMS_DIR, 'practical-exam.md'), `Eindtoets praktijk — ${mod.name}`)
|
|
300
|
-
)
|
|
342
|
+
writeJson(SRC_DATA, 'meetmoment-theorie.json', theoryAssessmentData)
|
|
343
|
+
writeJson(SRC_DATA, 'meetmoment-praktijk.json', practicalAssessmentData)
|
|
301
344
|
|
|
302
345
|
// ─── 6. generate per-week page stubs ─────────────────────────────────────────
|
|
303
346
|
|
|
@@ -305,27 +348,27 @@ const PAGE_TYPES = [
|
|
|
305
348
|
{
|
|
306
349
|
tplFile: 'theorie.html',
|
|
307
350
|
suffix: 'theorie',
|
|
308
|
-
pageTitle: wk => `Theorie
|
|
351
|
+
pageTitle: wk => `Theorie ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
309
352
|
},
|
|
310
353
|
{
|
|
311
|
-
tplFile: '
|
|
312
|
-
suffix: '
|
|
313
|
-
pageTitle: wk => `
|
|
354
|
+
tplFile: 'meetmoment.html',
|
|
355
|
+
suffix: 'meetmoment',
|
|
356
|
+
pageTitle: wk => `Meetmoment ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
314
357
|
},
|
|
315
358
|
{
|
|
316
359
|
tplFile: 'oefeningen.html',
|
|
317
360
|
suffix: 'oefeningen',
|
|
318
|
-
pageTitle: wk => `Oefeningen
|
|
361
|
+
pageTitle: wk => `Oefeningen ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
319
362
|
},
|
|
320
363
|
{
|
|
321
364
|
tplFile: 'oefening.html',
|
|
322
365
|
suffix: 'oefening',
|
|
323
|
-
pageTitle: wk => `Oefening —
|
|
366
|
+
pageTitle: wk => `Oefening — ${sectionLabel(wk.prefix, wk.week)}`,
|
|
324
367
|
},
|
|
325
368
|
{
|
|
326
369
|
tplFile: 'inleveropdracht.html',
|
|
327
370
|
suffix: 'inleveropdracht',
|
|
328
|
-
pageTitle: wk => `Inleveropdracht
|
|
371
|
+
pageTitle: wk => `Inleveropdracht ${sectionLabel(wk.prefix, wk.week)} — ${wk.title}`,
|
|
329
372
|
},
|
|
330
373
|
]
|
|
331
374
|
|
|
@@ -340,21 +383,31 @@ for (const { tplFile, suffix, pageTitle } of PAGE_TYPES) {
|
|
|
340
383
|
weekTitle: wk.title,
|
|
341
384
|
pageTitle: pageTitle(wk),
|
|
342
385
|
})
|
|
343
|
-
fs.writeFileSync(path.join(PAGES,
|
|
386
|
+
fs.writeFileSync(path.join(PAGES, `${wk.dirName}-${suffix}.html`), out)
|
|
344
387
|
}
|
|
345
388
|
}
|
|
346
389
|
|
|
347
|
-
// ─── 7. copy static pages (checklist,
|
|
390
|
+
// ─── 7. copy static pages (checklist, assessments) with title substitution ─────────
|
|
348
391
|
|
|
349
392
|
const STATIC_PAGES = [
|
|
350
393
|
{ src: 'checklist.html', pageTitle: `Checklist — ${mod.name}` },
|
|
351
|
-
{
|
|
352
|
-
|
|
394
|
+
{
|
|
395
|
+
src: 'meetmoment-theorie.html',
|
|
396
|
+
pageTitle: `${theoryAssessmentData.navLabel} — ${mod.name}`,
|
|
397
|
+
assessmentTitle: theoryAssessmentData.navLabel,
|
|
398
|
+
assessmentDescription: theoryAssessmentData.description,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
src: 'meetmoment-praktijk.html',
|
|
402
|
+
pageTitle: `${practicalAssessmentData.navLabel} — ${mod.name}`,
|
|
403
|
+
assessmentTitle: practicalAssessmentData.navLabel,
|
|
404
|
+
assessmentDescription: practicalAssessmentData.description,
|
|
405
|
+
},
|
|
353
406
|
]
|
|
354
407
|
|
|
355
|
-
for (const { src, pageTitle } of STATIC_PAGES) {
|
|
408
|
+
for (const { src, pageTitle, assessmentTitle, assessmentDescription } of STATIC_PAGES) {
|
|
356
409
|
const tpl = fs.readFileSync(path.join(TEMPLATES, src), 'utf8')
|
|
357
|
-
const out = applyTemplate(tpl, { pageTitle })
|
|
410
|
+
const out = applyTemplate(tpl, { pageTitle, assessmentTitle, assessmentDescription })
|
|
358
411
|
fs.writeFileSync(path.join(PAGES, src), out)
|
|
359
412
|
}
|
|
360
413
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curio-sd/e-module-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A tool for building e-modules for Curio SD",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"templates",
|
|
12
12
|
"public",
|
|
13
13
|
"build.mjs",
|
|
14
|
+
"build-pdf.mjs",
|
|
14
15
|
"vite.config.js",
|
|
15
16
|
"vite-plugin-html-includes.js"
|
|
16
17
|
],
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"marked": "^18.0.5",
|
|
31
32
|
"marked-highlight": "^2.2.4",
|
|
32
33
|
"monaco-editor": "^0.53.0",
|
|
34
|
+
"pdfkit": "^0.19.1",
|
|
33
35
|
"tailwindcss": "^4.1.8",
|
|
34
36
|
"vite": "^8.0.16",
|
|
35
37
|
"yaml": "^2.9.0"
|
package/src/css/main.css
CHANGED
|
@@ -23,39 +23,6 @@
|
|
|
23
23
|
body {
|
|
24
24
|
@apply text-zinc-800;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
/* Given that markdown is placed inside main, we want some basic styling for headings */
|
|
28
|
-
main h2 {
|
|
29
|
-
@apply mt-8 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
main h3 {
|
|
33
|
-
@apply mt-4 text-base font-bold text-zinc-900
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
main h4 {
|
|
37
|
-
@apply mt-3 text-sm font-semibold text-zinc-900
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
main h5 {
|
|
41
|
-
@apply mt-2 text-sm font-normal uppercase text-zinc-900
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
main h6 {
|
|
45
|
-
@apply mt-2 text-sm font-light uppercase text-zinc-900
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main p {
|
|
49
|
-
@apply mt-2 text-sm leading-relaxed text-zinc-700;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
main ul {
|
|
53
|
-
@apply mt-2 text-sm list-inside list-disc space-y-1.5 pl-1;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
main ol {
|
|
57
|
-
@apply mt-2 text-sm list-inside list-decimal space-y-1.5 pl-1;
|
|
58
|
-
}
|
|
59
26
|
}
|
|
60
27
|
|
|
61
28
|
@layer components {
|
|
@@ -189,73 +156,103 @@
|
|
|
189
156
|
@apply mx-3 mb-2 mt-1 flex items-center gap-2 border border-zinc-600 bg-zinc-900 px-3 py-2.5 text-[13px] font-medium text-zinc-100 transition hover:border-zinc-500 hover:bg-zinc-800 hover:text-white;
|
|
190
157
|
}
|
|
191
158
|
|
|
159
|
+
.nav-group-toggle {
|
|
160
|
+
@apply flex w-full items-start justify-between px-3 pb-1 pt-4 text-left;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nav-group-chevron {
|
|
164
|
+
@apply mt-0.5 h-3 w-3 shrink-0 text-zinc-500 transition-transform duration-200;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.nav-group-children {
|
|
168
|
+
display: grid;
|
|
169
|
+
grid-template-rows: 1fr;
|
|
170
|
+
transition: grid-template-rows 0.2s ease;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.nav-group-children.collapsed {
|
|
174
|
+
grid-template-rows: 0fr;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.nav-group-inner {
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
}
|
|
180
|
+
|
|
192
181
|
/* ── Theory prose typography ─────────────────────────────────────────────── */
|
|
193
182
|
|
|
194
|
-
|
|
183
|
+
main {
|
|
195
184
|
@apply space-y-6 text-zinc-700;
|
|
196
185
|
}
|
|
197
186
|
|
|
198
|
-
|
|
187
|
+
main h2 {
|
|
199
188
|
@apply mt-8 text-xl font-semibold text-zinc-900 first:mt-0;
|
|
200
189
|
}
|
|
201
190
|
|
|
202
|
-
|
|
191
|
+
main h3 {
|
|
203
192
|
@apply mt-4 text-base font-semibold text-zinc-900;
|
|
204
193
|
}
|
|
205
194
|
|
|
206
|
-
|
|
195
|
+
main p {
|
|
207
196
|
@apply leading-relaxed;
|
|
208
197
|
}
|
|
209
198
|
|
|
210
|
-
|
|
199
|
+
main ul {
|
|
211
200
|
@apply list-inside list-disc space-y-1.5 pl-1;
|
|
212
201
|
}
|
|
213
202
|
|
|
214
|
-
|
|
203
|
+
main ol {
|
|
215
204
|
@apply list-inside list-decimal space-y-1.5 pl-1;
|
|
216
205
|
}
|
|
217
206
|
|
|
218
|
-
|
|
207
|
+
main li {
|
|
219
208
|
@apply leading-relaxed;
|
|
220
209
|
}
|
|
221
210
|
|
|
222
|
-
|
|
211
|
+
main a {
|
|
223
212
|
@apply font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 transition hover:decoration-zinc-900;
|
|
224
213
|
}
|
|
225
214
|
|
|
226
|
-
|
|
215
|
+
main code {
|
|
227
216
|
@apply rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-sm text-zinc-800;
|
|
228
217
|
}
|
|
229
218
|
|
|
230
|
-
|
|
219
|
+
main pre {
|
|
231
220
|
@apply overflow-x-auto bg-zinc-950 p-5 font-mono text-[13px] leading-relaxed text-zinc-300;
|
|
232
221
|
}
|
|
233
222
|
|
|
234
|
-
|
|
223
|
+
main pre code {
|
|
235
224
|
@apply bg-transparent p-0 text-inherit;
|
|
236
225
|
}
|
|
237
226
|
|
|
238
|
-
|
|
227
|
+
main blockquote {
|
|
239
228
|
@apply border-l-4 border-zinc-300 pl-4 italic text-zinc-500;
|
|
240
229
|
}
|
|
241
230
|
|
|
242
|
-
|
|
231
|
+
main table {
|
|
243
232
|
@apply w-full text-left text-sm;
|
|
244
233
|
}
|
|
245
234
|
|
|
246
|
-
|
|
235
|
+
main thead tr {
|
|
247
236
|
@apply border-b border-zinc-200 text-zinc-500;
|
|
248
237
|
}
|
|
249
238
|
|
|
250
|
-
|
|
239
|
+
main tbody tr {
|
|
251
240
|
@apply border-b border-zinc-100 last:border-0;
|
|
252
241
|
}
|
|
253
242
|
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
main th,
|
|
244
|
+
main td {
|
|
256
245
|
@apply py-2 pr-4;
|
|
257
246
|
}
|
|
258
247
|
|
|
248
|
+
main details {
|
|
249
|
+
@apply rounded bg-zinc-100 p-4;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main details summary {
|
|
253
|
+
@apply cursor-pointer font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 transition hover:decoration-zinc-900;
|
|
254
|
+
}
|
|
255
|
+
|
|
259
256
|
/* ── Custom components ───────────────────────────────────────────────────── */
|
|
260
257
|
|
|
261
258
|
x-card {
|
|
@@ -313,4 +310,81 @@
|
|
|
313
310
|
x-nav>p>a:last-child {
|
|
314
311
|
@apply border-transparent bg-zinc-900 text-white hover:border-transparent hover:bg-zinc-800 focus-visible:ring-zinc-900;
|
|
315
312
|
}
|
|
313
|
+
|
|
314
|
+
/* ── Theory side panel ───────────────────────────────────────────────────── */
|
|
315
|
+
|
|
316
|
+
.theory-panel {
|
|
317
|
+
@apply fixed inset-y-0 right-0 z-40 flex w-[480px] max-w-[90vw] flex-col border-l border-zinc-200 bg-white shadow-xl;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.theory-panel.panel-ready {
|
|
321
|
+
@apply transition-transform duration-300 ease-out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.theory-panel.panel-hidden {
|
|
325
|
+
@apply translate-x-full;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.theory-panel-header {
|
|
329
|
+
@apply flex shrink-0 items-center border-b border-zinc-200 bg-zinc-50;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.theory-panel-tabs {
|
|
333
|
+
@apply flex flex-1 overflow-x-auto;
|
|
334
|
+
scrollbar-width: none;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.theory-panel-tabs::-webkit-scrollbar {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.theory-tab {
|
|
342
|
+
@apply whitespace-nowrap border-b-2 border-transparent px-4 py-3 text-sm font-medium text-zinc-500 transition hover:text-zinc-900;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.theory-tab-active {
|
|
346
|
+
@apply border-zinc-900 text-zinc-900;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.theory-panel-close {
|
|
350
|
+
@apply shrink-0 p-3 text-zinc-400 transition hover:text-zinc-700;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.theory-panel-body {
|
|
354
|
+
@apply relative min-h-0 flex-1 flex flex-col;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.theory-panel-loader {
|
|
358
|
+
@apply absolute inset-0 z-10 flex items-center justify-center bg-white/80 backdrop-blur-sm;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.theory-panel-loader-hidden {
|
|
362
|
+
@apply hidden;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.theory-panel-spinner {
|
|
366
|
+
@apply h-8 w-8 animate-spin rounded-full border-2 border-zinc-200 border-t-zinc-700;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.theory-panel-iframe {
|
|
370
|
+
@apply min-h-0 flex-1 w-full border-0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.theory-panel-toggle {
|
|
374
|
+
@apply fixed bottom-6 right-6 shadow z-50 flex items-center gap-2 bg-zinc-900 px-4 py-2.5 text-sm font-medium text-white shadow-lg transition hover:bg-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-900 focus-visible:ring-offset-2;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ── Embedded theory mode (inside iframe) ────────────────────────────────── */
|
|
378
|
+
|
|
379
|
+
[data-embedded] {
|
|
380
|
+
@apply bg-white;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
[data-embedded] [data-theory] {
|
|
384
|
+
@apply px-6 py-8;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
[data-embedded] main {
|
|
388
|
+
@apply !p-0;
|
|
389
|
+
}
|
|
316
390
|
}
|
package/src/js/checklist.js
CHANGED
|
@@ -20,7 +20,7 @@ export function initChecklist() {
|
|
|
20
20
|
class="mt-0.5 h-4 w-4 border-zinc-300 text-zinc-900 focus:ring-zinc-900"
|
|
21
21
|
${checked ? 'checked' : ''}
|
|
22
22
|
/>
|
|
23
|
-
<span class="text-sm leading-relaxed ${checked ? 'text-zinc-400 line-through' : 'text-zinc-700'}">${item.text}</span>
|
|
23
|
+
<span class="text-sm leading-relaxed ${checked ? 'text-zinc-400 line-through' : 'text-zinc-700'}">${item.textHtml ?? item.text}</span>
|
|
24
24
|
</label>
|
|
25
25
|
`
|
|
26
26
|
})
|
|
@@ -12,7 +12,7 @@ export function renderExerciseMeta(exercise) {
|
|
|
12
12
|
diffEl.textContent = `Oefening ${String(exercise.id).padStart(2, '0')} · ${label}`
|
|
13
13
|
}
|
|
14
14
|
if (titleEl) titleEl.textContent = exercise.title
|
|
15
|
-
if (descEl) descEl.
|
|
15
|
+
if (descEl) descEl.innerHTML = exercise.descriptionInlineHtml ?? ''
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function markExerciseSolved(week, id) {
|
|
@@ -38,7 +38,7 @@ export function initExternalExercise(exercise, weekNum, { onSolved } = {}) {
|
|
|
38
38
|
const deliverablesEl = document.querySelector('[data-external-deliverables]')
|
|
39
39
|
const feedback = document.querySelector('[data-feedback]')
|
|
40
40
|
|
|
41
|
-
if (taskEl) taskEl.innerHTML = exercise.
|
|
41
|
+
if (taskEl) taskEl.innerHTML = exercise.taskHtml || exercise.descriptionInlineHtml || ''
|
|
42
42
|
if (stepsEl) stepsEl.innerHTML = renderSteps(exercise.steps)
|
|
43
43
|
if (envEl) envEl.textContent = exercise.environment || 'Je eigen omgeving (editor, server of tool)'
|
|
44
44
|
if (deliverablesEl) {
|
|
@@ -48,15 +48,14 @@ export function initExternalExercise(exercise, weekNum, { onSolved } = {}) {
|
|
|
48
48
|
document.querySelector('[data-hint]')?.addEventListener('click', () => {
|
|
49
49
|
showFeedback(
|
|
50
50
|
feedback,
|
|
51
|
-
|
|
51
|
+
exercise.hintHtml || '<p class="text-zinc-600">Werk de stappen rustig door in je eigen omgeving.</p>'
|
|
52
52
|
)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
document.querySelector('[data-solution]')?.addEventListener('click', () => {
|
|
56
|
-
const solution = exercise.solution || 'Vergelijk je werk met de theorie en vraag feedback aan je docent.'
|
|
57
56
|
showFeedback(
|
|
58
57
|
feedback,
|
|
59
|
-
`<p class="font-medium text-zinc-900">Voorbeelduitwerking</p
|
|
58
|
+
`<p class="font-medium text-zinc-900">Voorbeelduitwerking</p>${exercise.solutionHtml || '<p class="mt-2 text-sm text-zinc-600">Vergelijk je werk met de theorie en vraag feedback aan je docent.</p>'}`
|
|
60
59
|
)
|
|
61
60
|
})
|
|
62
61
|
|
package/src/js/exercises/hub.js
CHANGED
|
@@ -3,6 +3,10 @@ import { sitePath } from '../site-path.js'
|
|
|
3
3
|
|
|
4
4
|
const DIFFICULTY = ['', 'Beginner', 'Beginner', 'Gemiddeld', 'Gemiddeld', 'Gevorderd', 'Gevorderd', 'Expert', 'Expert']
|
|
5
5
|
|
|
6
|
+
function stripHtml(html) {
|
|
7
|
+
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export function initExerciseHub(weekData, weekNum) {
|
|
7
11
|
const container = document.querySelector('[data-exercise-list]')
|
|
8
12
|
if (!container) return
|
|
@@ -36,7 +40,7 @@ export function initExerciseHub(weekData, weekNum) {
|
|
|
36
40
|
<h3 class="font-medium text-zinc-900">${ex.title}</h3>
|
|
37
41
|
${done ? '<span class="badge-done">Voltooid</span>' : ''}
|
|
38
42
|
</div>
|
|
39
|
-
<p class="mt-1.5 text-sm leading-relaxed text-zinc-500">${ex.description}</p>
|
|
43
|
+
<p class="mt-1.5 text-sm leading-relaxed text-zinc-500">${ex.descriptionInlineHtml ?? ex.description ?? ''}</p>
|
|
40
44
|
${modeBadge}
|
|
41
45
|
</div>
|
|
42
46
|
</a>
|