@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The legacy move: a store app or theme installed under its old id (folder `src/apps/mailchimp/`) moves to its permanent
|
|
2
|
+
// id (`app_` or `theme_` plus 16 hex) when it is updated. Exposes legacyOf (is a legacy copy installed?), installedHere (does the store list show an entry as installed?) and moveChanges
|
|
3
|
+
// (one list of changes: new folder, old files deleted, every id rewritten; docs/store-ids.md Table 1). Pure; the site's
|
|
4
|
+
// paths are passed in (store.ts binds them to the site's config). Depends on lib/folderChanges.mjs.
|
|
5
|
+
import type { Change, RepoFile } from './github';
|
|
6
|
+
import { folderChanges } from '../lib/folderChanges.mjs';
|
|
7
|
+
import type { StoreRecord } from './store';
|
|
8
|
+
|
|
9
|
+
/** Where a site keeps what names an app or a theme (the site's config, and the popups' folder). */
|
|
10
|
+
export type SitePaths = {
|
|
11
|
+
appsFolder: string;
|
|
12
|
+
themesFolder: string;
|
|
13
|
+
installed: string;
|
|
14
|
+
settings: string;
|
|
15
|
+
theme: string;
|
|
16
|
+
icons: string;
|
|
17
|
+
blockSettings: string;
|
|
18
|
+
popups: string;
|
|
19
|
+
cookieList: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Next = { files: { path: string; content: string | Uint8Array }[]; record: StoreRecord };
|
|
23
|
+
|
|
24
|
+
/** The old id of a catalog entry (`entry.legacy`) when a folder of that name is installed from the store (`folders`: the
|
|
25
|
+
* apps or themes the site has, with `store` when the folder has a store.json); else null (none, or the site's own). */
|
|
26
|
+
export function legacyOf(entry: { legacy?: string }, folders: { id: string; store?: unknown }[]): string | null {
|
|
27
|
+
return entry.legacy && folders.some((f) => f.id === entry.legacy && f.store) ? entry.legacy : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether the store's list counts a catalog entry as installed: its own id is installed, or its old id (`entry.legacy`)
|
|
31
|
+
* is, as a copy from the store (`folders`: the apps or themes the site has). `installed`: the ids installed. */
|
|
32
|
+
export function installedHere(
|
|
33
|
+
entry: { id: string; legacy?: string },
|
|
34
|
+
installed: string[],
|
|
35
|
+
folders: { id: string; store?: unknown }[],
|
|
36
|
+
): boolean {
|
|
37
|
+
const old = legacyOf(entry, folders);
|
|
38
|
+
return installed.includes(entry.id) || (old !== null && installed.includes(old));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parse = (text: string): any => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A file with a value changed (`change` gives the new JSON, or undefined when nothing in it names the legacy id). */
|
|
50
|
+
function rewrite(files: RepoFile[], change: (json: any) => any): Change[] {
|
|
51
|
+
return files.flatMap((f) => {
|
|
52
|
+
const json = parse(f.text);
|
|
53
|
+
const out = json && typeof json === 'object' ? change(json) : undefined;
|
|
54
|
+
return out === undefined ? [] : [{ path: f.path, content: `${JSON.stringify(out, null, 2)}\n`, oid: f.oid }];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const inFolder = (site: RepoFile[], folder: string) => site.filter((f) => f.path.startsWith(`${folder}/`) && f.path.endsWith('.json'));
|
|
59
|
+
const only = (site: RepoFile[], path: string) => site.filter((f) => f.path === path);
|
|
60
|
+
|
|
61
|
+
/** The changes that make the folder `current` (the legacy folder, or the item's own when it has no legacy) the store's
|
|
62
|
+
* version of `entry` in its permanent folder. With a legacy id they also delete the legacy folder's files and rewrite
|
|
63
|
+
* every value that names it (`site`: the site's files now). In one commit. */
|
|
64
|
+
export function moveChanges(
|
|
65
|
+
entry: { id: string },
|
|
66
|
+
legacy: string | null,
|
|
67
|
+
next: Next,
|
|
68
|
+
current: RepoFile[],
|
|
69
|
+
site: RepoFile[],
|
|
70
|
+
paths: SitePaths,
|
|
71
|
+
): Change[] {
|
|
72
|
+
const isApp = entry.id.startsWith('app_');
|
|
73
|
+
const folder = (id: string) => `${isApp ? paths.appsFolder : paths.themesFolder}/${id}`;
|
|
74
|
+
if (!legacy) return folderChanges(folder(entry.id), current, next);
|
|
75
|
+
const swap = (v: unknown) => (v === legacy ? entry.id : undefined);
|
|
76
|
+
const changes = [...folderChanges(folder(entry.id), [], next), ...folderChanges(folder(legacy), current, null)];
|
|
77
|
+
if (isApp) {
|
|
78
|
+
changes.push(
|
|
79
|
+
...rewrite(only(site, paths.installed), (j) =>
|
|
80
|
+
Array.isArray(j.installed) && j.installed.includes(legacy)
|
|
81
|
+
? { ...j, installed: [...new Set(j.installed.map((x: unknown) => (x === legacy ? entry.id : x)))].sort() }
|
|
82
|
+
: undefined,
|
|
83
|
+
),
|
|
84
|
+
...rewrite(inFolder(site, paths.popups), (j) => (swap(j.type) ? { ...j, type: entry.id } : undefined)),
|
|
85
|
+
...rewrite(inFolder(site, paths.cookieList), (j) => (swap(j.app) ? { ...j, app: entry.id } : undefined)),
|
|
86
|
+
...rewrite(only(site, paths.blockSettings), (j) => {
|
|
87
|
+
const off = Array.isArray(j.off)
|
|
88
|
+
? j.off.map((x: unknown) => (typeof x === 'string' && x.endsWith(`/${legacy}`) ? `${x.slice(0, -legacy.length)}${entry.id}` : x))
|
|
89
|
+
: [];
|
|
90
|
+
return off.some((x: unknown, i: number) => x !== j.off[i]) ? { ...j, off } : undefined;
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
// The settings file is renamed with its content kept.
|
|
94
|
+
const old = site.find((f) => f.path === `${paths.settings}/${legacy}.json`);
|
|
95
|
+
if (old)
|
|
96
|
+
changes.push(
|
|
97
|
+
{ path: old.path, content: null, oid: old.oid },
|
|
98
|
+
{ path: `${paths.settings}/${entry.id}.json`, content: old.text, oid: null },
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
changes.push(
|
|
102
|
+
...rewrite(only(site, paths.theme), (j) => (swap(j.theme) ? { ...j, theme: entry.id } : undefined)),
|
|
103
|
+
...rewrite(only(site, paths.icons), (j) =>
|
|
104
|
+
typeof j.pack === 'string' && j.pack.startsWith(`${legacy}/`)
|
|
105
|
+
? { ...j, pack: `${entry.id}${j.pack.slice(legacy.length)}` }
|
|
106
|
+
: undefined,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return changes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The site's files as they are after `changes` (a changed file with its new text, a deleted one gone, a new one added),
|
|
114
|
+
* for what must be worked out from the site after a move (the cookie list). */
|
|
115
|
+
export function appliedTo(site: RepoFile[], changes: Change[]): RepoFile[] {
|
|
116
|
+
const by = new Map(changes.map((c) => [c.path, c]));
|
|
117
|
+
const kept = site.flatMap((f) => {
|
|
118
|
+
const c = by.get(f.path);
|
|
119
|
+
return !c ? [f] : typeof c.content === 'string' ? [{ ...f, text: c.content }] : [];
|
|
120
|
+
});
|
|
121
|
+
const added = changes.filter((c) => typeof c.content === 'string' && !site.some((f) => f.path === c.path));
|
|
122
|
+
return [...kept, ...added.map((c) => ({ path: c.path, oid: '', text: c.content as string }))];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Two lists of changes as one: for a path in both, the later one wins (a file rewritten by the move, then deleted by the
|
|
126
|
+
* cookie list), its `oid` that of the site's file; so no file is changed twice. */
|
|
127
|
+
export function mergeChanges(first: Change[], later: Change[]): Change[] {
|
|
128
|
+
const oid = new Map(first.map((c) => [c.path, c.oid]));
|
|
129
|
+
const paths = new Set(later.map((c) => c.path));
|
|
130
|
+
return [...first.filter((c) => !paths.has(c.path)), ...later.map((c) => (oid.has(c.path) ? { ...c, oid: oid.get(c.path)! } : c))];
|
|
131
|
+
}
|
package/src/licenseData.ts
CHANGED
|
@@ -20,8 +20,8 @@ export type ThirdParty =
|
|
|
20
20
|
/** The licence of one's own work that grants nothing ("all rights reserved"), as npm writes it. */
|
|
21
21
|
export const UNLICENSED = 'UNLICENSED';
|
|
22
22
|
|
|
23
|
-
/** Who brings a work: the editor, the site's own code, or an app or a theme (`app/<id>`, `
|
|
24
|
-
export type LicenseWhere = 'editor' | 'site' | `app/${string}` | `
|
|
23
|
+
/** Who brings a work: the editor, the site's own code, or an app or a theme (`app/<id>`, `theme/<id>`, LicenseOwner.id). */
|
|
24
|
+
export type LicenseWhere = 'editor' | 'site' | `app/${string}` | `theme/${string}`;
|
|
25
25
|
|
|
26
26
|
/** A third-party work as the build found it: its id (a package's name; `ikoner/<id>` for an icon set; `<owner>/<name>`
|
|
27
27
|
* for another work an app or a theme declares), its name, version, who made it, its licence's name and text (`standard`:
|
|
@@ -46,7 +46,7 @@ export type LicensedWork = {
|
|
|
46
46
|
/** An app or a theme with its own licence (`text`: LICENSE in its folder, none when it is UNLICENSED) and the works it
|
|
47
47
|
* brings (their ids: those it declares, and those shipped through them alone), and whether the site is built with it. */
|
|
48
48
|
export type LicenseOwner = {
|
|
49
|
-
id: `app/${string}` | `
|
|
49
|
+
id: `app/${string}` | `theme/${string}`;
|
|
50
50
|
kind: 'app' | 'theme';
|
|
51
51
|
/** Its folder's name in src/apps/ or src/themes/. */
|
|
52
52
|
key: string;
|
package/src/markdown.mjs
CHANGED
|
@@ -130,8 +130,8 @@ export const textColorSchema = $markSchema('textColor', () => ({
|
|
|
130
130
|
},
|
|
131
131
|
}));
|
|
132
132
|
|
|
133
|
-
// The colour of a section (packages/editor/lib/section-marker.mjs): the marker line before an H2, or before the label above it
|
|
134
|
-
// (eyebrow), is read into the
|
|
133
|
+
// The colour of a section (packages/editor/lib/section-marker.mjs): the marker line before an H1 or H2, or before the label above it
|
|
134
|
+
// (eyebrow), is read into the heading's attribute `section` and written back before it. It must be read before remarkSiteBlocks
|
|
135
135
|
// sees it, so it is the first plugin of that kind in makeEditor. A marker with nothing to colour is dropped.
|
|
136
136
|
const htmlOf = (node) => {
|
|
137
137
|
const only = node?.type === 'paragraph' && node.children?.length === 1 ? node.children[0] : node;
|
|
@@ -143,12 +143,12 @@ const remarkSectionMarkers = $remark('remarkSectionMarkers', () => () => (tree)
|
|
|
143
143
|
const color = markerColor(htmlOf(child));
|
|
144
144
|
if (color === null) return true;
|
|
145
145
|
const next = isLabelLine(htmlOf(parts[i + 1])) ? parts[i + 2] : parts[i + 1];
|
|
146
|
-
if (next?.type === 'heading' && next.depth
|
|
146
|
+
if (next?.type === 'heading' && next.depth <= 2) next.section = color;
|
|
147
147
|
return false;
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
// The commonmark heading with the section colour as an attribute (only an H2 has one). Written back before the heading, or
|
|
151
|
+
// The commonmark heading with the section colour as an attribute (only an H1 or H2 has one). Written back before the heading, or
|
|
152
152
|
// before its label, which is already the last thing written (a line of HTML).
|
|
153
153
|
/** @type {import('@milkdown/kit/utils').$NodeSchema<'heading'>} */
|
|
154
154
|
export const headingWithSection = headingSchema.extendSchema((prev) => (ctx) => {
|
|
@@ -159,7 +159,7 @@ export const headingWithSection = headingSchema.extendSchema((prev) => (ctx) =>
|
|
|
159
159
|
parseMarkdown: {
|
|
160
160
|
match: base.parseMarkdown.match,
|
|
161
161
|
runner: (state, node, type) => {
|
|
162
|
-
state.openNode(type, { level: node.depth, section: node.depth
|
|
162
|
+
state.openNode(type, { level: node.depth, section: node.depth <= 2 ? (node.section ?? null) : null });
|
|
163
163
|
state.next(node.children);
|
|
164
164
|
state.closeNode();
|
|
165
165
|
},
|
|
@@ -168,8 +168,11 @@ export const headingWithSection = headingSchema.extendSchema((prev) => (ctx) =>
|
|
|
168
168
|
match: base.toMarkdown.match,
|
|
169
169
|
runner: (state, node) => {
|
|
170
170
|
const { level, section } = node.attrs;
|
|
171
|
-
const
|
|
172
|
-
|
|
171
|
+
const top = state.top();
|
|
172
|
+
// The document has no children before its first node is written: a heading at the very top starts them.
|
|
173
|
+
if (top && !top.children) top.children = [];
|
|
174
|
+
const written = top?.children;
|
|
175
|
+
if (level <= 2 && SECTION_COLORS.includes(section) && Array.isArray(written)) {
|
|
173
176
|
const last = written.at(-1);
|
|
174
177
|
const at = last?.type === 'html' && isLabelLine(last.value) ? written.length - 1 : written.length;
|
|
175
178
|
written.splice(at, 0, { type: 'html', value: sectionMarker(section) });
|
package/src/markdown.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Tests for src/markdown.mjs: the section colour marker is read into the H2's `section` attribute and written back as it was
|
|
1
|
+
// Tests for src/markdown.mjs: the section colour marker is read into the H1's or H2's `section` attribute and written back as it was
|
|
2
2
|
// (before the label, if any). Milkdown needs a DOM; jsdom stands in. Run: node --test packages/editor/src/markdown.test.mjs
|
|
3
3
|
import { test } from 'node:test';
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
@@ -46,6 +46,20 @@ test('a coloured section comes back byte for byte', async () => {
|
|
|
46
46
|
assert.deepEqual(nodes, ['heading', 'paragraph', 'heading:tint', 'paragraph', 'heading:accent', 'paragraph']);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test('a coloured H1 comes back byte for byte', async () => {
|
|
50
|
+
const text = marked('tint', '# About\n\nText\n');
|
|
51
|
+
const { saved, nodes } = await through(text);
|
|
52
|
+
assert.equal(saved, text);
|
|
53
|
+
assert.deepEqual(nodes, ['heading:tint', 'paragraph']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('the marker comes before a label above the H1', async () => {
|
|
57
|
+
const text = marked('accent', '<p class="block-eyebrow">Welcome</p>\n\n# About\n\nText\n');
|
|
58
|
+
const { saved, nodes } = await through(text);
|
|
59
|
+
assert.equal(saved, text);
|
|
60
|
+
assert.ok(nodes.includes('heading:accent'));
|
|
61
|
+
});
|
|
62
|
+
|
|
49
63
|
test('a text without markers is written as before', async () => {
|
|
50
64
|
const text = '# Title\n\n## One\n\nText\n';
|
|
51
65
|
assert.equal((await through(text)).saved, text);
|
|
@@ -58,7 +72,7 @@ test('the marker comes before a label above the H2', async () => {
|
|
|
58
72
|
assert.ok(nodes.includes('heading:surface'));
|
|
59
73
|
});
|
|
60
74
|
|
|
61
|
-
test('a marker not followed by an H2 is dropped', async () => {
|
|
75
|
+
test('a marker not followed by an H1 or H2 is dropped', async () => {
|
|
62
76
|
assert.deepEqual(await through(marked('tint', 'Plain text\n\n## B\n')), {
|
|
63
77
|
saved: 'Plain text\n\n## B\n',
|
|
64
78
|
nodes: ['paragraph', 'heading'],
|
package/src/menuChildren.ts
CHANGED
|
@@ -18,16 +18,16 @@ const listOf = (ids: string[]): MenuChild[] =>
|
|
|
18
18
|
|
|
19
19
|
/** The sub-items of a menu item, in their order: none for most. */
|
|
20
20
|
export function childrenOf(id: string, files: RepoFile[]): MenuChild[] {
|
|
21
|
-
if (id === '
|
|
22
|
-
if (id === '
|
|
21
|
+
if (id === 'lists') return listOf(listsShown(files));
|
|
22
|
+
if (id === 'company') return companyParts.map((p) => ({ id: p.id, label: p.label, href: `#company/${p.id}` }));
|
|
23
23
|
if (id === 'apps') {
|
|
24
24
|
const installed = installedApps(files);
|
|
25
25
|
return apps.filter((a) => installed.includes(a.id)).map((a) => ({ id: a.id, label: a.label, href: `#apps/${a.id}` }));
|
|
26
26
|
}
|
|
27
|
-
if (id === '
|
|
27
|
+
if (id === 'redirects') {
|
|
28
28
|
return [
|
|
29
|
-
{ id: '
|
|
30
|
-
{ id: '
|
|
29
|
+
{ id: 'all', label: t.redirects.all, href: `#${TAB_ROUTES.redirects}` },
|
|
30
|
+
{ id: 'not-found', label: t.redirects.missing.title, href: `#${TAB_ROUTES.missing}` },
|
|
31
31
|
];
|
|
32
32
|
}
|
|
33
33
|
return [];
|
package/src/menuOpen.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Whether the main menu's drawer is open, for the drawer (MainMenu.tsx) and the menu buttons (Screen.tsx, the rail): at expanded
|
|
2
2
|
// and wider the standard drawer's remembered state (sidebar.ts), below it the modal drawer's, which is not remembered.
|
|
3
|
-
// Which one is on screen is lib/menu-layout.mjs.
|
|
3
|
+
// Which one is on screen is lib/menu-layout.mjs. From expanded to below large the standard drawer yields to the rail while
|
|
4
|
+
// the standard side panel is open (drawerYield.mjs, sidePanel.ts); the remembered state does not change for that.
|
|
5
|
+
// Exposes useMenuOpen.
|
|
4
6
|
import { useCallback, useSyncExternalStore } from 'react';
|
|
5
7
|
import { setSidebar, useSidebar } from './sidebar';
|
|
6
|
-
import {
|
|
8
|
+
import { drawerShown } from './drawerYield.mjs';
|
|
9
|
+
import { setDrawerReopened, useDrawerReopened, usePanelOpen } from './sidePanel';
|
|
10
|
+
import { useAtLeast, useMenuLayout } from './ui';
|
|
7
11
|
|
|
8
12
|
let modalOpen = false;
|
|
9
13
|
const listeners = new Set<() => void>();
|
|
@@ -28,7 +32,19 @@ export function useMenuOpen() {
|
|
|
28
32
|
const wide = useMenuLayout().expanded === 'standard';
|
|
29
33
|
const standard = useSidebar();
|
|
30
34
|
const modal = useModalOpen();
|
|
31
|
-
const
|
|
32
|
-
const
|
|
35
|
+
const panel = usePanelOpen();
|
|
36
|
+
const large = useAtLeast('large');
|
|
37
|
+
const reopened = useDrawerReopened();
|
|
38
|
+
const open = wide ? drawerShown(standard, panel, large, reopened) : modal;
|
|
39
|
+
const setOpen = useCallback(
|
|
40
|
+
(next: boolean) => {
|
|
41
|
+
if (!wide) return setModal(next);
|
|
42
|
+
// While the drawer yields, opening it is for as long as the panel stays open; closing it is remembered.
|
|
43
|
+
if (next && panel && !large) return setDrawerReopened(true);
|
|
44
|
+
setDrawerReopened(false);
|
|
45
|
+
setSidebar(next);
|
|
46
|
+
},
|
|
47
|
+
[wide, panel, large],
|
|
48
|
+
);
|
|
33
49
|
return { open, setOpen, toggle: () => setOpen(!open) };
|
|
34
50
|
}
|
package/src/movedRoutes.d.mts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
/** Where each moved screen is now, by its old address. */
|
|
2
2
|
export const MOVED: Record<string, string>;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Old route prefix (Danish, one or more segments, as the routes were before the ids got English names) -> its new
|
|
5
|
+
* English prefix. Only a route that starts with the old prefix is renamed, and the rest of it is data (a file path, a
|
|
6
|
+
* slug, an id) that keeps its name. A key segment `*` matches any one segment and keeps it (in the value too).
|
|
7
|
+
* Old links keep working through it. The longest matching key wins.
|
|
8
|
+
*/
|
|
9
|
+
export const RENAMED: Record<string, string>;
|
|
10
|
+
export function movedRoute(route: string, more?: Record<string, string>, renamed?: Record<string, string>): string;
|
package/src/movedRoutes.mjs
CHANGED
|
@@ -1,35 +1,116 @@
|
|
|
1
|
-
// The editor's screens that have moved, by their old address after the "#": a link or a bookmark to one
|
|
2
|
-
// it is now. Exposes MOVED and movedRoute (pure). Used by App.tsx (routeOf), which adds the app store's own.
|
|
1
|
+
// The editor's screens that have moved or been renamed, by their old address after the "#": a link or a bookmark to one
|
|
2
|
+
// opens it where it is now. Exposes MOVED, RENAMED and movedRoute (pure). Used by App.tsx (routeOf), which adds the app store's own.
|
|
3
3
|
|
|
4
4
|
/** Where each moved screen is now, by its old address. */
|
|
5
5
|
export const MOVED = /** @type {Record<string, string>} */ ({
|
|
6
|
-
farver: '
|
|
7
|
-
'virksomheden/logo': '
|
|
8
|
-
'tema/blokvalg': '
|
|
9
|
-
dele: '
|
|
10
|
-
tema: '
|
|
11
|
-
'tema/farver': '
|
|
12
|
-
'tema/logo': '
|
|
13
|
-
'tema/dele': '
|
|
14
|
-
'tema/dele/editor': '
|
|
15
|
-
'virksomheden/bank': '
|
|
16
|
-
videresendelser: 'seo/
|
|
17
|
-
'videresendelser/alle': 'seo/
|
|
18
|
-
'videresendelser/ikke-fundet': 'seo/
|
|
6
|
+
farver: 'appearance/colors',
|
|
7
|
+
'virksomheden/logo': 'appearance/logo',
|
|
8
|
+
'tema/blokvalg': 'block-settings',
|
|
9
|
+
dele: 'appearance/parts',
|
|
10
|
+
tema: 'appearance',
|
|
11
|
+
'tema/farver': 'appearance/colors',
|
|
12
|
+
'tema/logo': 'appearance/logo',
|
|
13
|
+
'tema/dele': 'appearance/parts',
|
|
14
|
+
'tema/dele/editor': 'appearance/parts/editor',
|
|
15
|
+
'virksomheden/bank': 'company/payment/bank',
|
|
16
|
+
videresendelser: 'seo/redirects',
|
|
17
|
+
'videresendelser/alle': 'seo/redirects/all',
|
|
18
|
+
'videresendelser/ikke-fundet': 'seo/redirects/not-found',
|
|
19
19
|
// The settings of the booking and newsletter apps were on a screen "Promovering"; they are on the apps' own pages now.
|
|
20
20
|
promovering: 'apps',
|
|
21
|
-
'promovering/calendly': 'apps/
|
|
22
|
-
'promovering/mailchimp': 'apps/
|
|
21
|
+
'promovering/calendly': 'apps/app_a030dfba69793996',
|
|
22
|
+
'promovering/mailchimp': 'apps/app_5c4b6ba067ef127f',
|
|
23
23
|
// The header's menus and settings were on a screen of their own under "Globalt"; they are under "Udseende" now.
|
|
24
|
-
header: '
|
|
24
|
+
header: 'appearance/header',
|
|
25
25
|
// The footer's form was the file's own screen; it is under "Udseende" now.
|
|
26
|
-
'src/data/footer.json': '
|
|
26
|
+
'src/data/footer.json': 'appearance/footer',
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Old route prefix (Danish, one or more segments, as the routes were before the ids got English names) -> its new
|
|
31
|
+
* English prefix. Only a route that starts with the old prefix is renamed, and the rest of it is data (a file path, a
|
|
32
|
+
* slug, an id) that keeps its name. A key segment `*` matches any one segment and keeps it (in the value too).
|
|
33
|
+
* Old links keep working through it. The longest matching key wins.
|
|
34
|
+
*/
|
|
35
|
+
export const RENAMED = /** @type {Record<string, string>} */ ({
|
|
36
|
+
indstillinger: 'settings',
|
|
37
|
+
'indstillinger/appstore/temaer': 'settings/appstore/themes',
|
|
38
|
+
'apps/*/indstillinger': 'apps/*/settings',
|
|
39
|
+
'apps/*/om': 'apps/*/about',
|
|
40
|
+
'apps/udvikler': 'apps/developer',
|
|
41
|
+
udseende: 'appearance',
|
|
42
|
+
'udseende/temaer': 'appearance/themes',
|
|
43
|
+
'udseende/ikonpakker': 'appearance/icon-packs',
|
|
44
|
+
'udseende/bevaegelse': 'appearance/motion',
|
|
45
|
+
'udseende/sidekolonne': 'appearance/side-column',
|
|
46
|
+
'udseende/farver': 'appearance/colors',
|
|
47
|
+
'udseende/farver/reservefarver': 'appearance/colors/fallback-colors',
|
|
48
|
+
'udseende/dele': 'appearance/parts',
|
|
49
|
+
'udseende/dele/blokke': 'appearance/parts/blocks',
|
|
50
|
+
'udseende/dele/liste': 'appearance/parts/list',
|
|
51
|
+
'udseende/dele/vaerdier': 'appearance/parts/values',
|
|
52
|
+
'udseende/standardsider': 'appearance/default-pages',
|
|
53
|
+
maaling: 'analytics',
|
|
54
|
+
privatliv: 'privacy',
|
|
55
|
+
'privatliv/cookiebanner': 'privacy/cookie-banner',
|
|
56
|
+
'privatliv/samtykker': 'privacy/consents',
|
|
57
|
+
'privatliv/privatlivssider': 'privacy/privacy-pages',
|
|
58
|
+
lister: 'lists',
|
|
59
|
+
liste: 'list',
|
|
60
|
+
'liste/kategorier': 'list/tags',
|
|
61
|
+
'liste/udtalelser': 'list/testimonials',
|
|
62
|
+
'liste/mobilmenu': 'list/mobile-menu',
|
|
63
|
+
ny: 'new',
|
|
64
|
+
'ny/kategorier': 'new/tags',
|
|
65
|
+
'ny/udtalelser': 'new/testimonials',
|
|
66
|
+
slettet: 'deleted',
|
|
67
|
+
'slettet/kategorier': 'deleted/tags',
|
|
68
|
+
'slettet/udtalelser': 'deleted/testimonials',
|
|
69
|
+
'slettet/mobilmenu': 'deleted/mobile-menu',
|
|
70
|
+
'slettet/filer': 'deleted/files',
|
|
71
|
+
virksomheden: 'company',
|
|
72
|
+
'virksomheden/kontakt': 'company/contact',
|
|
73
|
+
'virksomheden/sociale-medier': 'company/social-media',
|
|
74
|
+
'virksomheden/betaling': 'company/payment',
|
|
75
|
+
blokke: 'blocks',
|
|
76
|
+
filer: 'files',
|
|
77
|
+
'seo/videresendelser': 'seo/redirects',
|
|
78
|
+
'seo/videresendelser/alle': 'seo/redirects/all',
|
|
79
|
+
'seo/videresendelser/ikke-fundet': 'seo/redirects/not-found',
|
|
80
|
+
'seo/interne-links': 'seo/internal-links',
|
|
81
|
+
'seo/vurdering': 'seo/rating',
|
|
82
|
+
blokvalg: 'block-settings',
|
|
83
|
+
avanceret: 'advanced',
|
|
84
|
+
licenser: 'licenses',
|
|
85
|
+
konto: 'account',
|
|
86
|
+
notifikationer: 'notifications',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The address a screen is at now: where it moved to (`more` first, then MOVED), else the address with its longest old
|
|
91
|
+
* prefix in `renamed` replaced and the rest as it is, else the same address.
|
|
31
92
|
* @param {string} route
|
|
32
93
|
* @param {Record<string, string>} [more]
|
|
94
|
+
* @param {Record<string, string>} [renamed]
|
|
33
95
|
* @returns {string}
|
|
34
96
|
*/
|
|
35
|
-
export const movedRoute = (route, more = {}
|
|
97
|
+
export const movedRoute = (route, more = {}, renamed = RENAMED) => {
|
|
98
|
+
const moved = more[route] ?? MOVED[route];
|
|
99
|
+
if (moved !== undefined) return moved;
|
|
100
|
+
const parts = route.split('/');
|
|
101
|
+
let best = null;
|
|
102
|
+
for (const key of Object.keys(renamed)) {
|
|
103
|
+
const old = key.split('/');
|
|
104
|
+
if (old.length > parts.length || (best && old.length <= best.old.length)) continue;
|
|
105
|
+
if (old.every((segment, n) => segment === '*' || segment === parts[n])) best = { key, old };
|
|
106
|
+
}
|
|
107
|
+
if (!best) return route;
|
|
108
|
+
const kept = parts.slice(0, best.old.length);
|
|
109
|
+
let n = 0;
|
|
110
|
+
const now = renamed[best.key].split('/').map((segment) => {
|
|
111
|
+
if (segment !== '*') return segment;
|
|
112
|
+
while (best.old[n] !== '*') n += 1;
|
|
113
|
+
return kept[n++];
|
|
114
|
+
});
|
|
115
|
+
return [...now, ...parts.slice(best.old.length)].join('/');
|
|
116
|
+
};
|
package/src/movedRoutes.test.mjs
CHANGED
|
@@ -4,19 +4,141 @@ import assert from 'node:assert/strict';
|
|
|
4
4
|
import { movedRoute } from './movedRoutes.mjs';
|
|
5
5
|
|
|
6
6
|
test('the old header screen opens Appearance › Header', () => {
|
|
7
|
-
assert.equal(movedRoute('header'), '
|
|
7
|
+
assert.equal(movedRoute('header'), 'appearance/header');
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
test('the old footer form opens Appearance › Footer', () => {
|
|
11
|
-
assert.equal(movedRoute('src/data/footer.json'), '
|
|
11
|
+
assert.equal(movedRoute('src/data/footer.json'), 'appearance/footer');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
test('an address that has not moved stays', () => {
|
|
15
|
-
assert.equal(movedRoute('
|
|
16
|
-
assert.equal(movedRoute('
|
|
15
|
+
assert.equal(movedRoute('appearance/header'), 'appearance/header');
|
|
16
|
+
assert.equal(movedRoute('lists'), 'lists');
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
test('the extra moves come first', () => {
|
|
20
|
-
assert.equal(movedRoute('butik', { butik: '
|
|
21
|
-
assert.equal(movedRoute('tema', { butik: 'x' }), '
|
|
20
|
+
assert.equal(movedRoute('butik', { butik: 'settings/appstore' }), 'settings/appstore');
|
|
21
|
+
assert.equal(movedRoute('tema', { butik: 'x' }), 'appearance');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('only the old prefix of an address is renamed, the rest stays', () => {
|
|
25
|
+
const renamed = { indstillinger: 'settings', 'udseende/dele': 'appearance/parts', liste: 'list' };
|
|
26
|
+
assert.equal(movedRoute('indstillinger/appstore', {}, renamed), 'settings/appstore');
|
|
27
|
+
assert.equal(movedRoute('udseende/dele/liste/kontakt', {}, renamed), 'appearance/parts/liste/kontakt');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('a key segment * matches any one segment and keeps it', () => {
|
|
31
|
+
const renamed = { 'apps/*/indstillinger': 'apps/*/settings' };
|
|
32
|
+
assert.equal(movedRoute('apps/x/indstillinger/variant', {}, renamed), 'apps/x/settings/variant');
|
|
33
|
+
assert.equal(movedRoute('apps/x/om', {}, renamed), 'apps/x/om');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('data inside an address is not renamed', () => {
|
|
37
|
+
assert.equal(movedRoute('files/filer/doc.pdf'), 'files/filer/doc.pdf');
|
|
38
|
+
assert.equal(movedRoute('blog/farver'), 'blog/farver');
|
|
39
|
+
assert.equal(movedRoute('list/temaer'), 'list/temaer');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('old prefixes are renamed, the data after them stays', () => {
|
|
43
|
+
assert.equal(movedRoute('filer/filer/doc.pdf'), 'files/filer/doc.pdf');
|
|
44
|
+
assert.equal(movedRoute('liste/temaer'), 'list/temaer');
|
|
45
|
+
assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'appearance/parts/list/kontakt');
|
|
46
|
+
assert.equal(movedRoute('indstillinger/appstore'), 'settings/appstore');
|
|
47
|
+
assert.equal(movedRoute('udseende/farver'), 'appearance/colors');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('every old route prefix opens the screen where it is now', () => {
|
|
51
|
+
const cases = {
|
|
52
|
+
indstillinger: 'settings',
|
|
53
|
+
'indstillinger/appstore/temaer': 'settings/appstore/themes',
|
|
54
|
+
'apps/google-analytics/indstillinger': 'apps/google-analytics/settings',
|
|
55
|
+
'apps/udvikler/acme': 'apps/developer/acme',
|
|
56
|
+
'apps/cms-version/om': 'apps/cms-version/about',
|
|
57
|
+
'apps/content-colors/indstillinger/variant': 'apps/content-colors/settings/variant',
|
|
58
|
+
udseende: 'appearance',
|
|
59
|
+
'udseende/temaer': 'appearance/themes',
|
|
60
|
+
'udseende/temaer/material': 'appearance/themes/material',
|
|
61
|
+
'udseende/ikonpakker': 'appearance/icon-packs',
|
|
62
|
+
'udseende/bevaegelse': 'appearance/motion',
|
|
63
|
+
'udseende/sidekolonne': 'appearance/side-column',
|
|
64
|
+
'udseende/farver/reservefarver': 'appearance/colors/fallback-colors',
|
|
65
|
+
'udseende/dele': 'appearance/parts',
|
|
66
|
+
'udseende/dele/editor': 'appearance/parts/editor',
|
|
67
|
+
'udseende/dele/blokke': 'appearance/parts/blocks',
|
|
68
|
+
'udseende/dele/vaerdier': 'appearance/parts/values',
|
|
69
|
+
'udseende/standardsider': 'appearance/default-pages',
|
|
70
|
+
'udseende/logo': 'appearance/logo',
|
|
71
|
+
maaling: 'analytics',
|
|
72
|
+
'maaling/google-analytics': 'analytics/google-analytics',
|
|
73
|
+
privatliv: 'privacy',
|
|
74
|
+
'privatliv/cookiebanner': 'privacy/cookie-banner',
|
|
75
|
+
'privatliv/samtykker': 'privacy/consents',
|
|
76
|
+
'privatliv/privatlivssider': 'privacy/privacy-pages',
|
|
77
|
+
lister: 'lists',
|
|
78
|
+
liste: 'list',
|
|
79
|
+
'liste/priser/cookiescanner': 'list/priser/cookiescanner',
|
|
80
|
+
'liste/kategorier': 'list/tags',
|
|
81
|
+
ny: 'new',
|
|
82
|
+
'ny/popups/mailchimp': 'new/popups/mailchimp',
|
|
83
|
+
'ny/kategorier': 'new/tags',
|
|
84
|
+
'ny/udtalelser': 'new/testimonials',
|
|
85
|
+
slettet: 'deleted',
|
|
86
|
+
'slettet/priser': 'deleted/priser',
|
|
87
|
+
'slettet/kategorier': 'deleted/tags',
|
|
88
|
+
'slettet/udtalelser': 'deleted/testimonials',
|
|
89
|
+
'liste/udtalelser': 'list/testimonials',
|
|
90
|
+
'liste/mobilmenu': 'list/mobile-menu',
|
|
91
|
+
'slettet/mobilmenu': 'deleted/mobile-menu',
|
|
92
|
+
'slettet/filer': 'deleted/files',
|
|
93
|
+
virksomheden: 'company',
|
|
94
|
+
'virksomheden/kontakt': 'company/contact',
|
|
95
|
+
'virksomheden/sociale-medier': 'company/social-media',
|
|
96
|
+
'virksomheden/betaling/mobilepay': 'company/payment/mobilepay',
|
|
97
|
+
blokke: 'blocks',
|
|
98
|
+
'blokke/accordion': 'blocks/accordion',
|
|
99
|
+
filer: 'files',
|
|
100
|
+
'seo/videresendelser': 'seo/redirects',
|
|
101
|
+
'seo/videresendelser/alle': 'seo/redirects/all',
|
|
102
|
+
'seo/videresendelser/ikke-fundet': 'seo/redirects/not-found',
|
|
103
|
+
'seo/interne-links': 'seo/internal-links',
|
|
104
|
+
'seo/vurdering': 'seo/rating',
|
|
105
|
+
blokvalg: 'block-settings',
|
|
106
|
+
avanceret: 'advanced',
|
|
107
|
+
licenser: 'licenses',
|
|
108
|
+
konto: 'account',
|
|
109
|
+
notifikationer: 'notifications',
|
|
110
|
+
'licenser/mit': 'licenses/mit',
|
|
111
|
+
};
|
|
112
|
+
for (const [old, now] of Object.entries(cases)) assert.equal(movedRoute(old), now, old);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('a new address stays', () => {
|
|
116
|
+
assert.equal(movedRoute('settings/appstore'), 'settings/appstore');
|
|
117
|
+
assert.equal(movedRoute('blog/mit-indlaeg'), 'blog/mit-indlaeg');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a moved address wins over a renamed segment', () => {
|
|
121
|
+
assert.equal(movedRoute('tema', {}, { tema: 'x' }), 'appearance');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('an old route to the app store opens it at its English route', () => {
|
|
125
|
+
assert.equal(movedRoute('indstillinger/appstore'), 'settings/appstore');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('the old colours route opens the colours screen under Appearance', () => {
|
|
129
|
+
assert.equal(movedRoute('farver'), 'appearance/colors');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('an old parts route keeps a list id and opens at its English route', () => {
|
|
133
|
+
assert.equal(movedRoute('udseende/dele/liste/kontakt'), 'appearance/parts/list/kontakt');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('the old theme routes open the screens under Appearance', () => {
|
|
137
|
+
assert.equal(movedRoute('tema'), 'appearance');
|
|
138
|
+
assert.equal(movedRoute('tema/dele/editor'), 'appearance/parts/editor');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the old booking and newsletter settings open the apps by their permanent ids', () => {
|
|
142
|
+
assert.equal(movedRoute('promovering/calendly'), 'apps/app_a030dfba69793996');
|
|
143
|
+
assert.equal(movedRoute('promovering/mailchimp'), 'apps/app_5c4b6ba067ef127f');
|
|
22
144
|
});
|
package/src/nav.ts
CHANGED
|
@@ -9,7 +9,7 @@ type Step = { at: number; prev: string | null };
|
|
|
9
9
|
|
|
10
10
|
const step = () => history.state as Step | null;
|
|
11
11
|
|
|
12
|
-
/** The screen of an address ("#
|
|
12
|
+
/** The screen of an address ("#list/sider" → "list/sider", "#" → ""); one with a broken %-sign is
|
|
13
13
|
* the start screen. */
|
|
14
14
|
export function screenOf(hash: string) {
|
|
15
15
|
try {
|
package/src/notificationData.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The notifications (#
|
|
1
|
+
// The notifications (#notifications, Notifications.tsx): what the editor and the apps have to say that is worth
|
|
2
2
|
// reading again, newest first, with when it came and the link it had. Each has a type: the editor's own (`site`: the
|
|
3
3
|
// site is updated or not, `saved`: something is saved, deleted, restored or installed, `failed`: it could not be,
|
|
4
4
|
// `updates`: an app from the store updated on its own, AppList.tsx autoUpdate), or
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Puts the page's header (the layout's `article > header`) in a panel of this colour, with the intro (HTML, in the text's own
|
|
2
|
+
* class) after it; no colour (null) leaves the header without a panel. A panel the site's page has is first taken away.
|
|
3
|
+
* @param {Document} doc @param {string | null} color @param {string} introHtml */
|
|
4
|
+
export function setH1Panel(doc: Document, color: string | null, introHtml: string): void;
|