@curio-sd/e-module-builder 0.1.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.
- package/LICENSE +7 -0
- package/bin/cli.js +83 -0
- package/build.mjs +339 -0
- package/package.json +29 -0
- package/public/Logo Schaalbaar (1).svg +293 -0
- package/public/favicon.svg +6 -0
- package/public/logo.svg +293 -0
- package/src/css/main.css +283 -0
- package/src/js/checklist.js +99 -0
- package/src/js/exercises/exercise-runner.js +184 -0
- package/src/js/exercises/exercise-shared.js +37 -0
- package/src/js/exercises/external-exercise.js +71 -0
- package/src/js/exercises/hub.js +57 -0
- package/src/js/exercises/load-exercise.js +309 -0
- package/src/js/exercises/validators.js +53 -0
- package/src/js/home.js +86 -0
- package/src/js/layout.js +42 -0
- package/src/js/monaco-setup.js +49 -0
- package/src/js/nav.js +165 -0
- package/src/js/quiz.js +111 -0
- package/src/js/site-path.js +18 -0
- package/src/js/storage.js +51 -0
- package/src/js/theory.js +41 -0
- package/src/js/thuiswerk.js +161 -0
- package/src/partials/head.html +6 -0
- package/templates/index.html +181 -0
- package/templates/page.html +29 -0
- package/templates/pages/checklist.html +37 -0
- package/templates/pages/inleveropdracht.html +28 -0
- package/templates/pages/oefening.html +158 -0
- package/templates/pages/oefeningen.html +44 -0
- package/templates/pages/theorie.html +22 -0
- package/templates/pages/toets-praktijk.html +25 -0
- package/templates/pages/toets-theorie.html +25 -0
- package/templates/pages/toets.html +33 -0
- package/vite-plugin-html-includes.js +23 -0
- package/vite.config.js +42 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getSolvedExercises } from './exercise-shared.js'
|
|
2
|
+
import { sitePath } from '../site-path.js'
|
|
3
|
+
|
|
4
|
+
const DIFFICULTY = ['', 'Beginner', 'Beginner', 'Gemiddeld', 'Gemiddeld', 'Gevorderd', 'Gevorderd', 'Expert', 'Expert']
|
|
5
|
+
|
|
6
|
+
export function initExerciseHub(weekData, weekNum) {
|
|
7
|
+
const container = document.querySelector('[data-exercise-list]')
|
|
8
|
+
if (!container) return
|
|
9
|
+
|
|
10
|
+
const solved = getSolvedExercises(weekNum)
|
|
11
|
+
const external = weekData.mode === 'external'
|
|
12
|
+
|
|
13
|
+
const subtitle = document.querySelector('[data-hub-subtitle]')
|
|
14
|
+
if (subtitle) {
|
|
15
|
+
subtitle.textContent = external
|
|
16
|
+
? `${weekData.title || `Week ${weekNum}`} — 8 opdrachten voor je eigen omgeving`
|
|
17
|
+
: `${weekData.title || `Week ${weekNum}`} — 8 oefeningen met oplopende moeilijkheid`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const items = weekData.exercises
|
|
21
|
+
.map((ex) => {
|
|
22
|
+
const label = DIFFICULTY[ex.difficulty] || 'Beginner'
|
|
23
|
+
const done = solved.includes(ex.id)
|
|
24
|
+
const modeBadge = external
|
|
25
|
+
? '<span class="badge mt-3">Eigen omgeving</span>'
|
|
26
|
+
: `<span class="badge mt-3">${label}</span>`
|
|
27
|
+
|
|
28
|
+
return `
|
|
29
|
+
<a
|
|
30
|
+
href="${sitePath(`/pages/week${weekNum}-oefening.html`)}?id=${ex.id}"
|
|
31
|
+
class="group flex items-start gap-5 px-6 py-5 transition hover:bg-zinc-50"
|
|
32
|
+
>
|
|
33
|
+
<span class="mt-0.5 font-mono text-sm text-zinc-300 transition group-hover:text-zinc-900">${String(ex.id).padStart(2, '0')}</span>
|
|
34
|
+
<div class="flex-1">
|
|
35
|
+
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
36
|
+
<h3 class="font-medium text-zinc-900">${ex.title}</h3>
|
|
37
|
+
${done ? '<span class="badge-done">Voltooid</span>' : ''}
|
|
38
|
+
</div>
|
|
39
|
+
<p class="mt-1.5 text-sm leading-relaxed text-zinc-500">${ex.description}</p>
|
|
40
|
+
${modeBadge}
|
|
41
|
+
</div>
|
|
42
|
+
</a>
|
|
43
|
+
`
|
|
44
|
+
})
|
|
45
|
+
.join('')
|
|
46
|
+
|
|
47
|
+
container.innerHTML = `<div class="card divide-y divide-zinc-100 p-0">${items}</div>`
|
|
48
|
+
|
|
49
|
+
const progressEl = document.querySelector('[data-hub-progress]')
|
|
50
|
+
const progressBar = document.querySelector('[data-hub-progress-bar]')
|
|
51
|
+
if (progressEl) {
|
|
52
|
+
const total = weekData.exercises.length
|
|
53
|
+
const percent = total ? Math.round((solved.length / total) * 100) : 0
|
|
54
|
+
progressEl.textContent = `${solved.length} / ${total}`
|
|
55
|
+
if (progressBar) progressBar.style.width = `${percent}%`
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { renderExerciseMeta, markExerciseSolved } from './exercise-shared.js'
|
|
2
|
+
import { sitePath } from '../site-path.js'
|
|
3
|
+
|
|
4
|
+
import { initExternalExercise } from './external-exercise.js'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async function loadWeekData(weekNum) {
|
|
9
|
+
|
|
10
|
+
return import(`../../data/exercises/week${weekNum}.json`).then((m) => m.default)
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function isExternalMode(weekData, exercise) {
|
|
17
|
+
|
|
18
|
+
return weekData?.mode === 'external' || exercise?.type === 'external'
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function showMissingContent(weekNum) {
|
|
25
|
+
|
|
26
|
+
const panel = document.querySelector('[data-exercise-content]')
|
|
27
|
+
|
|
28
|
+
panel?.querySelector('[data-exercise-interactive]')?.classList.add('hidden')
|
|
29
|
+
|
|
30
|
+
panel?.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
31
|
+
|
|
32
|
+
panel?.querySelector('[data-hint]')?.closest('.mb-4')?.classList.add('hidden')
|
|
33
|
+
|
|
34
|
+
panel?.insertAdjacentHTML(
|
|
35
|
+
|
|
36
|
+
'beforeend',
|
|
37
|
+
|
|
38
|
+
`<div class="card border-l-4 border-l-amber-500">
|
|
39
|
+
|
|
40
|
+
<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
|
+
|
|
48
|
+
</div>`
|
|
49
|
+
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export async function initExercisePage(weekNum) {
|
|
57
|
+
|
|
58
|
+
const params = new URLSearchParams(window.location.search)
|
|
59
|
+
|
|
60
|
+
const id = parseInt(params.get('id') || '1', 10)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
let weekData
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
|
|
68
|
+
weekData = await loadWeekData(weekNum)
|
|
69
|
+
|
|
70
|
+
} catch {
|
|
71
|
+
|
|
72
|
+
document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
|
|
73
|
+
|
|
74
|
+
'beforeend',
|
|
75
|
+
|
|
76
|
+
`<p class="text-red-600">Oefeningdata voor week ${weekNum} niet gevonden.</p>`
|
|
77
|
+
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
const exercise = weekData?.exercises?.find((e) => e.id === id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if (!exercise) {
|
|
91
|
+
|
|
92
|
+
document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
|
|
93
|
+
|
|
94
|
+
'beforeend',
|
|
95
|
+
|
|
96
|
+
'<p class="text-red-600">Oefening niet gevonden.</p>'
|
|
97
|
+
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if (!exercise.type || exercise.type === 'text') {
|
|
107
|
+
const panel = document.querySelector('[data-exercise-content]')
|
|
108
|
+
panel?.querySelector('[data-exercise-interactive]')?.classList.add('hidden')
|
|
109
|
+
panel?.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
110
|
+
panel?.querySelector('[data-exercise-actions]')?.classList.add('hidden')
|
|
111
|
+
|
|
112
|
+
const descEl = document.querySelector('[data-exercise-description]')
|
|
113
|
+
if (descEl) {
|
|
114
|
+
descEl.classList.remove('hidden')
|
|
115
|
+
descEl.innerHTML = exercise.descriptionHtml ?? ''
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
renderExerciseMeta(exercise)
|
|
119
|
+
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isExternalMode(weekData, exercise)) {
|
|
124
|
+
|
|
125
|
+
const missing = !exercise.task?.trim() && !exercise.description?.trim()
|
|
126
|
+
|
|
127
|
+
if (missing) {
|
|
128
|
+
|
|
129
|
+
showMissingContent(weekNum)
|
|
130
|
+
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
initExternalExercise(exercise, weekNum, {
|
|
136
|
+
|
|
137
|
+
onSolved: () => markExerciseSolved(weekNum, exercise.id),
|
|
138
|
+
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
142
|
+
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
renderExerciseMeta(exercise)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
const missingContent =
|
|
154
|
+
|
|
155
|
+
!exercise.starterCss?.trim() ||
|
|
156
|
+
|
|
157
|
+
!exercise.previewHtml?.trim() ||
|
|
158
|
+
|
|
159
|
+
(exercise.type !== 'areas' && !exercise.checks?.length)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if (missingContent) {
|
|
164
|
+
|
|
165
|
+
showMissingContent(weekNum)
|
|
166
|
+
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
const {
|
|
174
|
+
|
|
175
|
+
initCssPlayground,
|
|
176
|
+
|
|
177
|
+
initAreasExercise,
|
|
178
|
+
|
|
179
|
+
initResponsiveExercise,
|
|
180
|
+
|
|
181
|
+
} = await import('./exercise-runner.js')
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
document.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
186
|
+
|
|
187
|
+
document.querySelector('[data-exercise-interactive]')?.classList.remove('hidden')
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
const cssPanel = document.querySelector('[data-exercise-css-panel]')
|
|
192
|
+
|
|
193
|
+
const areasPanel = document.querySelector('[data-exercise-areas-panel]')
|
|
194
|
+
|
|
195
|
+
const responsiveBar = document.querySelector('[data-responsive-bar]')
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
const onSolved = () => markExerciseSolved(weekNum, exercise.id)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if (exercise.type === 'areas') {
|
|
204
|
+
|
|
205
|
+
cssPanel?.classList.add('hidden')
|
|
206
|
+
|
|
207
|
+
areasPanel?.classList.remove('hidden')
|
|
208
|
+
|
|
209
|
+
responsiveBar?.classList.add('hidden')
|
|
210
|
+
|
|
211
|
+
document.querySelector('[data-solution]')?.classList.add('hidden')
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
const containerInput = document.querySelector('[data-areas-container]')
|
|
216
|
+
|
|
217
|
+
if (containerInput) containerInput.value = exercise.starterAreas || ''
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
const selectsEl = document.querySelector('[data-area-selects]')
|
|
222
|
+
|
|
223
|
+
if (selectsEl) {
|
|
224
|
+
|
|
225
|
+
selectsEl.innerHTML = renderAreaSelects(exercise.areaItems, exercise.areaOptions)
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
initAreasExercise(exercise, { onSolved })
|
|
232
|
+
|
|
233
|
+
} else {
|
|
234
|
+
|
|
235
|
+
areasPanel?.classList.add('hidden')
|
|
236
|
+
|
|
237
|
+
cssPanel?.classList.remove('hidden')
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if (exercise.type === 'responsive') {
|
|
242
|
+
|
|
243
|
+
responsiveBar?.classList.remove('hidden')
|
|
244
|
+
|
|
245
|
+
initResponsiveExercise(exercise, { onSolved })
|
|
246
|
+
|
|
247
|
+
} else {
|
|
248
|
+
|
|
249
|
+
responsiveBar?.classList.add('hidden')
|
|
250
|
+
|
|
251
|
+
initCssPlayground(exercise, { onSolved })
|
|
252
|
+
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
260
|
+
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
function renderNavButtons(week, currentId, total) {
|
|
266
|
+
|
|
267
|
+
const prev = document.querySelector('[data-prev-exercise]')
|
|
268
|
+
|
|
269
|
+
const next = document.querySelector('[data-next-exercise]')
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if (prev) {
|
|
274
|
+
|
|
275
|
+
if (currentId > 1) {
|
|
276
|
+
|
|
277
|
+
prev.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId - 1}`
|
|
278
|
+
|
|
279
|
+
prev.classList.remove('hidden')
|
|
280
|
+
|
|
281
|
+
} else {
|
|
282
|
+
|
|
283
|
+
prev.classList.add('hidden')
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if (next) {
|
|
292
|
+
|
|
293
|
+
if (currentId < total) {
|
|
294
|
+
|
|
295
|
+
next.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId + 1}`
|
|
296
|
+
|
|
297
|
+
next.classList.remove('hidden')
|
|
298
|
+
|
|
299
|
+
} else {
|
|
300
|
+
|
|
301
|
+
next.classList.add('hidden')
|
|
302
|
+
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function normalizeCss(css) {
|
|
2
|
+
return css.toLowerCase().replace(/\s+/g, ' ').trim()
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} css
|
|
7
|
+
* @param {Array<{type: string, value?: string, values?: string[], pattern?: string, msg: string}>} checks
|
|
8
|
+
*/
|
|
9
|
+
export function runChecks(css, checks) {
|
|
10
|
+
const n = normalizeCss(css)
|
|
11
|
+
return checks.map((check) => {
|
|
12
|
+
let ok = false
|
|
13
|
+
switch (check.type) {
|
|
14
|
+
case 'includes':
|
|
15
|
+
ok = n.includes(check.value.toLowerCase())
|
|
16
|
+
break
|
|
17
|
+
case 'includesAll':
|
|
18
|
+
ok = check.values.every((v) => n.includes(v.toLowerCase()))
|
|
19
|
+
break
|
|
20
|
+
case 'includesAny':
|
|
21
|
+
ok = check.values.some((v) => n.includes(v.toLowerCase()))
|
|
22
|
+
break
|
|
23
|
+
case 'regex':
|
|
24
|
+
ok = new RegExp(check.pattern, 'i').test(css)
|
|
25
|
+
break
|
|
26
|
+
case 'mediaQuery':
|
|
27
|
+
ok = n.includes('@media') && check.values.every((v) => n.includes(v.toLowerCase()))
|
|
28
|
+
break
|
|
29
|
+
default:
|
|
30
|
+
ok = false
|
|
31
|
+
}
|
|
32
|
+
return { ok, msg: check.msg }
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateAreas(containerValue, selects, expected) {
|
|
37
|
+
const errors = []
|
|
38
|
+
const normalized = containerValue.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
39
|
+
const expectedNorm = expected.container.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
40
|
+
|
|
41
|
+
if (normalized !== expectedNorm) {
|
|
42
|
+
errors.push('grid-template-areas op de container klopt niet')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
selects.forEach((select) => {
|
|
46
|
+
const id = select.dataset.areaItem
|
|
47
|
+
if (select.value !== expected.items[id]) {
|
|
48
|
+
errors.push(`Item "${id.toUpperCase()}" heeft verkeerde grid-area`)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return errors
|
|
53
|
+
}
|
package/src/js/home.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import manifest from '../data/manifest.json'
|
|
2
|
+
import { sitePath } from './site-path.js'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function initHome() {
|
|
7
|
+
|
|
8
|
+
const mod = manifest.module
|
|
9
|
+
|
|
10
|
+
const contentStatus = manifest.content?.status || 'empty'
|
|
11
|
+
|
|
12
|
+
const isReady = contentStatus === 'generated'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const titleEl = document.querySelector('[data-home-title]')
|
|
17
|
+
|
|
18
|
+
const descEl = document.querySelector('[data-home-description]')
|
|
19
|
+
|
|
20
|
+
const labelEl = document.querySelector('[data-home-label]')
|
|
21
|
+
|
|
22
|
+
const setupEl = document.querySelector('[data-home-setup]')
|
|
23
|
+
|
|
24
|
+
const readyEl = document.querySelector('[data-home-ready]')
|
|
25
|
+
|
|
26
|
+
const curriculumEl = document.querySelector('[data-home-curriculum]')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if (labelEl) labelEl.textContent = mod.subtitle
|
|
31
|
+
|
|
32
|
+
if (titleEl) titleEl.textContent = mod.name
|
|
33
|
+
|
|
34
|
+
if (descEl) descEl.textContent = mod.description
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if (setupEl) setupEl.classList.toggle('hidden', isReady)
|
|
39
|
+
|
|
40
|
+
if (readyEl) readyEl.classList.toggle('hidden', !isReady)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if (!isReady || !curriculumEl) return
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
const weekLinks = (week) => week.pages
|
|
49
|
+
|
|
50
|
+
.filter((p) => p.key !== 'oefening')
|
|
51
|
+
|
|
52
|
+
.map((p) => `<a href="${sitePath(p.href)}" class="text-link">${p.label}</a>`)
|
|
53
|
+
|
|
54
|
+
.join('\n ')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
curriculumEl.innerHTML = manifest.weeks
|
|
59
|
+
|
|
60
|
+
.map(
|
|
61
|
+
|
|
62
|
+
(w) => `
|
|
63
|
+
|
|
64
|
+
<div class="card-interactive bg-white">
|
|
65
|
+
|
|
66
|
+
<span class="week-label">Week ${String(w.week).padStart(2, '0')}</span>
|
|
67
|
+
|
|
68
|
+
<h3 class="text-lg font-medium text-zinc-900">${w.title}</h3>
|
|
69
|
+
|
|
70
|
+
<p class="mt-2 text-sm leading-relaxed text-zinc-500">${w.summary}</p>
|
|
71
|
+
|
|
72
|
+
<div class="mt-6 flex flex-wrap gap-5">
|
|
73
|
+
|
|
74
|
+
${weekLinks(w)}
|
|
75
|
+
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
</div>`
|
|
79
|
+
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
.join('\n')
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
package/src/js/layout.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import '../css/main.css'
|
|
2
|
+
import { sitePath } from './site-path.js'
|
|
3
|
+
|
|
4
|
+
const MENU_SVG = `<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16" /></svg>`
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wraps [data-page-content] in the shared app shell (sidebar + topbar).
|
|
8
|
+
* Pages only need their unique <main> content inside data-page-content.
|
|
9
|
+
*/
|
|
10
|
+
export function mountLayout() {
|
|
11
|
+
const icon = document.querySelector('link[rel="icon"]')
|
|
12
|
+
if (icon) icon.href = sitePath('/favicon.svg')
|
|
13
|
+
|
|
14
|
+
const contentEl = document.querySelector('[data-page-content]')
|
|
15
|
+
if (!contentEl || document.querySelector('[data-app-shell]')) return
|
|
16
|
+
|
|
17
|
+
const content = contentEl.innerHTML.trim()
|
|
18
|
+
contentEl.remove()
|
|
19
|
+
|
|
20
|
+
const shell = document.createElement('div')
|
|
21
|
+
shell.className = 'app-shell'
|
|
22
|
+
shell.dataset.appShell = ''
|
|
23
|
+
shell.innerHTML = `
|
|
24
|
+
<div data-nav-overlay class="nav-overlay"></div>
|
|
25
|
+
<aside data-sidebar class="sidebar-panel">
|
|
26
|
+
<nav data-module-nav></nav>
|
|
27
|
+
</aside>
|
|
28
|
+
<div class="md:pl-60">
|
|
29
|
+
<header class="topbar">
|
|
30
|
+
<div class="flex items-center gap-4 px-4 py-3 md:px-8">
|
|
31
|
+
<button data-nav-toggle type="button" class="p-2 text-zinc-600 transition hover:text-zinc-900 md:hidden" aria-label="Menu openen">
|
|
32
|
+
${MENU_SVG}
|
|
33
|
+
</button>
|
|
34
|
+
<div data-breadcrumbs></div>
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
${content}
|
|
38
|
+
</div>
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
document.body.insertBefore(shell, document.body.firstChild)
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as monaco from 'monaco-editor'
|
|
2
|
+
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
|
3
|
+
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
|
|
4
|
+
|
|
5
|
+
self.MonacoEnvironment = {
|
|
6
|
+
getWorker(_, label) {
|
|
7
|
+
if (label === 'css' || label === 'scss' || label === 'less') {
|
|
8
|
+
return new cssWorker()
|
|
9
|
+
}
|
|
10
|
+
return new editorWorker()
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {HTMLElement} container
|
|
16
|
+
* @param {string} initialValue
|
|
17
|
+
* @param {(value: string) => void} [onChange]
|
|
18
|
+
*/
|
|
19
|
+
export function createCssEditor(container, initialValue = '', onChange) {
|
|
20
|
+
const editor = monaco.editor.create(container, {
|
|
21
|
+
value: initialValue,
|
|
22
|
+
language: 'css',
|
|
23
|
+
theme: 'vs-dark',
|
|
24
|
+
fontSize: 14,
|
|
25
|
+
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
|
26
|
+
minimap: { enabled: false },
|
|
27
|
+
lineNumbers: 'on',
|
|
28
|
+
scrollBeyondLastLine: false,
|
|
29
|
+
automaticLayout: true,
|
|
30
|
+
tabSize: 2,
|
|
31
|
+
wordWrap: 'on',
|
|
32
|
+
padding: { top: 12 },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (onChange) {
|
|
36
|
+
editor.onDidChangeModelContent(() => onChange(editor.getValue()))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return editor
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setEditorValue(editor, value) {
|
|
43
|
+
if (!editor) return
|
|
44
|
+
editor.setValue(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getEditorValue(editor) {
|
|
48
|
+
return editor?.getValue() ?? ''
|
|
49
|
+
}
|