@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/Consents.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "
|
|
1
|
+
// "Consents" (#privacy/consents, Part.opens 'consents'): the visitors' answers in the cookie banner, as the site's
|
|
2
2
|
// Worker keeps them (consentData.ts, packages/editor/worker/consent.js), newest first, so the site can show that a visitor said yes. Over
|
|
3
3
|
// the list the search by consent id (the one the banner shows the visitor under "Cookieindstillinger", asked of the
|
|
4
4
|
// Worker, so older answers are found too) and the order (kept in this browser); in the top bar the download of all of
|
|
@@ -32,7 +32,7 @@ import { t } from './i18n';
|
|
|
32
32
|
|
|
33
33
|
type Sort = 'newest' | 'oldest';
|
|
34
34
|
const SORTS: Sort[] = ['newest', 'oldest'];
|
|
35
|
-
const SORT_KEY = 'editor-sort-
|
|
35
|
+
const SORT_KEY = 'editor-sort-consents';
|
|
36
36
|
|
|
37
37
|
// The yes and the no of an answer as one line, in the banner's order: "Ja: Statistik · Nej: Marketing".
|
|
38
38
|
function summary(answer: ConsentAnswer) {
|
|
@@ -46,7 +46,7 @@ function summary(answer: ConsentAnswer) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export default function Consents({ env, back }: { env: Env; back: { href: string; label: string } }) {
|
|
49
|
-
const [query, setQuery] = useKept('
|
|
49
|
+
const [query, setQuery] = useKept('consents-search', '');
|
|
50
50
|
const [sort, setSort] = useState<Sort>(() => {
|
|
51
51
|
try {
|
|
52
52
|
const saved = localStorage.getItem(SORT_KEY);
|
package/src/DataForm.tsx
CHANGED
|
@@ -309,12 +309,12 @@ function Form({
|
|
|
309
309
|
title={dataFile ? dataFile.label : isNew ? collection.create! : entryName(folder!, clean(fields, initial)) || kindLabel}
|
|
310
310
|
back={
|
|
311
311
|
header.includes(collection.id)
|
|
312
|
-
? { href: '#
|
|
312
|
+
? { href: '#appearance/header', label: t.common.parts.header }
|
|
313
313
|
: collection.id === 'footer'
|
|
314
|
-
? { href: '#
|
|
314
|
+
? { href: '#appearance/footer', label: t.common.parts.footer }
|
|
315
315
|
: collection.kind === 'files' && collection.files.length === 1
|
|
316
316
|
? { href: '#', label: siteName }
|
|
317
|
-
: { href: `#
|
|
317
|
+
: { href: `#list/${collection.id}`, label: collection.label }
|
|
318
318
|
}
|
|
319
319
|
status={fixed ? savedStatus({ savedAt, saving, deleting }) : undefined}
|
|
320
320
|
view={view}
|
package/src/Deleted.tsx
CHANGED
|
@@ -38,7 +38,7 @@ const fileName = (path: string) => path.split('/').pop() ?? path;
|
|
|
38
38
|
|
|
39
39
|
export default function Deleted({ id, files, env }: { id: string; files: RepoFile[]; env: Env }) {
|
|
40
40
|
// The files from "Filer" (pictures, PDFs, videos, zip files), not a list's.
|
|
41
|
-
const pictures = id === '
|
|
41
|
+
const pictures = id === 'files';
|
|
42
42
|
const collection = pictures ? undefined : collections.find((c) => c.id === id && restorable(c));
|
|
43
43
|
const folder = pictures ? MEDIA : collection && collection.kind !== 'files' ? collection.folder : '';
|
|
44
44
|
const [deleted, setDeleted] = useState<DeletedFile[] | null>(null);
|
|
@@ -62,12 +62,12 @@ export default function Deleted({ id, files, env }: { id: string; files: RepoFil
|
|
|
62
62
|
if (!folder)
|
|
63
63
|
return (
|
|
64
64
|
<Screen title={t.app.notFound} back={{ href: '#', label: siteName }}>
|
|
65
|
-
<EmptyNote>{t.app.nothingAt(`
|
|
65
|
+
<EmptyNote>{t.app.nothingAt(`deleted/${id}`)}</EmptyNote>
|
|
66
66
|
</Screen>
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
const label = pictures ? t.history.files : collection!.label;
|
|
70
|
-
const back = pictures ? { href: '#
|
|
70
|
+
const back = pictures ? { href: '#files', label: t.common.parts.media } : { href: `#list/${id}`, label: collection!.label };
|
|
71
71
|
|
|
72
72
|
async function open(file: DeletedFile) {
|
|
73
73
|
if (!pictures) return restore(file);
|
package/src/EditorFooter.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The editor's footer: one line under the content of every screen (Screen.tsx), with a link to "Licenser" (#
|
|
1
|
+
// The editor's footer: one line under the content of every screen (Screen.tsx), with a link to "Licenser" (#licenses,
|
|
2
2
|
// Licenses.tsx). Exposes EditorFooter. Uses LinkButton from ui.tsx; the safe area at the bottom is Screen's main.
|
|
3
3
|
import { t } from './i18n';
|
|
4
4
|
import { LinkButton } from './ui';
|
|
@@ -6,7 +6,7 @@ import { LinkButton } from './ui';
|
|
|
6
6
|
export default function EditorFooter() {
|
|
7
7
|
return (
|
|
8
8
|
<footer className="mt-12 flex justify-center">
|
|
9
|
-
<LinkButton href="#
|
|
9
|
+
<LinkButton href="#licenses" kind="text" size="small">
|
|
10
10
|
{t.common.parts.licenses}
|
|
11
11
|
</LinkButton>
|
|
12
12
|
</footer>
|
package/src/EntryForm.tsx
CHANGED
|
@@ -99,7 +99,7 @@ export const newMarkdown = (collection: MarkdownCollection, off: string[] = []):
|
|
|
99
99
|
// The address of a saved page or post: a page's own `path`, or else /<file name>/ (packages/editor/site/content.ts).
|
|
100
100
|
// '' for a new one.
|
|
101
101
|
// Where the redirects are, as the menu has it: "Indhold" › "Videresendelser" › "Alle videresendelser".
|
|
102
|
-
const redirectsPath = () => [placeOf('
|
|
102
|
+
const redirectsPath = () => [placeOf('redirects'), t.common.parts.redirects, t.redirects.all].filter(Boolean).join(' › ');
|
|
103
103
|
|
|
104
104
|
function addressNow(collection: MarkdownCollection, file: RepoFile, data: Record<string, unknown>) {
|
|
105
105
|
if (!file.path) return '';
|
|
@@ -623,7 +623,7 @@ function Form({
|
|
|
623
623
|
<WrongField.Provider value={wrongField.field}>
|
|
624
624
|
<FormFrame
|
|
625
625
|
title={formTitle}
|
|
626
|
-
back={{ href: `#
|
|
626
|
+
back={{ href: `#list/${collection.id}`, label: collection.label }}
|
|
627
627
|
status={savedStatus({ savedAt, saving, deleting, isNew })}
|
|
628
628
|
view={isNew ? undefined : url}
|
|
629
629
|
dirty={dirty}
|
package/src/FooterSettings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Footer" (#
|
|
1
|
+
// "Udseende" › "Footer" (#appearance/footer): the footer's text under the name, its two headings and its links (footer.json),
|
|
2
2
|
// on one screen laid out as a page's form (SettingsSections.tsx): a preview of the footer as the form has it, on a phone and
|
|
3
3
|
// a computer (partPreview.tsx), over the sections Text, Contact and Links. "Gem ændringer" saves the file in its own
|
|
4
4
|
// commit (useDataFiles.ts). Exposes FooterSettings (default). Uses fields.tsx and footerPreview.mjs.
|
|
@@ -30,7 +30,7 @@ const placeOf = (path: string) => places.find((p) => p.names.includes(path.split
|
|
|
30
30
|
// The class of a link in the footer (Footer.astro), for a footer whose page has no link to copy.
|
|
31
31
|
const THEME = { linkClass: 'md-footer-item' };
|
|
32
32
|
|
|
33
|
-
const back = { href: '#
|
|
33
|
+
const back = { href: '#appearance', label: t.common.parts.appearance };
|
|
34
34
|
|
|
35
35
|
export default function FooterSettings({ env }: { env: Env }) {
|
|
36
36
|
const layout = useSettingsSections(places);
|
package/src/HeaderSettings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Header" (#
|
|
1
|
+
// "Udseende" › "Header" (#appearance/header): what is in the header on every page, on one screen laid out as a page's form
|
|
2
2
|
// (SettingsSections.tsx): a preview of the header as the form has it, on a phone and a computer (partPreview.tsx), over the
|
|
3
3
|
// sections Main menu (menu.json), Mobile menu (mobile-menu.json), Action button and Menu button (header.json). "Gem
|
|
4
4
|
// ændringer" saves each changed file in its own commit (useDataFiles.ts). Exposes HeaderSettings (default). Uses
|
|
@@ -19,7 +19,7 @@ import type { Env } from './App';
|
|
|
19
19
|
import { t } from './i18n';
|
|
20
20
|
|
|
21
21
|
// The header's three files, by their collections (defaults/collections.ts), as the preview names them.
|
|
22
|
-
const FILES = { menu: 'menu', mobileMenu: '
|
|
22
|
+
const FILES = { menu: 'menu', mobileMenu: 'mobile-menu', header: 'header-button' } as const;
|
|
23
23
|
const parts: DataFilePart[] = Object.values(FILES).flatMap((id) => {
|
|
24
24
|
const collection = collections.find((c): c is FilesCollection => c.id === id && c.kind === 'files');
|
|
25
25
|
return collection ? [{ collection, dataFile: collection.files[0] }] : [];
|
|
@@ -51,7 +51,7 @@ const shapeOf = (values: Values) => ({
|
|
|
51
51
|
header: values[at(FILES.header)] ?? {},
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
const back = { href: '#
|
|
54
|
+
const back = { href: '#appearance', label: t.common.parts.appearance };
|
|
55
55
|
|
|
56
56
|
export default function HeaderSettings({ env }: { env: Env }) {
|
|
57
57
|
const layout = useSettingsSections(places);
|
package/src/Keywords.tsx
CHANGED
|
@@ -26,7 +26,7 @@ type Keyword = { word: string; primary: boolean; title: string; href: string; ad
|
|
|
26
26
|
type Counts = Record<string, { href: string; pages: number }>;
|
|
27
27
|
|
|
28
28
|
type Sort = 'az' | 'za' | 'most' | 'fewest';
|
|
29
|
-
const SORT_KEY = 'editor-sort-
|
|
29
|
+
const SORT_KEY = 'editor-sort-keywords';
|
|
30
30
|
function savedSort(): Sort {
|
|
31
31
|
try {
|
|
32
32
|
const s = localStorage.getItem(SORT_KEY);
|
|
@@ -85,11 +85,11 @@ function keywordsOf(files: RepoFile[]): Keyword[] {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export default function Keywords({ files }: { files: RepoFile[] }) {
|
|
88
|
-
const [query, setQuery] = useKept('
|
|
88
|
+
const [query, setQuery] = useKept('keywords-search', '');
|
|
89
89
|
const [sort, setSort] = useState(savedSort);
|
|
90
|
-
const [which, setWhich] = useKept<'' | 'primary' | 'secondary'>('
|
|
91
|
-
const [linked, setLinked] = useKept<'' | 'with' | 'without'>('
|
|
92
|
-
const [kind, setKind] = useKept('
|
|
90
|
+
const [which, setWhich] = useKept<'' | 'primary' | 'secondary'>('keywords-kind', '');
|
|
91
|
+
const [linked, setLinked] = useKept<'' | 'with' | 'without'>('keywords-links', '');
|
|
92
|
+
const [kind, setKind] = useKept('keywords-page', '');
|
|
93
93
|
// undefined while they load; null when the site has none (not built since they came, or offline).
|
|
94
94
|
const [counts, setCounts] = useState<Counts | null>();
|
|
95
95
|
useEffect(() => {
|
package/src/Library.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// and posts can be filtered by their status and by what they pick (a post's tags), a list of what is
|
|
6
6
|
// shown on pages (Promovering) by a page, its kind and whether it is on, as "Filer" (Filters). In a list that can be deleted from, the clock in the top bar ("Slettede …") opens
|
|
7
7
|
// what was deleted (Deleted.tsx), as in "Filer". The tabs the apps installed add to a list (AppDefinition.tabs, Cookiescanner's
|
|
8
|
-
// "Problemer" on "Cookies") are in its top app bar after the list's own, each at #
|
|
8
|
+
// "Problemer" on "Cookies") are in its top app bar after the list's own, each at #list/<list>/<app>. "Use theme" and the move arrows show the
|
|
9
9
|
// loading indicator while they run (only the pressed one).
|
|
10
10
|
import { Fragment, useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
11
11
|
import {
|
|
@@ -116,7 +116,7 @@ import { offBlocks } from './registry';
|
|
|
116
116
|
import { sideChoicesOf, sideSettingOf } from './SideColumn';
|
|
117
117
|
|
|
118
118
|
// Where a part opens: its list, or the file itself when it is one file (the menu).
|
|
119
|
-
export const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#
|
|
119
|
+
export const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#list/${c.id}`);
|
|
120
120
|
|
|
121
121
|
const countOf = (c: Collection, files: RepoFile[]): number =>
|
|
122
122
|
c.kind === 'files' ? c.files.length : files.filter((f) => f.path.startsWith(`${c.folder}/`)).length;
|
|
@@ -124,39 +124,39 @@ const countOf = (c: Collection, files: RepoFile[]): number =>
|
|
|
124
124
|
// A part's name, where it opens, and how many entries it has (null for one file): for the main drawer (MainMenu.tsx).
|
|
125
125
|
export function partOf(id: string, files: RepoFile[]): { label: string; href: string; count: number | null } {
|
|
126
126
|
const { parts } = t.common;
|
|
127
|
-
if (id === '
|
|
128
|
-
if (id === 'header') return { label: parts.header, href: '#
|
|
129
|
-
if (id === 'footer') return { label: parts.footer, href: '#
|
|
130
|
-
if (id === '
|
|
131
|
-
if (id === '
|
|
132
|
-
if (id === '
|
|
133
|
-
if (id === '
|
|
134
|
-
if (id === '
|
|
127
|
+
if (id === 'company') return { label: parts.company, href: '#company', count: null };
|
|
128
|
+
if (id === 'header') return { label: parts.header, href: '#appearance/header', count: null };
|
|
129
|
+
if (id === 'footer') return { label: parts.footer, href: '#appearance/footer', count: null };
|
|
130
|
+
if (id === 'lists') return { label: parts.lists, href: '#lists', count: listsShown(files).length };
|
|
131
|
+
if (id === 'blocks') return { label: parts.blocks, href: '#blocks', count: blockParts.length };
|
|
132
|
+
if (id === 'files') return { label: parts.media, href: '#files', count: mediaOf(files).length + filesOf(files).length };
|
|
133
|
+
if (id === 'appearance') return { label: parts.appearance, href: '#appearance', count: null };
|
|
134
|
+
if (id === 'block-settings') return { label: parts.blockSettings, href: '#block-settings', count: null };
|
|
135
135
|
if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
|
|
136
136
|
if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
|
|
137
|
-
if (id === '
|
|
137
|
+
if (id === 'redirects') return { label: parts.redirects, href: `#${REDIRECTS_ROUTE}`, count: null };
|
|
138
138
|
const seoPart = seoParts.find((p) => p.id === id);
|
|
139
139
|
if (seoPart) return { label: seoPart.label, href: `#seo/${id}`, count: null };
|
|
140
140
|
// The screens under "Udseende" (Theme): the ones the editor has, then the site's own (appearance).
|
|
141
141
|
const appearanceLabel = {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
themes: parts.themes,
|
|
143
|
+
'icon-packs': parts.iconPacks,
|
|
144
|
+
motion: parts.motion,
|
|
145
|
+
'side-column': parts.sideColumn,
|
|
146
|
+
colors: parts.colors,
|
|
147
147
|
logo: parts.brand,
|
|
148
|
-
|
|
148
|
+
parts: parts.editorParts,
|
|
149
149
|
}[id];
|
|
150
150
|
const appearancePart = appearanceLabel ?? appearance.find((p) => p.id === id)?.label;
|
|
151
|
-
if (appearancePart) return { label: appearancePart, href: `#
|
|
151
|
+
if (appearancePart) return { label: appearancePart, href: `#appearance/${id}`, count: null };
|
|
152
152
|
// The parts of a screen with parts ("Privatliv"): where each opens, or its screen's address with its id.
|
|
153
153
|
for (const screen of partScreens) {
|
|
154
154
|
const own = screen.parts.find((p) => p.id === id);
|
|
155
155
|
if (own) return { label: own.label, href: own.href ?? `#${screen.id}/${id}`, count: null };
|
|
156
156
|
}
|
|
157
157
|
if (id === 'morpheme') return { label: parts.morpheme, href: `#${CMS_ROUTE}`, count: null };
|
|
158
|
-
if (id === '
|
|
159
|
-
if (id === '
|
|
158
|
+
if (id === 'advanced') return { label: parts.advanced, href: '#advanced', count: null };
|
|
159
|
+
if (id === 'licenses') return { label: parts.licenses, href: '#licenses', count: null };
|
|
160
160
|
const screen = partScreens.find((s) => s.id === id);
|
|
161
161
|
if (screen) return { label: screen.label, href: `#${id}`, count: null };
|
|
162
162
|
if (id === 'seo') return { label: parts.seo, href: '#seo', count: null };
|
|
@@ -199,12 +199,12 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
|
|
|
199
199
|
return (
|
|
200
200
|
<Parts
|
|
201
201
|
title={part.label}
|
|
202
|
-
route={`
|
|
202
|
+
route={`company/${part.id}`}
|
|
203
203
|
parts={[...part.parts, ...appPartsIn(part, files)]}
|
|
204
|
-
back={{ href: '#
|
|
204
|
+
back={{ href: '#company', label: t.common.parts.company }}
|
|
205
205
|
/>
|
|
206
206
|
);
|
|
207
|
-
return <Parts title={t.common.parts.company} route="
|
|
207
|
+
return <Parts title={t.common.parts.company} route="company" parts={companyParts} />;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// "Udseende": first "Temaer" (Themes), with the theme the site is built with at the end of its row (or, after a swap,
|
|
@@ -218,28 +218,28 @@ export function Company({ part, files }: { part?: Part; files: RepoFile[] }) {
|
|
|
218
218
|
export function Theme({ files }: { files: RepoFile[] }) {
|
|
219
219
|
const chosen = themes.find((x) => x.id === chosenTheme(files)) ?? theme;
|
|
220
220
|
const parts: Part[] = [
|
|
221
|
-
{ id: '
|
|
221
|
+
{ id: 'themes', label: t.common.parts.themes, icon: icons.themes, detail: chosen.name },
|
|
222
222
|
{
|
|
223
|
-
id: '
|
|
223
|
+
id: 'icon-packs',
|
|
224
224
|
label: t.common.parts.iconPacks,
|
|
225
|
-
icon: icons
|
|
225
|
+
icon: icons['icon-packs'],
|
|
226
226
|
detail: iconPacks.find((x) => x.id === chosenPack(files))?.name,
|
|
227
227
|
},
|
|
228
|
-
{ id: '
|
|
228
|
+
{ id: 'motion', label: t.common.parts.motion, icon: icons.motion, detail: t.library.motionSchemes[motionOf(files)] },
|
|
229
229
|
{
|
|
230
|
-
id: '
|
|
230
|
+
id: 'side-column',
|
|
231
231
|
label: t.common.parts.sideColumn,
|
|
232
|
-
icon: icons
|
|
232
|
+
icon: icons['side-column'],
|
|
233
233
|
detail: sideChoicesOf(files).find((x) => x.type === sideSettingOf(files))?.label ?? t.library.sideNone,
|
|
234
234
|
},
|
|
235
|
-
{ id: '
|
|
235
|
+
{ id: 'colors', label: t.common.parts.colors, icon: icons.colors, trailing: <ThemeColors files={files} /> },
|
|
236
236
|
{ id: 'logo', label: t.common.parts.brand, icon: icons.logo },
|
|
237
237
|
{ id: 'header', label: t.common.parts.header, icon: icons.header },
|
|
238
238
|
{ id: 'footer', label: t.common.parts.footer, icon: icons.footer },
|
|
239
239
|
...appearance,
|
|
240
|
-
{ id: '
|
|
240
|
+
{ id: 'parts', label: t.common.parts.editorParts, icon: icons.parts },
|
|
241
241
|
];
|
|
242
|
-
return <Parts title={t.common.parts.appearance} route="
|
|
242
|
+
return <Parts title={t.common.parts.appearance} route="appearance" parts={parts} back={backFrom('appearance')} />;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
/** By "Farver": the site's primary, secondary and tertiary colours (accent, secondary, tertiary) in the mode the editor
|
|
@@ -252,8 +252,8 @@ function ThemeColors({ files }: { files: RepoFile[] }) {
|
|
|
252
252
|
);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
const backToAppearance = { href: '#
|
|
256
|
-
const backToThemes = { href: '#
|
|
255
|
+
const backToAppearance = { href: '#appearance', label: t.common.parts.appearance };
|
|
256
|
+
const backToThemes = { href: '#appearance/themes', label: t.common.parts.themes };
|
|
257
257
|
|
|
258
258
|
/** Under a theme's name: whether the site is built with it ("I brug") or is changing to it ("Skifter hertil"). */
|
|
259
259
|
function themeStatus(id: string, files: RepoFile[]): string | undefined {
|
|
@@ -261,7 +261,7 @@ function themeStatus(id: string, files: RepoFile[]): string | undefined {
|
|
|
261
261
|
if (id === chosenTheme(files)) return t.library.themeNext;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
/** "Temaer" (#
|
|
264
|
+
/** "Temaer" (#appearance/themes): the themes installed (src/themes/, packages/editor/src/themes.ts; and one from the store the site's build
|
|
265
265
|
* does not have yet, as "Installeres…"), by name, each with its picture, its version and who made it, and whether it is
|
|
266
266
|
* in use or the store has a newer version, opening its page (ThemePage). More are in "Appstore", under "Indstillinger". */
|
|
267
267
|
export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
@@ -281,7 +281,7 @@ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
|
281
281
|
return (
|
|
282
282
|
<ListItem
|
|
283
283
|
key={x.id}
|
|
284
|
-
href={`#
|
|
284
|
+
href={`#appearance/themes/${x.id}`}
|
|
285
285
|
leading={<ThemePicture src={x.icon} />}
|
|
286
286
|
headline={x.name}
|
|
287
287
|
supporting={t.library.themeByline(x.version, x.author)}
|
|
@@ -296,7 +296,7 @@ export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
|
296
296
|
);
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
/** "Ikonpakker" (#
|
|
299
|
+
/** "Ikonpakker" (#appearance/icon-packs): the icon packs installed (the system's own, packages/editor/icons/, and those the themes
|
|
300
300
|
* installed bring; iconPacks.ts), by name, each with who made it, its licence, the theme it came with and a few of its
|
|
301
301
|
* icons as they are, the one chosen ticked (chosenPack), with "I brug" by the one the site and the editor are built with
|
|
302
302
|
* and "Skifter hertil …" by the one chosen until they are. A tap on another asks first and then saves it in the site's
|
|
@@ -365,7 +365,7 @@ export function motionOf(files: RepoFile[]): MotionScheme {
|
|
|
365
365
|
return schemeInUse(scheme, theme.motion, themes.find((x) => x.id === STANDARD_THEME)?.motion);
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
/** "Bevægelse" (#
|
|
368
|
+
/** "Bevægelse" (#appearance/motion): Material 3's motion schemes and our calm one (lib/motion.ts), each with how it
|
|
369
369
|
* moves, the one in use ticked (motionOf) and "Temaets valg" by the theme's own. A tap on another asks first and then
|
|
370
370
|
* saves it in the site's file (config.motion) at once, in its own commit, as an icon pack is; the editor moves by it at
|
|
371
371
|
* once (applyMotion, App.tsx), the site after it is built. */
|
|
@@ -422,7 +422,7 @@ export function MotionSchemes({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
|
422
422
|
);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
/** A theme's page (#
|
|
425
|
+
/** A theme's page (#appearance/themes/<id>), the same for every theme, made to show what it would make of the site, so
|
|
426
426
|
* it can be chosen or not: its picture, name, version and who made it, and whether it is in use; "Brug temaet", for any
|
|
427
427
|
* other than the one chosen, which asks first and then saves it in the site's file (config.theme) at once, in its own
|
|
428
428
|
* commit, and the site is built with it; the pictures of the site in it (its screenshots/, packages/editor/scripts/theme-screenshots.mjs),
|
|
@@ -505,37 +505,37 @@ export function ThemePage({ of, env, files }: { of: ThemeInfo; env: Env; files:
|
|
|
505
505
|
<ListItem key={text} icon={glyph} headline={text} />
|
|
506
506
|
))}
|
|
507
507
|
</Group>
|
|
508
|
-
<OwnerLicenses id={`
|
|
508
|
+
<OwnerLicenses id={`theme/${of.id}`} name={of.name} author={of.author} version={of.version} declared={of} />
|
|
509
509
|
<DeveloperRow name={of.author} />
|
|
510
510
|
<ThemeStoreButtons of={of} env={env} files={files} />
|
|
511
511
|
</Screen>
|
|
512
512
|
);
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
/** "Dele" (#
|
|
515
|
+
/** "Dele" (#appearance/parts): the parts the editor and the site are built of, each on a screen of its own: the editor's
|
|
516
516
|
* (Parts.tsx), the theme's UI components, the blocks (the standard ones and the apps') and each list of kinds
|
|
517
517
|
* ("Promovering", its own kinds and the apps'), as the theme shows them (BlockParts.tsx), and the theme's design tokens ("Værdier", Tokens.tsx). */
|
|
518
518
|
export function PartsHome() {
|
|
519
519
|
const parts: Part[] = [
|
|
520
|
-
{ id: 'editor', label: t.parts.screens.editor, icon: icons.
|
|
521
|
-
{ id: 'ui', label: t.parts.screens.components, icon: icons.
|
|
522
|
-
{ id: '
|
|
523
|
-
...kindLists().map((c) => ({ id: `
|
|
524
|
-
{ id: '
|
|
520
|
+
{ id: 'editor', label: t.parts.screens.editor, icon: icons.parts },
|
|
521
|
+
{ id: 'ui', label: t.parts.screens.components, icon: icons.appearance },
|
|
522
|
+
{ id: 'blocks', label: t.parts.screens.blocks, icon: icons.blocks },
|
|
523
|
+
...kindLists().map((c) => ({ id: `list/${c.id}`, label: c.label, icon: icons[c.id] ?? icons.lists })),
|
|
524
|
+
{ id: 'values', label: t.parts.screens.values, icon: Ruler },
|
|
525
525
|
];
|
|
526
|
-
return <Parts title={t.common.parts.editorParts} route="
|
|
526
|
+
return <Parts title={t.common.parts.editorParts} route="appearance/parts" parts={parts} back={backToAppearance} />;
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
/** "Søgemaskineoptimering" (#seo), no longer in the menu: "Videresendelser" (Redirects.tsx, #seo/
|
|
529
|
+
/** "Søgemaskineoptimering" (#seo), no longer in the menu: "Videresendelser" (Redirects.tsx, #seo/redirects) and then
|
|
530
530
|
* the site's own parts (seoParts: "Interne links"), each on a screen of its own (CompanyForm.tsx). "SEO-vurdering"
|
|
531
|
-
* (SeoScores.tsx, #seo/
|
|
531
|
+
* (SeoScores.tsx, #seo/rating) is hidden: nothing links to it. */
|
|
532
532
|
export function Seo() {
|
|
533
|
-
const parts: Part[] = [{ id: '
|
|
533
|
+
const parts: Part[] = [{ id: 'redirects', label: t.common.parts.redirects, icon: icons.redirects }, ...seoParts];
|
|
534
534
|
return <Parts title={t.common.parts.seo} route="seo" parts={parts} back={backFrom('seo')} />;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
537
|
// A screen with parts of the site's own (partScreens): "Privatliv"; and with the settings of the apps installed with its
|
|
538
|
-
// category (PartScreen.apps): "Analytics" and "Promovering", each opening the app's form (#
|
|
538
|
+
// category (PartScreen.apps): "Analytics" and "Promovering", each opening the app's form (#analytics/google-analytics).
|
|
539
539
|
export function PartsScreen({ screen, files }: { screen: PartScreen; files: RepoFile[] }) {
|
|
540
540
|
const parts = [...screen.parts, ...appPartsIn(screen, files)];
|
|
541
541
|
return (
|
|
@@ -551,11 +551,11 @@ export function PartsScreen({ screen, files }: { screen: PartScreen; files: Repo
|
|
|
551
551
|
// and the funnel with the filters by group and by whether the block is on a page, each picked one as a chip.
|
|
552
552
|
type BlockSort = 'group' | 'az' | 'za';
|
|
553
553
|
const BLOCK_SORTS: BlockSort[] = ['group', 'az', 'za'];
|
|
554
|
-
const BLOCK_SORT_KEY = 'editor-sort-
|
|
554
|
+
const BLOCK_SORT_KEY = 'editor-sort-blocks';
|
|
555
555
|
type Use = '' | 'used' | 'unused';
|
|
556
556
|
|
|
557
557
|
export function Blocks({ files }: { files: RepoFile[] }) {
|
|
558
|
-
const [query, setQuery] = useKept('
|
|
558
|
+
const [query, setQuery] = useKept('blocks-search', '');
|
|
559
559
|
const [sort, setSort] = useState<BlockSort>(() => {
|
|
560
560
|
try {
|
|
561
561
|
const saved = localStorage.getItem(BLOCK_SORT_KEY);
|
|
@@ -563,8 +563,8 @@ export function Blocks({ files }: { files: RepoFile[] }) {
|
|
|
563
563
|
} catch {}
|
|
564
564
|
return 'group';
|
|
565
565
|
});
|
|
566
|
-
const [group, setGroup] = useKept('
|
|
567
|
-
const [use, setUse] = useKept<Use>('
|
|
566
|
+
const [group, setGroup] = useKept('blocks-group', '');
|
|
567
|
+
const [use, setUse] = useKept<Use>('blocks-use', '');
|
|
568
568
|
// The blocks on a page (any copy, also a hidden one), by their type (a part's id).
|
|
569
569
|
const used = useMemo(() => new Set(blockParts.filter((p) => pagesWith(p.id, files).length).map((p) => p.id)), [files]);
|
|
570
570
|
const bothUses = blockParts.some((p) => used.has(p.id)) && blockParts.some((p) => !used.has(p.id));
|
|
@@ -648,9 +648,9 @@ export function Blocks({ files }: { files: RepoFile[] }) {
|
|
|
648
648
|
{!inGroups.length ? (
|
|
649
649
|
<p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
|
|
650
650
|
) : sort === 'group' ? (
|
|
651
|
-
inGroups.map((g) => <PartGroup key={g.title} title={g.title} route="
|
|
651
|
+
inGroups.map((g) => <PartGroup key={g.title} title={g.title} route="blocks" parts={g.parts} />)
|
|
652
652
|
) : (
|
|
653
|
-
lettered.map((g) => <PartGroup key={g.key ?? 'all'} title={g.key ?? undefined} route="
|
|
653
|
+
lettered.map((g) => <PartGroup key={g.key ?? 'all'} title={g.key ?? undefined} route="blocks" parts={g.items} />)
|
|
654
654
|
)}
|
|
655
655
|
</Screen>
|
|
656
656
|
);
|
|
@@ -973,7 +973,7 @@ type View = 'liste' | 'menu';
|
|
|
973
973
|
const seoKey = (c: Collection) => `editor-seo-${c.id}`;
|
|
974
974
|
function savedSeo(c: Collection): boolean {
|
|
975
975
|
try {
|
|
976
|
-
return localStorage.getItem(seoKey(c)) === '
|
|
976
|
+
return localStorage.getItem(seoKey(c)) === 'show';
|
|
977
977
|
} catch {
|
|
978
978
|
return false;
|
|
979
979
|
}
|
|
@@ -1004,16 +1004,16 @@ const saved = new Map<string, { oid: string; date: Date }>();
|
|
|
1004
1004
|
|
|
1005
1005
|
// The ways a list can be sorted; by time only where the entries have one: blog posts by their date,
|
|
1006
1006
|
// pages by when they were last saved; by their kind where they have kinds (Promovering).
|
|
1007
|
-
type Sort = '
|
|
1007
|
+
type Sort = 'newest' | 'oldest' | 'az' | 'za' | 'type';
|
|
1008
1008
|
const sortLabel = (c: Collection, s: Sort) => {
|
|
1009
1009
|
const { sorts } = t.library;
|
|
1010
|
-
return c.id === 'pages' && s === '
|
|
1010
|
+
return c.id === 'pages' && s === 'newest' ? sorts.savedNewest : c.id === 'pages' && s === 'oldest' ? sorts.savedOldest : sorts[s];
|
|
1011
1011
|
};
|
|
1012
1012
|
|
|
1013
1013
|
const byTitle = (a: Item, b: Item) => byText(a.title, b.title);
|
|
1014
1014
|
const compare: Record<Sort, (a: Item, b: Item) => number> = {
|
|
1015
|
-
|
|
1016
|
-
|
|
1015
|
+
newest: (a, b) => (b.date ?? '').localeCompare(a.date ?? '') || byTitle(a, b),
|
|
1016
|
+
oldest: (a, b) => (a.date ?? '').localeCompare(b.date ?? '') || byTitle(a, b),
|
|
1017
1017
|
az: byTitle,
|
|
1018
1018
|
za: (a, b) => byTitle(b, a),
|
|
1019
1019
|
type: (a, b) => byText(a.kind ?? '', b.kind ?? '') || byTitle(a, b),
|
|
@@ -1039,7 +1039,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1039
1039
|
// The tabs the apps installed add to it, and the one open (none: the list's own).
|
|
1040
1040
|
const appTabs = tabsOf(collection.id, apps, installedApps(files));
|
|
1041
1041
|
const appTab = appTabs.find((a) => a.app.id === tab);
|
|
1042
|
-
const [query, setQuery] = useKept(`${collection.id}-
|
|
1042
|
+
const [query, setQuery] = useKept(`${collection.id}-search`, '');
|
|
1043
1043
|
// The move that runs (row and target row, null: none); only its arrow shows busy, all arrows stay disabled meanwhile.
|
|
1044
1044
|
const [moving, setMoving] = useState<{ from: number; to: number } | null>(null);
|
|
1045
1045
|
const busy = moving !== null;
|
|
@@ -1058,7 +1058,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1058
1058
|
collection.kind === 'files' || (collection.kind === 'folder' && collection.orderable) || items.length < 2
|
|
1059
1059
|
? []
|
|
1060
1060
|
: collection.id === 'blog' || pages
|
|
1061
|
-
? ['
|
|
1061
|
+
? ['newest', 'oldest', 'az', 'za']
|
|
1062
1062
|
: collection.kind === 'folder' && collection.kinds
|
|
1063
1063
|
? ['az', 'za', 'type']
|
|
1064
1064
|
: ['az', 'za'];
|
|
@@ -1099,7 +1099,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1099
1099
|
// A list of what is shown on pages: only those on one page or the posts of one tag (and those on every
|
|
1100
1100
|
// page), picked among the pages and tags the entries are on, by their names (relationsOf), the tags
|
|
1101
1101
|
// under a heading of their own (`tag:<slug>`); all again when that one is no longer among them.
|
|
1102
|
-
const [onPage, setOnPage] = useKept(`${collection.id}-
|
|
1102
|
+
const [onPage, setOnPage] = useKept(`${collection.id}-page`, '');
|
|
1103
1103
|
const pageNames = useMemo(() => {
|
|
1104
1104
|
if (!items.some((i) => i.shownOn)) return [];
|
|
1105
1105
|
const { pages: all, fixed, categories } = relationsOf(files);
|
|
@@ -1247,7 +1247,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1247
1247
|
function pickSeo(next: boolean) {
|
|
1248
1248
|
setSeo(next);
|
|
1249
1249
|
try {
|
|
1250
|
-
localStorage.setItem(seoKey(collection), next ? '
|
|
1250
|
+
localStorage.setItem(seoKey(collection), next ? 'show' : 'hide');
|
|
1251
1251
|
} catch {}
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
@@ -1334,7 +1334,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1334
1334
|
/>
|
|
1335
1335
|
);
|
|
1336
1336
|
};
|
|
1337
|
-
const privacyScreen = partScreens.find((s) => s.id === '
|
|
1337
|
+
const privacyScreen = partScreens.find((s) => s.id === 'privacy');
|
|
1338
1338
|
return (
|
|
1339
1339
|
<Screen
|
|
1340
1340
|
title={collection.label}
|
|
@@ -1343,9 +1343,9 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1343
1343
|
collection.id === config.lists.cookies && privacyScreen
|
|
1344
1344
|
? { href: `#${privacyScreen.id}`, label: privacyScreen.label }
|
|
1345
1345
|
: lists.includes(collection.id)
|
|
1346
|
-
? { href: '#
|
|
1346
|
+
? { href: '#lists', label: t.common.parts.lists }
|
|
1347
1347
|
: header.includes(collection.id)
|
|
1348
|
-
? { href: '#
|
|
1348
|
+
? { href: '#appearance/header', label: t.common.parts.header }
|
|
1349
1349
|
: { href: '#', label: siteName }
|
|
1350
1350
|
}
|
|
1351
1351
|
tabs={
|
|
@@ -1357,7 +1357,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1357
1357
|
{ value: '', label: collection.label, icon: icons[collection.id as keyof typeof icons] ?? ListIcon },
|
|
1358
1358
|
...appTabs.map((a) => ({ value: a.app.id, label: siteText(a.tab.label), icon: a.tab.icon })),
|
|
1359
1359
|
],
|
|
1360
|
-
onChange: (v) => (location.hash = v ? `
|
|
1360
|
+
onChange: (v) => (location.hash = v ? `list/${collection.id}/${v}` : `list/${collection.id}`),
|
|
1361
1361
|
}
|
|
1362
1362
|
: undefined
|
|
1363
1363
|
}
|
|
@@ -1369,7 +1369,7 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1369
1369
|
icon={ClockCounterClockwise}
|
|
1370
1370
|
label={t.history.deletedOf(collection.label)}
|
|
1371
1371
|
|
|
1372
|
-
onClick={() => (location.hash = `
|
|
1372
|
+
onClick={() => (location.hash = `deleted/${collection.id}`)}
|
|
1373
1373
|
/>
|
|
1374
1374
|
)}
|
|
1375
1375
|
</>
|
package/src/Licenses.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Licenser" (#
|
|
1
|
+
// "Licenser" (#licenses), under "Indstillinger": the third-party work the site and the editor are made with, as the
|
|
2
2
|
// site was last built (/editor/licenses.json, licensesIntegration in packages/editor/lib/licenses.mjs), with a search by
|
|
3
3
|
// name, who made it and the licence's name. First the icon packs (the system's, each with the sets it takes icons from
|
|
4
4
|
// besides its own), then what the editor and the site's own code bring (the npm packages shipped in what is downloaded,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// then each theme the site is built with and each app installed, under its name: its own licence first, then what it
|
|
7
7
|
// brings (what it declares, its icon packs and what is shipped through them alone). Each row has the work's name, its
|
|
8
8
|
// version, licence and who made it, and what it is and where it is used; one with a text opens it
|
|
9
|
-
// (#
|
|
9
|
+
// (#licenses/<id>, LicensePage). An app's and a theme's page has its group too (OwnerLicenses), and one in the store,
|
|
10
10
|
// from the catalog, the same without the texts (StoreLicenseRows).
|
|
11
11
|
import { useEffect, useState } from 'react';
|
|
12
12
|
import { Certificate, Code, Image, Shapes, TextAa, type Icon } from '@phosphor-icons/react';
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type ThirdParty,
|
|
33
33
|
} from './licenseData';
|
|
34
34
|
|
|
35
|
-
export const LICENSES_ROUTE = '
|
|
35
|
+
export const LICENSES_ROUTE = 'licenses';
|
|
36
36
|
|
|
37
37
|
const KIND_ICONS: Record<LicenseKind, Icon> = { font: TextAa, icons: Shapes, library: Code, image: Image };
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ const ownerName = (o: Pick<LicenseOwner, 'kind' | 'key' | 'name'>) =>
|
|
|
46
46
|
/** Who brings a work, as a sentence's end: "i editoren og på sitet", "i temaet Material". */
|
|
47
47
|
function placesOf(where: LicenseWhere[], owners: LicenseOwner[]) {
|
|
48
48
|
// The editor and the site first, then the themes and the apps.
|
|
49
|
-
const rank = (w: LicenseWhere) => (w === 'editor' ? 0 : w === 'site' ? 1 : w.startsWith('
|
|
49
|
+
const rank = (w: LicenseWhere) => (w === 'editor' ? 0 : w === 'site' ? 1 : w.startsWith('theme/') ? 2 : 3);
|
|
50
50
|
const named = [...where]
|
|
51
51
|
.sort((a, b) => rank(a) - rank(b))
|
|
52
52
|
.map((w) => {
|
|
@@ -159,7 +159,7 @@ const worksOf = (owner: LicenseOwner, file: LicensesFile) =>
|
|
|
159
159
|
|
|
160
160
|
export default function Licenses() {
|
|
161
161
|
const file = useLicenses();
|
|
162
|
-
const [query, setQuery] = useKept('
|
|
162
|
+
const [query, setQuery] = useKept('licenses-search', '');
|
|
163
163
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
164
164
|
const matches = (...texts: (string | null | undefined)[]) => {
|
|
165
165
|
const all = texts.filter(Boolean).join(' ').toLowerCase();
|
|
@@ -170,7 +170,7 @@ export default function Licenses() {
|
|
|
170
170
|
|
|
171
171
|
const owners = file?.owners ?? [];
|
|
172
172
|
// The system's icon packs (a theme's own are under it, `where` it).
|
|
173
|
-
const packs = file ? sorted(file.works.filter((w) => w.kind === 'icons' && !w.where.some((x) => x.startsWith('
|
|
173
|
+
const packs = file ? sorted(file.works.filter((w) => w.kind === 'icons' && !w.where.some((x) => x.startsWith('theme/')))) : [];
|
|
174
174
|
// What the editor and the site bring: the CMS's own licence first, the rest A–Å.
|
|
175
175
|
const { cms, rest: system } = splitCms(
|
|
176
176
|
file ? sorted(file.works.filter((w) => w.kind !== 'icons' && w.where.some((x) => x === 'editor' || x === 'site'))) : [],
|
|
@@ -233,7 +233,7 @@ const paragraphsOf = (text: string) =>
|
|
|
233
233
|
)
|
|
234
234
|
.filter(Boolean);
|
|
235
235
|
|
|
236
|
-
/** One licence (#
|
|
236
|
+
/** One licence (#licenses/<id>): a work's, or an app's or a theme's own: its name, licence, who made it, what it is and
|
|
237
237
|
* its version, and its text. */
|
|
238
238
|
export function LicensePage({ id }: { id: string }) {
|
|
239
239
|
const file = useLicenses();
|
|
@@ -295,7 +295,7 @@ export function OwnerLicenses({
|
|
|
295
295
|
}) {
|
|
296
296
|
const file = useLicenses();
|
|
297
297
|
const owner = file?.owners.find((o) => o.id === id);
|
|
298
|
-
const [kind, key] = id.split('/') as ['app' | '
|
|
298
|
+
const [kind, key] = id.split('/') as ['app' | 'theme', string];
|
|
299
299
|
const self = owner ?? {
|
|
300
300
|
id,
|
|
301
301
|
kind: kind === 'app' ? ('app' as const) : ('theme' as const),
|