@morphemecms/cms 0.7.0 → 0.8.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/defaults/collections.ts +56 -0
  3. package/defaults/company.ts +13 -4
  4. package/defaults/texts.ts +17 -0
  5. package/icons/lucide/LICENSE-phosphor +21 -0
  6. package/icons/lucide/facebook-logo.svg +1 -0
  7. package/icons/lucide/linkedin-logo.svg +1 -0
  8. package/icons/lucide/pack.json +16 -1
  9. package/icons/lucide/pinterest-logo.svg +1 -0
  10. package/icons/lucide/x-logo.svg +1 -0
  11. package/icons/material-symbols/LICENSE-phosphor +21 -0
  12. package/icons/material-symbols/facebook-logo.svg +1 -0
  13. package/icons/material-symbols/linkedin-logo.svg +1 -0
  14. package/icons/material-symbols/pack.json +15 -0
  15. package/icons/material-symbols/pinterest-logo.svg +1 -0
  16. package/icons/material-symbols/x-logo.svg +1 -0
  17. package/icons/phosphor/facebook-logo.svg +1 -0
  18. package/icons/phosphor/linkedin-logo.svg +1 -0
  19. package/icons/phosphor/pinterest-logo.svg +1 -0
  20. package/icons/phosphor/x-logo.svg +1 -0
  21. package/lib/lead.d.mts +5 -0
  22. package/lib/lead.mjs +10 -0
  23. package/lib/lead.test.mjs +65 -0
  24. package/lib/licenses.mjs +1 -1
  25. package/package.json +2 -1
  26. package/scripts/icon-packs.mjs +51 -13
  27. package/scripts/material-fallback.test.mjs +49 -0
  28. package/site/icons.d.mts +13 -0
  29. package/site/icons.mjs +25 -3
  30. package/site/materialSymbols.d.mts +8 -2
  31. package/site/materialSymbols.mjs +27 -11
  32. package/site/materialSymbols.test.mjs +44 -0
  33. package/site/schema.ts +2 -0
  34. package/site/site.ts +9 -2
  35. package/site/social.d.mts +37 -0
  36. package/site/social.mjs +92 -0
  37. package/site/social.test.mjs +80 -0
  38. package/site/theme.d.mts +2 -0
  39. package/site/theme.mjs +28 -11
  40. package/site/themeImport.test.mjs +40 -0
  41. package/src/EntryForm.tsx +21 -58
  42. package/src/FormFrame.tsx +5 -5
  43. package/src/Parts.tsx +8 -30
  44. package/src/Screen.tsx +12 -5
  45. package/src/editor.css +70 -5
  46. package/src/i18n/da-DK/parts.ts +1 -1
  47. package/src/i18n/da-DK/theme.ts +4 -0
  48. package/src/i18n/en-US/parts.ts +1 -1
  49. package/src/i18n/en-US/theme.ts +4 -0
  50. package/src/icon.tsx +1 -1
  51. package/src/materialIcons.ts +5 -7
  52. package/src/pagePreview.tsx +21 -1
  53. package/src/postLead.d.mts +7 -0
  54. package/src/postLead.mjs +9 -0
  55. package/src/postLead.test.mjs +28 -0
  56. package/src/ui.tsx +41 -96
  57. package/src/virtual.d.ts +5 -0
  58. package/starter/package.json +1 -1
  59. package/starter/src/data/social.json +14 -0
  60. package/theme/components/PostAuthor.astro +20 -0
  61. package/theme/components/PostCard.tsx +26 -0
  62. package/theme/components/PostHeader.astro +36 -0
  63. package/theme/components/PostMeta.tsx +27 -2
  64. package/theme/components/RelatedPosts.astro +29 -0
  65. package/theme/components/ShareRow.astro +48 -0
  66. package/theme/layouts/PostLayout.astro +28 -54
  67. package/theme/styles/base.css +164 -127
  68. package/theme/styles/blocks.css +160 -155
  69. package/theme/styles/components.css +43 -34
  70. package/theme/styles/global.css +316 -303
  71. package/theme/styles/ui.css +43 -34
  72. package/theme/ui/copyLink.script.ts +17 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @morphemecms/cms
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b201ffd: Material is now every theme's fallback: a theme imports `@morphemecms/cms/theme/styles/global.css`, anything it does not style is Material's, and its own rules always win.
8
+ - 3c68cc9: Richer blog post page: author role, tags and share buttons in the header, a lead paragraph, and related-post cards; new Company › Social media settings.
9
+
10
+ ### Patch Changes
11
+
12
+ - 0358c3e: Show a page or post form's sections as an "on this page" list on a card at the end of the form, on large screens, and keep the editor's content column centred on the window.
13
+ - 1323fa8: Wide buttons fill the width only on phones; the save bar's buttons sit at the end on larger screens.
14
+
15
+ ## 0.7.1
16
+
17
+ ### Patch Changes
18
+
19
+ - 36e2e3b: Fix the Cloudflare build stopping with "Too many open files" by serving all Material Symbols icons as one module.
20
+
3
21
  ## 0.7.0
