@curio-sd/e-module-builder 0.3.1 → 0.5.0

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.
@@ -66,6 +66,9 @@ export function initAreasExercise(exercise, { onSolved } = {}) {
66
66
 
67
67
  if (!containerInput || !preview) return
68
68
 
69
+ const outerClassMatch = exercise.previewHtml.match(/<\w+[^>]*\bclass="([^"]*)"/)
70
+ const gridClass = (outerClassMatch?.[1] ?? '').split(/\s+/)[0] || 'grid-container'
71
+
69
72
  const updatePreview = () => {
70
73
  const areas = containerInput.value.trim()
71
74
  const itemStyles = Array.from(selects)
@@ -74,7 +77,7 @@ export function initAreasExercise(exercise, { onSolved } = {}) {
74
77
 
75
78
  preview.innerHTML = `
76
79
  <style>
77
- .news-grid {
80
+ .${gridClass} {
78
81
  display: grid;
79
82
  grid-template-columns: ${exercise.gridColumns || '1fr 1fr 1fr'};
80
83
  grid-template-areas: ${areas || '"a b c"'};
@@ -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.textContent = exercise.description
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.task || exercise.description || ''
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
- `<p class="text-zinc-600">${(exercise.hint || 'Werk de stappen rustig door in je eigen omgeving.').replace(/\n/g, '<br>')}</p>`
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><p class="mt-2 text-sm text-zinc-600">${solution.replace(/\n/g, '<br>')}</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
 
@@ -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>
@@ -1,107 +1,59 @@
1
1
  import { renderExerciseMeta, markExerciseSolved } from './exercise-shared.js'
2
2
  import { sitePath } from '../site-path.js'
3
-
4
3
  import { initExternalExercise } from './external-exercise.js'
5
-
6
-
4
+ import { initTheoryPanel } from './theory-panel.js'
7
5
 
8
6
  async function loadWeekData(weekNum) {
9
-
10
7
  return import(`../../data/exercises/week${weekNum}.json`).then((m) => m.default)
11
-
12
8
  }
13
9
 
14
-
15
-
16
10
  function isExternalMode(weekData, exercise) {
17
-
18
11
  return weekData?.mode === 'external' || exercise?.type === 'external'
19
-
20
12
  }
21
13
 
22
-
23
-
24
14
  function showMissingContent(weekNum) {
25
-
26
15
  const panel = document.querySelector('[data-exercise-content]')
27
16
 
28
17
  panel?.querySelector('[data-exercise-interactive]')?.classList.add('hidden')
29
-
30
18
  panel?.querySelector('[data-exercise-external]')?.classList.add('hidden')
31
-
32
19
  panel?.querySelector('[data-hint]')?.closest('.mb-4')?.classList.add('hidden')
33
-
34
20
  panel?.insertAdjacentHTML(
35
-
36
21
  'beforeend',
37
-
38
22
  `<div class="card border-l-4 border-l-amber-500">
39
-
40
23
  <p class="font-medium text-zinc-900">Content ontbreekt</p>
41
-
42
- <p class="mt-2 text-sm text-zinc-600">De oefening staat in de navigatie, maar is nog niet gegenereerd. Run in een terminal:</p>
43
-
44
- <pre class="code-block mt-3 text-sm"><code>npm run generate-content</code></pre>
45
-
46
- <p class="mt-2 text-sm text-zinc-500">Herlaad daarna deze pagina.</p>
47
-
24
+ <p class="mt-2 text-sm text-zinc-600">De oefening staat in de navigatie, maar de inhoud is niet beschikbaar.</p>
48
25
  </div>`
49
-
50
26
  )
51
-
52
27
  }
53
28
 
54
-
55
-
56
29
  export async function initExercisePage(weekNum) {
57
-
58
30
  const params = new URLSearchParams(window.location.search)
59
-
60
31
  const id = parseInt(params.get('id') || '1', 10)
61
-
62
-
63
-
64
32
  let weekData
65
33
 
66
34
  try {
67
-
68
35
  weekData = await loadWeekData(weekNum)
69
-
70
36
  } catch {
71
-
72
37
  document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
73
-
74
38
  'beforeend',
75
-
76
39
  `<p class="text-red-600">Oefeningdata voor week ${weekNum} niet gevonden.</p>`
77
-
78
40
  )
79
41
 
80
42
  return
81
-
82
43
  }
83
44
 
84
-
85
-
86
45
  const exercise = weekData?.exercises?.find((e) => e.id === id)
87
46
 
88
-
89
-
90
47
  if (!exercise) {
91
-
92
48
  document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
93
-
94
49
  'beforeend',
95
-
96
50
  '<p class="text-red-600">Oefening niet gevonden.</p>'
97
-
98
51
  )
99
52
 
100
53
  return
101
-
102
54
  }
103
55
 
104
-
56
+ initTheoryPanel(exercise.linked_theory)
105
57
 
106
58
  if (!exercise.type || exercise.type === 'text') {
107
59
  const panel = document.querySelector('[data-exercise-content]')
@@ -121,189 +73,100 @@ export async function initExercisePage(weekNum) {
121
73
  }
122
74
 
123
75
  if (isExternalMode(weekData, exercise)) {
124
-
125
76
  const missing = !exercise.task?.trim() && !exercise.description?.trim()
126
77
 
127
78
  if (missing) {
128
-
129
79
  showMissingContent(weekNum)
130
-
131
80
  return
132
-
133
81
  }
134
82
 
135
83
  initExternalExercise(exercise, weekNum, {
136
-
137
84
  onSolved: () => markExerciseSolved(weekNum, exercise.id),
138
-
139
85
  })
140
86
 
141
87
  renderNavButtons(weekNum, id, weekData.exercises.length)
142
-
143
88
  return
144
-
145
89
  }
146
90
 
147
-
148
-
149
91
  renderExerciseMeta(exercise)
150
92
 
151
-
152
-
153
93
  const missingContent =
154
-
155
- !exercise.starterCss?.trim() ||
156
-
94
+ (exercise.type === 'areas' ? !exercise.starterAreas?.trim() : !exercise.starterCss?.trim()) ||
157
95
  !exercise.previewHtml?.trim() ||
158
-
159
96
  (exercise.type !== 'areas' && !exercise.checks?.length)
160
97
 
161
-
162
-
163
98
  if (missingContent) {
164
-
165
99
  showMissingContent(weekNum)
166
-
167
100
  return
168
-
169
101
  }
170
102
 
171
-
172
-
173
103
  const {
174
-
175
104
  initCssPlayground,
176
-
177
105
  initAreasExercise,
178
-
179
106
  initResponsiveExercise,
180
-
107
+ renderAreaSelects,
181
108
  } = await import('./exercise-runner.js')
182
109
 
183
-
184
-
185
110
  document.querySelector('[data-exercise-external]')?.classList.add('hidden')
186
-
187
111
  document.querySelector('[data-exercise-interactive]')?.classList.remove('hidden')
188
112
 
189
-
190
-
191
113
  const cssPanel = document.querySelector('[data-exercise-css-panel]')
192
-
193
114
  const areasPanel = document.querySelector('[data-exercise-areas-panel]')
194
-
195
115
  const responsiveBar = document.querySelector('[data-responsive-bar]')
196
-
197
-
198
-
199
116
  const onSolved = () => markExerciseSolved(weekNum, exercise.id)
200
117
 
201
-
202
-
203
118
  if (exercise.type === 'areas') {
204
-
205
119
  cssPanel?.classList.add('hidden')
206
-
207
120
  areasPanel?.classList.remove('hidden')
208
-
209
121
  responsiveBar?.classList.add('hidden')
210
-
211
122
  document.querySelector('[data-solution]')?.classList.add('hidden')
212
123
 
213
-
214
-
215
124
  const containerInput = document.querySelector('[data-areas-container]')
216
125
 
217
126
  if (containerInput) containerInput.value = exercise.starterAreas || ''
218
127
 
219
-
220
-
221
128
  const selectsEl = document.querySelector('[data-area-selects]')
222
129
 
223
130
  if (selectsEl) {
224
-
225
131
  selectsEl.innerHTML = renderAreaSelects(exercise.areaItems, exercise.areaOptions)
226
-
227
132
  }
228
133
 
229
-
230
-
231
134
  initAreasExercise(exercise, { onSolved })
232
-
233
135
  } else {
234
-
235
136
  areasPanel?.classList.add('hidden')
236
-
237
137
  cssPanel?.classList.remove('hidden')
238
138
 
239
-
240
-
241
139
  if (exercise.type === 'responsive') {
242
-
243
140
  responsiveBar?.classList.remove('hidden')
244
-
245
141
  initResponsiveExercise(exercise, { onSolved })
246
-
247
142
  } else {
248
-
249
143
  responsiveBar?.classList.add('hidden')
250
-
251
144
  initCssPlayground(exercise, { onSolved })
252
-
253
145
  }
254
-
255
146
  }
256
147
 
257
-
258
-
259
148
  renderNavButtons(weekNum, id, weekData.exercises.length)
260
-
261
149
  }
262
150
 
263
-
264
-
265
151
  function renderNavButtons(week, currentId, total) {
266
-
267
152
  const prev = document.querySelector('[data-prev-exercise]')
268
-
269
153
  const next = document.querySelector('[data-next-exercise]')
270
154
 
271
-
272
-
273
155
  if (prev) {
274
-
275
156
  if (currentId > 1) {
276
-
277
157
  prev.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId - 1}`
278
-
279
158
  prev.classList.remove('hidden')
280
-
281
159
  } else {
282
-
283
160
  prev.classList.add('hidden')
284
-
285
161
  }
286
-
287
162
  }
