@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613
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 +31 -0
- package/astro.mjs +2 -1
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +59 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +9 -8
- package/scripts/check-content.mjs +12 -2
- package/scripts/check-language.mjs +0 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/editor-screens.mjs +3 -3
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +79 -78
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +39 -24
- package/src/BlockDialog.tsx +39 -18
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +10 -20
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +3 -3
- package/src/FooterSettings.tsx +2 -2
- package/src/FormFrame.tsx +4 -3
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +75 -82
- package/src/Licenses.tsx +8 -8
- package/src/ListFab.tsx +13 -0
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +11 -20
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +14 -22
- package/src/Screen.tsx +16 -6
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +6 -8
- package/src/ThemeStore.tsx +37 -16
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +7 -8
- package/src/checkSite.ts +86 -20
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +59 -38
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/licenseData.ts +3 -3
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuChildren.ts +5 -5
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +102 -21
- package/src/movedRoutes.test.mjs +128 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/schema.ts +2 -2
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +36 -31
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +33 -22
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/Base.astro +2 -2
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
|
@@ -35,7 +35,7 @@ export default function testimonialsList(say: Messages): OwnedList {
|
|
|
35
35
|
to: 'categories',
|
|
36
36
|
multiple: true,
|
|
37
37
|
add: own.addTag,
|
|
38
|
-
edit: { to: `
|
|
38
|
+
edit: { to: `new/${config.lists.tags}`, text: own.entries.createTag },
|
|
39
39
|
},
|
|
40
40
|
{ name: 'frontPage', label: own.entries.frontPage, type: 'boolean', default: true },
|
|
41
41
|
],
|
package/src/store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The app store ("Indstillinger" › "Appstore", #
|
|
1
|
+
// The app store ("Indstillinger" › "Appstore", #settings/appstore, AppList.tsx): the apps that are not in the site
|
|
2
2
|
// and can be installed. The store is reached through the site's Worker (/api/store/catalog, /api/store/files/<oid>;
|
|
3
3
|
// the Worker holds the site's key to it, and the editor never calls the store itself): each app of its catalog with
|
|
4
4
|
// its id, its version and the version of the contract it is written to (APP_API), its name, who made it, its icon
|
|
@@ -29,6 +29,8 @@ import type { StoreLicenses } from './licenseData';
|
|
|
29
29
|
import { APP_API, THEME_API } from '../lib/contracts.mjs';
|
|
30
30
|
import { RECORD, STANDARD, isText, validApp, validTheme } from '../lib/storeEntries.mjs';
|
|
31
31
|
import { timedFetch } from './storeFetch';
|
|
32
|
+
import { folderChanges } from '../lib/folderChanges.mjs';
|
|
33
|
+
import { moveChanges as moveItem, type SitePaths } from './legacyMove';
|
|
32
34
|
|
|
33
35
|
// The contracts this editor runs (lib/contracts.mjs, also read by the site's Worker).
|
|
34
36
|
export { APP_API, THEME_API };
|
|
@@ -111,6 +113,9 @@ export const SCREENSHOTS = 'screenshots';
|
|
|
111
113
|
/** An app in the store's catalog. */
|
|
112
114
|
export type StoreApp = {
|
|
113
115
|
id: string;
|
|
116
|
+
/** Its readable name for addresses (`publisher/name`) and the id it had before its permanent one (`legacy`). */
|
|
117
|
+
handle?: string;
|
|
118
|
+
legacy?: string;
|
|
114
119
|
version: string;
|
|
115
120
|
api: number;
|
|
116
121
|
label: string;
|
|
@@ -145,6 +150,8 @@ export type StoreApp = {
|
|
|
145
150
|
* site in it (screenshots/<n>-<key>.jpg, t.library.themeShots) and its folder, file by file. */
|
|
146
151
|
export type StoreTheme = {
|
|
147
152
|
id: string;
|
|
153
|
+
handle?: string;
|
|
154
|
+
legacy?: string;
|
|
148
155
|
version: string;
|
|
149
156
|
api: number;
|
|
150
157
|
name: string;
|
|
@@ -183,10 +190,10 @@ export type StoreRecord = { version: string; ref: string; files: StoreFile[] };
|
|
|
183
190
|
/** The store's address in the editor, under "Indstillinger" (its apps; its themes at STORE_THEMES), its address
|
|
184
191
|
* before (STORE_BEFORE, which opens it, App.tsx MOVED), and that of a developer's page (DEVELOPER_ROUTE/<slug>); no
|
|
185
192
|
* app can have the ids of the last two. */
|
|
186
|
-
export const STORE_ROUTE = '
|
|
187
|
-
export const STORE_THEMES = '
|
|
193
|
+
export const STORE_ROUTE = 'settings/appstore';
|
|
194
|
+
export const STORE_THEMES = 'settings/appstore/themes';
|
|
188
195
|
export const STORE_BEFORE = 'apps/butik';
|
|
189
|
-
export const DEVELOPER_ROUTE = 'apps/
|
|
196
|
+
export const DEVELOPER_ROUTE = 'apps/developer';
|
|
190
197
|
|
|
191
198
|
/** An app's folder in the site, and the note in it of where it came from. */
|
|
192
199
|
export const appFolder = (id: string) => `${config.apps.folder}/${id}`;
|
|
@@ -420,33 +427,6 @@ export function themeChanges(
|
|
|
420
427
|
return folderChanges(themeFolder(id), current, next);
|
|
421
428
|
}
|
|
422
429
|
|
|
423
|
-
/** The changes that make a folder in the site (`current`, its files now) the store's version of an app or a theme, with
|
|
424
|
-
* the note of where it came from, or remove it (`next` null). */
|
|
425
|
-
function folderChanges(
|
|
426
|
-
folder: string,
|
|
427
|
-
current: RepoFile[],
|
|
428
|
-
next: { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord } | null,
|
|
429
|
-
): Change[] {
|
|
430
|
-
const now = new Map(current.map((f) => [f.path, f.oid]));
|
|
431
|
-
const wanted: { path: string; content: string | Uint8Array; oid?: string }[] = next
|
|
432
|
-
? [
|
|
433
|
-
...next.files.map((f) => ({
|
|
434
|
-
path: `${folder}/${f.path}`,
|
|
435
|
-
content: f.content,
|
|
436
|
-
oid: next.record.files.find((r) => r.path === f.path)?.oid,
|
|
437
|
-
})),
|
|
438
|
-
{ path: `${folder}/${RECORD}`, content: `${JSON.stringify(next.record, null, 2)}\n` },
|
|
439
|
-
]
|
|
440
|
-
: [];
|
|
441
|
-
return [
|
|
442
|
-
// A file the site has as it is in the store is left as it is.
|
|
443
|
-
...wanted
|
|
444
|
-
.filter((w) => !w.oid || now.get(w.path) !== w.oid)
|
|
445
|
-
.map((w) => ({ path: w.path, content: w.content, oid: now.get(w.path) ?? null })),
|
|
446
|
-
...current.filter((f) => !wanted.some((w) => w.path === f.path)).map((f) => ({ path: f.path, content: null, oid: f.oid })),
|
|
447
|
-
];
|
|
448
|
-
}
|
|
449
|
-
|
|
450
430
|
/** The changes that make an app's folder in the site (`current`, its files now) the store's version, with the note of
|
|
451
431
|
* where it came from, or remove it (`next` null: uninstalling); and the site's file of apps with it among those
|
|
452
432
|
* installed or not (`installed`: their ids now, `file`: the file). In one commit. */
|
|
@@ -474,6 +454,31 @@ export function recordIn(current: RepoFile[], root: string): StoreRecord | undef
|
|
|
474
454
|
}
|
|
475
455
|
}
|
|
476
456
|
|
|
457
|
+
/** Where the site keeps what names an app or a theme (docs/store-ids.md); the popups' folder is the collection's. */
|
|
458
|
+
const sitePaths: SitePaths = {
|
|
459
|
+
appsFolder: config.apps.folder,
|
|
460
|
+
themesFolder: config.themes,
|
|
461
|
+
installed: config.apps.installed,
|
|
462
|
+
settings: config.apps.settings,
|
|
463
|
+
theme: config.theme,
|
|
464
|
+
icons: config.icons,
|
|
465
|
+
blockSettings: config.blockSettings,
|
|
466
|
+
popups: 'src/data/popups',
|
|
467
|
+
cookieList: config.cookieList,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/** The changes that update a store app or theme to its permanent folder: from its legacy folder (`legacy`, its files now
|
|
471
|
+
* `current`) with every id in the site's files (`site`) rewritten, or, without `legacy`, as appChanges or themeChanges
|
|
472
|
+
* do for the folder. In one commit (legacyMove.ts). */
|
|
473
|
+
export const moveChanges = (
|
|
474
|
+
entry: { id: string },
|
|
475
|
+
legacy: string | null,
|
|
476
|
+
next: Parameters<typeof moveItem>[2],
|
|
477
|
+
current: RepoFile[],
|
|
478
|
+
site: RepoFile[],
|
|
479
|
+
) => moveItem(entry, legacy, next, current, site, sitePaths);
|
|
480
|
+
export { appliedTo, installedHere, legacyOf, mergeChanges } from './legacyMove';
|
|
481
|
+
|
|
477
482
|
/** The files of an app installed from the store that were changed in the site since (in its folder, `current`, not
|
|
478
483
|
* as its note says), by their paths in the folder: an update writes over them. */
|
|
479
484
|
export function changedFiles(id: string, record: StoreRecord, current: RepoFile[], root = appFolder(id)): string[] {
|
package/src/tagUses.d.mts
CHANGED
|
@@ -11,7 +11,7 @@ export function promotionTags(d: Record<string, unknown>): string[];
|
|
|
11
11
|
*/
|
|
12
12
|
export function tagUses<F extends TagFile>(files: F[], folders: TagFolders): Map<string, Map<string, F>>;
|
|
13
13
|
/**
|
|
14
|
-
* A file record's use of a tag: the Filer address the file record is for, as a path under
|
|
14
|
+
* A file record's use of a tag: the Filer address the file record is for, as a path under files/.
|
|
15
15
|
* @param {TagFile} file
|
|
16
16
|
* @param {string} folder the file records folder
|
|
17
17
|
* @returns {{ path: string; name: string } | null}
|
package/src/tagUses.mjs
CHANGED
|
@@ -87,7 +87,7 @@ export function tagUses(files, folders) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* A file record's use of a tag: the Filer address the file record is for, as a path under
|
|
90
|
+
* A file record's use of a tag: the Filer address the file record is for, as a path under files/.
|
|
91
91
|
* @param {TagFile} file
|
|
92
92
|
* @param {string} folder the file records folder
|
|
93
93
|
* @returns {{ path: string; name: string } | null}
|
|
@@ -95,7 +95,7 @@ export function tagUses(files, folders) {
|
|
|
95
95
|
export function recordUse(file, folder) {
|
|
96
96
|
const record = folder !== undefined && isRecordPath(file.path, folder) ? parseRecord(file.text) : null;
|
|
97
97
|
if (!record) return null;
|
|
98
|
-
return { path: `
|
|
98
|
+
return { path: `files${record.src}`, name: record.src.slice(record.src.lastIndexOf('/') + 1) };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
package/src/tagUses.test.mjs
CHANGED
|
@@ -127,7 +127,7 @@ test('taggedWith lists the file records with a tag in their categories', () => {
|
|
|
127
127
|
|
|
128
128
|
test('recordUse gives the Filer path and the file name of a file record', () => {
|
|
129
129
|
assert.deepEqual(recordUse(andromeda, 'src/data/files'), {
|
|
130
|
-
path: '
|
|
130
|
+
path: 'files/images/uploads/2026/10/andromeda.webp',
|
|
131
131
|
name: 'andromeda.webp',
|
|
132
132
|
});
|
|
133
133
|
});
|
package/src/topBarSlot.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Where the editor's top bar stands: one
|
|
2
|
-
// rail or drawer (`aside`; App.tsx wraps the layout in TopBarLayout), which Screen.tsx draws its bar into.
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// Where the editor's top bar stands: one place at the top of the content column, beside the full-height navigation
|
|
2
|
+
// rail or drawer (`aside`; App.tsx wraps the layout in TopBarLayout), which Screen.tsx draws its bar into. The bar scrolls
|
|
3
|
+
// away with the page. Exposes TopBarLayout and useTopBarSlot.
|
|
4
|
+
// While the side panel is open the place ends at the panel's start edge (.editor-top-bar-slot in editor.css).
|
|
5
|
+
import { createContext, useContext, useState, type ReactNode } from 'react';
|
|
5
6
|
|
|
6
7
|
const Slot = createContext<HTMLElement | null>(null);
|
|
7
8
|
|
|
@@ -13,22 +14,13 @@ export const useTopBarSlot = () => useContext(Slot);
|
|
|
13
14
|
* screen's bar does not first stand in the screen and then move. */
|
|
14
15
|
export function TopBarLayout({ above, aside, children }: { above?: ReactNode; aside?: ReactNode; children: ReactNode }) {
|
|
15
16
|
const [slot, setSlot] = useState<HTMLDivElement | null>(null);
|
|
16
|
-
const [layout, setLayout] = useState<HTMLDivElement | null>(null);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!slot || !layout) return;
|
|
19
|
-
const measure = () => layout.style.setProperty('--top-bar-height', `${slot.offsetHeight}px`);
|
|
20
|
-
measure();
|
|
21
|
-
const watch = new ResizeObserver(measure);
|
|
22
|
-
watch.observe(slot);
|
|
23
|
-
return () => watch.disconnect();
|
|
24
|
-
}, [slot, layout]);
|
|
25
17
|
return (
|
|
26
|
-
<div
|
|
18
|
+
<div>
|
|
27
19
|
{above}
|
|
28
20
|
<div className="flex">
|
|
29
21
|
{aside}
|
|
30
22
|
<div className="min-w-0 flex-1">
|
|
31
|
-
<div ref={setSlot} className="
|
|
23
|
+
<div ref={setSlot} className="editor-top-bar-slot" />
|
|
32
24
|
<Slot.Provider value={slot}>{slot && children}</Slot.Provider>
|
|
33
25
|
</div>
|
|
34
26
|
</div>
|
package/src/ui.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The editor's parts: every button, field, switch, chip and row a screen is made of, each with one
|
|
2
2
|
// look, set here. A screen uses these and never writes the classes of a button or a field itself
|
|
3
3
|
// (CLAUDE.md), so the editor looks the same everywhere and a change here changes it everywhere. They
|
|
4
|
-
// are all shown, in every state, on the screen #
|
|
4
|
+
// are all shown, in every state, on the screen #appearance/parts (Parts.tsx). The screen itself, with its top bar,
|
|
5
5
|
// is Screen.tsx.
|
|
6
6
|
import {
|
|
7
7
|
useEffect,
|
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
SiteDatePicker,
|
|
73
73
|
SiteDialog,
|
|
74
74
|
siteDialogModalClass,
|
|
75
|
-
|
|
75
|
+
SiteFab,
|
|
76
76
|
SiteFabMenu,
|
|
77
77
|
SiteIconButton,
|
|
78
78
|
SiteLoadingIndicator,
|
|
@@ -1096,7 +1096,7 @@ const PAGE_STATES: PageState[] = ['published', 'draft', 'unlisted', 'scheduled']
|
|
|
1096
1096
|
* menu links to it and its date. */
|
|
1097
1097
|
export type Pickable = { value: string; label: string; state?: PageState; inMenu?: boolean; date?: string };
|
|
1098
1098
|
|
|
1099
|
-
type PickSort = '
|
|
1099
|
+
type PickSort = 'newest' | 'oldest' | 'az' | 'za';
|
|
1100
1100
|
type InMenu = '' | 'in' | 'out';
|
|
1101
1101
|
|
|
1102
1102
|
/** The order and the filters of a sheet of choices (PagePicker, MultiChoice), as over the lists: the arrows with A–Å and
|
|
@@ -1106,7 +1106,7 @@ type InMenu = '' | 'in' | 'out';
|
|
|
1106
1106
|
* and `arrange` orders and filters choices. */
|
|
1107
1107
|
export function usePickTools(all: Pickable[], keep: string) {
|
|
1108
1108
|
const dated = all.some((o) => o.date);
|
|
1109
|
-
const sorts: PickSort[] = dated ? ['
|
|
1109
|
+
const sorts: PickSort[] = dated ? ['newest', 'oldest', 'az', 'za'] : ['az', 'za'];
|
|
1110
1110
|
const sortKey = `editor-sort-valg-${keep}`;
|
|
1111
1111
|
const [savedSort, setSort] = useState<PickSort | null>(() => {
|
|
1112
1112
|
try {
|
|
@@ -1151,8 +1151,8 @@ export function usePickTools(all: Pickable[], keep: string) {
|
|
|
1151
1151
|
|
|
1152
1152
|
const byDate = (a: Pickable, b: Pickable) => (b.date ?? '').localeCompare(a.date ?? '');
|
|
1153
1153
|
const compare: Record<PickSort, (a: Pickable, b: Pickable) => number> = {
|
|
1154
|
-
|
|
1155
|
-
|
|
1154
|
+
newest: (a, b) => byDate(a, b) || byText(a.label, b.label),
|
|
1155
|
+
oldest: (a, b) => byDate(b, a) || byText(a.label, b.label),
|
|
1156
1156
|
az: (a, b) => byText(a.label, b.label),
|
|
1157
1157
|
za: (a, b) => byText(b.label, a.label),
|
|
1158
1158
|
};
|
|
@@ -2541,6 +2541,9 @@ export function useAtLeast(name: Exclude<Breakpoint, 'compact'>) {
|
|
|
2541
2541
|
return useMedia(atLeast(name));
|
|
2542
2542
|
}
|
|
2543
2543
|
|
|
2544
|
+
/** Whether the window is at least as wide as the breakpoint right now (not kept up to date: for a tap's handler). */
|
|
2545
|
+
export const atLeastNow = (name: Exclude<Breakpoint, 'compact'>) => matchMedia(atLeast(name)).matches;
|
|
2546
|
+
|
|
2544
2547
|
/** How the main menu is laid out in this window (lib/menu-layout.mjs): whether the navigation rail stands, which way it
|
|
2545
2548
|
* expands, and whether the top bar has the menu button. */
|
|
2546
2549
|
export function useMenuLayout() {
|
|
@@ -2602,7 +2605,7 @@ export function SideSheet({ title, onClose, children }: { title: string; onClose
|
|
|
2602
2605
|
headlineId={id}
|
|
2603
2606
|
closeLabel={t.common.close}
|
|
2604
2607
|
onClose={onClose}
|
|
2605
|
-
className="editor-reveal sticky top-
|
|
2608
|
+
className="editor-reveal sticky top-8 max-h-[calc(100dvh-3rem)] self-start"
|
|
2606
2609
|
>
|
|
2607
2610
|
<div className="space-y-5">{children}</div>
|
|
2608
2611
|
</SiteSideSheet>
|
|
@@ -2649,12 +2652,12 @@ export function SectionList({
|
|
|
2649
2652
|
}
|
|
2650
2653
|
|
|
2651
2654
|
/** The "on this page" card at the end of a long form on large and wider screens: the theme's outlined card, as tall as
|
|
2652
|
-
* its content (it scrolls inside only when that is taller than the window
|
|
2655
|
+
* its content (it scrolls inside only when that is taller than the window), with the small `caption`
|
|
2653
2656
|
* (Afsnit), the form's `title` under it and the form's SectionList (`children`). It stays in view while the form
|
|
2654
2657
|
* scrolls. Its place is FormColumns' `pane`, against the window's end edge; below large it is not drawn. */
|
|
2655
2658
|
export function SectionPane({ caption, title, children }: { caption: string; title: string; children: ReactNode }) {
|
|
2656
2659
|
return (
|
|
2657
|
-
<div className={`${siteCardClass('outlined')} max-h-[calc(100dvh-
|
|
2660
|
+
<div className={`${siteCardClass('outlined')} max-h-[calc(100dvh-2*var(--section-pane-gap))] overflow-y-auto`}>
|
|
2658
2661
|
<p className="section-pane-caption m-0 text-muted">{caption}</p>
|
|
2659
2662
|
<h2 className="section-pane-title m-0 mb-3 text-ink">{title}</h2>
|
|
2660
2663
|
{children}
|
|
@@ -2737,11 +2740,11 @@ function MainRail({ label, onOpen, children }: { label: string; onOpen: () => vo
|
|
|
2737
2740
|
// menu button. The header cell starts at the drawer items' outer edge: the row has no start padding, and the header (the
|
|
2738
2741
|
// account row, the back row) brings the items' inline padding itself, so its pill (editor.css, .editor-main-drawer-header)
|
|
2739
2742
|
// has the shape of an item's and the avatar or arrow stands over the items' icons. The button is centred on the 1.5rem box
|
|
2740
|
-
// of the items' trailing arrows (a negative end margin from the button's own width).
|
|
2743
|
+
// of the items' trailing arrows (a negative end margin from the button's own width). The row is the top bar's height (`--top-app-bar-height`), so the divider (middle inset) under it stands level with the top bar's line.
|
|
2741
2744
|
function MainDrawerMenuButton({ onClick, header }: { onClick: () => void; header?: ReactNode }) {
|
|
2742
2745
|
return (
|
|
2743
2746
|
<>
|
|
2744
|
-
<div className="editor-main-drawer-header
|
|
2747
|
+
<div className="editor-main-drawer-header flex min-h-(--top-app-bar-height,4rem) items-center gap-1 p-(--navigation-drawer-item-padding,0_1rem) py-0 ps-0">
|
|
2745
2748
|
<div className="min-w-0 flex-1">{header}</div>
|
|
2746
2749
|
<MenuButton label={t.library.closeMenu} open onClick={onClick} className="-me-[calc((var(--icon-button-width)-1.5rem)/2)]" />
|
|
2747
2750
|
</div>
|
|
@@ -3114,13 +3117,13 @@ export function FormColumns({
|
|
|
3114
3117
|
cards?: boolean;
|
|
3115
3118
|
sticky?: boolean;
|
|
3116
3119
|
/** A SectionPane, on large and wider screens against the window's end edge under the top bar (--section-pane-width
|
|
3117
|
-
* wide,
|
|
3120
|
+
* wide, in an absolute strip along the content that its card sticks in while the form scrolls; editor.css lets the content column keep clear of it); below large nothing.
|
|
3118
3121
|
* Instead of `first` and `side`. */
|
|
3119
3122
|
pane?: ReactNode;
|
|
3120
3123
|
}) {
|
|
3121
3124
|
// On a wide computer screen (large) each of the right column's parts on the theme's outlined card.
|
|
3122
3125
|
const carded = useAtLeast('large') && cards;
|
|
3123
|
-
const right = `space-y-6 large:col-start-2 ${carded ? siteCardClass('outlined') : ''} ${sticky ? 'large:sticky large:top-
|
|
3126
|
+
const right = `space-y-6 large:col-start-2 ${carded ? siteCardClass('outlined') : ''} ${sticky ? 'large:sticky large:top-8' : ''}`;
|
|
3124
3127
|
return (
|
|
3125
3128
|
<div
|
|
3126
3129
|
className={`flex flex-col gap-6 ${
|
|
@@ -3131,8 +3134,8 @@ export function FormColumns({
|
|
|
3131
3134
|
<div className="min-w-0 space-y-6 large:col-start-1 large:row-span-2 large:row-start-1">{main}</div>
|
|
3132
3135
|
{side && <div className={`${right} ${first ? 'large:row-start-2' : 'large:row-span-2 large:row-start-1'}`}>{side}</div>}
|
|
3133
3136
|
{pane && (
|
|
3134
|
-
<div className="editor-section-pane hidden large:
|
|
3135
|
-
{pane}
|
|
3137
|
+
<div className="editor-section-pane hidden large:absolute large:inset-y-0 large:end-0 large:z-10 large:block large:w-(--section-pane-width) large:pe-(--section-pane-gap)">
|
|
3138
|
+
<div className="large:sticky large:top-(--section-pane-gap) large:mt-(--section-pane-gap)">{pane}</div>
|
|
3136
3139
|
</div>
|
|
3137
3140
|
)}
|
|
3138
3141
|
</div>
|
|
@@ -3183,7 +3186,7 @@ export function LeadingToken({ kind, value, weight }: { kind: TokenKind; value:
|
|
|
3183
3186
|
// ---------------------------------------------------------------------------------------------------
|
|
3184
3187
|
// The sheet from the bottom
|
|
3185
3188
|
|
|
3186
|
-
/** A screen's top bar (Screen.tsx): the theme's top app bar (Material 3's), at the top of the screen
|
|
3189
|
+
/** A screen's top bar (Screen.tsx): the theme's top app bar (Material 3's), at the top of the screen (it scrolls away with the page),
|
|
3187
3190
|
* below a phone's clock, and marked as the theme marks it once the screen has scrolled under it (data-stuck). */
|
|
3188
3191
|
export function TopAppBar({ children }: { children: ReactNode }) {
|
|
3189
3192
|
const [stuck, setStuck] = useState(false);
|
|
@@ -3194,7 +3197,7 @@ export function TopAppBar({ children }: { children: ReactNode }) {
|
|
|
3194
3197
|
return () => removeEventListener('scroll', check);
|
|
3195
3198
|
}, []);
|
|
3196
3199
|
return (
|
|
3197
|
-
<SiteTopAppBar className="
|
|
3200
|
+
<SiteTopAppBar className="pt-[env(safe-area-inset-top)]" data-stuck={stuck || undefined}>
|
|
3198
3201
|
{children}
|
|
3199
3202
|
</SiteTopAppBar>
|
|
3200
3203
|
);
|
|
@@ -3855,14 +3858,22 @@ export function NewChoice<T extends string>({
|
|
|
3855
3858
|
);
|
|
3856
3859
|
}
|
|
3857
3860
|
|
|
3858
|
-
/** One action of a FAB menu: its icon, its name and
|
|
3859
|
-
|
|
3861
|
+
/** One action of a FAB menu: its icon, its name and what it does: a link (`href`) or a button (`onClick`). While `busy` its
|
|
3862
|
+
* action runs, and it is disabled as with `disabled`. */
|
|
3863
|
+
export type FabAction = { icon: Icon; label: string; href?: string; onClick?: () => void; busy?: boolean; disabled?: boolean };
|
|
3860
3864
|
|
|
3861
3865
|
// An action's icon, drawn.
|
|
3862
3866
|
const iconOf = ({ icon: Item }: FabAction) => <Item />;
|
|
3863
3867
|
|
|
3868
|
+
// What an action's element gets besides its icon and name: busy is disabled and aria-busy (a link has neither).
|
|
3869
|
+
const fabProps = ({ href, onClick, busy, disabled }: FabAction) => ({
|
|
3870
|
+
href,
|
|
3871
|
+
onClick,
|
|
3872
|
+
...(href === undefined ? { disabled: disabled || busy, 'aria-busy': busy || undefined } : {}),
|
|
3873
|
+
});
|
|
3874
|
+
|
|
3864
3875
|
/** The screen's main actions at the bottom right (.editor-fab, editor.css): the theme's FAB menu, opened and closed as
|
|
3865
|
-
* on the site (setUpFabMenu), or with one action that action as
|
|
3876
|
+
* on the site (setUpFabMenu), or with one action that action as a FAB (its icon, its name as its tooltip and read by screen readers; a link, or a button); nothing without any. Drawn in a portal
|
|
3866
3877
|
* on <body>, since the screen's entrance animation (a transform) would make its <main> the containing block of a fixed FAB. */
|
|
3867
3878
|
export function FabMenu({ icon: Glyph, label, items }: { icon: Icon; label: string; items: FabAction[] }) {
|
|
3868
3879
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -3879,10 +3890,10 @@ export function FabMenu({ icon: Glyph, label, items }: { icon: Icon; label: stri
|
|
|
3879
3890
|
icon={<Glyph />}
|
|
3880
3891
|
label={label}
|
|
3881
3892
|
closeLabel={t.common.close}
|
|
3882
|
-
items={items.map((item) => ({
|
|
3893
|
+
items={items.map((item) => ({ label: item.label, icon: iconOf(item), ...fabProps(item) }))}
|
|
3883
3894
|
/>
|
|
3884
3895
|
) : (
|
|
3885
|
-
<
|
|
3896
|
+
<SiteFab icon={iconOf(items[0])} label={items[0].label} title={items[0].label} {...fabProps(items[0])} />
|
|
3886
3897
|
)}
|
|
3887
3898
|
</div>,
|
|
3888
3899
|
document.body,
|
package/src/virtual.d.ts
CHANGED
|
@@ -27,3 +27,9 @@ declare module 'virtual:theme-coverage' {
|
|
|
27
27
|
} | null;
|
|
28
28
|
export default coverage;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
// The id of the app whose folder the importing file is in (appSelf in site/appSelf.mjs; app contract 3).
|
|
32
|
+
declare module '@app/self' {
|
|
33
|
+
const self: { readonly id: string };
|
|
34
|
+
export default self;
|
|
35
|
+
}
|
package/starter/package.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@material-symbols/svg-400": "0.48.2",
|
|
33
33
|
"@material/material-color-utilities": "^0.3.0",
|
|
34
34
|
"@milkdown/kit": "^7.22.3",
|
|
35
|
-
"@morphemecms/cms": "0.
|
|
35
|
+
"@morphemecms/cms": "0.12.0",
|
|
36
36
|
"@phosphor-icons/core": "^2.1.1",
|
|
37
37
|
"@phosphor-icons/react": "^2.1.10",
|
|
38
38
|
"@tailwindcss/typography": "^0.5.20",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The top of a page's text (PageLayout.astro): the link back to its parent page and the page's H1 (data-editor="heading", which
|
|
3
|
+
// the editor's preview fills in). Props: parent ({ href, title }), heading, headingHtml (the heading with its coloured words).
|
|
4
|
+
import Icon from '@theme/components/Icon.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
parent?: { href: string; title: string };
|
|
8
|
+
heading: string;
|
|
9
|
+
headingHtml?: string;
|
|
10
|
+
}
|
|
11
|
+
const { parent, heading, headingHtml } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{parent && (
|
|
15
|
+
<a href={parent.href} class="inline-flex items-center gap-1.5 type-body-medium text-muted hover:text-accent">
|
|
16
|
+
<Icon name="arrow-left" size={16} />
|
|
17
|
+
{parent.title}
|
|
18
|
+
</a>
|
|
19
|
+
)}
|
|
20
|
+
<h1 data-editor="heading" class="theme-title mt-3 max-w-[22ch]">
|
|
21
|
+
{headingHtml ? <Fragment set:html={headingHtml} /> : heading}
|
|
22
|
+
</h1>
|
package/theme/layouts/Base.astro
CHANGED
|
@@ -164,7 +164,7 @@ const jsonLd = {
|
|
|
164
164
|
</head>
|
|
165
165
|
<body class="flex min-h-dvh flex-col">
|
|
166
166
|
<a
|
|
167
|
-
href="#
|
|
167
|
+
href="#main-content"
|
|
168
168
|
class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:rounded-full focus:bg-accent focus:px-4 focus:py-2 focus:text-on-accent"
|
|
169
169
|
>
|
|
170
170
|
{siteT.theme.skipToContent}
|
|
@@ -172,7 +172,7 @@ const jsonLd = {
|
|
|
172
172
|
|
|
173
173
|
<Header />
|
|
174
174
|
|
|
175
|
-
<main id="
|
|
175
|
+
<main id="main-content" class="flex-1">
|
|
176
176
|
<slot />
|
|
177
177
|
</main>
|
|
178
178
|
|
|
@@ -8,12 +8,23 @@
|
|
|
8
8
|
// paragraph is the lead (type-lead, ui.css: the --type-lead-* tokens), muted and 52ch wide.
|
|
9
9
|
// data-editor marks what the editor's preview fills in with what is in its form ("Forhåndsvis",
|
|
10
10
|
// packages/editor/src/pagePreview.tsx): the heading, the text, and the booking prompt at the end.
|
|
11
|
+
// A page whose H1 has a section colour (the marker before it, lib/section-marker.mjs) draws the heading and the text up to the
|
|
12
|
+
// first H2 in one panel (data-editor="h1-section", sectionClass); with a column beside the text the panel holds the heading only.
|
|
11
13
|
import Base from '@theme/layouts/Base.astro';
|
|
12
|
-
import
|
|
14
|
+
import PageHeading from '@theme/components/PageHeading.astro';
|
|
13
15
|
import EndBlock from '@theme/components/EndBlock.astro';
|
|
14
16
|
import Block from '@morphemecms/cms/src/standard/Block.astro';
|
|
15
17
|
import { blockLoads, endBlock, sideBlock } from '@morphemecms/cms/src/standard/views';
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getPages,
|
|
20
|
+
h1SectionColor,
|
|
21
|
+
pageHeading,
|
|
22
|
+
pageHeadingHtml,
|
|
23
|
+
parentPage,
|
|
24
|
+
stateOf,
|
|
25
|
+
withoutH1,
|
|
26
|
+
type Page,
|
|
27
|
+
} from '@morphemecms/cms/site/content';
|
|
17
28
|
import { blockTexts, nav } from '@morphemecms/cms/site/site';
|
|
18
29
|
import { firstPicture, picturesOf } from '@morphemecms/cms/site/images.mjs';
|
|
19
30
|
import { splitBlocks } from '@morphemecms/cms/lib/blocks.mjs';
|
|
@@ -22,6 +33,7 @@ import { offeredTemplates } from '@morphemecms/cms/site/templates';
|
|
|
22
33
|
import { markdownHtml } from '@morphemecms/cms/lib/inline';
|
|
23
34
|
import Container from '@theme/components/Container.astro';
|
|
24
35
|
import RichText from '@theme/components/RichText.astro';
|
|
36
|
+
import { introOf, sectionClass } from '@morphemecms/cms/lib/text-sections.mjs';
|
|
25
37
|
import Snackbar from '@theme/ui/Snackbar';
|
|
26
38
|
import { isPreview, livePath, PREVIEW, previewId } from '@morphemecms/cms/site/noindex.mjs';
|
|
27
39
|
import { siteT } from '@morphemecms/cms/src/i18n';
|
|
@@ -51,7 +63,10 @@ const picture = firstPicture(page.body);
|
|
|
51
63
|
// e.g. "Book en tid", as on most pages) is shown across the page below the text and the column next to it; not when
|
|
52
64
|
// it is hidden (RichText.astro leaves hidden blocks out).
|
|
53
65
|
// Without the H1 at the top of the text (the page's heading, drawn above).
|
|
54
|
-
const
|
|
66
|
+
const text = await Astro.slots.render('default');
|
|
67
|
+
const html = withoutH1(text);
|
|
68
|
+
// The colour of the H1's section (the marker before the H1): the heading and the text up to the first H2 are one panel.
|
|
69
|
+
const h1Color = h1SectionColor(text);
|
|
55
70
|
const end = endBlock(html);
|
|
56
71
|
// The questions in the text's "Spørgsmål og svar" blocks (not the hidden ones), for the structured data
|
|
57
72
|
// (an FAQPage, packages/editor/site/schema.ts); the answers as HTML, as on the page (the block's load.ts).
|
|
@@ -76,6 +91,12 @@ const side =
|
|
|
76
91
|
(await blockLoads[sideType].load({}, { texts: blockTexts[sideType] ?? {}, index: 0, path: current, preview, html: sideText })) !== null
|
|
77
92
|
? sideType
|
|
78
93
|
: null;
|
|
94
|
+
// In a coloured H1 section the text up to the first H2 (intro) is in the panel with the heading; with a column beside the text
|
|
95
|
+
// the text stays whole in its grid and the panel holds the heading only.
|
|
96
|
+
const { intro, rest } = h1Color && !side ? introOf(sideText) : { intro: '', rest: sideText };
|
|
97
|
+
const textClass = side
|
|
98
|
+
? 'prose max-w-[66ch]'
|
|
99
|
+
: 'prose max-w-none [&>:not(.not-prose)]:max-w-[66ch] [&>p:first-child]:type-lead [&>p:first-child]:text-muted [&>p:first-child]:max-w-[52ch]';
|
|
79
100
|
---
|
|
80
101
|
|
|
81
102
|
<Base
|
|
@@ -96,28 +117,24 @@ const side =
|
|
|
96
117
|
>
|
|
97
118
|
<article data-template={template}>
|
|
98
119
|
<Container as="header" class="pt-8 medium:pt-14">
|
|
99
|
-
{
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
{
|
|
103
|
-
|
|
120
|
+
{h1Color ? (
|
|
121
|
+
<section class={sectionClass(h1Color)} data-editor="h1-section">
|
|
122
|
+
<PageHeading parent={parent} heading={heading} headingHtml={headingHtml} />
|
|
123
|
+
{intro && (
|
|
124
|
+
<div data-editor="intro" class={textClass}>
|
|
125
|
+
<RichText html={intro} path={current} preview={preview} />
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</section>
|
|
129
|
+
) : (
|
|
130
|
+
<PageHeading parent={parent} heading={heading} headingHtml={headingHtml} />
|
|
104
131
|
)}
|
|
105
|
-
<h1 data-editor="heading" class="theme-title mt-3 max-w-[22ch]">
|
|
106
|
-
{headingHtml ? <Fragment set:html={headingHtml} /> : heading}
|
|
107
|
-
</h1>
|
|
108
132
|
</Container>
|
|
109
133
|
|
|
110
134
|
<Container as="div" class="mt-8 medium:mt-12">
|
|
111
135
|
<div {...(side ? { class: 'page-with-side', 'data-side': template === 'sidebar-left' ? 'left' : 'right' } : {})}>
|
|
112
|
-
<div
|
|
113
|
-
|
|
114
|
-
class={
|
|
115
|
-
side
|
|
116
|
-
? 'prose max-w-[66ch]'
|
|
117
|
-
: 'prose max-w-none [&>:not(.not-prose)]:max-w-[66ch] [&>p:first-child]:type-lead [&>p:first-child]:text-muted [&>p:first-child]:max-w-[52ch]'
|
|
118
|
-
}
|
|
119
|
-
>
|
|
120
|
-
<RichText html={sideText} path={current} preview={preview} />
|
|
136
|
+
<div data-editor="text" class={textClass}>
|
|
137
|
+
<RichText html={rest} path={current} preview={preview} />
|
|
121
138
|
</div>
|
|
122
139
|
{side && (
|
|
123
140
|
<aside class="page-side">
|
package/theme/styles/global.css
CHANGED