4
22
 
5
23
  ### Minor Changes
@@ -174,6 +174,62 @@ export const contactFields: Field[] = [
174
174
  },
175
175
  ];
176
176
 
177
+ // The social media (src/data/social.json, a part of its own under "Virksomheden", company.ts): the site's profiles, and
178
+ // which share buttons a post shows. A switch that is not in the file is on (company.ts gives the form that default).
179
+ // A profile address: empty, or one that begins with https://.
180
+ const httpsAddress: [string, string] = ['^$|^https://\\S+$', 'Skriv en adresse, der begynder med https://'];
181
+
182
+ export const socialFields: Field[] = [
183
+ {
184
+ name: 'facebook',
185
+ label: 'Facebook',
186
+ type: 'string',
187
+ hint: 'Adressen på sitets profil. Vises i Googles data om virksomheden.',
188
+ pattern: httpsAddress,
189
+ },
190
+ {
191
+ name: 'instagram',
192
+ label: 'Instagram',
193
+ type: 'string',
194
+ pattern: httpsAddress,
195
+ },
196
+ {
197
+ name: 'linkedin',
198
+ label: 'LinkedIn',
199
+ type: 'string',
200
+ pattern: httpsAddress,
201
+ },
202
+ {
203
+ name: 'x',
204
+ label: 'X',
205
+ type: 'string',
206
+ pattern: httpsAddress,
207
+ },
208
+ {
209
+ name: 'youtube',
210
+ label: 'YouTube',
211
+ type: 'string',
212
+ pattern: httpsAddress,
213
+ },
214
+ {
215
+ name: 'tiktok',
216
+ label: 'TikTok',
217
+ type: 'string',
218
+ pattern: httpsAddress,
219
+ },
220
+ {
221
+ name: 'pinterest',
222
+ label: 'Pinterest',
223
+ type: 'string',
224
+ pattern: httpsAddress,
225
+ },
226
+ { name: 'shareFacebook', label: 'Del på Facebook', type: 'boolean' },
227
+ { name: 'shareX', label: 'Del på X', type: 'boolean' },
228
+ { name: 'shareLinkedin', label: 'Del på LinkedIn', type: 'boolean' },
229
+ { name: 'sharePinterest', label: 'Del på Pinterest', type: 'boolean', hint: 'Vises kun, når indlægget har et billede.' },
230
+ { name: 'shareCopyLink', label: 'Kopiér link', type: 'boolean' },
231
+ ];
232
+
177
233
  // The bank details, a part of its own under "Betaling" (company.ts, src/data/payment.json).
