@morphemecms/cms 0.12.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 +11 -0
- 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/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- 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/storeEntries.test.mjs +3 -0
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/package.json +1 -1
- package/scripts/app-screenshots.mjs +7 -7
- package/scripts/check-content.mjs +10 -1
- package/scripts/check-language.mjs +0 -1
- package/scripts/editor-screens.mjs +3 -3
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +78 -77
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +15 -15
- package/src/BlockDialog.tsx +1 -1
- 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/DataForm.tsx +3 -3
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +2 -2
- package/src/FooterSettings.tsx +2 -2
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +71 -71
- package/src/Licenses.tsx +8 -8
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/Media.tsx +7 -7
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +8 -8
- package/src/Screen.tsx +3 -3
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +2 -2
- package/src/ThemeStore.tsx +4 -4
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +5 -5
- package/src/checkSite.ts +86 -20
- package/src/editor.css +4 -16
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +4 -4
- package/src/fabScreens.ts +4 -4
- 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/licenseData.ts +3 -3
- package/src/menuChildren.ts +5 -5
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +100 -19
- package/src/movedRoutes.test.mjs +123 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/schema.ts +2 -2
- 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 +4 -4
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/ui.tsx +7 -7
- package/theme/layouts/Base.astro +2 -2
package/src/movedRoutes.test.mjs
CHANGED
|
@@ -4,21 +4,138 @@ 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');
|
|
22
139
|
});
|
|
23
140
|
|
|
24
141
|
test('the old booking and newsletter settings open the apps by their permanent ids', () => {
|
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
|
package/src/schema.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type Field = {
|
|
|
77
77
|
* picked when it is made). */
|
|
78
78
|
locked?: boolean;
|
|
79
79
|
/** select: the choices on a page of their own under the form, at its address with the field's name after it
|
|
80
|
-
* (#apps/content-colors/
|
|
80
|
+
* (#apps/content-colors/settings/variant), in place of the sheet, drawn by this with what each gives (a picture of
|
|
81
81
|
* it); the field links there. Only a field of a part's own form (CompanyForm.tsx: an app's settings), not one in a
|
|
82
82
|
* group or a list; elsewhere the choices are in the sheet. */
|
|
83
83
|
page?: ComponentType<FieldPageProps>;
|
|
@@ -162,7 +162,7 @@ export type Template = {
|
|
|
162
162
|
* bar opens the booking sheet, as "Book en tid" does), else the entry itself opens. */
|
|
163
163
|
export type Opens = { label: string; as?: string };
|
|
164
164
|
|
|
165
|
-
/** A place in the editor (a route, e.g. '
|
|
165
|
+
/** A place in the editor (a route, e.g. 'company/contact') and the text of the link to it. */
|
|
166
166
|
export type Edit = { to: string; text: string };
|
|
167
167
|
|
|
168
168
|
// A kind of item in a list of blocks (a fixed page's sections; in the editor cards in its text, SectionsField.tsx).
|
package/src/site.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { appIdOf, installedApps, withKinds, type AppDefinition, type AppList, ty
|
|
|
9
9
|
import { appBlocks, appFiles, appFolders, appPictures, appShared, appStores } from './appFiles';
|
|
10
10
|
import type { BlockDefinition, SiteBlock } from './blocks';
|
|
11
11
|
import type { ListRows } from './fields';
|
|
12
|
-
import { siteT, siteText, t, translatingSite } from './i18n';
|
|
12
|
+
import { inSiteLanguage, siteT, siteText, t, translatingSite } from './i18n';
|
|
13
13
|
import { byText, type Block, type Collection, type DataFile, type Edit, type Field, type OwnedList, type Part } from './schema';
|
|
14
14
|
import { blockTypeOf, type SharedTexts } from './shared';
|
|
15
15
|
import { appFolder, needsOf, SCREENSHOTS, shotFile } from './store';
|
|
@@ -19,17 +19,40 @@ const written = new WeakMap<object, object>();
|
|
|
19
19
|
/** A collection, a file, a part or a block as the site wrote it, in its language: for the commits. */
|
|
20
20
|
export const asWritten = <T extends object>(shown: T): T => (written.get(shown) as T | undefined) ?? shown;
|
|
21
21
|
|
|
22
|
-
// Only the keys a thing has, so it is the same as it was but for its texts.
|
|
23
|
-
function
|
|
22
|
+
// Only the keys a thing has, so it is the same as it was but for its texts. `link`: remember `from` as what it was written as.
|
|
23
|
+
function shownAs<T extends object>(from: T, texts: Partial<T>, link: boolean): T {
|
|
24
24
|
const to = { ...from };
|
|
25
25
|
for (const key of Object.keys(texts) as (keyof T)[]) if (from[key] !== undefined) to[key] = texts[key]!;
|
|
26
|
-
written.set(to, from);
|
|
26
|
+
if (link) written.set(to, from);
|
|
27
27
|
return to;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** The site's parts with every text of theirs said by `say`: the label, the hint, the link elsewhere,
|
|
31
|
-
* the message of a pattern, the options, a switch's two words … of each field, block, file and part.
|
|
32
|
-
|
|
31
|
+
* the message of a pattern, the options, a switch's two words … of each field, block, file and part.
|
|
32
|
+
* `sayWritten`: what asWritten gives back for each thing it returns, with its texts said by it (the standard parts'
|
|
33
|
+
* English in the site's language, inSiteLanguage); without it, asWritten gives back the thing as it was. */
|
|
34
|
+
export function translator(say: (text: string) => string, sayWritten?: (text: string) => string) {
|
|
35
|
+
const shownTexts = build(say, !sayWritten);
|
|
36
|
+
if (!sayWritten) return shownTexts;
|
|
37
|
+
const writtenTexts = build(sayWritten, false);
|
|
38
|
+
const both =
|
|
39
|
+
<A extends object>(one: (x: A) => A, said: (x: A) => A) =>
|
|
40
|
+
(x: A): A => {
|
|
41
|
+
const to = one(x);
|
|
42
|
+
written.set(to, said(x));
|
|
43
|
+
return to;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
...shownTexts,
|
|
47
|
+
block: both(shownTexts.block, writtenTexts.block),
|
|
48
|
+
collection: both(shownTexts.collection, writtenTexts.collection),
|
|
49
|
+
part: both(shownTexts.part, writtenTexts.part),
|
|
50
|
+
app: both(shownTexts.app, writtenTexts.app),
|
|
51
|
+
} as typeof shownTexts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function build(say: (text: string) => string, link: boolean) {
|
|
55
|
+
const shown = <T extends object>(from: T, texts: Partial<T>): T => shownAs(from, texts, link);
|
|
33
56
|
const maybe = (text?: string) => (text === undefined ? text : say(text));
|
|
34
57
|
const edit = (e?: Edit) => e && { ...e, text: say(e.text) };
|
|
35
58
|
const field = (f: Field): Field =>
|
|
@@ -94,7 +117,8 @@ export function translator(say: (text: string) => string) {
|
|
|
94
117
|
/** A colour under "Farver" (colorFields in @site/colors): its CSS variable without "--", its name and what it is for. */
|
|
95
118
|
type ColorField = (typeof site.colorFields)[number];
|
|
96
119
|
|
|
97
|
-
|
|
120
|
+
// A commit names a part in the site's language: the standard parts' English texts through inSiteLanguage, the site's own as written.
|
|
121
|
+
const local = translator(siteText, inSiteLanguage);
|
|
98
122
|
const each = <T>(all: T[], one: (x: T) => T) => (translatingSite ? all.map(one) : all);
|
|
99
123
|
|
|
100
124
|
// The screens under "Blokke" of the blocks with shared texts (shared.ts in the block's folder), named and
|
|
@@ -117,7 +141,7 @@ function sharedPartsOf(blocks: Folders<BlockDefinition>, shared: Folders<SharedT
|
|
|
117
141
|
fields,
|
|
118
142
|
defaults: values,
|
|
119
143
|
preview: { type, ...preview },
|
|
120
|
-
back: { href: '#
|
|
144
|
+
back: { href: '#blocks', label: t.common.parts.blocks },
|
|
121
145
|
},
|
|
122
146
|
];
|
|
123
147
|
});
|
|
@@ -142,7 +166,7 @@ export const writtenBlockParts: Part[] = [...site.blockParts, ...sharedParts, ..
|
|
|
142
166
|
/** The parts of "Virksomheden"; one with parts of its own (Part.parts) has them going back to it. */
|
|
143
167
|
export const companyParts = each(
|
|
144
168
|
site.companyParts.map((p) =>
|
|
145
|
-
p.parts ? { ...p, parts: p.parts.map((s) => ({ ...s, back: { href: `#
|
|
169
|
+
p.parts ? { ...p, parts: p.parts.map((s) => ({ ...s, back: { href: `#company/${p.id}`, label: p.label } })) } : p,
|
|
146
170
|
),
|
|
147
171
|
local.part,
|
|
148
172
|
);
|
|
@@ -155,9 +179,9 @@ export const partScreens = site.partScreens.map((s) => ({
|
|
|
155
179
|
local.part,
|
|
156
180
|
),
|
|
157
181
|
}));
|
|
158
|
-
/** The site's own parts under "Udseende" (#
|
|
182
|
+
/** The site's own parts under "Udseende" (#appearance, Library.tsx Theme), after the logo and icon, each going back to it. */
|
|
159
183
|
export const appearance = each(
|
|
160
|
-
site.appearance.map((p) => ({ ...p, back: { href: '#
|
|
184
|
+
site.appearance.map((p) => ({ ...p, back: { href: '#appearance', label: t.common.parts.appearance } })),
|
|
161
185
|
local.part,
|
|
162
186
|
);
|
|
163
187
|
/** Where a screen goes back to: the start screen. The menu's sections ("Indhold" …) are lists in the drawer, not screens,
|
|
@@ -190,7 +214,7 @@ export const placeOf = (id: string): string | undefined => groups.find((g) => g.
|
|
|
190
214
|
* ("Udseende › Standardsider"): the part with its field in the file of the standard pages. */
|
|
191
215
|
export function standardPageAt(key: string): string {
|
|
192
216
|
const chosenIn = (p: Part) => p.file === site.config.standardPages && !!p.fields?.some((f) => f.name === key);
|
|
193
|
-
const screen = [{ id: '
|
|
217
|
+
const screen = [{ id: 'appearance', label: t.common.parts.appearance, parts: appearance }, ...partScreens].find((s) =>
|
|
194
218
|
s.parts.some(chosenIn),
|
|
195
219
|
);
|
|
196
220
|
if (!screen) return '';
|
|
@@ -314,7 +338,7 @@ export const icons = {
|
|
|
314
338
|
),
|
|
315
339
|
};
|
|
316
340
|
|
|
317
|
-
/** The short lists, under "Lister" (#
|
|
341
|
+
/** The short lists, under "Lister" (#lists): the site's, the standard blocks' and the apps', which go back there too. */
|
|
318
342
|
export const lists = [...site.lists, ...ownedLists.map((l) => l.id)];
|
|
319
343
|
|
|
320
344
|
/** The lists under "Lister" now: the site's, the standard blocks' and those of the apps installed, without the cookie
|
|
@@ -14,7 +14,7 @@ export default defineBlock({
|
|
|
14
14
|
label: t.blocks.accordion.label,
|
|
15
15
|
icon: Rows,
|
|
16
16
|
hint: t.blocks.accordion.hint,
|
|
17
|
-
edit: { to: '
|
|
17
|
+
edit: { to: 'blocks/accordion', text: t.blocks.accordion.edit },
|
|
18
18
|
fields: [
|
|
19
19
|
...headFields,
|
|
20
20
|
{
|
|
@@ -5,7 +5,7 @@ import { siteT, t } from '../../i18n';
|
|
|
5
5
|
import { defineShared } from '../../shared';
|
|
6
6
|
|
|
7
7
|
/** The rows it is shown with where no page has it: its screen under "Blokke" and "Tilgængelige
|
|
8
|
-
*
|
|
8
|
+
* blocks" (index.tsx). In the site's language, like the drawing. */
|
|
9
9
|
export const example = { items: siteT.blocks.accordion.example.map((label) => ({ label, href: '' })) };
|
|
10
10
|
|
|
11
11
|
export default defineShared({
|
|
@@ -11,7 +11,7 @@ export default defineBlock({
|
|
|
11
11
|
label: t.blocks.buttons.label,
|
|
12
12
|
icon: CursorClick,
|
|
13
13
|
hint: t.blocks.buttons.hint,
|
|
14
|
-
edit: { to: '
|
|
14
|
+
edit: { to: 'blocks/buttons', text: t.blocks.buttons.edit },
|
|
15
15
|
fields: [{ ...buttonList, hint: t.blocks.buttons.itemsHint }],
|
|
16
16
|
preview: true,
|
|
17
17
|
detail: (p) => [
|
|
@@ -13,7 +13,7 @@ export default defineBlock({
|
|
|
13
13
|
label: t.blocks.cards.label,
|
|
14
14
|
icon: Cards,
|
|
15
15
|
hint: t.blocks.cards.hint,
|
|
16
|
-
edit: { to: '
|
|
16
|
+
edit: { to: 'blocks/cards', text: t.blocks.cards.edit },
|
|
17
17
|
fields: [
|
|
18
18
|
...headFields,
|
|
19
19
|
// An icon is the empty choice: so are the cards from before there was a choice.
|
|
@@ -4,7 +4,7 @@ import { siteT, t } from '../../i18n';
|
|
|
4
4
|
import { defineShared } from '../../shared';
|
|
5
5
|
|
|
6
6
|
/** The cards it is shown with where no page has it: its screen under "Blokke" and "Tilgængelige
|
|
7
|
-
*
|
|
7
|
+
* blocks" (index.tsx). In the site's language, like the drawing. With a page, so the link's text,
|
|
8
8
|
* which is edited there, is on them. */
|
|
9
9
|
export const example = { items: siteT.blocks.cards.example.map((c) => ({ ...c, href: '/' })) };
|
|
10
10
|
|
|
@@ -11,7 +11,7 @@ export default defineBlock({
|
|
|
11
11
|
label: t.blocks.cookies.label,
|
|
12
12
|
icon: Cookie,
|
|
13
13
|
hint: t.blocks.cookies.hint,
|
|
14
|
-
edit: { to: `
|
|
14
|
+
edit: { to: `list/${config.lists.cookies}`, text: t.blocks.cookies.edit },
|
|
15
15
|
fields: [],
|
|
16
16
|
example: {},
|
|
17
17
|
});
|
|
@@ -49,7 +49,7 @@ export default defineBlock({
|
|
|
49
49
|
label: t.blocks.posts.label,
|
|
50
50
|
icon: Newspaper,
|
|
51
51
|
hint: t.blocks.posts.hint,
|
|
52
|
-
edit: { to: '
|
|
52
|
+
edit: { to: 'blocks/posts', text: t.blocks.posts.edit },
|
|
53
53
|
fields: [
|
|
54
54
|
...headFields,
|
|
55
55
|
// A list (the empty choice: so are the blocks from before there was a choice), or the newest large over the list
|
|
@@ -36,7 +36,7 @@ export default defineBlock({
|
|
|
36
36
|
label: t.blocks.testimonials.label,
|
|
37
37
|
icon: Quotes,
|
|
38
38
|
hint: t.blocks.testimonials.hint,
|
|
39
|
-
edit: { to: `
|
|
39
|
+
edit: { to: `list/${config.lists.testimonials}`, text: t.blocks.testimonials.edit },
|
|
40
40
|
fields: [
|
|
41
41
|
...headFields,
|
|
42
42
|
{
|
|
@@ -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
|
|
@@ -190,10 +190,10 @@ export type StoreRecord = { version: string; ref: string; files: StoreFile[] };
|
|
|
190
190
|
/** The store's address in the editor, under "Indstillinger" (its apps; its themes at STORE_THEMES), its address
|
|
191
191
|
* before (STORE_BEFORE, which opens it, App.tsx MOVED), and that of a developer's page (DEVELOPER_ROUTE/<slug>); no
|
|
192
192
|
* app can have the ids of the last two. */
|
|
193
|
-
export const STORE_ROUTE = '
|
|
194
|
-
export const STORE_THEMES = '
|
|
193
|
+
export const STORE_ROUTE = 'settings/appstore';
|
|
194
|
+
export const STORE_THEMES = 'settings/appstore/themes';
|
|
195
195
|
export const STORE_BEFORE = 'apps/butik';
|
|
196
|
-
export const DEVELOPER_ROUTE = 'apps/
|
|
196
|
+
export const DEVELOPER_ROUTE = 'apps/developer';
|
|
197
197
|
|
|
198
198
|
/** An app's folder in the site, and the note in it of where it came from. */
|
|
199
199
|
export const appFolder = (id: string) => `${config.apps.folder}/${id}`;
|
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/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,
|
|
@@ -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
|
};
|
|
@@ -2740,11 +2740,11 @@ function MainRail({ label, onOpen, children }: { label: string; onOpen: () => vo
|
|
|
2740
2740
|
// menu button. The header cell starts at the drawer items' outer edge: the row has no start padding, and the header (the
|
|
2741
2741
|
// account row, the back row) brings the items' inline padding itself, so its pill (editor.css, .editor-main-drawer-header)
|
|
2742
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
|
|
2743
|
-
// 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.
|
|
2744
2744
|
function MainDrawerMenuButton({ onClick, header }: { onClick: () => void; header?: ReactNode }) {
|
|
2745
2745
|
return (
|
|
2746
2746
|
<>
|
|
2747
|
-
<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">
|
|
2748
2748
|
<div className="min-w-0 flex-1">{header}</div>
|
|
2749
2749
|
<MenuButton label={t.library.closeMenu} open onClick={onClick} className="-me-[calc((var(--icon-button-width)-1.5rem)/2)]" />
|
|
2750
2750
|
</div>
|
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
|
|