@morphemecms/cms 0.2.0 → 0.3.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/CHANGELOG.md +6 -0
- package/defaults/collections.ts +11 -0
- package/defaults/editor.ts +2 -0
- package/defaults/standardPages.ts +3 -17
- package/defaults/texts.ts +3 -0
- package/icons/lucide/calendar-x.svg +1 -0
- package/icons/lucide/identification-card.svg +1 -0
- package/icons/lucide/money.svg +1 -0
- package/icons/lucide/receipt.svg +1 -0
- package/icons/lucide/wallet.svg +1 -0
- package/icons/material-symbols/calendar-x.svg +1 -0
- package/icons/material-symbols/identification-card.svg +1 -0
- package/icons/material-symbols/money.svg +1 -0
- package/icons/material-symbols/receipt.svg +1 -0
- package/icons/material-symbols/wallet.svg +1 -0
- package/icons/phosphor/calendar-x.svg +1 -0
- package/icons/phosphor/identification-card.svg +1 -0
- package/icons/phosphor/money.svg +1 -0
- package/icons/phosphor/receipt.svg +1 -0
- package/icons/phosphor/wallet.svg +1 -0
- package/lib/side-block.d.mts +45 -0
- package/lib/side-block.mjs +75 -0
- package/lib/side-block.test.d.mts +1 -0
- package/lib/side-block.test.mjs +79 -0
- package/package.json +1 -1
- package/scripts/check-side.mjs +56 -0
- package/scripts/check-themes.mjs +15 -1
- package/scripts/editor-roundtrip.mjs +13 -2
- package/scripts/fixtures/side/block/View.tsx +9 -0
- package/scripts/fixtures/side/block/load.ts +8 -0
- package/scripts/fixtures/side/side-off.md +8 -0
- package/scripts/fixtures/side/side-plain.md +7 -0
- package/scripts/fixtures/side/side-wide.md +8 -0
- package/scripts/fixtures/side/side.json +3 -0
- package/scripts/icon-packs.mjs +3 -3
- package/site/content.config.ts +4 -0
- package/site/defineSite.d.mts +7 -1
- package/site/defineSite.mjs +7 -1
- package/site/icons.d.mts +21 -0
- package/site/icons.mjs +16 -1
- package/site/schema.ts +1 -1
- package/site/side.ts +10 -0
- package/site/site.ts +2 -0
- package/src/App.tsx +3 -1
- package/src/EntryForm.tsx +17 -6
- package/src/Library.tsx +9 -2
- package/src/SideColumn.tsx +83 -0
- package/src/frontmatter.d.mts +2 -0
- package/src/frontmatter.mjs +6 -0
- package/src/frontmatter.test.d.mts +1 -0
- package/src/frontmatter.test.mjs +27 -0
- package/src/i18n/da-DK/app.ts +1 -0
- package/src/i18n/da-DK/common.ts +1 -0
- package/src/i18n/da-DK/library.ts +9 -0
- package/src/i18n/en-US/app.ts +1 -0
- package/src/i18n/en-US/common.ts +2 -0
- package/src/i18n/en-US/library.ts +12 -0
- package/src/schema.ts +2 -0
- package/src/standard/Block.astro +7 -2
- package/src/standard/BlockSite.astro +2 -2
- package/src/standard/accordion/lead.ts +4 -0
- package/src/standard/cards/lead.ts +4 -0
- package/src/standard/frontLeads.ts +12 -0
- package/src/standard/hero/index.tsx +1 -1
- package/src/standard/hero/lead.ts +4 -0
- package/src/standard/leads.d.mts +8 -0
- package/src/standard/leads.mjs +14 -0
- package/src/standard/leads.test.d.mts +1 -0
- package/src/standard/leads.test.mjs +27 -0
- package/src/standard/posts/lead.ts +4 -0
- package/src/standard/steps/lead.ts +4 -0
- package/src/standard/testimonials/lead.ts +4 -0
- package/src/standard/view.tsx +5 -2
- package/src/standard/views.ts +8 -0
- package/theme/layouts/PageLayout.astro +19 -5
- package/theme/styles/components.css +23 -0
package/site/defineSite.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* What a site can say of itself, else the standard: the owner's picture (beside a blog post, and shown when a page with
|
|
22
22
|
* none of its own is shared), the booking page and the pages a booking ends on, the page about the owner, the block
|
|
23
23
|
* drawn after every blog post, the word a page's title can end in, pictures that are the same though their names say
|
|
24
|
-
* otherwise,
|
|
24
|
+
* otherwise, the file of its redirects from old addresses, and the logo in the structured data.
|
|
25
25
|
* @typedef {object} SiteChoices
|
|
26
26
|
* @property {{ src: string; alt: string } | null} picture The owner's picture, or none.
|
|
27
27
|
* @property {{ path: string; thanks: string[] }} booking The booking page, where "Book en tid" goes when no booking sheet
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
* @property {{ rules: string; map: string }} siteRedirects The site's other redirects: rules ("<from> <to> 301", one to a
|
|
37
37
|
* line) and the Worker's map (`{ ids, paths }`, worker/index.js), shown and changed with the editor's own under
|
|
38
38
|
* Videresendelser (src/redirectData.ts).
|
|
39
|
+
* @property {string} logo The address of the site's logo in the structured data (the business's `logo`,
|
|
40
|
+
* site/schema.ts), a path on the site ("/logo.svg").
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
/** @type {SiteChoices} */
|
|
@@ -47,6 +49,7 @@ const CHOICES = {
|
|
|
47
49
|
titleEnd: '',
|
|
48
50
|
samePictures: {},
|
|
49
51
|
siteRedirects: { rules: 'public/_redirects', map: 'src/data/old-addresses.json' },
|
|
52
|
+
logo: '/logo.svg',
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
/** The same on every site. */
|
|
@@ -81,6 +84,9 @@ const STANDARD = {
|
|
|
81
84
|
/** How the site moves, `{ "scheme": "expressive" | "standard" | "calm" }`, under "Udseende" › "Bevægelse" (Motion in
|
|
82
85
|
* src/Motion.tsx; lib/motion.ts, site/motion.ts). Without it, the theme's ("motion" in its theme.json). */
|
|
83
86
|
motion: 'src/data/motion.json',
|
|
87
|
+
/** The block drawn in a column beside the text of ordinary pages, `{ "block": "<type>" | null }`, under "Udseende" ›
|
|
88
|
+
* "Sidekolonne" (SideColumn in src/SideColumn.tsx; site/side.ts). Without it, no page has a column. */
|
|
89
|
+
side: 'src/data/side.json',
|
|
84
90
|
/** The folder of the themes installed, one for each (<folder>/) (site/theme.mjs); one from the store (src/store.ts) is
|
|
85
91
|
* copied into it and deleted from it. */
|
|
86
92
|
themes: 'src/themes',
|
package/site/icons.d.mts
CHANGED
|
@@ -105,6 +105,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
105
105
|
bus: string;
|
|
106
106
|
'calendar-blank': string;
|
|
107
107
|
'calendar-check': string;
|
|
108
|
+
'calendar-x': string;
|
|
108
109
|
car: string;
|
|
109
110
|
cards: string;
|
|
110
111
|
'caret-down': string;
|
|
@@ -152,6 +153,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
152
153
|
handshake: string;
|
|
153
154
|
heart: string;
|
|
154
155
|
house: string;
|
|
156
|
+
'identification-card': string;
|
|
155
157
|
image: string;
|
|
156
158
|
'image-square': string;
|
|
157
159
|
info: string;
|
|
@@ -173,6 +175,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
173
175
|
medal: string;
|
|
174
176
|
megaphone: string;
|
|
175
177
|
minus: string;
|
|
178
|
+
money: string;
|
|
176
179
|
moon: string;
|
|
177
180
|
newspaper: string;
|
|
178
181
|
'paint-brush-broad': string;
|
|
@@ -186,6 +189,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
186
189
|
'puzzle-piece': string;
|
|
187
190
|
question: string;
|
|
188
191
|
quotes: string;
|
|
192
|
+
receipt: string;
|
|
189
193
|
rows: string;
|
|
190
194
|
ruler: string;
|
|
191
195
|
shapes: string;
|
|
@@ -222,6 +226,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
222
226
|
users: string;
|
|
223
227
|
'users-three': string;
|
|
224
228
|
'video-camera': string;
|
|
229
|
+
wallet: string;
|
|
225
230
|
warning: string;
|
|
226
231
|
'warning-circle': string;
|
|
227
232
|
'wave-sine': string;
|
|
@@ -258,6 +263,7 @@ export const LUCIDE: {
|
|
|
258
263
|
bus: string;
|
|
259
264
|
'calendar-blank': string;
|
|
260
265
|
'calendar-check': string;
|
|
266
|
+
'calendar-x': string;
|
|
261
267
|
car: string;
|
|
262
268
|
cards: string;
|
|
263
269
|
'caret-down': string;
|
|
@@ -306,6 +312,7 @@ export const LUCIDE: {
|
|
|
306
312
|
'hard-drives': string;
|
|
307
313
|
heart: string;
|
|
308
314
|
house: string;
|
|
315
|
+
'identification-card': string;
|
|
309
316
|
image: string;
|
|
310
317
|
'image-square': string;
|
|
311
318
|
info: string;
|
|
@@ -327,6 +334,7 @@ export const LUCIDE: {
|
|
|
327
334
|
medal: string;
|
|
328
335
|
megaphone: string;
|
|
329
336
|
minus: string;
|
|
337
|
+
money: string;
|
|
330
338
|
moon: string;
|
|
331
339
|
newspaper: string;
|
|
332
340
|
'paint-brush-broad': string;
|
|
@@ -340,6 +348,7 @@ export const LUCIDE: {
|
|
|
340
348
|
'puzzle-piece': string;
|
|
341
349
|
question: string;
|
|
342
350
|
quotes: string;
|
|
351
|
+
receipt: string;
|
|
343
352
|
rows: string;
|
|
344
353
|
ruler: string;
|
|
345
354
|
scan: string;
|
|
@@ -378,6 +387,7 @@ export const LUCIDE: {
|
|
|
378
387
|
users: string;
|
|
379
388
|
'users-three': string;
|
|
380
389
|
'video-camera': string;
|
|
390
|
+
wallet: string;
|
|
381
391
|
warning: string;
|
|
382
392
|
'warning-circle': string;
|
|
383
393
|
'wave-sine': string;
|
|
@@ -411,6 +421,7 @@ export const PACK_MAPS: {
|
|
|
411
421
|
bus: string;
|
|
412
422
|
'calendar-blank': string;
|
|
413
423
|
'calendar-check': string;
|
|
424
|
+
'calendar-x': string;
|
|
414
425
|
car: string;
|
|
415
426
|
cards: string;
|
|
416
427
|
'caret-down': string;
|
|
@@ -458,6 +469,7 @@ export const PACK_MAPS: {
|
|
|
458
469
|
handshake: string;
|
|
459
470
|
heart: string;
|
|
460
471
|
house: string;
|
|
472
|
+
'identification-card': string;
|
|
461
473
|
image: string;
|
|
462
474
|
'image-square': string;
|
|
463
475
|
info: string;
|
|
@@ -479,6 +491,7 @@ export const PACK_MAPS: {
|
|
|
479
491
|
medal: string;
|
|
480
492
|
megaphone: string;
|
|
481
493
|
minus: string;
|
|
494
|
+
money: string;
|
|
482
495
|
moon: string;
|
|
483
496
|
newspaper: string;
|
|
484
497
|
'paint-brush-broad': string;
|
|
@@ -492,6 +505,7 @@ export const PACK_MAPS: {
|
|
|
492
505
|
'puzzle-piece': string;
|
|
493
506
|
question: string;
|
|
494
507
|
quotes: string;
|
|
508
|
+
receipt: string;
|
|
495
509
|
rows: string;
|
|
496
510
|
ruler: string;
|
|
497
511
|
shapes: string;
|
|
@@ -528,6 +542,7 @@ export const PACK_MAPS: {
|
|
|
528
542
|
users: string;
|
|
529
543
|
'users-three': string;
|
|
530
544
|
'video-camera': string;
|
|
545
|
+
wallet: string;
|
|
531
546
|
warning: string;
|
|
532
547
|
'warning-circle': string;
|
|
533
548
|
'wave-sine': string;
|
|
@@ -562,6 +577,7 @@ export const PACK_MAPS: {
|
|
|
562
577
|
bus: string;
|
|
563
578
|
'calendar-blank': string;
|
|
564
579
|
'calendar-check': string;
|
|
580
|
+
'calendar-x': string;
|
|
565
581
|
car: string;
|
|
566
582
|
cards: string;
|
|
567
583
|
'caret-down': string;
|
|
@@ -610,6 +626,7 @@ export const PACK_MAPS: {
|
|
|
610
626
|
'hard-drives': string;
|
|
611
627
|
heart: string;
|
|
612
628
|
house: string;
|
|
629
|
+
'identification-card': string;
|
|
613
630
|
image: string;
|
|
614
631
|
'image-square': string;
|
|
615
632
|
info: string;
|
|
@@ -631,6 +648,7 @@ export const PACK_MAPS: {
|
|
|
631
648
|
medal: string;
|
|
632
649
|
megaphone: string;
|
|
633
650
|
minus: string;
|
|
651
|
+
money: string;
|
|
634
652
|
moon: string;
|
|
635
653
|
newspaper: string;
|
|
636
654
|
'paint-brush-broad': string;
|
|
@@ -644,6 +662,7 @@ export const PACK_MAPS: {
|
|
|
644
662
|
'puzzle-piece': string;
|
|
645
663
|
question: string;
|
|
646
664
|
quotes: string;
|
|
665
|
+
receipt: string;
|
|
647
666
|
rows: string;
|
|
648
667
|
ruler: string;
|
|
649
668
|
scan: string;
|
|
@@ -682,6 +701,7 @@ export const PACK_MAPS: {
|
|
|
682
701
|
users: string;
|
|
683
702
|
'users-three': string;
|
|
684
703
|
'video-camera': string;
|
|
704
|
+
wallet: string;
|
|
685
705
|
warning: string;
|
|
686
706
|
'warning-circle': string;
|
|
687
707
|
'wave-sine': string;
|
|
@@ -690,4 +710,5 @@ export const PACK_MAPS: {
|
|
|
690
710
|
x: string;
|
|
691
711
|
};
|
|
692
712
|
};
|
|
713
|
+
export function phosphorNames(used: any): any[];
|
|
693
714
|
export function isIconName(name: any): any;
|
package/site/icons.mjs
CHANGED
|
@@ -118,6 +118,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
118
118
|
bus: 'directions_bus',
|
|
119
119
|
'calendar-blank': 'calendar_today',
|
|
120
120
|
'calendar-check': 'event_available',
|
|
121
|
+
'calendar-x': 'event_busy',
|
|
121
122
|
car: 'directions_car',
|
|
122
123
|
cards: 'cards_stack',
|
|
123
124
|
'caret-down': 'keyboard_arrow_down',
|
|
@@ -165,6 +166,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
165
166
|
handshake: 'handshake',
|
|
166
167
|
heart: 'favorite',
|
|
167
168
|
house: 'home',
|
|
169
|
+
'identification-card': 'id_card',
|
|
168
170
|
image: 'image',
|
|
169
171
|
'image-square': 'image',
|
|
170
172
|
info: 'info',
|
|
@@ -186,6 +188,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
186
188
|
medal: 'military_tech',
|
|
187
189
|
megaphone: 'campaign',
|
|
188
190
|
minus: 'remove',
|
|
191
|
+
money: 'payments',
|
|
189
192
|
moon: 'dark_mode',
|
|
190
193
|
newspaper: 'newspaper',
|
|
191
194
|
'paint-brush-broad': 'brush',
|
|
@@ -199,6 +202,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
199
202
|
'puzzle-piece': 'extension',
|
|
200
203
|
question: 'help',
|
|
201
204
|
quotes: 'format_quote',
|
|
205
|
+
receipt: 'receipt',
|
|
202
206
|
rows: 'view_agenda',
|
|
203
207
|
ruler: 'straighten',
|
|
204
208
|
shapes: 'shapes',
|
|
@@ -235,6 +239,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
235
239
|
users: 'group',
|
|
236
240
|
'users-three': 'groups',
|
|
237
241
|
'video-camera': 'videocam',
|
|
242
|
+
wallet: 'wallet',
|
|
238
243
|
warning: 'warning',
|
|
239
244
|
'warning-circle': 'error',
|
|
240
245
|
'wave-sine': 'animation',
|
|
@@ -272,6 +277,7 @@ export const LUCIDE = {
|
|
|
272
277
|
bus: 'bus',
|
|
273
278
|
'calendar-blank': 'calendar',
|
|
274
279
|
'calendar-check': 'calendar-check',
|
|
280
|
+
'calendar-x': 'calendar-x',
|
|
275
281
|
car: 'car',
|
|
276
282
|
cards: 'gallery-vertical-end',
|
|
277
283
|
'caret-down': 'chevron-down',
|
|
@@ -320,6 +326,7 @@ export const LUCIDE = {
|
|
|
320
326
|
'hard-drives': 'server',
|
|
321
327
|
heart: 'heart',
|
|
322
328
|
house: 'house',
|
|
329
|
+
'identification-card': 'id-card',
|
|
323
330
|
image: 'image',
|
|
324
331
|
'image-square': 'image',
|
|
325
332
|
info: 'info',
|
|
@@ -341,6 +348,7 @@ export const LUCIDE = {
|
|
|
341
348
|
medal: 'medal',
|
|
342
349
|
megaphone: 'megaphone',
|
|
343
350
|
minus: 'minus',
|
|
351
|
+
money: 'banknote',
|
|
344
352
|
moon: 'moon',
|
|
345
353
|
newspaper: 'newspaper',
|
|
346
354
|
'paint-brush-broad': 'paintbrush',
|
|
@@ -354,6 +362,7 @@ export const LUCIDE = {
|
|
|
354
362
|
'puzzle-piece': 'puzzle',
|
|
355
363
|
question: 'circle-question-mark',
|
|
356
364
|
quotes: 'quote',
|
|
365
|
+
receipt: 'receipt',
|
|
357
366
|
rows: 'rows-2',
|
|
358
367
|
ruler: 'ruler',
|
|
359
368
|
scan: 'scan',
|
|
@@ -392,6 +401,7 @@ export const LUCIDE = {
|
|
|
392
401
|
users: 'users',
|
|
393
402
|
'users-three': 'users-round',
|
|
394
403
|
'video-camera': 'video',
|
|
404
|
+
wallet: 'wallet',
|
|
395
405
|
warning: 'triangle-alert',
|
|
396
406
|
'warning-circle': 'circle-alert',
|
|
397
407
|
'wave-sine': 'spline',
|
|
@@ -403,6 +413,11 @@ export const LUCIDE = {
|
|
|
403
413
|
/** Each system pack's map: Phosphor's name → its set's icon (Phosphor's: its own names, no map). */
|
|
404
414
|
export const PACK_MAPS = { 'material-symbols': MATERIAL_SYMBOLS, lucide: LUCIDE };
|
|
405
415
|
|
|
416
|
+
/** The names of Phosphor's pack: the icons the site and the editor draw (`used`), and every name the other system
|
|
417
|
+
* packs' maps name (an app's icon, which the CMS itself may not draw), sorted. */
|
|
418
|
+
export const phosphorNames = (used) =>
|
|
419
|
+
[...new Set([...used, ...Object.values(PACK_MAPS).flatMap(Object.keys), ...Object.values(MATERIAL_DESIGN_ICONS).flatMap(Object.keys)])].sort();
|
|
420
|
+
|
|
406
421
|
/** The names the system's pack has an icon by in its maps (its set's and Material Design Icons'); all for Phosphor's. */
|
|
407
422
|
const mapped = (pack) => (PACK_MAPS[pack] ? [...Object.keys(PACK_MAPS[pack]), ...Object.keys(MATERIAL_DESIGN_ICONS[pack] ?? {})] : null);
|
|
408
423
|
|
|
@@ -553,7 +568,7 @@ function systemPackProblems(id, used) {
|
|
|
553
568
|
);
|
|
554
569
|
}
|
|
555
570
|
const written = Object.keys(packIcons(id));
|
|
556
|
-
const wanted = names ??
|
|
571
|
+
const wanted = names ?? phosphorNames(used);
|
|
557
572
|
const unwritten = wanted.filter((name) => !written.includes(name)).sort();
|
|
558
573
|
const stray = written.filter((name) => !wanted.includes(name)).sort();
|
|
559
574
|
if (unwritten.length) problems.push(`${dir}/ mangler ${unwritten.join(', ')}: kør npm run icons.`);
|
package/site/schema.ts
CHANGED
|
@@ -45,7 +45,7 @@ export const business = {
|
|
|
45
45
|
description: site.description,
|
|
46
46
|
url: abs('/'),
|
|
47
47
|
...(site.portrait ? { image: abs(site.portrait) } : {}),
|
|
48
|
-
logo: abs(
|
|
48
|
+
logo: abs(site.logo),
|
|
49
49
|
telephone: site.phoneIntl,
|
|
50
50
|
email: site.email,
|
|
51
51
|
...(site.cvr ? { taxID: site.cvr } : {}),
|
package/site/side.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The site's choice of the block drawn beside the text of its pages: src/data/side.json, `{ "block": "<type>" | null }`.
|
|
2
|
+
// Optional: a site without the file has no column. Exposes sideSetting; read by sideBlock in src/standard/views.ts.
|
|
3
|
+
const found = import.meta.glob<{ block?: unknown }>('/src/data/side.json', { eager: true, import: 'default' });
|
|
4
|
+
|
|
5
|
+
/** The contents of src/data/side.json, or null when the site has none. */
|
|
6
|
+
export const sideSetting: { block: string | null } | null = (() => {
|
|
7
|
+
const file = found['/src/data/side.json'];
|
|
8
|
+
if (!file) return null;
|
|
9
|
+
return { block: typeof file.block === 'string' && file.block ? file.block : null };
|
|
10
|
+
})();
|
package/site/site.ts
CHANGED
|
@@ -52,6 +52,8 @@ export const site = {
|
|
|
52
52
|
/** The owner's picture (config.ts), or '' when the site has none. */
|
|
53
53
|
portrait: config.picture?.src ?? '',
|
|
54
54
|
portraitAlt: config.picture?.alt ?? '',
|
|
55
|
+
/** The logo in the structured data (config.ts), '/logo.svg' unless the site says otherwise. */
|
|
56
|
+
logo: config.logo || '/logo.svg',
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
// Primary navigation (menu.json, "Hovedmenu"). Labels, URLs and sub-items started out as the old site's menu.
|
package/src/App.tsx
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// post (SeoScores.tsx); and seo/<part> the site's own (seoParts)
|
|
22
22
|
// udseende how the site looks (Library.tsx Theme): udseende/temaer the themes installed (Themes) and udseende/temaer/<theme>
|
|
23
23
|
// a theme's page, with the button that swaps to it (ThemePage), udseende/ikonpakker the icon packs (IconPacks),
|
|
24
|
-
// udseende/bevaegelse how it moves (MotionSchemes),
|
|
24
|
+
// udseende/bevaegelse how it moves (MotionSchemes), udseende/sidekolonne the block beside the pages' text (SideColumn),
|
|
25
25
|
// udseende/farver the colours (Colors.tsx;
|
|
26
26
|
// udseende/farver/reservefarver the site's own while an app changes the pages'), udseende/logo the logo
|
|
27
27
|
// and icon (BrandForm.tsx), udseende/dele the parts (Library.tsx PartsHome): udseende/dele/editor the editor's, every
|
|
@@ -126,6 +126,7 @@ import {
|
|
|
126
126
|
} from './store';
|
|
127
127
|
import Colors, { colorsOf, FALLBACK_ROUTE } from './Colors';
|
|
128
128
|
import { applyColors, applyMotion } from './theme';
|
|
129
|
+
import SideColumn from './SideColumn';
|
|
129
130
|
import { apps, notOffered } from './registry';
|
|
130
131
|
import {
|
|
131
132
|
Account,
|
|
@@ -829,6 +830,7 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
829
830
|
if (route === 'udseende/temaer') return <Themes env={env} files={files} />;
|
|
830
831
|
if (route === 'udseende/ikonpakker') return <IconPacks env={env} files={files} />;
|
|
831
832
|
if (route === 'udseende/bevaegelse') return <MotionSchemes env={env} files={files} />;
|
|
833
|
+
if (route === 'udseende/sidekolonne') return <SideColumn env={env} files={files} />;
|
|
832
834
|
// A theme installed has its page; one from the store that is not (or not yet built), its page in the store.
|
|
833
835
|
const themeShown = route.startsWith('udseende/temaer/') && themes.find((x) => x.id === route.slice(16));
|
|
834
836
|
if (themeShown) return <ThemePage key={themeShown.id} of={themeShown} env={env} files={files} />;
|
package/src/EntryForm.tsx
CHANGED
|
@@ -14,7 +14,9 @@ import { addressOf, moveRedirects, REDIRECTS, retarget } from './addresses';
|
|
|
14
14
|
import { changesOf, flatten, readAll, REDIRECT_FILES, setRedirect } from './redirectData';
|
|
15
15
|
import { TAB_ROUTES } from './Redirects';
|
|
16
16
|
import { choose } from './ask';
|
|
17
|
-
import {
|
|
17
|
+
import { sideSettingOf } from './SideColumn';
|
|
18
|
+
import { sideSwitchShown } from '../lib/side-block.mjs';
|
|
19
|
+
import { booleanRead, booleanWrite, readFields, splitFrontmatter, writeFields } from './frontmatter.mjs';
|
|
18
20
|
import MarkdownEditor, { type MarkdownHandle } from './MarkdownEditor';
|
|
19
21
|
import { useBlockDialog } from './BlockDialog';
|
|
20
22
|
import { blockHtml } from '../lib/blocks.mjs';
|
|
@@ -115,7 +117,7 @@ function valuesOf(collection: MarkdownCollection, file: RepoFile): Record<string
|
|
|
115
117
|
collection.fields.map((f) => {
|
|
116
118
|
const v = data[f.name];
|
|
117
119
|
if (f.name === 'path') return [f.name, addressNow(collection, file, data)];
|
|
118
|
-
if (f.type === 'boolean') return [f.name, v
|
|
120
|
+
if (f.type === 'boolean') return [f.name, booleanRead(v, f.missing)];
|
|
119
121
|
if (f.multiple) return [f.name, Array.isArray(v) ? v.map(String) : []];
|
|
120
122
|
return [f.name, v == null ? '' : String(v)];
|
|
121
123
|
}),
|
|
@@ -128,9 +130,12 @@ const scrolling = (): ScrollBehavior => (matchMedia('(prefers-reduced-motion: re
|
|
|
128
130
|
const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
|
|
129
131
|
|
|
130
132
|
// The values as they are saved: a field only in use with another's value (Field.only: "Tidspunkt" with
|
|
131
|
-
// "Planlagt") is empty without it, so it leaves the file.
|
|
133
|
+
// "Planlagt") is empty without it, so it leaves the file; a switch that is on when its key is missing (Field.missing,
|
|
134
|
+
// "Sidekolonne") leaves the file when it is on (booleanWrite).
|
|
132
135
|
const inForm = (fields: Field[], values: Record<string, unknown>) =>
|
|
133
|
-
Object.fromEntries(
|
|
136
|
+
Object.fromEntries(
|
|
137
|
+
fields.map((f) => [f.name, inUse(f, fields, values) ? (f.type === 'boolean' ? booleanWrite(values[f.name], f.missing) : values[f.name]) : '']),
|
|
138
|
+
);
|
|
134
139
|
|
|
135
140
|
type Props = { collection: MarkdownCollection; file: RepoFile; env: Env };
|
|
136
141
|
|
|
@@ -593,7 +598,11 @@ function Form({
|
|
|
593
598
|
const layout = collection.fields.filter((f) => f.layout);
|
|
594
599
|
const relaid = !isNew && changed.some((f) => f.layout);
|
|
595
600
|
const sameLayout = (data: Record<string, unknown>) =>
|
|
596
|
-
layout.every((f) =>
|
|
601
|
+
layout.every((f) =>
|
|
602
|
+
f.type === 'boolean'
|
|
603
|
+
? booleanRead(data[f.name], f.missing) === (values[f.name] === true)
|
|
604
|
+
: String(data[f.name] || '') === String(values[f.name] || ''),
|
|
605
|
+
);
|
|
597
606
|
const changes = (...names: string[]) => isNew || relaid || changed.some((f) => names.includes(f.name));
|
|
598
607
|
const others =
|
|
599
608
|
isNew || relaid
|
|
@@ -630,8 +639,10 @@ function Form({
|
|
|
630
639
|
set('date', String(v).slice(0, 10));
|
|
631
640
|
};
|
|
632
641
|
// The fields in use (Field.only).
|
|
642
|
+
// "Sidekolonne" only while src/data/side.json chooses a block and the page is not "Hele bredden" (sideSwitchShown).
|
|
643
|
+
const sideShown = sideSwitchShown({ block: sideSettingOf(env.files) }, values);
|
|
633
644
|
const field = (f: Field) =>
|
|
634
|
-
inUse(f, collection.fields, values) && <FieldInput key={f.name} field={f} value={values[f.name]} onChange={(v) => onChange(f, v)} />;
|
|
645
|
+
inUse(f, collection.fields, values) && (f.name !== 'side' || sideShown) && <FieldInput key={f.name} field={f} value={values[f.name]} onChange={(v) => onChange(f, v)} />;
|
|
635
646
|
|
|
636
647
|
return (
|
|
637
648
|
<Fields.Provider value={fields}>
|
package/src/Library.tsx
CHANGED
|
@@ -113,6 +113,7 @@ import { colorsOf } from './Colors';
|
|
|
113
113
|
import { OwnerLicenses } from './Licenses';
|
|
114
114
|
import { apps } from './site';
|
|
115
115
|
import { offBlocks } from './registry';
|
|
116
|
+
import { sideChoicesOf, sideSettingOf } from './SideColumn';
|
|
116
117
|
|
|
117
118
|
// Where a part opens: its list, or the file itself when it is one file (the menu).
|
|
118
119
|
const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
|
|
@@ -208,8 +209,8 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
|
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
// "Udseende": first "Temaer" (Themes), with the theme the site is built with at the end of its row (or, after a swap,
|
|
211
|
-
// until the site is built with the new one, that one), "Ikonpakker" (IconPacks), with the pack chosen,
|
|
212
|
-
// (MotionSchemes), with the scheme chosen; then how the
|
|
212
|
+
// until the site is built with the new one, that one), "Ikonpakker" (IconPacks), with the pack chosen, "Bevægelse"
|
|
213
|
+
// (MotionSchemes), with the scheme chosen, and "Sidekolonne" (SideColumn.tsx), with the block chosen; then how the
|
|
213
214
|
// site looks, the colours (Colors.tsx), the logo and
|
|
214
215
|
// icon (BrandForm.tsx) and the site's own parts there (appearance in packages/editor/defaults/editor.ts: "Standardsider", a screen of its
|
|
215
216
|
// own, CompanyForm.tsx, going back here), and how the editor and the blocks look, their parts ("Dele", PartsHome), each on a
|
|
@@ -226,6 +227,12 @@ export function Theme({ files }: { files: RepoFile[] }) {
|
|
|
226
227
|
detail: iconPacks.find((x) => x.id === chosenPack(files))?.name,
|
|
227
228
|
},
|
|
228
229
|
{ id: 'bevaegelse', label: t.common.parts.motion, icon: icons.bevaegelse, detail: t.library.motionSchemes[motionOf(files)] },
|
|
230
|
+
{
|
|
231
|
+
id: 'sidekolonne',
|
|
232
|
+
label: t.common.parts.sideColumn,
|
|
233
|
+
icon: icons.sidekolonne,
|
|
234
|
+
detail: sideChoicesOf(files).find((x) => x.type === sideSettingOf(files))?.label ?? t.library.sideNone,
|
|
235
|
+
},
|
|
229
236
|
{ id: 'farver', label: t.common.parts.colors, icon: icons.farver, trailing: <ThemeColors files={files} /> },
|
|
230
237
|
{ id: 'logo', label: t.common.parts.brand, icon: icons.logo },
|
|
231
238
|
...appearance,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// "Udseende" › "Sidekolonne" (#udseende/sidekolonne): the block drawn in a column beside the text of the pages, chosen
|
|
2
|
+
// among "Ingen" and the installed blocks that can be (`beside` in their load.ts), saved in the site's file (config.side)
|
|
3
|
+
// in its own commit. Exposes SideColumn, sideSettingOf and sideChoicesOf (also used by the page form and by Library.tsx).
|
|
4
|
+
// Uses ui.tsx (ChoiceRow, Group), registry.tsx (siteBlocks) and lib/side-block.mjs.
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { config } from '@site/editor';
|
|
7
|
+
import { ConflictError, type RepoFile } from './github';
|
|
8
|
+
import Screen from './Screen';
|
|
9
|
+
import { ChoiceRow, EmptyNote, Group } from './ui';
|
|
10
|
+
import type { Env } from './App';
|
|
11
|
+
import { kindAndName } from './deploy';
|
|
12
|
+
import { ask } from './ask';
|
|
13
|
+
import { installedApps } from './apps';
|
|
14
|
+
import { siteBlocks } from './registry';
|
|
15
|
+
import { locale, siteT, t } from './i18n';
|
|
16
|
+
import { sideChoices } from '../lib/side-block.mjs';
|
|
17
|
+
|
|
18
|
+
// The text of each block's load.ts (the editor never imports it: it runs on the site), by the block's type.
|
|
19
|
+
const sources: Record<string, string> = Object.fromEntries(
|
|
20
|
+
Object.entries({
|
|
21
|
+
...import.meta.glob<string>('./standard/*/load.ts', { eager: true, query: '?raw', import: 'default' }),
|
|
22
|
+
...import.meta.glob<string>('/src/apps/*/blocks/*/load.ts', { eager: true, query: '?raw', import: 'default' }),
|
|
23
|
+
}).map(([path, text]) => [path.split('/').at(-2)!, text]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const capital = (s: string) => s.charAt(0).toLocaleUpperCase(locale) + s.slice(1);
|
|
27
|
+
const back = { href: '#udseende', label: t.common.parts.appearance };
|
|
28
|
+
|
|
29
|
+
/** The block type the site's file (config.side) chooses, or null. */
|
|
30
|
+
export function sideSettingOf(files: { path: string; text: string }[]): string | null {
|
|
31
|
+
try {
|
|
32
|
+
const block = JSON.parse(files.find((f) => f.path === config.side)?.text ?? '{}').block;
|
|
33
|
+
return typeof block === 'string' && block ? block : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The installed blocks that can be drawn beside a page's text, with their names in the editor's language. */
|
|
40
|
+
export const sideChoicesOf = (files: { path: string; text: string }[]) =>
|
|
41
|
+
sideChoices(siteBlocks, sources, installedApps(files)) as { type: string; label: string }[];
|
|
42
|
+
|
|
43
|
+
/** "Sidekolonne": "Ingen" and each block that can be drawn beside the text, the one chosen ticked. A tap on another asks
|
|
44
|
+
* first and then saves it (`{ "block": "<type>" }`, or `{ "block": null }`) at once, in its own commit, as an icon pack is. */
|
|
45
|
+
export default function SideColumn({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
46
|
+
const [saving, setSaving] = useState(false);
|
|
47
|
+
const chosen = sideSettingOf(files);
|
|
48
|
+
const choices = sideChoicesOf(files);
|
|
49
|
+
|
|
50
|
+
async function choose(type: string | null, name: string) {
|
|
51
|
+
if (saving || type === chosen) return;
|
|
52
|
+
if (!(await ask(type ? t.library.changeSideAsk(name) : t.library.changeSideNoneAsk, t.library.changeSide))) return;
|
|
53
|
+
const path = config.side;
|
|
54
|
+
const file = files.find((f) => f.path === path);
|
|
55
|
+
const text = `${JSON.stringify({ block: type }, null, 2)}\n`;
|
|
56
|
+
setSaving(true);
|
|
57
|
+
env.notify({ text: t.common.saving });
|
|
58
|
+
try {
|
|
59
|
+
const change = t.app.changes.chosen(kindAndName(t.app.changes.sideColumn, name));
|
|
60
|
+
const siteName = choices.find((x) => x.type === type)?.label ?? name;
|
|
61
|
+
const message = type ? siteT.library.sideCommit(siteName) : siteT.library.sideCommitNone;
|
|
62
|
+
const oids = await env.commit(message, [{ path, content: text, oid: file?.oid || null }], change);
|
|
63
|
+
env.onSaved({ path, oid: oids[path], text });
|
|
64
|
+
env.notify({ text: t.library.sideSaved(capital(change)), kind: 'saved' });
|
|
65
|
+
} catch (e) {
|
|
66
|
+
env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
67
|
+
} finally {
|
|
68
|
+
setSaving(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Screen title={t.common.parts.sideColumn} wide back={back}>
|
|
74
|
+
<Group>
|
|
75
|
+
<ChoiceRow selected={!chosen} onClick={() => choose(null, t.library.sideNone)} headline={t.library.sideNone} supporting={t.library.sideNoneHint} />
|
|
76
|
+
{choices.map((x) => (
|
|
77
|
+
<ChoiceRow key={x.type} selected={x.type === chosen} onClick={() => choose(x.type, x.label)} headline={x.label} />
|
|
78
|
+
))}
|
|
79
|
+
</Group>
|
|
80
|
+
{!choices.length && <EmptyNote>{t.library.sideEmpty}</EmptyNote>}
|
|
81
|
+
</Screen>
|
|
82
|
+
);
|
|
83
|
+
}
|
package/src/frontmatter.d.mts
CHANGED
|
@@ -4,3 +4,5 @@ export function splitFrontmatter(text: any): {
|
|
|
4
4
|
};
|
|
5
5
|
export function readFields(yaml: any): any;
|
|
6
6
|
export function writeFields(yaml: any, changes: any): string;
|
|
7
|
+
export function booleanRead(value: any, missing: any): boolean;
|
|
8
|
+
export function booleanWrite(value: any, missing: any): any;
|
package/src/frontmatter.mjs
CHANGED
|
@@ -40,3 +40,9 @@ function toNode(doc, key, value) {
|
|
|
40
40
|
}
|
|
41
41
|
return node;
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
// A true/false field that is true when it is missing from the file (Field.missing, "Sidekolonne": `side`): booleanRead
|
|
45
|
+
// gives the form's value for what is in the file; booleanWrite what is written for the form's value (nothing, '', when
|
|
46
|
+
// it is the missing one, so the key leaves the file).
|
|
47
|
+
export const booleanRead = (value, missing) => (value === undefined ? missing === true : value === true);
|
|
48
|
+
export const booleanWrite = (value, missing) => (missing !== undefined && value === missing ? '' : value);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Tests booleanRead and booleanWrite (frontmatter.mjs): the page switch "Sidekolonne" is `side: false` when off and the
|
|
2
|
+
// key is missing when on. Run: node --test src/frontmatter.test.mjs
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import { booleanRead, booleanWrite, writeFields } from './frontmatter.mjs';
|
|
6
|
+
|
|
7
|
+
const yaml = 'title: "Om os"\nwide: false';
|
|
8
|
+
|
|
9
|
+
test('a missing key reads as on, side: false as off', () => {
|
|
10
|
+
assert.equal(booleanRead(undefined, true), true);
|
|
11
|
+
assert.equal(booleanRead(false, true), false);
|
|
12
|
+
assert.equal(booleanRead(true, true), true);
|
|
13
|
+
assert.equal(booleanRead(undefined, undefined), false);
|
|
14
|
+
assert.equal(booleanRead(true, undefined), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('off writes side: false, on removes the key and leaves the rest as it was', () => {
|
|
18
|
+
const off = writeFields(yaml, { side: booleanWrite(false, true) });
|
|
19
|
+
assert.equal(off, `${yaml}\nside: false`);
|
|
20
|
+
assert.equal(writeFields(off, { side: booleanWrite(true, true) }), yaml);
|
|
21
|
+
assert.equal(writeFields(yaml, { side: booleanWrite(true, true) }), yaml);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('a switch without a missing value is written as it is', () => {
|
|
25
|
+
assert.equal(booleanWrite(true, undefined), true);
|
|
26
|
+
assert.equal(booleanWrite(false, undefined), false);
|
|
27
|
+
});
|
package/src/i18n/da-DK/app.ts
CHANGED
package/src/i18n/da-DK/common.ts
CHANGED
|
@@ -64,6 +64,15 @@ export const library: Messages['library'] = {
|
|
|
64
64
|
changeMotion: 'Skift bevægelse',
|
|
65
65
|
motionSaved: (change) => `${change}. Hjemmesiden får den nye bevægelse, når den er bygget.`,
|
|
66
66
|
motionCommit: (name) => `Editor: skift bevægelse til ${name}`,
|
|
67
|
+
sideNone: 'Ingen',
|
|
68
|
+
sideNoneHint: 'Siderne har ingen kolonne ved siden af teksten',
|
|
69
|
+
sideEmpty: 'Ingen installeret blok kan tegnes ved siden af en sides tekst.',
|
|
70
|
+
changeSideAsk: (name) => `Siderne får kolonnen ${name}, når hjemmesiden er bygget om et par minutter.`,
|
|
71
|
+
changeSideNoneAsk: 'Siderne mister kolonnen ved siden af teksten, når hjemmesiden er bygget om et par minutter.',
|
|
72
|
+
changeSide: 'Skift sidekolonne',
|
|
73
|
+
sideSaved: (change) => `${change}. Hjemmesiden får ændringen, når den er bygget.`,
|
|
74
|
+
sideCommit: (name) => `Editor: skift sidekolonne til ${name}`,
|
|
75
|
+
sideCommitNone: 'Editor: slå sidekolonnen fra',
|
|
67
76
|
themeIconPackStuck: 'Temaets ikonpakke er i brug, så temaet kan ikke afinstalleres. Vælg en anden ikonpakke først.',
|
|
68
77
|
themeDropsIconPack: (name, pack) =>
|
|
69
78
|
`Den nye version af ${name} har ikke ikonpakken ${pack}, som er i brug. Vælg en anden ikonpakke først.`,
|
package/src/i18n/en-US/app.ts
CHANGED
package/src/i18n/en-US/common.ts
CHANGED
|
@@ -115,6 +115,8 @@ export const common = {
|
|
|
115
115
|
iconPacks: 'Icon packs',
|
|
116
116
|
/** How the site moves, Material 3's motion schemes (#udseende/bevaegelse, MotionSchemes in Library.tsx). */
|
|
117
117
|
motion: 'Motion',
|
|
118
|
+
/** The block drawn in a column beside the text of the pages (#udseende/sidekolonne, SideColumn.tsx). */
|
|
119
|
+
sideColumn: 'Side column',
|
|
118
120
|
colors: 'Colors',
|
|
119
121
|
brand: 'Logo and icon',
|
|
120
122
|
blockSettings: 'Block settings',
|
|
@@ -94,6 +94,18 @@ export const library = {
|
|
|
94
94
|
changeMotion: 'Change motion',
|
|
95
95
|
motionSaved: (change: string) => `${change}. The website gets the new motion once it’s built.`,
|
|
96
96
|
motionCommit: (name: string) => `Editor: change motion to ${name}`,
|
|
97
|
+
/** Under "Sidekolonne" (SideColumn.tsx): the choice of no block and what is under it, what is said when no block can be
|
|
98
|
+
* drawn there, asked before one is chosen, the question's button, what is said after it is saved, and the commit, in
|
|
99
|
+
* the site's language (siteT). */
|
|
100
|
+
sideNone: 'None',
|
|
101
|
+
sideNoneHint: 'The pages have no column beside their text',
|
|
102
|
+
sideEmpty: 'No installed block can be drawn beside the text of a page.',
|
|
103
|
+
changeSideAsk: (name: string) => `The pages get the column ${name} once the website is built in a few minutes.`,
|
|
104
|
+
changeSideNoneAsk: 'The pages lose the column beside their text once the website is built in a few minutes.',
|
|
105
|
+
changeSide: 'Change side column',
|
|
106
|
+
sideSaved: (change: string) => `${change}. The website gets the change once it’s built.`,
|
|
107
|
+
sideCommit: (name: string) => `Editor: change side column to ${name}`,
|
|
108
|
+
sideCommitNone: 'Editor: turn off the side column',
|
|
97
109
|
/** A theme whose icon pack is chosen cannot be uninstalled, nor updated to a version without it (ThemeStore.tsx). */
|
|
98
110
|
themeIconPackStuck: "The theme's icon pack is in use, so the theme can’t be uninstalled. Choose another icon pack first.",
|
|
99
111
|
themeDropsIconPack: (name: string, pack: string) =>
|
package/src/schema.ts
CHANGED