178
234
  export const bankFields: Field[] = [
179
235
  { name: 'bank', label: 'Bank', type: 'string', required: true },
@@ -1,15 +1,24 @@
1
1
  // "Virksomheden": the business, in parts, each a screen of its own (CompanyForm.tsx): its phone, e-mail and address
2
2
  // (src/data/contact.json) and its bank details (src/data/payment.json, under "Betaling", where a site may add its
3
3
  // other ways to pay, and where the settings of the apps installed that are a way to pay come after, as MobilePay's),
4
- // used all over the site. The logo and the icon are under "Udseende" (BrandForm.tsx); the link to
5
- // the calendar in Calendly is the app Calendly's (src/apps/calendly/).
6
- import { AddressBook, Bank, CreditCard } from '@phosphor-icons/react';
4
+ // used all over the site; and its social media (src/data/social.json): the profiles and the post's share buttons.
5
+ // The logo and the icon are under "Udseende" (BrandForm.tsx); the link to the calendar in Calendly is the app Calendly's (src/apps/calendly/).
6
+ import { AddressBook, Bank, CreditCard, ShareNetwork } from '@phosphor-icons/react';
7
7
  import type { Part } from '../src/schema';
8
- import { bankFields, contactFields } from '@site/collections';
8
+ import { bankFields, contactFields, socialFields } from '@site/collections';
9
9
 
10
10
  /** The parts of "Virksomheden", in their order, at #virksomheden/<id> ("Betaling"'s at #virksomheden/betaling/<id>). */
11
11
  export const companyParts: Part[] = [
12
12
  { id: 'kontakt', label: 'Kontakt', icon: AddressBook, file: 'src/data/contact.json', fields: contactFields },
13
+ {
14
+ id: 'sociale-medier',
15
+ label: 'Sociale medier',
16
+ icon: ShareNetwork,
17
+ file: 'src/data/social.json',
18
+ fields: socialFields,
19
+ // A switch missing from the file (or a file not there yet) is on; the form would show it off without this.
20
+ defaults: { shareFacebook: true, shareX: true, shareLinkedin: true, sharePinterest: true, shareCopyLink: true },
21
+ },
13
22
  {
14
23
  id: 'betaling',
15
24
  label: 'Betaling',
package/defaults/texts.ts CHANGED
@@ -290,6 +290,23 @@ export const texts: Record<string, Record<string, string>> = {
290
290
  'Knaptekst (book)': 'Button text (book)',
291
291
  'Knaptekst (ring)': 'Button text (call)',
292
292
  'Telefonnummeret vises efter teksten.': 'The phone number is shown after the text.',
293
+ 'Sociale medier': 'Social media',
294
+ 'Adressen på sitets profil. Vises i Googles data om virksomheden.':
295
+ "The address of the site's profile. Shown in Google's data about the business.",
296
+ 'Skriv en adresse, der begynder med https://': 'Write an address that starts with https://',
297
+ Facebook: 'Facebook',
298
+ Instagram: 'Instagram',
299
+ LinkedIn: 'LinkedIn',
300
+ X: 'X',
301
+ YouTube: 'YouTube',
302
+ TikTok: 'TikTok',
303
+ Pinterest: 'Pinterest',
304
+ 'Del på Facebook': 'Share on Facebook',
305
+ 'Del på X': 'Share on X',
306
+ 'Del på LinkedIn': 'Share on LinkedIn',
307
+ 'Del på Pinterest': 'Share on Pinterest',
308
+ 'Vises kun, når indlægget har et billede.': 'Shown only when the post has a picture.',
309
+ 'Kopiér link': 'Copy link',
293
310
  Blokke: 'Blocks',
294
311
  Blokskabeloner: 'Block templates',
295
312
  Knapper: 'Buttons',
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Phosphor Icons
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2.04C6.5 2.04 2 6.53 2 12.06C2 17.06 5.66 21.21 10.44 21.96V14.96H7.9V12.06H10.44V9.85C10.44 7.34 11.93 5.96 14.22 5.96C15.31 5.96 16.45 6.15 16.45 6.15V8.62H15.19C13.95 8.62 13.56 9.39 13.56 10.18V12.06H16.34L15.89 14.96H13.56V21.96A10 10 0 0 0 22 12.06C22 6.53 17.5 2.04 12 2.04Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3H19M18.5 18.5V13.2A3.26 3.26 0 0 0 15.24 9.94C14.39 9.94 13.4 10.46 12.92 11.24V10.13H10.13V18.5H12.92V13.57C12.92 12.8 13.54 12.17 14.31 12.17A1.4 1.4 0 0 1 15.71 13.57V18.5H18.5M6.88 8.56A1.68 1.68 0 0 0 8.56 6.88C8.56 5.95 7.81 5.19 6.88 5.19A1.69 1.69 0 0 0 5.19 6.88C5.19 7.81 5.95 8.56 6.88 8.56M8.27 18.5V10.13H5.5V18.5H8.27Z" /></svg>
@@ -15,8 +15,23 @@
15
15
  "source": "@mdi/svg@7.4.47",
16
16
  "licenseFile": "LICENSE-material-design-icons",
17
17
  "icons": [
18
+ "facebook-logo",
18
19
  "github-logo",
19
- "google-logo"
20
+ "google-logo",
21
+ "linkedin-logo",
22
+ "pinterest-logo"
23
+ ]
24
+ },
25
+ {
26
+ "name": "Phosphor",
27
+ "author": "Phosphor Icons",
28
+ "license": "MIT",
29
+ "url": "https://phosphoricons.com",
30
+ "version": "2.1.1",
31
+ "source": "@phosphor-icons/core@2.1.1",
32
+ "licenseFile": "LICENSE-phosphor",
33
+ "icons": [
34
+ "x-logo"
20
35
  ]
21
36
  }
22
37
  ]
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.04,21.54C10,21.83 10.97,22 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2A10,10 0 0,0 2,12C2,16.25 4.67,19.9 8.44,21.34C8.35,20.56 8.26,19.27 8.44,18.38L9.59,13.44C9.59,13.44 9.3,12.86 9.3,11.94C9.3,10.56 10.16,9.53 11.14,9.53C12,9.53 12.4,10.16 12.4,10.97C12.4,11.83 11.83,13.06 11.54,14.24C11.37,15.22 12.06,16.08 13.06,16.08C14.84,16.08 16.22,14.18 16.22,11.5C16.22,9.1 14.5,7.46 12.03,7.46C9.21,7.46 7.55,9.56 7.55,11.77C7.55,12.63 7.83,13.5 8.29,14.07C8.38,14.13 8.38,14.21 8.35,14.36L8.06,15.45C8.06,15.62 7.95,15.68 7.78,15.56C6.5,15 5.76,13.18 5.76,11.71C5.76,8.55 8,5.68 12.32,5.68C15.76,5.68 18.44,8.15 18.44,11.43C18.44,14.87 16.31,17.63 13.26,17.63C12.29,17.63 11.34,17.11 11,16.5L10.33,18.87C10.1,19.73 9.47,20.88 9.04,21.57V21.54Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M215,219.85a8,8,0,0,1-7,4.15H160a8,8,0,0,1-6.75-3.71l-40.49-63.63L53.92,221.38a8,8,0,0,1-11.84-10.76l61.77-68L41.25,44.3A8,8,0,0,1,48,32H96a8,8,0,0,1,6.75,3.71l40.49,63.63,58.84-64.72a8,8,0,0,1,11.84,10.76l-61.77,67.95,62.6,98.38A8,8,0,0,1,215,219.85Z"/></svg>
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Phosphor Icons
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2.04C6.5 2.04 2 6.53 2 12.06C2 17.06 5.66 21.21 10.44 21.96V14.96H7.9V12.06H10.44V9.85C10.44 7.34 11.93 5.96 14.22 5.96C15.31 5.96 16.45 6.15 16.45 6.15V8.62H15.19C13.95 8.62 13.56 9.39 13.56 10.18V12.06H16.34L15.89 14.96H13.56V21.96A10 10 0 0 0 22 12.06C22 6.53 17.5 2.04 12 2.04Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3H19M18.5 18.5V13.2A3.26 3.26 0 0 0 15.24 9.94C14.39 9.94 13.4 10.46 12.92 11.24V10.13H10.13V18.5H12.92V13.57C12.92 12.8 13.54 12.17 14.31 12.17A1.4 1.4 0 0 1 15.71 13.57V18.5H18.5M6.88 8.56A1.68 1.68 0 0 0 8.56 6.88C8.56 5.95 7.81 5.19 6.88 5.19A1.69 1.69 0 0 0 5.19 6.88C5.19 7.81 5.95 8.56 6.88 8.56M8.27 18.5V10.13H5.5V18.5H8.27Z" /></svg>
@@ -18,12 +18,27 @@
18
18
  "arrows-in-line-horizontal",
19
19
  "arrows-out-line-horizontal",
20
20
  "binoculars",
21
+ "facebook-logo",
21
22
  "github-logo",
22
23
  "google-logo",
23
24
  "hard-drives",
25
+ "linkedin-logo",
26
+ "pinterest-logo",
24
27
  "scan",
25
28
  "swatches"
26
29
  ]
30
+ },
31
+ {
32
+ "name": "Phosphor",
33
+ "author": "Phosphor Icons",
34
+ "license": "MIT",
35
+ "url": "https://phosphoricons.com",
36
+ "version": "2.1.1",
37
+ "source": "@phosphor-icons/core@2.1.1",
38
+ "licenseFile": "LICENSE-phosphor",
39
+ "icons": [
40
+ "x-logo"
41
+ ]
27
42
  }