288
163
 
289
-
290
-
291
164
  if (next) {
292
-
293
165
  if (currentId < total) {
294
-
295
166
  next.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId + 1}`
296
-
297
167
  next.classList.remove('hidden')
298
-
299
168
  } else {
300
-
301
169
  next.classList.add('hidden')
302
-
303
170
  }
304
-
305
171
  }
306
-
307
172
  }
308
-
309
-
@@ -0,0 +1,95 @@
1
+ import { sitePath } from '../site-path.js'
2
+
3
+ const WEEK_RE = /^([a-zA-Z]+)(\d+)$/
4
+
5
+ const BOOK_SVG = `<svg class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.966 8.966 0 0 0-6 2.292m0-14.25v14.25" /></svg>`
6
+
7
+ export function parseLinkedTheory(linkedTheory) {
8
+ if (!Array.isArray(linkedTheory) || linkedTheory.length === 0) return []
9
+
10
+ return linkedTheory
11
+ .map((entry) => {
12
+ if (typeof entry !== 'string') return null
13
+ const m = entry.trim().match(WEEK_RE)
14
+ if (!m) return null
15
+ const prefix = m[1]
16
+ const num = parseInt(m[2], 10)
17
+ const label = `${prefix.charAt(0).toUpperCase()}${prefix.slice(1)} ${num}`
18
+ return { key: entry.trim(), label, num, dirName: entry.trim() }
19
+ })
20
+ .filter(Boolean)
21
+ }
22
+
23
+ export function theoryIframeSrc(dirName) {
24
+ return sitePath(`/pages/${dirName}-theorie.html`) + '?embedded=1'
25
+ }
26
+
27
+ export function initTheoryPanel(linkedTheory) {
28
+ const toggle = document.querySelector('[data-theory-toggle]')
29
+ const panel = document.querySelector('[data-theory-panel]')
30
+
31
+ const tabs = parseLinkedTheory(linkedTheory)
32
+
33
+ if (!tabs.length) {
34
+ toggle?.classList.add('hidden')
35
+ panel?.classList.add('panel-hidden')
36
+ return
37
+ }
38
+
39
+ toggle?.classList.remove('hidden')
40
+
41
+ const tabsEl = panel?.querySelector('[data-theory-tabs]')
42
+ const iframe = panel?.querySelector('[data-theory-iframe]')
43
+ const loader = panel?.querySelector('[data-theory-loader]')
44
+
45
+ if (!tabsEl || !iframe || !panel) return
46
+
47
+ function showLoader() { loader?.classList.remove('theory-panel-loader-hidden') }
48
+ function hideLoader() { loader?.classList.add('theory-panel-loader-hidden') }
49
+
50
+ iframe.addEventListener('load', hideLoader)
51
+
52
+ tabsEl.innerHTML = tabs
53
+ .map(
54
+ (t, i) =>
55
+ `<button class="theory-tab${i === 0 ? ' theory-tab-active' : ''}" data-tab="${t.key}" type="button">${t.label}</button>`
56
+ )
57
+ .join('')
58
+
59
+ function activateTab(key) {
60
+ tabsEl.querySelectorAll('[data-tab]').forEach((btn) => {
61
+ btn.classList.toggle('theory-tab-active', btn.dataset.tab === key)
62
+ })
63
+ const tab = tabs.find((t) => t.key === key)
64
+ if (tab) {
65
+ showLoader()
66
+ iframe.src = theoryIframeSrc(tab.dirName)
67
+ }
68
+ }
69
+
70
+ tabsEl.addEventListener('click', (e) => {
71
+ const btn = e.target.closest('[data-tab]')
72
+ if (btn) activateTab(btn.dataset.tab)
73
+ })
74
+
75
+ activateTab(tabs[0].key)
76
+
77
+ const closeBtn = panel.querySelector('[data-theory-panel-close]')
78
+ closeBtn?.addEventListener('click', () => {
79
+ panel.classList.add('panel-hidden')
80
+ toggle?.setAttribute('aria-expanded', 'false')
81
+ })
82
+
83
+ toggle?.addEventListener('click', () => {
84
+ const isHidden = panel.classList.contains('panel-hidden')
85
+ panel.classList.toggle('panel-hidden', !isHidden)
86
+ toggle.setAttribute('aria-expanded', String(isHidden))
87
+ })
88
+
89
+ toggle?.setAttribute('aria-expanded', 'false')
90
+
91
+ // Enable the slide transition after the first frame has been committed so
92
+ // the panel doesn't animate from its off-screen position on page load.
93
+ // Double-rAF ensures this works even when initTheoryPanel is called synchronously.
94
+ requestAnimationFrame(() => requestAnimationFrame(() => panel.classList.add('panel-ready')))
95
+ }
package/src/js/home.js CHANGED
@@ -31,7 +31,7 @@ export function initHome() {
31
31
 
32
32
  if (titleEl) titleEl.textContent = mod.name
33
33
 
34
- if (descEl) descEl.textContent = mod.description
34
+ if (descEl) descEl.innerHTML = mod.description
35
35
 
36
36
 
37
37
 
package/src/js/nav.js CHANGED
@@ -13,9 +13,7 @@ function buildNavItems() {
13
13
  })
14
14
  }
