@morphemecms/cms 0.13.0-next-20261106212516 → 0.13.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 +12 -1
- package/defaults/collections.ts +39 -52
- package/defaults/company.ts +5 -1
- package/defaults/editor.ts +27 -14
- package/defaults/i18n/da-DK/collections.ts +11 -8
- package/defaults/i18n/da-DK/editor.ts +1 -1
- package/defaults/i18n/da-DK/rules.ts +1 -0
- package/defaults/rules.ts +13 -1
- package/lib/focusTrap.d.mts +20 -0
- package/lib/focusTrap.mjs +28 -0
- package/lib/focusTrap.test.mjs +41 -0
- package/lib/folderChanges.mjs +1 -1
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.imports.test.mjs +33 -0
- package/lib/licenses.mjs +7 -4
- package/lib/menu.mjs +9 -4
- package/lib/menu.test.mjs +45 -18
- package/lib/storeEntries.d.mts +0 -1
- package/lib/storeEntries.mjs +2 -4
- package/lib/storeEntries.test.mjs +1 -6
- package/package.json +1 -1
- package/scripts/starter-gitattributes.test.mjs +22 -0
- package/site/header.d.mts +2 -1
- package/site/header.mjs +7 -4
- package/site/header.test.mjs +8 -2
- package/site/social.d.mts +65 -8
- package/site/social.mjs +132 -14
- package/site/social.test.mjs +109 -5
- package/src/App.tsx +29 -7
- package/src/AppList.tsx +46 -200
- package/src/BrandForm.tsx +2 -2
- package/src/CompanyForm.tsx +7 -2
- package/src/HeaderSettings.tsx +4 -4
- package/src/Library.tsx +20 -13
- package/src/LinkList.tsx +165 -0
- package/src/MainMenu.tsx +24 -4
- package/src/MoveButtons.tsx +15 -0
- package/src/Screen.tsx +12 -3
- package/src/StoreSearchRow.tsx +67 -0
- package/src/StoreStatus.tsx +67 -0
- package/src/ThemeStore.tsx +16 -37
- package/src/ThemeStoreScreen.tsx +62 -0
- package/src/checkSite.ts +3 -2
- package/src/cmsNotice.d.mts +7 -0
- package/src/cmsNotice.mjs +11 -0
- package/src/cmsNotice.test.mjs +50 -0
- package/src/fields.tsx +38 -13
- package/src/headerPreview.d.mts +14 -4
- package/src/headerPreview.mjs +10 -8
- package/src/headerPreview.test.mjs +12 -4
- package/src/headerTheme.tsx +19 -26
- package/src/i18n/da-DK/common.ts +1 -0
- package/src/i18n/da-DK/forms.ts +12 -0
- package/src/i18n/en-US/common.ts +2 -0
- package/src/i18n/en-US/forms.ts +15 -0
- package/src/levelStep.d.mts +10 -0
- package/src/levelStep.mjs +66 -0
- package/src/levelStep.test.mjs +119 -0
- package/src/linkAddress.d.mts +21 -0
- package/src/linkAddress.mjs +50 -0
- package/src/linkAddress.test.mjs +57 -0
- package/src/moveItem.d.mts +9 -0
- package/src/moveItem.mjs +17 -0
- package/src/moveItem.test.mjs +16 -0
- package/src/movedRoutes.mjs +8 -3
- package/src/movedRoutes.test.mjs +11 -2
- package/src/schema.ts +11 -0
- package/src/site.ts +1 -0
- package/src/store.ts +5 -33
- package/src/storeSort.tsx +39 -0
- package/src/themeFilter.d.mts +17 -0
- package/src/themeFilter.mjs +16 -0
- package/src/themeFilter.test.mjs +24 -0
- package/src/themeUi.ts +1 -0
- package/src/ui.tsx +2 -1
- package/starter/.gitattributes +2 -0
- package/starter/package.json +1 -1
- package/starter/src/data/social.json +1 -7
- package/theme/blocks/buttons/Buttons.tsx +12 -2
- package/theme/components/Header.astro +21 -31
- package/theme/components/HeaderCta.tsx +21 -0
- package/theme/components/HeaderPhoneButton.tsx +28 -0
- package/theme/components/MobileMenu.tsx +56 -43
- package/theme/components/headerParts.ts +5 -4
- package/theme/styles/global.css +22 -5
- package/theme/styles/ui.css +110 -7
- package/theme/ui/NavigationDrawer.tsx +6 -1
- package/src/legacyMove.test.mjs +0 -137
- package/src/legacyMove.ts +0 -131
package/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
# @morphemecms/cms
|
|
2
2
|
|
|
3
|
-
## 0.13.0
|
|
3
|
+
## 0.13.0
|
|
4
4
|
|
|
5
5
|
### Minor Changes
|
|
6
6
|
|
|
7
7
|
- 1193da4: The standard parts and their ids are in English. Danish comes from `defaults/i18n/da-DK/`. A site that overrides a standard part by its old id must use the new id. Old editor links and bookmarks still open (they redirect to the new address). The filters and sorts that the editor saved in the browser reset once.
|
|
8
|
+
- 10276ee: The header's "Call to action" can show the phone number (from Business › Contact; it calls on a phone and leads to the contact page on larger screens) as a text button before its button, and both share a row at the bottom of the menu; the menu opens from the side its button is on, and no drawer (site or editor) shows a gap at the screen's edge when its spring overshoots. In the editor, the back gesture goes from a section's list to the main menu, Header and Footer are under Appearance, and Business is a section of its own after Blocks and Apps.
|
|
8
9
|
- 9ddc437: A list item can be selected with a long press on a phone (`selectable` on the theme's `ListItem`); in the editor's notifications this selects notifications to delete.
|
|
9
10
|
- 32b4395: The bell in the editor's top bar opens the notifications in a side sheet over the screen you are on, instead of a screen of their own.
|
|
11
|
+
- 774ab69: Social media profiles are a list of links added with the + button; a link to Facebook, X, LinkedIn or Pinterest gets its name and icon from the address, and the share buttons are under their own heading.
|
|
10
12
|
- 82d64b7: The start screen has a "Continue where you left off" card with the last three pages and blog posts saved, a notice of what was updated since your last visit (each with a close button), and the licenses link sits in a footer at the bottom of every screen.
|
|
13
|
+
- 03ad8a9: Move the theme store from the Appstore to its own screen, Theme store, first under Appearance.
|
|
11
14
|
|
|
12
15
|
### Patch Changes
|
|
13
16
|
|
|
17
|
+
- ccefd29: The licence checks know `@app/self` (app contract 3): an app that imports it no longer needs it in `thirdParty`, and
|
|
18
|
+
the site's licence list counts its virtual module as the site's own.
|
|
19
|
+
- e9d6b34: The editor's notifications also show when the website updated Morpheme from npm, undid an update or got stuck.
|
|
14
20
|
- a283bcc: The drawer's divider stands level with the top bar's line.
|
|
21
|
+
- 6fa3043: The store no longer moves old store copies; all sites use permanent ids.
|
|
15
22
|
- 9c24224: The text editor's empty-line hint says "Write text here…" again, on a line no taller than the hint.
|
|
23
|
+
- 697a7dc: List items follow Material 3's focus layer, corners and hover colours.
|
|
24
|
+
- 8fd74d0: Editor menu: Header and Footer come right after Themes under Appearance, and Logo and icon is under Business.
|
|
25
|
+
- 3a6157b: New sites get a `.gitattributes` that keeps LF line endings on Windows.
|
|
16
26
|
- 3961b6d: The text toolbar no longer scrolls sideways on a phone: the tools that do not fit are on a second row, shown with the arrow at its end.
|
|
27
|
+
- 34f69c5: Open website, in the editor's top bar, opens the site in a new window instead of a new tab.
|
|
17
28
|
|
|
18
29
|
## 0.12.0
|
|
19
30
|
|
package/defaults/collections.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { config } from '@site/config';
|
|
|
13
13
|
import { standardPaths } from '@site/standardPages';
|
|
14
14
|
|
|
15
15
|
// The fields of one button of the action bar (the bottom bar on a phone, ActionBar.astro) and of the button in the header
|
|
16
|
-
// ("
|
|
16
|
+
// ("Call to action", src/data/header.json): its text, where it leads, and the promotion or page it leads to.
|
|
17
17
|
const barButtonLabel: Field = {
|
|
18
18
|
name: 'label',
|
|
19
19
|
label: 'Button text',
|
|
@@ -218,59 +218,36 @@ export const contactFields: Field[] = [
|
|
|
218
218
|
},
|
|
219
219
|
];
|
|
220
220
|
|
|
221
|
-
// The social media (src/data/social.json, a part of its own under "Business", company.ts): the site's profiles,
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
const httpsAddress: [string, string] = ['^$|^https://\\S+$', 'Write an address that starts with https://'];
|
|
225
|
-
|
|
221
|
+
// The social media (src/data/social.json, a part of its own under "Business", company.ts): the site's profiles, a list of
|
|
222
|
+
// links added with the FAB, each one's network and icon known by its address (networkOf in site/social.mjs), and which
|
|
223
|
+
// share buttons a post shows. A switch that is not in the file is on (company.ts gives the form that default).
|
|
226
224
|
export const socialFields: Field[] = [
|
|
227
225
|
{
|
|
228
|
-
name: '
|
|
229
|
-
label: '
|
|
230
|
-
type: '
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
type: 'string',
|
|
244
|
-
pattern: httpsAddress,
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: 'x',
|
|
248
|
-
label: 'X',
|
|
249
|
-
type: 'string',
|
|
250
|
-
pattern: httpsAddress,
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
name: 'youtube',
|
|
254
|
-
label: 'YouTube',
|
|
255
|
-
type: 'string',
|
|
256
|
-
pattern: httpsAddress,
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
name: 'tiktok',
|
|
260
|
-
label: 'TikTok',
|
|
261
|
-
type: 'string',
|
|
262
|
-
pattern: httpsAddress,
|
|
226
|
+
name: 'profiles',
|
|
227
|
+
label: 'Profiles',
|
|
228
|
+
type: 'list',
|
|
229
|
+
links: true,
|
|
230
|
+
hint: "The site's profiles on social media. Shown in Google's data about the business.",
|
|
231
|
+
summary: 'url',
|
|
232
|
+
fields: [
|
|
233
|
+
{
|
|
234
|
+
name: 'url',
|
|
235
|
+
label: 'Address',
|
|
236
|
+
type: 'string',
|
|
237
|
+
required: true,
|
|
238
|
+
pattern: ['^https://\\S+$', 'Write an address that starts with https://'],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
263
241
|
},
|
|
264
242
|
{
|
|
265
|
-
name: '
|
|
266
|
-
label: '
|
|
267
|
-
type: '
|
|
268
|
-
|
|
243
|
+
name: 'shareFacebook',
|
|
244
|
+
label: 'Facebook',
|
|
245
|
+
type: 'boolean',
|
|
246
|
+
heading: { label: 'Share buttons', hint: 'The buttons at the top of each blog post.' },
|
|
269
247
|
},
|
|
270
|
-
{ name: '
|
|
271
|
-
{ name: '
|
|
272
|
-
{ name: '
|
|
273
|
-
{ name: 'sharePinterest', label: 'Share on Pinterest', type: 'boolean', hint: 'Shown only when the post has a picture.' },
|
|
248
|
+
{ name: 'shareX', label: 'X', type: 'boolean' },
|
|
249
|
+
{ name: 'shareLinkedin', label: 'LinkedIn', type: 'boolean' },
|
|
250
|
+
{ name: 'sharePinterest', label: 'Pinterest', type: 'boolean', hint: 'Shown only when the post has a picture.' },
|
|
274
251
|
{ name: 'shareCopyLink', label: 'Copy link', type: 'boolean' },
|
|
275
252
|
];
|
|
276
253
|
|
|
@@ -869,22 +846,32 @@ export const collections: Collection[] = [
|
|
|
869
846
|
optional: true,
|
|
870
847
|
fields: [
|
|
871
848
|
{
|
|
849
|
+
// The call to action: the button and the site's phone number (contact settings) as a text button before it, in
|
|
850
|
+
// the header and at the bottom of the navigation drawer (Header.astro, MobileMenu.tsx; site/header.mjs).
|
|
872
851
|
name: 'button',
|
|
873
|
-
label: '
|
|
852
|
+
label: 'Call to action',
|
|
874
853
|
type: 'object',
|
|
875
854
|
fields: [
|
|
876
855
|
{
|
|
877
856
|
name: 'enabled',
|
|
878
|
-
label: '
|
|
857
|
+
label: 'Button',
|
|
879
858
|
type: 'boolean',
|
|
880
859
|
toggle: ['On', 'Off'],
|
|
881
860
|
default: false,
|
|
882
|
-
hint: 'The button is in the header.',
|
|
861
|
+
hint: 'The button is in the header and at the bottom of the menu.',
|
|
883
862
|
},
|
|
884
863
|
barButtonLabel,
|
|
885
864
|
barButtonTo,
|
|
886
865
|
barButtonOpen,
|
|
887
866
|
barButtonHref,
|
|
867
|
+
{
|
|
868
|
+
name: 'phone',
|
|
869
|
+
label: 'Phone number',
|
|
870
|
+
type: 'boolean',
|
|
871
|
+
toggle: ['On', 'Off'],
|
|
872
|
+
default: true,
|
|
873
|
+
hint: 'The phone number from the contact settings, before the button in the header and at the bottom of the menu.',
|
|
874
|
+
},
|
|
888
875
|
],
|
|
889
876
|
},
|
|
890
877
|
{
|
package/defaults/company.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// "Business": 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 "Payment", 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; and its social media (src/data/social.json): the profiles
|
|
4
|
+
// used all over the site; and its social media (src/data/social.json): the profiles, a list of links added with the FAB, and
|
|
5
|
+
// the post's share buttons.
|
|
5
6
|
// The logo and the icon are under "Appearance" (BrandForm.tsx); the link to the calendar in Calendly is the app Calendly's (morpheme-apps/apps/calendly/).
|
|
6
7
|
import { AddressBook, Bank, CreditCard, ShareNetwork } from '@phosphor-icons/react';
|
|
7
8
|
import type { Part } from '../src/schema';
|
|
8
9
|
import { bankFields, contactFields, socialFields } from '@site/collections';
|
|
10
|
+
import { upgradeSocial } from '../site/social.mjs';
|
|
9
11
|
|
|
10
12
|
/** The parts of "Business", in their order, at #company/<id> ("Payment"'s at #company/payment/<id>). */
|
|
11
13
|
export const companyParts: Part[] = [
|
|
@@ -16,6 +18,8 @@ export const companyParts: Part[] = [
|
|
|
16
18
|
icon: ShareNetwork,
|
|
17
19
|
file: 'src/data/social.json',
|
|
18
20
|
fields: socialFields,
|
|
21
|
+
// A file from before the profiles were a list (a key for each network) is read and saved as one.
|
|
22
|
+
upgrade: upgradeSocial,
|
|
19
23
|
// A switch missing from the file (or a file not there yet) is on; the form would show it off without this.
|
|
20
24
|
defaults: { shareFacebook: true, shareX: true, shareLinkedin: true, sharePinterest: true, shareCopyLink: true },
|
|
21
25
|
},
|
package/defaults/editor.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// standard parts: a site has them unless its src/site/ has a file of the same name (siteFile in site/theme.mjs), which
|
|
4
4
|
// may take the standard one from @morphemecms/cms/defaults/<file> and add its own.
|
|
5
5
|
// collections the parts of the site and their fields (collections.ts)
|
|
6
|
-
// groups, settings, lists, header, icons the menu and the start screen
|
|
7
|
-
// appearance the site's own parts under "Appearance", after the
|
|
6
|
+
// groups, settings, lists, header, icons the menu and the start screen ("Appearance" starts with "Theme store", the store's themes)
|
|
7
|
+
// appearance the site's own parts under "Appearance", after the colours ("Default pages")
|
|
8
8
|
// companyParts, blockParts the screens under "Business" and "Blocks" (company.ts, blockTexts.ts)
|
|
9
9
|
// partScreens parts of the start screen that are screens with parts of their own (privacy.ts)
|
|
10
10
|
// seoParts the site's own parts under "Search engine optimization" (#seo/<id>), in the menu under "Content" (internalLinks.ts)
|
|
@@ -112,7 +112,7 @@ export const icons: Record<string, Icon> = {
|
|
|
112
112
|
tags: Tag,
|
|
113
113
|
popups: Megaphone,
|
|
114
114
|
company: Buildings,
|
|
115
|
-
// The sections of the main menu that are not also a part.
|
|
115
|
+
// The sections of the main menu that are not also a part ("Business" is the part "company"'s icon).
|
|
116
116
|
content: Article,
|
|
117
117
|
global: GlobeSimple,
|
|
118
118
|
settings: GearSix,
|
|
@@ -127,10 +127,11 @@ export const icons: Record<string, Icon> = {
|
|
|
127
127
|
'block-settings': ToggleRight,
|
|
128
128
|
apps: PuzzlePiece,
|
|
129
129
|
appstore: Storefront,
|
|
130
|
+
'theme-store': Storefront,
|
|
130
131
|
morpheme: Sparkle,
|
|
131
132
|
advanced: Wrench,
|
|
132
133
|
licenses: Certificate,
|
|
133
|
-
// The screens under "Appearance" (Library.tsx Theme).
|
|
134
|
+
// The screens under "Appearance" (Library.tsx Theme), the first the store's themes.
|
|
134
135
|
themes: Swatches,
|
|
135
136
|
colors: Palette,
|
|
136
137
|
logo: Sticker,
|
|
@@ -140,7 +141,7 @@ export const icons: Record<string, Icon> = {
|
|
|
140
141
|
parts: Shapes,
|
|
141
142
|
};
|
|
142
143
|
|
|
143
|
-
/** The site's own parts under "Appearance" (#appearance), after the
|
|
144
|
+
/** The site's own parts under "Appearance" (#appearance), after the colours, each a screen of its own at #appearance/<id>:
|
|
144
145
|
* "Default pages", which pages are the front page, the blog page, the 404 page and the contact page, as a list
|
|
145
146
|
* (standardPageFieldsOf in collections.ts; the privacy policy and the cookie policy are under "Privacy", privacy.ts). */
|
|
146
147
|
export const appearance: Part[] = [
|
|
@@ -157,12 +158,13 @@ export const appearance: Part[] = [
|
|
|
157
158
|
// The editor's menu: eight sections in a fixed order, shown in the order written here, never A–Z. "Dashboard" is a link to the
|
|
158
159
|
// start screen (`href`) without a list; the others have a list of parts, each an id: the content the owner writes and
|
|
159
160
|
// uploads ("Content": the pages, the blog, the short lists, the pop-ups, the tags, the files, "internal links" (seoParts)
|
|
160
|
-
// and "redirects", Redirects.tsx),
|
|
161
|
-
//
|
|
162
|
-
// and
|
|
163
|
-
// looks ("Appearance": the theme, the
|
|
164
|
-
//
|
|
165
|
-
// and the
|
|
161
|
+
// and "redirects", Redirects.tsx), the blocks ("Blocks": their shared texts, and "Block selection", which of them can be
|
|
162
|
+
// put in), the apps ("Apps": the apps that come with the site, src/apps/, AppList.tsx, and "appstore"), the business
|
|
163
|
+
// ("Business": the logo and icon first (company/logo, BrandForm.tsx), then the section of the editor's part "company": its parts, name, contact and booking, are its rows; lib/menu.mjs),
|
|
164
|
+
// how the site looks ("Appearance": the theme store, the header with the menus and the footer (what is on every page), the theme,
|
|
165
|
+
// the icon packs, the motion, the side column (SideColumn.tsx), the colors, the standard pages (appearance)
|
|
166
|
+
// and the editor's parts: Colors.tsx, BrandForm.tsx and Parts.tsx), the parts of "Privacy" (privacy.ts) and the settings
|
|
167
|
+
// of the whole site ("Settings").
|
|
166
168
|
// Each part is a collection (collections.ts), except those named above, "privacy"'s (partScreens) and the editor's own
|
|
167
169
|
// "company" (companyParts), "blocks" (blockParts), id files (the pictures, PDFs, videos and zip files), "header", "lists"
|
|
168
170
|
// and "block-settings" (BlockSettings.tsx).
|
|
@@ -174,13 +176,24 @@ export const groups: { id: string; title: string; href?: string; ids: string[] }
|
|
|
174
176
|
title: 'Content',
|
|
175
177
|
ids: ['pages', 'blog', 'lists', 'popups', 'tags', 'files', internalLinks.id, 'redirects'],
|
|
176
178
|
},
|
|
177
|
-
{ id: 'global', title: 'Globals', ids: ['header', 'footer', 'company'] },
|
|
178
|
-
{ id: 'apps', title: 'Apps', ids: ['apps', 'appstore'] },
|
|
179
179
|
{ id: 'blocks', title: 'Blocks', ids: ['blocks', 'block-settings'] },
|
|
180
|
+
{ id: 'apps', title: 'Apps', ids: ['apps', 'appstore'] },
|
|
181
|
+
{ id: 'company', title: 'Business', ids: ['logo', 'company'] },
|
|
180
182
|
{
|
|
181
183
|
id: 'appearance',
|
|
182
184
|
title: 'Appearance',
|
|
183
|
-
ids: [
|
|
185
|
+
ids: [
|
|
186
|
+
'theme-store',
|
|
187
|
+
'header',
|
|
188
|
+
'footer',
|
|
189
|
+
'themes',
|
|
190
|
+
'icon-packs',
|
|
191
|
+
'motion',
|
|
192
|
+
'side-column',
|
|
193
|
+
'colors',
|
|
194
|
+
...appearance.map((p) => p.id),
|
|
195
|
+
'parts',
|
|
196
|
+
],
|
|
184
197
|
},
|
|
185
198
|
{ id: 'privacy', title: 'Privacy', ids: privacy.parts.map((p) => p.id) },
|
|
186
199
|
{ id: 'settings', title: 'Settings', ids: settingsIds },
|
|
@@ -51,12 +51,12 @@ export const collections: Record<string, string> = {
|
|
|
51
51
|
'CVR number': 'CVR-nummer',
|
|
52
52
|
'Shown at the bottom of every page.': 'Vises nederst på hver side.',
|
|
53
53
|
'A CVR number has eight digits': 'Et CVR-nummer har otte cifre',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'
|
|
57
|
-
|
|
58
|
-
'Share
|
|
59
|
-
'
|
|
54
|
+
Profiles: 'Profiler',
|
|
55
|
+
Address: 'Adresse',
|
|
56
|
+
"The site's profiles on social media. Shown in Google's data about the business.":
|
|
57
|
+
'Sitets profiler på sociale medier. Vises i Googles data om virksomheden.',
|
|
58
|
+
'Share buttons': 'Delingsknapper',
|
|
59
|
+
'The buttons at the top of each blog post.': 'Knapperne øverst i hvert blogindlæg.',
|
|
60
60
|
'Shown only when the post has a picture.': 'Vises kun, når indlægget har et billede.',
|
|
61
61
|
'Copy link': 'Kopiér link',
|
|
62
62
|
'Reg. no.': 'Reg.nr.',
|
|
@@ -192,8 +192,11 @@ export const collections: Record<string, string> = {
|
|
|
192
192
|
'header button': 'headerknap',
|
|
193
193
|
'Header button': 'Headerknap',
|
|
194
194
|
'The header': 'Headeren',
|
|
195
|
-
'
|
|
196
|
-
'The button is in the header.': 'Knappen står i headeren.',
|
|
195
|
+
'Call to action': 'Opfordring til handling',
|
|
196
|
+
'The button is in the header and at the bottom of the menu.': 'Knappen står i headeren og nederst i menuen.',
|
|
197
|
+
'Phone number': 'Telefonnummer',
|
|
198
|
+
'The phone number from the contact settings, before the button in the header and at the bottom of the menu.':
|
|
199
|
+
'Telefonnummeret fra kontaktoplysningerne, før knappen i headeren og nederst i menuen.',
|
|
197
200
|
'Menu button position': 'Menuknappens placering',
|
|
198
201
|
Left: 'Venstre',
|
|
199
202
|
Right: 'Højre',
|
|
@@ -4,8 +4,8 @@ export const editor: Record<string, string> = {
|
|
|
4
4
|
'Default pages': 'Standardsider',
|
|
5
5
|
Dashboard: 'Oversigt',
|
|
6
6
|
Content: 'Indhold',
|
|
7
|
-
Globals: 'Globalt',
|
|
8
7
|
Blocks: 'Blokke',
|
|
8
|
+
Business: 'Virksomheden',
|
|
9
9
|
Appearance: 'Udseende',
|
|
10
10
|
Privacy: 'Privatliv',
|
|
11
11
|
Settings: 'Indstillinger',
|
|
@@ -15,6 +15,7 @@ export const rules: Record<string, string> = {
|
|
|
15
15
|
'Edit the action bar': 'Rediger handlingslinjen',
|
|
16
16
|
'Edit the default buttons': 'Rediger standardknapperne',
|
|
17
17
|
'The default pages must be different pages.': 'Standardsiderne skal være forskellige sider.',
|
|
18
|
+
'The address {url} is in the list of profiles more than once.': 'Adressen {url} er flere gange på listen over profiler.',
|
|
18
19
|
'The page "{side}" does not have the block {blok}, which {hvad} must have. Put it in on the page first.':
|
|
19
20
|
'Siden "{side}" har ikke blokken {blok}, som {hvad} skal have. Sæt den ind på siden først.',
|
|
20
21
|
'{side} must be a published page.': '{side} skal være en side, der er udgivet.',
|
package/defaults/rules.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { collections } from '@site/collections';
|
|
|
15
15
|
import { config } from '@site/config';
|
|
16
16
|
import { opensOf } from '@site/opens';
|
|
17
17
|
import { differentPages, standardPages, standardPaths } from '@site/standardPages';
|
|
18
|
+
import { twiceIn, upgradeSocial } from '../site/social.mjs';
|
|
18
19
|
|
|
19
20
|
const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
|
|
20
21
|
// A page's address: `path`, or /<file name>/ for pages made in the editor.
|
|
@@ -226,7 +227,7 @@ function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
/** What is wrong on the site by its own rules, a line for each, before it is built (checkSite.ts): a standard page that
|
|
229
|
-
* is not a page on the site, or the same page twice, and a button that opens a promotion that is gone, turned off or not
|
|
230
|
+
* is not a page on the site, or the same page twice, a profile on social media twice, and a button that opens a promotion that is gone, turned off or not
|
|
230
231
|
* shown on its page. */
|
|
231
232
|
export function siteProblems(files: { path: string; text: string }[]): string[] {
|
|
232
233
|
const problems: string[] = [];
|
|
@@ -241,6 +242,17 @@ export function siteProblems(files: { path: string; text: string }[]): string[]
|
|
|
241
242
|
if (!differentPages(standardOf(files)))
|
|
242
243
|
problems.push(`${config.standardPages}: ${siteText('The default pages must be different pages.')}`);
|
|
243
244
|
}
|
|
245
|
+
// A profile is on social media's list once (Business › Social media, social.json).
|
|
246
|
+
const social = files.find((f) => f.path === 'src/data/social.json');
|
|
247
|
+
if (social) {
|
|
248
|
+
try {
|
|
249
|
+
const profiles = upgradeSocial(JSON.parse(social.text) as Record<string, unknown>).profiles;
|
|
250
|
+
for (const url of twiceIn(Array.isArray(profiles) ? profiles : []))
|
|
251
|
+
problems.push(`${social.path}: ${siteText('The address {url} is in the list of profiles more than once.', { url })}`);
|
|
252
|
+
} catch {
|
|
253
|
+
// A file that is not JSON is found by the check of the files' own rules.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
244
256
|
const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
|
|
245
257
|
const slugs = new Set<string>();
|
|
246
258
|
for (const f of files) for (const m of f.text.matchAll(/(?:"|")open(?:"|"):\s*(?:"|")([^"&]+)/g)) slugs.add(m[1]);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The elements that are drawn now: one that is hidden (display: none, as the contact-page link is below the medium window
|
|
3
|
+
* size) has no boxes and cannot take the focus, so it is neither the first nor the last stop.
|
|
4
|
+
* @template {{ getClientRects(): { length: number } }} T
|
|
5
|
+
* @param {ArrayLike<T>} elements
|
|
6
|
+
* @returns {T[]}
|
|
7
|
+
*/
|
|
8
|
+
export function visibleElements<T extends {
|
|
9
|
+
getClientRects(): {
|
|
10
|
+
length: number;
|
|
11
|
+
};
|
|
12
|
+
}>(elements: ArrayLike<T>): T[];
|
|
13
|
+
/**
|
|
14
|
+
* The index to focus when Tab would leave the list, or null when the browser may move the focus itself.
|
|
15
|
+
* @param {boolean} shift whether Shift is held
|
|
16
|
+
* @param {number} current the index of the focused element, -1 when it is not in the list
|
|
17
|
+
* @param {number} count how many stops there are
|
|
18
|
+
* @returns {number | null}
|
|
19
|
+
*/
|
|
20
|
+
export function trapTarget(shift: boolean, current: number, count: number): number | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Keeps Tab inside a modal drawer (theme/components/MobileMenu.tsx does the DOM). Exposes visibleElements and trapTarget;
|
|
2
|
+
// no dependencies.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The elements that are drawn now: one that is hidden (display: none, as the contact-page link is below the medium window
|
|
6
|
+
* size) has no boxes and cannot take the focus, so it is neither the first nor the last stop.
|
|
7
|
+
* @template {{ getClientRects(): { length: number } }} T
|
|
8
|
+
* @param {ArrayLike<T>} elements
|
|
9
|
+
* @returns {T[]}
|
|
10
|
+
*/
|
|
11
|
+
export function visibleElements(elements) {
|
|
12
|
+
return Array.from(elements).filter((el) => el.getClientRects().length > 0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The index to focus when Tab would leave the list, or null when the browser may move the focus itself.
|
|
17
|
+
* @param {boolean} shift whether Shift is held
|
|
18
|
+
* @param {number} current the index of the focused element, -1 when it is not in the list
|
|
19
|
+
* @param {number} count how many stops there are
|
|
20
|
+
* @returns {number | null}
|
|
21
|
+
*/
|
|
22
|
+
export function trapTarget(shift, current, count) {
|
|
23
|
+
if (count === 0) return null;
|
|
24
|
+
if (current < 0) return shift ? count - 1 : 0;
|
|
25
|
+
if (shift && current === 0) return count - 1;
|
|
26
|
+
if (!shift && current === count - 1) return 0;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Tests lib/focusTrap.mjs: which stops count in the drawer and where Tab wraps.
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { trapTarget, visibleElements } from './focusTrap.mjs';
|
|
5
|
+
|
|
6
|
+
const el = (name, drawn) => ({ name, getClientRects: () => (drawn ? [{}] : []) });
|
|
7
|
+
|
|
8
|
+
test('visibleElements drops the elements that are not drawn', () => {
|
|
9
|
+
const list = [el('a', true), el('hidden', false), el('b', true)];
|
|
10
|
+
assert.deepEqual(
|
|
11
|
+
visibleElements(list).map((e) => e.name),
|
|
12
|
+
['a', 'b'],
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('the last stop is the last drawn one, not a hidden element after it', () => {
|
|
17
|
+
const list = visibleElements([el('first', true), el('last', true), el('hidden link', false)]);
|
|
18
|
+
assert.equal(trapTarget(false, 1, list.length), 0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Tab on the last stop wraps to the first', () => {
|
|
22
|
+
assert.equal(trapTarget(false, 2, 3), 0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('Shift+Tab on the first stop wraps to the last', () => {
|
|
26
|
+
assert.equal(trapTarget(true, 0, 3), 2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('Tab inside the list is left to the browser', () => {
|
|
30
|
+
assert.equal(trapTarget(false, 0, 3), null);
|
|
31
|
+
assert.equal(trapTarget(true, 2, 3), null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('focus outside the list goes to its first or last stop', () => {
|
|
35
|
+
assert.equal(trapTarget(false, -1, 3), 0);
|
|
36
|
+
assert.equal(trapTarget(true, -1, 3), 2);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('an empty list has no target', () => {
|
|
40
|
+
assert.equal(trapTarget(false, -1, 0), null);
|
|
41
|
+
});
|
package/lib/folderChanges.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The changes that make a folder in a site the store's version of an app or a theme, with its store.json note, or remove
|
|
2
|
-
// it. Exposes folderChanges (used by src/store.ts
|
|
2
|
+
// it. Exposes folderChanges (used by src/store.ts). Depends on ./storeEntries.mjs (RECORD).
|
|
3
3
|
import { RECORD } from './storeEntries.mjs';
|
|
4
4
|
|
|
5
5
|
/** @typedef {{ path: string, oid: string }} RepoFile
|
package/lib/licenses.d.mts
CHANGED
|
@@ -37,7 +37,7 @@ export function iconPackWorks(pack: IconPack, root: string): {
|
|
|
37
37
|
export function systemPackages(): Set<any>;
|
|
38
38
|
/** The packages a folder's code imports by name (`from 'motion'`, `import('…')`, CSS `@import '…'` and `@plugin '…'`;
|
|
39
39
|
* not `import type`, which ships nothing), each with the files that import it: not node's own, the site's aliases
|
|
40
|
-
* (@theme/, @site/, @data/, @saturn/) or Vite's and Astro's virtual modules. */
|
|
40
|
+
* (@theme/, @site/, @data/, @saturn/, an app's @app/self) or Vite's and Astro's virtual modules. */
|
|
41
41
|
export function importsIn(folder: any): Map<any, any>;
|
|
42
42
|
/** A work an app or a theme declares (an entry of its `thirdParty`), as "Licenser" shows it, with its licence's text:
|
|
43
43
|
* an npm package's from its folder (its id its name), with the `author` and the licence `file` (in the app's or the
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Tests that importsIn skips @app/self and site aliases
|
|
2
|
+
// Run with: node --test lib/licenses.imports.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { importsIn } from './licenses.mjs';
|
|
9
|
+
|
|
10
|
+
const keysOf = (code) => {
|
|
11
|
+
const folder = mkdtempSync(join(tmpdir(), 'imports-'));
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(join(folder, 'a.ts'), code);
|
|
14
|
+
return [...importsIn(folder).keys()].sort();
|
|
15
|
+
} finally {
|
|
16
|
+
rmSync(folder, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test("skips an app's @app/self", () => {
|
|
21
|
+
const code = `import self from '@app/self';\nimport { animate } from 'motion';\n`;
|
|
22
|
+
assert.deepEqual(keysOf(code), ['motion']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('lists another @app/ import as a package', () => {
|
|
26
|
+
const code = `import x from '@app/other';\n`;
|
|
27
|
+
assert.deepEqual(keysOf(code), ['@app/other']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('skips the site aliases', () => {
|
|
31
|
+
const code = `import t from '@theme/x';\nimport s from '@site/y';\n`;
|
|
32
|
+
assert.deepEqual(keysOf(code), []);
|
|
33
|
+
});
|
package/lib/licenses.mjs
CHANGED
|
@@ -205,7 +205,7 @@ const builtins = new Set(builtinModules);
|
|
|
205
205
|
|
|
206
206
|
/** The packages a folder's code imports by name (`from 'motion'`, `import('…')`, CSS `@import '…'` and `@plugin '…'`;
|
|
207
207
|
* not `import type`, which ships nothing), each with the files that import it: not node's own, the site's aliases
|
|
208
|
-
* (@theme/, @site/, @data/, @saturn/) or Vite's and Astro's virtual modules. */
|
|
208
|
+
* (@theme/, @site/, @data/, @saturn/, an app's @app/self) or Vite's and Astro's virtual modules. */
|
|
209
209
|
export function importsIn(folder) {
|
|
210
210
|
const found = new Map();
|
|
211
211
|
if (!existsSync(folder)) return found;
|
|
@@ -225,7 +225,10 @@ export function importsIn(folder) {
|
|
|
225
225
|
...code.matchAll(/@(?:import|plugin)\s+(?:url\()?\s*['"]([^'"]+)['"]/g),
|
|
226
226
|
].map((m) => m[1]);
|
|
227
227
|
for (const specifier of specifiers) {
|
|
228
|
-
if (
|
|
228
|
+
if (
|
|
229
|
+
/^(\.|\/|node:|@theme\/|@site\/|@data\/|@saturn\/|@app\/self$|virtual:|astro:|https?:|data:|\$)/.test(specifier) ||
|
|
230
|
+
specifier.startsWith('#')
|
|
231
|
+
)
|
|
229
232
|
continue;
|
|
230
233
|
const name = packageName(specifier);
|
|
231
234
|
if (builtins.has(name)) continue;
|
|
@@ -423,12 +426,12 @@ function whereOf(file, root) {
|
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
/** The bundler's helpers that it writes into the bundle (Vite's and Rolldown's virtual modules): their package. The
|
|
426
|
-
* site's own virtual modules (the apps' folders, the theme's icons and coverage) are made of its own files. */
|
|
429
|
+
* site's own virtual modules (the apps' folders and @app/self, the theme's icons and coverage) are made of its own files. */
|
|
427
430
|
const HELPERS = [
|
|
428
431
|
[/^\0vite\//, 'vite'],
|
|
429
432
|
[/^\0rolldown\//, 'rolldown'],
|
|
430
433
|
];
|
|
431
|
-
const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons|material-symbols|theme-coverage)$/, /^\0site-theme-icons?(:|$)
|
|
434
|
+
const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons|material-symbols|theme-coverage)$/, /^\0site-theme-icons?(:|$)/, /^\0app-self:/];
|
|
432
435
|
|
|
433
436
|
/** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
|
|
434
437
|
* (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
|
package/lib/menu.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// The editor's menu as data: the two-level tree (menuTree) and which section/item/child is open for a route (currentPlace).
|
|
2
|
-
//
|
|
2
|
+
// A section that is a part with a list of its own (Apps: the installed apps, `apps`; Business: the business's parts,
|
|
3
|
+
// `company`) has not that part as an item: each of its children is an item, after the others.
|
|
3
4
|
// currentPart (which item is open for a route) is exported only for its tests; currentPlace uses it as its last rule.
|
|
4
5
|
// Pure, no browser; MainMenu.tsx draws the drawer from it. No dependencies.
|
|
5
6
|
|
|
7
|
+
// The sections that are a part whose children are the section's rows.
|
|
8
|
+
const HOISTED = ['apps', 'company'];
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* @param {{ id: string, title: string, href?: string, ids: string[] }[]} groups
|
|
8
12
|
* @param {(id: string) => { label: string, href: string, count: number | null }} partOf
|
|
@@ -15,9 +19,10 @@ export function menuTree(groups, partOf, childrenOf) {
|
|
|
15
19
|
if (group.href !== undefined) {
|
|
16
20
|
entry.href = group.href;
|
|
17
21
|
}
|
|
18
|
-
// The
|
|
19
|
-
const
|
|
20
|
-
const
|
|
22
|
+
// The part the section is (#apps, #company) is not a row of it: its children are rows, after the other items.
|
|
23
|
+
const own = HOISTED.includes(group.id) && group.ids.includes(group.id);
|
|
24
|
+
const hoisted = own ? childrenOf(group.id) : [];
|
|
25
|
+
const rows = own ? group.ids.filter((id) => id !== group.id) : group.ids;
|
|
21
26
|
entry.items = rows.map((id) => {
|
|
22
27
|
const item = { id, ...partOf(id) };
|
|
23
28
|
const children = childrenOf(id);
|