28
43
  ]
29
44
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.04,21.54C10,21.83 10.97,22 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2A10,10 0 0,0 2,12C2,16.25 4.67,19.9 8.44,21.34C8.35,20.56 8.26,19.27 8.44,18.38L9.59,13.44C9.59,13.44 9.3,12.86 9.3,11.94C9.3,10.56 10.16,9.53 11.14,9.53C12,9.53 12.4,10.16 12.4,10.97C12.4,11.83 11.83,13.06 11.54,14.24C11.37,15.22 12.06,16.08 13.06,16.08C14.84,16.08 16.22,14.18 16.22,11.5C16.22,9.1 14.5,7.46 12.03,7.46C9.21,7.46 7.55,9.56 7.55,11.77C7.55,12.63 7.83,13.5 8.29,14.07C8.38,14.13 8.38,14.21 8.35,14.36L8.06,15.45C8.06,15.62 7.95,15.68 7.78,15.56C6.5,15 5.76,13.18 5.76,11.71C5.76,8.55 8,5.68 12.32,5.68C15.76,5.68 18.44,8.15 18.44,11.43C18.44,14.87 16.31,17.63 13.26,17.63C12.29,17.63 11.34,17.11 11,16.5L10.33,18.87C10.1,19.73 9.47,20.88 9.04,21.57V21.54Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M215,219.85a8,8,0,0,1-7,4.15H160a8,8,0,0,1-6.75-3.71l-40.49-63.63L53.92,221.38a8,8,0,0,1-11.84-10.76l61.77-68L41.25,44.3A8,8,0,0,1,48,32H96a8,8,0,0,1,6.75,3.71l40.49,63.63,58.84-64.72a8,8,0,0,1,11.84,10.76l-61.77,67.95,62.6,98.38A8,8,0,0,1,215,219.85Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm8,191.63V152h24a8,8,0,0,0,0-16H136V112a16,16,0,0,1,16-16h16a8,8,0,0,0,0-16H152a32,32,0,0,0-32,32v24H96a8,8,0,0,0,0,16h24v63.63a88,88,0,1,1,16,0Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M216,24H40A16,16,0,0,0,24,40V216a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V40A16,16,0,0,0,216,24Zm0,192H40V40H216V216ZM96,112v64a8,8,0,0,1-16,0V112a8,8,0,0,1,16,0Zm88,28v36a8,8,0,0,1-16,0V140a20,20,0,0,0-40,0v36a8,8,0,0,1-16,0V112a8,8,0,0,1,15.79-1.78A36,36,0,0,1,184,140ZM100,84A12,12,0,1,1,88,72,12,12,0,0,1,100,84Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,112c0,22.57-7.9,43.2-22.23,58.11C188.39,184,170.25,192,152,192c-17.88,0-29.82-5.86-37.43-12l-10.78,45.82A8,8,0,0,1,96,232a8.24,8.24,0,0,1-1.84-.21,8,8,0,0,1-6-9.62l32-136a8,8,0,0,1,15.58,3.66l-16.9,71.8C122,166,131.3,176,152,176c27.53,0,56-23.94,56-64A72,72,0,1,0,73.63,148a8,8,0,0,1-13.85,8A88,88,0,1,1,224,112Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M214.75,211.71l-62.6-98.38,61.77-67.95a8,8,0,0,0-11.84-10.76L143.24,99.34,102.75,35.71A8,8,0,0,0,96,32H48a8,8,0,0,0-6.75,12.3l62.6,98.37-61.77,68a8,8,0,1,0,11.84,10.76l58.84-64.72,40.49,63.63A8,8,0,0,0,160,224h48a8,8,0,0,0,6.75-12.29ZM164.39,208,62.57,48h29L193.43,208Z"/></svg>
package/lib/lead.d.mts ADDED
@@ -0,0 +1,5 @@
1
+ /** @param {string} html @returns {{ lead: string, rest: string }} */
2
+ export function splitLead(html: string): {
3
+ lead: string;
4
+ rest: string;
5
+ };
package/lib/lead.mjs ADDED
@@ -0,0 +1,10 @@
1
+ // A blog post's lead: its first paragraph, when its text starts with one (theme/layouts/PostLayout.astro draws it larger,
2
+ // with the featured picture after it; src/pagePreview.tsx does the same in the editor). Exposes splitLead.
3
+ /** @param {string} html @returns {{ lead: string, rest: string }} */
4
+ export function splitLead(html) {
5
+ const m = /^(?:\s|<!--[\s\S]*?-->)*(<p(?:\s[^>]*)?>([\s\S]*?)<\/p>)/i.exec(html);
6
+ if (m && m[2].replace(/<[^>]*>/g, '').trim() !== '') {
7
+ return { lead: m[1], rest: html.slice(m[0].length).trimStart() };
8
+ }
9
+ return { lead: '', rest: html };
10
+ }
@@ -0,0 +1,65 @@
1
+ // Tests for splitLead: the first text-bearing <p> is the lead; everything else stays in rest.
2
+ // Run with: node --test lib/lead.test.mjs
3
+ import { strict as assert } from 'node:assert';
4
+ import { test } from 'node:test';
5
+ import { splitLead } from './lead.mjs';
6
+
7
+ test('a text paragraph first is the lead', () => {
8
+ assert.deepEqual(splitLead('<p>Hello <a href="/x">you</a>.</p>\n<h2>Next</h2>'), {
9
+ lead: '<p>Hello <a href="/x">you</a>.</p>',
10
+ rest: '<h2>Next</h2>',
11
+ });
12
+ });
13
+
14
+ test('a heading first gives no lead', () => {
15
+ assert.deepEqual(splitLead('<h2>Title</h2><p>Text</p>'), { lead: '', rest: '<h2>Title</h2><p>Text</p>' });
16
+ });
17
+
18
+ test('a picture-only paragraph is no lead', () => {
19
+ assert.deepEqual(splitLead('<p><img src="/a.webp" alt=""></p><p>Text</p>'), {
20
+ lead: '',
21
+ rest: '<p><img src="/a.webp" alt=""></p><p>Text</p>',
22
+ });
23
+ });
24
+
25
+ test('a picture element alone is no lead', () => {
26
+ assert.deepEqual(splitLead('<p> <picture><img src="/a.webp" alt=""></picture> </p><p>T</p>'), {
27
+ lead: '',
28
+ rest: '<p> <picture><img src="/a.webp" alt=""></picture> </p><p>T</p>',
29
+ });
30
+ });
31
+
32
+ test('a block first gives no lead', () => {
33
+ assert.deepEqual(splitLead('<div data-block="points" data-props="{}"></div><p>Text</p>'), {
34
+ lead: '',
35
+ rest: '<div data-block="points" data-props="{}"></div><p>Text</p>',
36
+ });
37
+ });
38
+
39
+ test('leading whitespace is ignored', () => {
40
+ assert.deepEqual(splitLead('\n <p>Hi</p><p>More</p>'), { lead: '<p>Hi</p>', rest: '<p>More</p>' });
41
+ });
42
+
43
+ test('a comment first is ignored', () => {
44
+ assert.deepEqual(splitLead('<!-- x --><p>Hi</p>'), { lead: '<p>Hi</p>', rest: '' });
45
+ });
46
+
47
+ test('empty text', () => {
48
+ assert.deepEqual(splitLead(''), { lead: '', rest: '' });
49
+ });
50
+
51
+ test('one paragraph only', () => {
52
+ assert.deepEqual(splitLead('<p>Only</p>'), { lead: '<p>Only</p>', rest: '' });
53
+ });
54
+
55
+ test('a paragraph with attributes', () => {
56
+ assert.deepEqual(splitLead('<p class="x">Hi</p><p>B</p>'), { lead: '<p class="x">Hi</p>', rest: '<p>B</p>' });
57
+ });
58
+
59
+ test('a list first gives no lead', () => {
60
+ assert.deepEqual(splitLead('<ul><li>A</li></ul>'), { lead: '', rest: '<ul><li>A</li></ul>' });
61
+ });
62
+
63
+ test('a pre first gives no lead', () => {
64
+ assert.deepEqual(splitLead('<pre>x</pre><p>A</p>'), { lead: '', rest: '<pre>x</pre><p>A</p>' });
65
+ });
package/lib/licenses.mjs CHANGED
@@ -428,7 +428,7 @@ const HELPERS = [
428
428
  [/^\0vite\//, 'vite'],
429
429
  [/^\0rolldown\//, 'rolldown'],
430
430
  ];
431
- const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons)$/, /^\0site-theme-icons?(:|$)/];
431
+ const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons|material-symbols)$/, /^\0site-theme-icons?(:|$)/];
432
432
 