15
15
 
16
- for (const page of manifest.nav.examPages) {
17
- items.push(page)
18
- }
16
+ items.push(manifest.nav.assessmentSection)
19
17
 
20
18
  return items
21
19
  }
@@ -69,16 +67,68 @@ function renderNavGroup(group) {
69
67
  ? `<p class="mt-0.5 text-[10px] font-normal leading-snug text-zinc-500">${group.title}</p>`
70
68
  : ''
71
69
  return `
72
- <div class="space-y-0.5">
73
- <div class="px-3 pb-1 pt-4">
74
- <p class="text-[10px] font-semibold uppercase tracking-[0.14em] text-zinc-400">${group.label}</p>
75
- ${title}
70
+ <div class="space-y-0.5" data-nav-group="${group.label}">
71
+ <button class="nav-group-toggle" data-group-toggle="${group.label}">
72
+ <div>
73
+ <p class="text-[10px] font-semibold uppercase tracking-[0.14em] text-zinc-400">${group.label}</p>
74
+ ${title}
75
+ </div>
76
+ <svg class="nav-group-chevron" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
77
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
78
+ </svg>
79
+ </button>
80
+ <div class="nav-group-children" data-group-children="${group.label}">
81
+ <div class="nav-group-inner">${childLinks}</div>
76
82
  </div>
77
- ${childLinks}
78
83
  </div>
79
84
  `
80
85
  }