433
433
  /** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
434
434
  * (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
5
5
  "license": "MIT",
6
6
  "author": "Lars Adolfsen",
@@ -122,6 +122,7 @@
122
122
  "jsdom": "^29.1.1",
123
123
  "motion": "^14.0.0",
124
124
  "playwright": "^1.64.1",
125
+ "postcss": "^8.5.32",
125
126
  "prettier": "^3.9.10",
126
127
  "prettier-plugin-astro": "^1.1.2",
127
128
  "prettier-plugin-tailwindcss": "^0.8.1",
@@ -3,7 +3,8 @@
3
3
  // site and the editor draw, by its Phosphor name (usedIcons: the site's in packages/editor/src/icon.tsx, which the
4
4
  // content stores; the editor's as it imports them from @phosphor-icons/react and @phosphor-icons/core, found in the
5
5
  // code; the app store's), the set's icon its map names (MATERIAL_SYMBOLS, LUCIDE; one the set has none of from
6
- // Pictogrammers' Material Design Icons, MATERIAL_DESIGN_ICONS), as one <svg> with its viewBox, without a size or a colour
6
+ // Pictogrammers' Material Design Icons, MATERIAL_DESIGN_ICONS; one neither has, X's logo, from Phosphor's filled icon,
7
+ // PHOSPHOR_FILL_ICONS), as one <svg> with its viewBox, without a size or a colour
7
8
  // of its own: Svg and the theme's Vite plugin (themePlugin in packages/editor/site/theme.mjs) give it those. Phosphor's are its files
8
9
  // as they are (so a site with Phosphor's pack draws Phosphor's own icons, unchanged); Lucide's lines in a <g> with
9
10
  // Lucide's stroke (`-fill`: filled in). Beside them the pack's manifest (pack.json: name, author, licence, and the icons
@@ -19,7 +20,15 @@ import { execFileSync } from 'node:child_process';
19
20
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
20
21
  import { ICON_PACK_FILE, ICON_PACK_LICENSE, SYSTEM_PACKS } from '@morphemecms/cms/site/iconPacks.mjs';
21
22
  import { packFolder } from '@morphemecms/cms/site/icons.mjs';
22
- import { MATERIAL_DESIGN_ICONS, PACK_MAPS, PACK_SOURCES, phosphorFile, phosphorNames, usedIcons } from '@morphemecms/cms/site/icons.mjs';
23
+ import {
24
+ MATERIAL_DESIGN_ICONS,
25
+ PACK_MAPS,
26
+ PHOSPHOR_FILL_ICONS,
27
+ PACK_SOURCES,
28
+ phosphorFile,
29
+ phosphorNames,
30
+ usedIcons,
31
+ } from '@morphemecms/cms/site/icons.mjs';
23
32
 
24
33
  const MDI = 'material-design-icons';
25
34
  const version = (spec) => spec.replace(/^.*@/, '');
@@ -90,6 +99,10 @@ function sourcesOf(id) {
90
99
  const file = sourceFile[MDI](icon);
91
100
  return { name, icon, file, text: () => bare(file) };
92
101
  }),
102
+ ...(PHOSPHOR_FILL_ICONS[id] ?? []).map((name) => {
103
+ const file = `${phosphorPkg}assets/fill/${name}-fill.svg`;
104
+ return { name, icon: `${name}-fill`, file, text: () => bare(file) };
105
+ }),
93
106
  ];
94
107
  }
95
108
 
@@ -118,6 +131,7 @@ for (const id of SYSTEM_PACKS) {
118
131
  for (const { name, text } of sourcesOf(id)) writeFileSync(`${dir}${name}.svg`, text());
119
132
  const { name, author, license, url, package: spec } = PACK_SOURCES[id];
120
133
  const mdi = Object.keys(MATERIAL_DESIGN_ICONS[id] ?? {}).sort();
134
+ const fills = [...(PHOSPHOR_FILL_ICONS[id] ?? [])].sort();
121
135
  const manifest = {
122
136
  name,
123
137
  author,
@@ -125,19 +139,37 @@ for (const id of SYSTEM_PACKS) {
125
139
  url,
126
140
  version: version(spec),
127
141
  source: spec,
128
- ...(mdi.length
142
+ ...(mdi.length || fills.length
129
143
  ? {
130
144
  includes: [
131
- {
132
- name: PACK_SOURCES[MDI].name,
133
- author: PACK_SOURCES[MDI].author,
134
- license: PACK_SOURCES[MDI].license,
135
- url: PACK_SOURCES[MDI].url,
136
- version: version(PACK_SOURCES[MDI].package),
137
- source: PACK_SOURCES[MDI].package,
138
- licenseFile: `${ICON_PACK_LICENSE}-${MDI}`,
139
- icons: mdi,
140
- },
145
+ ...(mdi.length
146
+ ? [
147
+ {
148
+ name: PACK_SOURCES[MDI].name,
149
+ author: PACK_SOURCES[MDI].author,
150
+ license: PACK_SOURCES[MDI].license,
151
+ url: PACK_SOURCES[MDI].url,
152
+ version: version(PACK_SOURCES[MDI].package),
153
+ source: PACK_SOURCES[MDI].package,
154
+ licenseFile: `${ICON_PACK_LICENSE}-${MDI}`,
155
+ icons: mdi,
156
+ },
157
+ ]
158
+ : []),
159
+ ...(fills.length
160
+ ? [
161
+ {
162
+ name: PACK_SOURCES.phosphor.name,
163
+ author: PACK_SOURCES.phosphor.author,
164
+ license: PACK_SOURCES.phosphor.license,
165
+ url: PACK_SOURCES.phosphor.url,
166
+ version: version(PACK_SOURCES.phosphor.package),
167
+ source: PACK_SOURCES.phosphor.package,
168
+ licenseFile: `${ICON_PACK_LICENSE}-phosphor`,
169
+ icons: fills,
170
+ },
171
+ ]
172
+ : []),
141
173
  ],
142
174
  }
143
175
  : {}),
@@ -145,6 +177,12 @@ for (const id of SYSTEM_PACKS) {
145
177
  writeFileSync(`${dir}${ICON_PACK_FILE}`, `${JSON.stringify(manifest, null, 2)}\n`);
146
178
  writeFileSync(`${dir}${ICON_PACK_LICENSE}`, `${readFileSync(`${pkgs[id]}LICENSE`, 'utf8').trim()}\n`);
147
179
  if (mdi.length) writeFileSync(`${dir}${ICON_PACK_LICENSE}-${MDI}`, `${readFileSync(`${pkgs[MDI]}LICENSE`, 'utf8').trim()}\n`);
180
+ if (fills.length)
181
+ writeFileSync(
182
+ `${dir}${ICON_PACK_LICENSE}-phosphor`,
183
+ `${readFileSync(`${phosphorPkg}LICENSE`, 'utf8').trim()}
184
+ `,
185
+ );
148
186
  console.log(
149
187
  `${readdirSync(dir).filter((f) => f.endsWith('.svg')).length} ikoner fra ${spec}${mdi.length ? ` og ${PACK_SOURCES[MDI].package}` : ''} i ${dir}.`,
150
188
  );
@@ -0,0 +1,49 @@
1
+ // Checks that Material's CSS chain (theme/styles/: base, ui, blocks, components, global) sits in the fallback layer, so a theme wins.
2
+ // Reads the five files with postcss; no browser, no build. Exposes nothing; run by node --test.
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { readFileSync } from 'node:fs';
6
+ import postcss from 'postcss';
7
+
8
+ const names = ['base', 'ui', 'blocks', 'components', 'global'];
9
+ const roots = Object.fromEntries(
10
+ names.map((name) => [name, postcss.parse(readFileSync(new URL(`../theme/styles/${name}.css`, import.meta.url), 'utf8'))]),
11
+ );
12
+
13
+ const customOnly = (rule) => rule.nodes.every((node) => node.type === 'comment' || (node.type === 'decl' && node.prop.startsWith('--')));
14
+
15
+ // Top-level rules, also inside top-level @media and @supports, outside every layer.
16
+ function unlayeredRules(root) {
17
+ const found = [];
18
+ root.each((node) => {
19
+ if (node.type === 'rule') found.push(node);
20
+ else if (node.type === 'atrule' && ['media', 'supports'].includes(node.name)) {
21
+ node.each((child) => child.type === 'rule' && found.push(child));
22
+ }
23
+ });
24
+ return found;
25
+ }
26
+
27
+ test('no unlayered rule sets more than custom properties', () => {
28
+ for (const name of names) {
29
+ for (const rule of unlayeredRules(roots[name]).filter((rule) => !customOnly(rule))) {
30
+ assert.fail(`${name}.css line ${rule.source.start.line}: "${rule.selector}" sets more than custom properties outside a layer`);
31
+ }
32
+ }
33
+ });
34
+
35
+ test('base.css orders the layers with fallback last', () => {
36
+ const orders = [];
37
+ roots.base.walkAtRules('layer', (atRule) => atRule.nodes === undefined && orders.push(atRule.params));
38
+ assert.ok(orders.length > 0, 'base.css has no layer order statement');
39
+ const names = orders[0].split(',').map((name) => name.trim());
40
+ assert.equal(names.at(-1), 'fallback');
41
+ });
42
+
43
+ test('the CSS chain has no !important', () => {
44
+ for (const name of names) {
45
+ roots[name].walkDecls((decl) => {
46
+ assert.ok(!decl.important, `${name}.css line ${decl.source.start.line}: "${decl.prop}" is !important`);
47
+ });
48
+ }
49
+ });
package/site/icons.d.mts CHANGED
@@ -68,17 +68,30 @@ export const MATERIAL_DESIGN_ICONS: {
68
68
  'arrows-in-line-horizontal': string;
69
69
  'arrows-out-line-horizontal': string;
70
70
  binoculars: string;
71
+ 'facebook-logo': string;
71
72
  'github-logo': string;
72
73
  'google-logo': string;
74
+ 'linkedin-logo': string;
75
+ 'pinterest-logo': string;
73
76
  'hard-drives': string;
74
77
  scan: string;
75
78
  swatches: string;
76
79
  };
77
80
  lucide: {
81
+ 'facebook-logo': string;
78
82
  'github-logo': string;
79
83
  'google-logo': string;
84
+ 'linkedin-logo': string;
85
+ 'pinterest-logo': string;
80
86
  };
81
87
  };
88
+ /** For each system pack but Phosphor's: Phosphor's names of the icons that neither the pack's set nor Material Design Icons has
89
+ * (X's logo: Material Design Icons has only the old Twitter bird, which X's brand rules forbid), drawn from Phosphor's filled
90
+ * icon (`fill/<name>-fill.svg`). */
91
+ export const PHOSPHOR_FILL_ICONS: {
92
+ 'material-symbols': string[];
93
+ lucide: string[];
94
+ };
82
95
  /** Phosphor's name → Material Symbols' (Outlined, weight 400; `-fill` the filled one), for every icon the site and the
83
96
  * editor draw (usedIcons) but those in MATERIAL_DESIGN_ICONS['material-symbols']. */
84
97
  export const MATERIAL_SYMBOLS: {