81
86
 
87
+ function initCollapsible(navEl, defaultExpandedKeys) {
88
+ const STORAGE_KEY = 'nav-collapsed-groups'
89
+
90
+ function getCollapsed() {
91
+ try {
92
+ return new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'))
93
+ } catch {
94
+ return new Set()
95
+ }
96
+ }
97
+
98
+ function saveCollapsed(collapsed) {
99
+ localStorage.setItem(STORAGE_KEY, JSON.stringify([...collapsed]))
100
+ }
101
+
102
+ if (localStorage.getItem(STORAGE_KEY) === null) {
103
+ const allKeys = [...navEl.querySelectorAll('[data-group-toggle]')].map((b) => b.dataset.groupToggle)
104
+ saveCollapsed(new Set(allKeys.filter((k) => !defaultExpandedKeys.has(k))))
105
+ }
106
+
107
+ const collapsed = getCollapsed()
108
+
109
+ navEl.querySelectorAll('[data-group-toggle]').forEach((btn) => {
110
+ const key = btn.dataset.groupToggle
111
+ const children = navEl.querySelector(`[data-group-children="${key}"]`)
112
+ const chevron = btn.querySelector('.nav-group-chevron')
113
+ const hasActive = !!children?.querySelector('.text-white')
114
+
115
+ if (!hasActive && collapsed.has(key)) {
116
+ children?.classList.add('collapsed')
117
+ chevron?.classList.add('-rotate-90')
118
+ }
119
+
120
+ btn.addEventListener('click', () => {
121
+ const nowCollapsed = children?.classList.toggle('collapsed')
122
+ chevron?.classList.toggle('-rotate-90', nowCollapsed)
123
+
124
+ const saved = getCollapsed()
125
+ if (nowCollapsed) saved.add(key)
126
+ else saved.delete(key)
127
+ saveCollapsed(saved)
128
+ })
129
+ })
130
+ }
131
+
82
132
  export function getManifest() {
83
133
  return manifest
84
134
  }
@@ -108,6 +158,11 @@ export function initNav() {
108
158
  <div class="sidebar-scroll space-y-1">${links}</div>
109
159
  </div>
110
160
  `
161
+ const groups = NAV_ITEMS.filter((i) => i.children)
162
+ const defaultExpandedKeys = new Set()
163
+ if (groups[0]) defaultExpandedKeys.add(groups[0].label)
164
+ if (manifest.nav.assessmentSection?.label) defaultExpandedKeys.add(manifest.nav.assessmentSection.label)
165
+ initCollapsible(navEl, defaultExpandedKeys)
111
166
  }
112
167
 
113
168
  export function initMobileNav() {
package/src/js/theory.js CHANGED
@@ -15,7 +15,6 @@ export async function initTheory(week) {
15
15
  } catch {
16
16
  container.innerHTML = `
17
17
  <p class="text-red-600">Theorie voor week ${week} niet gevonden.</p>
18
- <p class="mt-2 text-sm text-zinc-500">Run <code class="font-mono">npm run generate-content</code> om content te genereren.</p>
19
18
  `
20
19
  return
21
20
  }
@@ -23,7 +22,6 @@ export async function initTheory(week) {
23
22
  if (!data?.html) {
24
23
  container.innerHTML = `
25
24
  <p class="text-amber-700">Theorie voor week ${week} is nog leeg.</p>
26
- <p class="mt-2 text-sm text-zinc-500">Run <code class="font-mono">npm run generate-content</code> om content te genereren.</p>
27
25
  `
28
26
  return
29
27
  }
@@ -70,11 +70,11 @@
70
70
  <span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-900 font-mono text-xs text-white">3</span>
71
71
 
72
72
  <div>
73
- <p class="font-medium text-zinc-900">Eindtoetsen toevoegen (optioneel)</p>
73
+ <p class="font-medium text-zinc-900">Meetmomenten toevoegen (optioneel)</p>
74
74
 
75
75
  <p class="mt-1 text-sm text-zinc-500">
76
- Plaats <code class="font-mono text-xs">content/exams/theory-exam.md</code> en
77
- <code class="font-mono text-xs">practical-exam.md</code> met vragen in YAML frontmatter.
76
+ Plaats <code class="font-mono text-xs">content/assessments/theory-assessment.md</code> en
77
+ <code class="font-mono text-xs">practical-assessment.md</code> met vragen in YAML frontmatter.
78
78
  </p>
79
79
  </div>
80
80
  </li>
@@ -120,12 +120,20 @@ npm run preview # preview van dist/</code></pre>
120
120
  </p>
121
121
  </div>
122
122
 
123
- <p class="mb-6 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Curriculum</p>
123
+ <div class="mb-6 flex items-center justify-between">
124
+ <p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Curriculum</p>
125
+
126
+ <a href="e-module.pdf"
127
+ download
128
+ class="inline-flex items-center gap-1.5 rounded border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-zinc-600 hover:bg-zinc-50">
129
+ Download als PDF
130
+ </a>
131
+ </div>
124
132
 
125
133
  <div class="mb-14 grid gap-px bg-zinc-200 sm:grid-cols-2"
126
134
  data-home-curriculum></div>
127
135
 
128
- <p class="mb-6 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Eindtoets</p>
136
+ <p class="mb-6 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Meetmoment</p>
129
137
 
130
138
  <div class="grid gap-px bg-zinc-200 sm:grid-cols-3">
131
139
  <a href="pages/checklist.html"
@@ -136,16 +144,16 @@ npm run preview # preview van dist/</code></pre>
136
144
  <p class="mt-1 text-sm text-zinc-500">Houd je voortgang bij</p>
137
145
  </a>
138
146
 
139
- <a href="pages/toets-theorie.html"
147
+ <a href="pages/meetmoment-theorie.html"
140
148
  class="card-interactive block bg-white">
141
- <h3 class="font-medium text-zinc-900">Eindtoets theorie</h3>
149
+ <h3 class="font-medium text-zinc-900">Meetmoment theorie</h3>
142
150
 
143
151
  <p class="mt-1 text-sm text-zinc-500">Meerkeuzevragen</p>
144
152
  </a>
145
153
 
146
- <a href="pages/toets-praktijk.html"
154
+ <a href="pages/meetmoment-praktijk.html"
147
155
  class="card-interactive block bg-white">
148
- <h3 class="font-medium text-zinc-900">Eindtoets praktijk</h3>
156
+ <h3 class="font-medium text-zinc-900">Meetmoment praktijk</h3>
149
157
 
150
158
  <p class="mt-1 text-sm text-zinc-500">Praktijkvragen</p>
151
159
  </a>