@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
package/src/Screen.tsx
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// topBarSlot.tsx's place, at the top of the content), above the content. Below medium, where there is no rail, the panel
|
|
3
3
|
// button at its start opens and closes the main drawer (MainMenu.tsx, menuOpen.ts; from medium it is the rail's; not on a screen with a back arrow); at expanded and wider the logo and the site's name after it (one link to the start screen,
|
|
4
4
|
// through `back.guard`), below expanded the back arrow, the title and the status as in an app (the logo only on the start
|
|
5
|
-
// screen); at the end the actions and the notifications, on every screen, and on the start screen (`home`) the link to the
|
|
5
|
+
// screen); at the end the page actions (below expanded), the actions and the notifications, on every screen, and on the start screen (`home`) the link to the
|
|
6
6
|
// website. At expanded and wider the way here as breadcrumbs (the screen above, when that is not the start
|
|
7
|
-
// screen), the title and the status stand in a row under the bar, at the top of the content
|
|
7
|
+
// screen), the title and the status stand in a row under the bar, at the top of the content, and the page actions
|
|
8
|
+
// (`pageActions`) at the end of that row. The bar stays at the top while
|
|
8
9
|
// scrolling and keeps clear of a phone's notch. A screen's tabs (`tabs`) are the bottom of the bar, under the title, as wide
|
|
9
10
|
// as the content, and stay with it. The back arrow goes to the screen above, back in the history when that is the screen
|
|
10
11
|
// before (nav.ts up). The content is as wide as a form reads well, or wider for lists and the grid of files (`wide`) and for
|
|
@@ -38,6 +39,9 @@ type Props<T extends string> = {
|
|
|
38
39
|
back?: Back;
|
|
39
40
|
status?: ReactNode;
|
|
40
41
|
actions?: ReactNode;
|
|
42
|
+
/** The screen's own actions (a form's Preview and View on site): from expanded at the end of the row under the bar,
|
|
43
|
+
* below expanded in the top bar, before `actions`. */
|
|
44
|
+
pageActions?: ReactNode;
|
|
41
45
|
drawer?: ReactNode;
|
|
42
46
|
tabs?: TabChoice<T>;
|
|
43
47
|
home?: boolean;
|
|
@@ -51,14 +55,14 @@ export function WebsiteLink() {
|
|
|
51
55
|
return <IconLink href="/" target="_blank" rel="noopener" icon={ArrowSquareOut} label={t.library.openWebsite} />;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
/** The bell in the top bar, before the link to the website: the notifications (#
|
|
58
|
+
/** The bell in the top bar, before the link to the website: the notifications (#notifications, Notifications.tsx), with a dot
|
|
55
59
|
* while some are not seen yet. */
|
|
56
60
|
export function NotificationsLink({ className = '' }: { className?: string }) {
|
|
57
61
|
const count = unseen(useNotifications());
|
|
58
|
-
const current = location.hash === '#
|
|
62
|
+
const current = location.hash === '#notifications';
|
|
59
63
|
return (
|
|
60
64
|
<IconLink
|
|
61
|
-
href="#
|
|
65
|
+
href="#notifications"
|
|
62
66
|
icon={Bell}
|
|
63
67
|
label={count ? t.library.notificationsNew(count) : t.library.notifications}
|
|
64
68
|
aria-current={current ? 'page' : undefined}
|
|
@@ -74,6 +78,7 @@ export default function Screen<T extends string>({
|
|
|
74
78
|
back,
|
|
75
79
|
status,
|
|
76
80
|
actions,
|
|
81
|
+
pageActions,
|
|
77
82
|
drawer,
|
|
78
83
|
tabs,
|
|
79
84
|
home = false,
|
|
@@ -135,7 +140,10 @@ export default function Screen<T extends string>({
|
|
|
135
140
|
)}
|
|
136
141
|
</div>
|
|
137
142
|
<div className="hidden min-w-0 flex-1 expanded:block" />
|
|
138
|
-
<div className="flex shrink-0 items-center justify-end gap-1">
|
|
143
|
+
<div className="flex shrink-0 items-center justify-end gap-1">
|
|
144
|
+
{pageActions && <div className="flex items-center gap-1 expanded:hidden">{pageActions}</div>}
|
|
145
|
+
{actions}
|
|
146
|
+
</div>
|
|
139
147
|
<NotificationsLink />
|
|
140
148
|
{home && <WebsiteLink />}
|
|
141
149
|
{drawer}
|
|
@@ -169,6 +177,8 @@ export default function Screen<T extends string>({
|
|
|
169
177
|
)}
|
|
170
178
|
<h1 className="truncate font-medium">{title}</h1>
|
|
171
179
|
{status && <p className="shrink-0 text-sm text-muted">{status}</p>}
|
|
180
|
+
{/* The icons are taller than the text: the negative margin keeps the row's height. */}
|
|
181
|
+
{pageActions && <div className="-my-3 ml-auto flex shrink-0 items-center gap-1">{pageActions}</div>}
|
|
172
182
|
</div>
|
|
173
183
|
{tabs ? (
|
|
174
184
|
<TabPanel id={id} value={tabs.value}>
|
package/src/SeoScores.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "SEO-vurdering" under "Søgemaskineoptimering" (#seo/
|
|
1
|
+
// "SEO-vurdering" under "Søgemaskineoptimering" (#seo/rating): every page and blog post with the score of its SEO
|
|
2
2
|
// rating (seo.ts, the same as at the top of "Søgemaskiner" on it) in a ring at the start of its row, and its
|
|
3
3
|
// primary keyword under its name, under "Sider" and "Blogindlæg", the lowest first. Over them, as over the lists, a
|
|
4
4
|
// search by name and keyword, the order (lowest or highest first, A–Å or Å–A, kept in this browser) and the funnel with
|
|
@@ -18,7 +18,7 @@ import { byText, type Collection, type MarkdownCollection } from './schema';
|
|
|
18
18
|
import { t } from './i18n';
|
|
19
19
|
|
|
20
20
|
/** Where it opens, under "Søgemaskineoptimering". */
|
|
21
|
-
export const SEO_SCORES_ROUTE = 'seo/
|
|
21
|
+
export const SEO_SCORES_ROUTE = 'seo/rating';
|
|
22
22
|
|
|
23
23
|
type Sort = 'low' | 'high' | 'az' | 'za';
|
|
24
24
|
const SORTS: Sort[] = ['low', 'high', 'az', 'za'];
|
|
@@ -56,7 +56,7 @@ function rate(collection: MarkdownCollection, file: RepoFile, relations: Env['re
|
|
|
56
56
|
export const isRated = (c: Collection): c is MarkdownCollection => c.kind === 'markdown' && c.fields.some((f) => f.name === 'seoTitle');
|
|
57
57
|
|
|
58
58
|
export default function SeoScores({ files, env }: { files: RepoFile[]; env: Env }) {
|
|
59
|
-
const [query, setQuery] = useKept('seo-
|
|
59
|
+
const [query, setQuery] = useKept('seo-search', '');
|
|
60
60
|
const [sort, setSort] = useState(savedSort);
|
|
61
61
|
function pickSort(next: Sort) {
|
|
62
62
|
setSort(next);
|
|
@@ -74,9 +74,9 @@ export default function SeoScores({ files, env }: { files: RepoFile[]; env: Env
|
|
|
74
74
|
);
|
|
75
75
|
const all = groups.flatMap((g) => g.items);
|
|
76
76
|
const average = all.length ? Math.round(all.reduce((sum, r) => sum + r.score, 0) / all.length) : null;
|
|
77
|
-
const [rating, setRating] = useKept<Rating | ''>('seo-
|
|
78
|
-
const [hasKeyword, setHasKeyword] = useKept<HasKeyword | ''>('seo-
|
|
79
|
-
const [kind, setKind] = useKept('seo-
|
|
77
|
+
const [rating, setRating] = useKept<Rating | ''>('seo-rating', '');
|
|
78
|
+
const [hasKeyword, setHasKeyword] = useKept<HasKeyword | ''>('seo-keywords', '');
|
|
79
|
+
const [kind, setKind] = useKept('seo-kind', '');
|
|
80
80
|
const shownKind = groups.some((g) => g.collection.id === kind) ? kind : '';
|
|
81
81
|
const kindName = groups.find((g) => g.collection.id === shownKind)?.collection.label ?? '';
|
|
82
82
|
const filters = [
|
package/src/SideColumn.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Sidekolonne" (#
|
|
1
|
+
// "Udseende" › "Sidekolonne" (#appearance/side-column): the block drawn in a column beside the text of the pages, chosen
|
|
2
2
|
// among "Ingen" and the installed blocks that can be (`beside` in their load.ts), saved in the site's file (config.side)
|
|
3
3
|
// in its own commit. Exposes SideColumn, sideSettingOf and sideChoicesOf (also used by the page form and by Library.tsx).
|
|
4
4
|
// Uses ui.tsx (ChoiceRow, Group), registry.tsx (siteBlocks) and lib/side-block.mjs.
|
|
@@ -24,7 +24,7 @@ const sources: Record<string, string> = Object.fromEntries(
|
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
const capital = (s: string) => s.charAt(0).toLocaleUpperCase(locale) + s.slice(1);
|
|
27
|
-
const back = { href: '#
|
|
27
|
+
const back = { href: '#appearance', label: t.common.parts.appearance };
|
|
28
28
|
|
|
29
29
|
/** The block type the site's file (config.side) chooses, or null. */
|
|
30
30
|
export function sideSettingOf(files: { path: string; text: string }[]): string | null {
|
package/src/StartFab.tsx
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// The start screen's FAB menu (Start in Library.tsx): a new page and a new blog post, each opening its empty form
|
|
2
|
-
// (#
|
|
3
|
-
// collections and icons of site.ts.
|
|
2
|
+
// (#new/<list>), as the FAB on its list does. Exposes newOnStart (the actions) and StartFab. Uses FabMenu (ui.tsx),
|
|
3
|
+
// START_FAB (fabScreens.ts) and the collections and icons of site.ts.
|
|
4
4
|
import { Plus } from '@phosphor-icons/react';
|
|
5
5
|
import { t } from './i18n';
|
|
6
|
+
import { START_FAB } from './fabScreens';
|
|
6
7
|
import { collections, icons } from './site';
|
|
7
8
|
import { FabMenu, type FabAction } from './ui';
|
|
8
9
|
|
|
9
|
-
/** The lists
|
|
10
|
-
const ON_START = ['pages', 'blog'];
|
|
11
|
-
|
|
12
|
-
/** The start screen's actions: the lists of ON_START the site has and can make a new entry in (`create`). */
|
|
10
|
+
/** The start screen's actions: the lists of START_FAB the site has and can make a new entry in (`create`). */
|
|
13
11
|
export function newOnStart(): FabAction[] {
|
|
14
|
-
return
|
|
12
|
+
return START_FAB.flatMap((id) => {
|
|
15
13
|
const c = collections.find((x) => x.id === id);
|
|
16
|
-
return c?.create ? [{ icon: icons[id] ?? Plus, label: c.create, href: `#
|
|
14
|
+
return c?.create ? [{ icon: icons[id] ?? Plus, label: c.create, href: `#new/${id}` }] : [];
|
|
17
15
|
});
|
|
18
16
|
}
|
|
19
17
|
|
package/src/ThemeStore.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The themes in the app store (store.ts): their tab in "Appstore" (#
|
|
1
|
+
// The themes in the app store (store.ts): their tab in "Appstore" (#settings/appstore/themes, StoreThemes), all themes, those
|
|
2
2
|
// in the store and those installed, with the same search, order and status filter as the apps; the page of one that
|
|
3
|
-
// is not installed (#
|
|
3
|
+
// is not installed (#appearance/themes/<id>, StoreThemePage), drawn from the catalog as a theme's page (its picture,
|
|
4
4
|
// name, version, who made it, "Installer", its pictures of a site in it, what it is and gives the site, and who made
|
|
5
5
|
// it); and on the page of one installed from the store (ThemePage in Library.tsx) "Opdater til …" and "Afinstaller"
|
|
6
6
|
// (ThemeStoreButtons). Installing copies its folder into the site's themes (config.themes/<id>/, with its store.json),
|
|
@@ -21,6 +21,8 @@ import { iconPacks, packOfTheme } from './iconPacks';
|
|
|
21
21
|
import { theme, themes, type ThemeInfo } from './themes';
|
|
22
22
|
import {
|
|
23
23
|
changedFiles,
|
|
24
|
+
legacyOf,
|
|
25
|
+
moveChanges,
|
|
24
26
|
newer,
|
|
25
27
|
notesSince,
|
|
26
28
|
STANDARD_THEME,
|
|
@@ -36,7 +38,7 @@ import { siteT, t } from './i18n';
|
|
|
36
38
|
import { kindAndName } from './deploy';
|
|
37
39
|
|
|
38
40
|
/** A theme's page (installed, or in the store). */
|
|
39
|
-
export const themeHref = (id: string) => `#
|
|
41
|
+
export const themeHref = (id: string) => `#appearance/themes/${id}`;
|
|
40
42
|
|
|
41
43
|
/** A theme's picture (its theme.json's icon), where a row has its icon. */
|
|
42
44
|
export function ThemePicture({ src }: { src: string }) {
|
|
@@ -63,8 +65,12 @@ export function useThemeCatalog(env: Env): ShownTheme[] | null | 'failed' {
|
|
|
63
65
|
// version updated to, until the site's build has it (and the editor reloads with it, deploy.ts keepCurrent).
|
|
64
66
|
const changed = new Map<string, string>();
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const themeHere = (id: string) => (changed.has(id) ? changed.get(id) !== 'uninstalled' : themes.some((x) => x.id === id));
|
|
69
|
+
|
|
70
|
+
/** Whether a theme is in the site now: installed and not uninstalled since, or installed since; or, with its `legacy` id,
|
|
71
|
+
* a copy under that old id from the store (legacyOf). */
|
|
72
|
+
export const themeInstalled = (id: string, legacy?: string) =>
|
|
73
|
+
themeHere(id) || (!!legacy && legacyOf({ legacy }, themes) !== null && themeHere(legacy));
|
|
68
74
|
|
|
69
75
|
/** A theme installed from the store that the site's build does not have yet. */
|
|
70
76
|
export const themePending = (id: string) => changed.get(id) === 'installed' && !themes.some((x) => x.id === id);
|
|
@@ -74,7 +80,7 @@ const versionOf = (x: ThemeInfo) => (changed.has(x.id) && changed.get(x.id) !==
|
|
|
74
80
|
|
|
75
81
|
/** The newer version of a theme from the store, when the store has one. */
|
|
76
82
|
export const themeUpdateOf = (x: ThemeInfo, catalog: ShownTheme[] | null | 'failed') => {
|
|
77
|
-
const found = x.store && Array.isArray(catalog) ? catalog.find((c) => c.id === x.id) : undefined;
|
|
83
|
+
const found = x.store && Array.isArray(catalog) ? catalog.find((c) => c.id === x.id || c.legacy === x.id) : undefined;
|
|
78
84
|
return found && newer(found.version, versionOf(x)) ? found : undefined;
|
|
79
85
|
};
|
|
80
86
|
|
|
@@ -124,11 +130,13 @@ function useThemeActions(env: Env) {
|
|
|
124
130
|
text: { busy: string; done: string; commit: string; change: string },
|
|
125
131
|
next: () => Promise<Parameters<typeof themeChanges>[2]>,
|
|
126
132
|
go: (current: RepoFile[]) => Promise<boolean> = async () => true,
|
|
133
|
+
// The id the theme is installed under, when it is a legacy copy that moves to `id` (moveChanges).
|
|
134
|
+
legacy: string | null = null,
|
|
127
135
|
): Promise<boolean> {
|
|
128
136
|
setRunning({ id, action, asking: false });
|
|
129
137
|
env.notify({ text: text.busy });
|
|
130
138
|
try {
|
|
131
|
-
const [current, wanted] = await Promise.all([env.folder(themeFolder(id)), next()]);
|
|
139
|
+
const [current, wanted] = await Promise.all([env.folder(themeFolder(legacy ?? id)), next()]);
|
|
132
140
|
if (!(await go(current))) {
|
|
133
141
|
env.notify(null);
|
|
134
142
|
return false;
|
|
@@ -136,7 +144,11 @@ function useThemeActions(env: Env) {
|
|
|
136
144
|
// `go` cleared the message (and its dialog stopped the button being busy): saving goes on.
|
|
137
145
|
setRunning({ id, action, asking: false });
|
|
138
146
|
env.notify({ text: text.busy });
|
|
139
|
-
await env.commit(
|
|
147
|
+
await env.commit(
|
|
148
|
+
text.commit,
|
|
149
|
+
legacy && wanted ? moveChanges({ id }, legacy, wanted, current, env.files) : themeChanges(id, current, wanted),
|
|
150
|
+
text.change,
|
|
151
|
+
);
|
|
140
152
|
env.notify({ text: text.done, kind: 'saved' });
|
|
141
153
|
return true;
|
|
142
154
|
} catch (e) {
|
|
@@ -165,7 +177,7 @@ function useThemeActions(env: Env) {
|
|
|
165
177
|
if (done) changed.set(x.id, 'installed');
|
|
166
178
|
return done;
|
|
167
179
|
},
|
|
168
|
-
async update(x: ShownTheme, record: StoreRecord) {
|
|
180
|
+
async update(x: ShownTheme, record: StoreRecord, legacy: string | null = null) {
|
|
169
181
|
const done = await change(
|
|
170
182
|
x.id,
|
|
171
183
|
'update',
|
|
@@ -190,11 +202,15 @@ function useThemeActions(env: Env) {
|
|
|
190
202
|
record.version,
|
|
191
203
|
x.version,
|
|
192
204
|
notesSince(x.releaseNotes, record.version, x.version),
|
|
193
|
-
changedFiles(x.id, record, current, themeFolder(x.id)),
|
|
205
|
+
changedFiles(x.id, record, current, themeFolder(legacy ?? x.id)),
|
|
194
206
|
);
|
|
195
207
|
},
|
|
208
|
+
legacy,
|
|
196
209
|
);
|
|
197
|
-
if (done
|
|
210
|
+
if (done && legacy) {
|
|
211
|
+
changed.set(x.id, 'installed');
|
|
212
|
+
changed.set(legacy, 'uninstalled');
|
|
213
|
+
} else if (done) changed.set(x.id, x.version);
|
|
198
214
|
return done;
|
|
199
215
|
},
|
|
200
216
|
async uninstall(id: string, name: string) {
|
|
@@ -221,11 +237,12 @@ function useThemeActions(env: Env) {
|
|
|
221
237
|
export function allThemes(catalog: ShownTheme[] | null | 'failed') {
|
|
222
238
|
const store = Array.isArray(catalog) ? catalog : [];
|
|
223
239
|
return [
|
|
224
|
-
...store.map((x) => ({ id: x.id, name: x.name, author: x.author, version: x.version, icon: x.icon, words: x.words })),
|
|
240
|
+
...store.map((x) => ({ id: x.id, legacy: x.legacy, name: x.name, author: x.author, version: x.version, icon: x.icon, words: x.words })),
|
|
225
241
|
...themes
|
|
226
|
-
.filter((x) => !store.some((s) => s.id === x.id))
|
|
242
|
+
.filter((x) => !store.some((s) => s.id === x.id || s.legacy === x.id))
|
|
227
243
|
.map((x) => ({
|
|
228
244
|
id: x.id,
|
|
245
|
+
legacy: undefined as string | undefined,
|
|
229
246
|
name: x.name,
|
|
230
247
|
author: x.author,
|
|
231
248
|
version: x.version,
|
|
@@ -247,13 +264,13 @@ export function ThemeRow({ of }: { of: ReturnType<typeof allThemes>[number] }) {
|
|
|
247
264
|
headline={of.name}
|
|
248
265
|
supporting={t.library.themeByline(of.version, of.author)}
|
|
249
266
|
truncate
|
|
250
|
-
count={themeInstalled(of.id) && (themePending(of.id) ? t.apps.pending : t.apps.installed)}
|
|
267
|
+
count={themeInstalled(of.id, of.legacy) && (themePending(of.id) ? t.apps.pending : t.apps.installed)}
|
|
251
268
|
arrow
|
|
252
269
|
/>
|
|
253
270
|
);
|
|
254
271
|
}
|
|
255
272
|
|
|
256
|
-
/** The page of a theme in the store that is not installed, or installed but not yet built (#
|
|
273
|
+
/** The page of a theme in the store that is not installed, or installed but not yet built (#appearance/themes/<id>),
|
|
257
274
|
* from the catalog: as a theme's page, with "Installer" (or "Afinstaller"). */
|
|
258
275
|
export function StoreThemePage({ id, env }: { id: string; env: Env }) {
|
|
259
276
|
const catalog = useThemeCatalog(env);
|
|
@@ -363,7 +380,11 @@ export function ThemeStoreButtons({ of, env, files, top }: { of: ThemeInfo; env:
|
|
|
363
380
|
if (top)
|
|
364
381
|
return update ? (
|
|
365
382
|
<div className="mb-8">
|
|
366
|
-
<Button
|
|
383
|
+
<Button
|
|
384
|
+
disabled={actions.saving}
|
|
385
|
+
busy={actions.busy(update.id, 'update')}
|
|
386
|
+
onClick={() => actions.update(update, of.store!, legacyOf(update, themes))}
|
|
387
|
+
>
|
|
367
388
|
{t.apps.update(update.version)}
|
|
368
389
|
</Button>
|
|
369
390
|
</div>
|
package/src/Tokens.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Dele" › "Værdier" (#
|
|
1
|
+
// "Dele" › "Værdier" (#appearance/parts/values): the theme's design tokens, as the theme in use sets them, to see them in one
|
|
2
2
|
// place: the corners, one row for each size the theme uses (the theme's base, base.css and components.css, which every
|
|
3
3
|
// theme has, and its UI parts', ui.css: each part's own variable, else the one ui.css gives it instead), named by
|
|
4
4
|
// Material 3's corner radius scale where the size is one of its steps, with what has it under; the shadows, one row for
|
|
@@ -15,7 +15,7 @@ import { Group, LeadingToken, ListItem, type TokenKind } from './ui';
|
|
|
15
15
|
import { locale, t } from './i18n';
|
|
16
16
|
|
|
17
17
|
const say = t.parts.tokens;
|
|
18
|
-
const back = { href: '#
|
|
18
|
+
const back = { href: '#appearance/parts', label: t.common.parts.editorParts };
|
|
19
19
|
|
|
20
20
|
/** A token: its CSS variable, its name here, and for a part's own the token or the value ui.css gives it when the theme
|
|
21
21
|
* does not set it (its `var(--chip-radius, var(--block-control-radius))`). */
|
package/src/apps.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// asking its own addresses in the Worker (`api`);
|
|
26
26
|
// screens in index.ts screens of its own under its page (#apps/<app>/<screen>, AppScreenPage), drawn as its page,
|
|
27
27
|
// which links to them (Cookiescanner's "Blokeret adresse");
|
|
28
|
-
// tabs in index.ts tabs it adds to the site's lists (#
|
|
28
|
+
// tabs in index.ts tabs it adds to the site's lists (#list/<list>/<app>, after the list's own tab in the top app
|
|
29
29
|
// bar, Library.tsx List), drawn as its page (Cookiescanner's "Problemer" on "Cookies");
|
|
30
30
|
// notifications in index.ts the kinds of notifications it sends, each turned off under "Konto", and how it finds
|
|
31
31
|
// them (`check`, with `api`; AppNotifications);
|
|
@@ -161,7 +161,7 @@ export type AppDefinition = {
|
|
|
161
161
|
* what it does ("nyhedsbrev"). The kind also says where its settings are: under "Indstillinger" on the site's screen
|
|
162
162
|
* with it (PartScreen.apps in partScreens: "analytics" on "Analytics"), or on a part of "Virksomheden" with it
|
|
163
163
|
* (Part.apps in companyParts: "payment" on "Betaling"), so they are where they are used and not on its page under
|
|
164
|
-
* "Apps", which only installs it; without such a screen, on a screen of their own (#apps/<id>/
|
|
164
|
+
* "Apps", which only installs it; without such a screen, on a screen of their own (#apps/<id>/settings). */
|
|
165
165
|
category?: AppCategory;
|
|
166
166
|
keywords?: string[];
|
|
167
167
|
/** The categories of cookies (lib/cookieCategories.mjs) of what it loads that sets cookies (a tag), with these
|
|
@@ -173,10 +173,10 @@ export type AppDefinition = {
|
|
|
173
173
|
* editor's parts (ui.tsx), its texts with siteText. */
|
|
174
174
|
page?: ComponentType<AppPageProps>;
|
|
175
175
|
/** Screens of its own under its page while it is installed, by the last part of their address (#apps/<id>/<key>; not
|
|
176
|
-
* "
|
|
176
|
+
* "settings", the Settings tab's field pages, and not "about", the Information tab: the editor matches those
|
|
177
177
|
* first, so a screen with either key is never shown), each linked to from its page. */
|
|
178
178
|
screens?: Record<string, AppScreen>;
|
|
179
|
-
/** Tabs it adds to the site's lists while it is installed, by the list's id, each at #
|
|
179
|
+
/** Tabs it adds to the site's lists while it is installed, by the list's id, each at #list/<list>/<app>. */
|
|
180
180
|
tabs?: Record<string, AppTab>;
|
|
181
181
|
notifications?: AppNotifications;
|
|
182
182
|
};
|
|
@@ -230,7 +230,7 @@ export function withKinds<C extends { id: string; kind: string; fields?: Field[]
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/** An app's kind of a list as it is turned off under "Blokindstillinger", among the blocks turned off ("popups/mailchimp",
|
|
233
|
-
* as its address under "+", #
|
|
233
|
+
* as its address under "+", #new/popups/mailchimp). */
|
|
234
234
|
export const kindKey = (list: string, app: string) => `${list}/${app}`;
|
|
235
235
|
|
|
236
236
|
/** The kinds a new entry of a list can be, under its "+": its own, and those of the apps installed and not turned off
|
|
@@ -251,9 +251,8 @@ export function kindsOffered(
|
|
|
251
251
|
export const tabsOf = (list: string, apps: SiteApp[], installed: string[]) =>
|
|
252
252
|
apps.flatMap((app) => (installed.includes(app.id) && app.tabs?.[list] ? [{ app, tab: app.tabs[list] }] : []));
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
export const appIdOf = (path: string) => path.split('/apps/')[1].split('/')[0];
|
|
254
|
+
// The id of an app from the path of a file in its folder (lib/appId.mjs, also read by the @app/self plugin).
|
|
255
|
+
export { appIdOf } from '../lib/appId.mjs';
|
|
257
256
|
|
|
258
257
|
/** The file of an app's settings (config.apps.settings/<id>.json). */
|
|
259
258
|
export const settingsFile = (id: string) => `${config.apps.settings}/${id}.json`;
|
package/src/checkSite.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The redirects (redirectData.ts): none sends on through another, and none in a loop. And the site's own rules
|
|
9
9
|
// (siteProblems in its rules.ts: a button that opens a promotion not on its page).
|
|
10
10
|
// The file records (src/data/files: the tags of files in Filer): each is valid, no tag is both chosen and removed, one per address.
|
|
11
|
-
// And the site's texts the editor has no translation of (untranslated).
|
|
11
|
+
// And the site's texts the editor has no translation of, and a converted standard text with no Danish (untranslated).
|
|
12
12
|
import { check } from './check';
|
|
13
13
|
import {
|
|
14
14
|
appearance,
|
|
@@ -29,7 +29,7 @@ import { colorProblems, lowContrast, MODES } from '../lib/colors';
|
|
|
29
29
|
import { completeColors, dynamicProblems } from '../lib/dynamic-colors';
|
|
30
30
|
import { APP_HOOKS, hooksOf } from '../lib/hooks';
|
|
31
31
|
import { texts } from '@site/texts';
|
|
32
|
-
import {
|
|
32
|
+
import { defaultsDaDK } from '../defaults/i18n/da-DK';
|
|
33
33
|
import type { Part } from './schema';
|
|
34
34
|
import { apps, asStandardTemplate, blockProblems, siteBlocks, standardPageOf, withPageBlocks } from './registry';
|
|
35
35
|
import { installedApps, settingsFile } from './apps';
|
|
@@ -37,6 +37,14 @@ import { appFolder, SCREENSHOTS, shotFile, shotOf } from './store';
|
|
|
37
37
|
import { appFolders } from './appFiles';
|
|
38
38
|
import { colorPairs as shownPairs, sharedParts, translator, writtenApps, writtenBlockParts, writtenCollections } from './site';
|
|
39
39
|
import { splitBlocks } from '../lib/blocks.mjs';
|
|
40
|
+
import { splitMissing } from '../lib/translate.mjs';
|
|
41
|
+
import { collections as standardCollections } from '../defaults/collections';
|
|
42
|
+
import { companyParts as standardCompany } from '../defaults/company';
|
|
43
|
+
import { blockParts as standardBlocks } from '../defaults/blockTexts';
|
|
44
|
+
import { colorFields as standardColorFields, colorPairs as standardColorPairs } from '../defaults/colors';
|
|
45
|
+
import { privacy as standardPrivacy } from '../defaults/privacy';
|
|
46
|
+
import { internalLinks as standardInternalLinks } from '../defaults/internalLinks';
|
|
47
|
+
import { appearance as standardAppearance, groups as standardGroups } from '../defaults/editor';
|
|
40
48
|
import { isRecordPath, recordProblems } from './fileRecords.mjs';
|
|
41
49
|
import { appTexts, DEFAULTS_LOCALE, locales, siteLocale, t, type Texts } from './i18n';
|
|
42
50
|
import { flatten, readAll, REDIRECT_FILES, type AllRedirects } from './redirectData';
|
|
@@ -212,19 +220,73 @@ function chainsOf(all: AllRedirects, flat: AllRedirects): string[] {
|
|
|
212
220
|
return lines;
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
type Definitions = {
|
|
224
|
+
collections: typeof collections;
|
|
225
|
+
parts: Part[];
|
|
226
|
+
screens: typeof partScreens;
|
|
227
|
+
groups: typeof groups;
|
|
228
|
+
colorFields: typeof colorFields;
|
|
229
|
+
colorPairs: typeof colorPairs;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** The definitions the walk goes over: the site's (the standard parts, merged with the site's own) ... */
|
|
233
|
+
const siteDefinitions: Definitions = {
|
|
234
|
+
collections,
|
|
235
|
+
parts: [...companyParts, ...partScreens.flatMap((s) => s.parts), ...appearance, ...seoParts, ...blockParts],
|
|
236
|
+
screens: partScreens,
|
|
237
|
+
groups,
|
|
238
|
+
colorFields,
|
|
239
|
+
colorPairs,
|
|
240
|
+
};
|
|
241
|
+
/** ... and the package's own standard parts, as defaults/ writes them, whatever the site overrides. */
|
|
242
|
+
const standardDefinitions: Definitions = {
|
|
243
|
+
collections: standardCollections,
|
|
244
|
+
parts: [...standardCompany, ...standardPrivacy.parts, ...standardAppearance, standardInternalLinks, ...standardBlocks],
|
|
245
|
+
screens: [standardPrivacy],
|
|
246
|
+
groups: standardGroups,
|
|
247
|
+
colorFields: standardColorFields,
|
|
248
|
+
colorPairs: standardColorPairs,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** Shows each text of the definitions to the translator `w`. */
|
|
252
|
+
function walkDefinitions(w: ReturnType<typeof translator>, d: Definitions) {
|
|
253
|
+
d.collections.forEach(w.collection);
|
|
254
|
+
d.parts.forEach(w.part);
|
|
255
|
+
d.screens.forEach((s) => w.group({ id: s.id, title: s.label, ids: [] }));
|
|
256
|
+
d.groups.forEach(w.group);
|
|
257
|
+
d.colorFields.forEach(w.color);
|
|
258
|
+
d.colorPairs.forEach(w.pair);
|
|
259
|
+
}
|
|
260
|
+
|
|
215
261
|
/** The site's own texts with no translation, by the editor's other languages, by the file they belong
|
|
216
262
|
* in: the site's in @site/texts, a block's in its texts.ts (or the site's, for what it takes from
|
|
217
263
|
* the site). The editor shows them as they are written. Not the ones said in code (the messages in
|
|
218
|
-
* rules.ts, the lines on the blocks' cards).
|
|
264
|
+
* rules.ts, the lines on the blocks' cards). A text of the package's standard parts with no translation in a language
|
|
265
|
+
* (defaults/i18n/da-DK/ for da-DK, a blank value too) is under 'defaults/i18n/da-DK/index.ts': it stops `check:content`. */
|
|
219
266
|
export function untranslated(): Record<string, Record<string, string[]>> {
|
|
220
|
-
// The editor shows a site's definitions translated already on a site in another language than
|
|
221
|
-
// meets is traced back to the one it was translated from (the translations' tables are by it).
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
const tables =
|
|
267
|
+
// The editor shows a site's definitions translated already on a site in another language than the standard parts', so a text
|
|
268
|
+
// the walk meets is traced back to the one it was translated from (the translations' tables are by it). The standard parts are
|
|
269
|
+
// written in English (the keys of defaults/i18n/da-DK/); the texts of an app are still written in Danish (its texts.ts, until
|
|
270
|
+
// the apps are converted); every other text is written in the site's language. A text needs a translation in each other language.
|
|
271
|
+
const tables = Object.values(appTexts).map((table) => Object.entries(table['en-US'] ?? {}));
|
|
225
272
|
const danish = new Set(tables.flatMap((entries) => entries.map(([text]) => text)));
|
|
226
273
|
const original = new Map(tables.flatMap((entries) => entries.map(([text, translated]) => [translated, text] as const)));
|
|
227
|
-
|
|
274
|
+
// An entry with no Danish (a blank value) is no translation. The walk meets the Danish as it is shown.
|
|
275
|
+
const converted = Object.entries(defaultsDaDK);
|
|
276
|
+
const english = new Set(converted.map(([text]) => text));
|
|
277
|
+
const inDanish = Object.fromEntries(converted.filter(([, danish]) => danish.trim()));
|
|
278
|
+
for (const [text, danish] of Object.entries(inDanish)) original.set(danish, text);
|
|
279
|
+
const writtenIn = (text: string) => (english.has(text) ? DEFAULTS_LOCALE : danish.has(text) ? 'da-DK' : siteLocale);
|
|
280
|
+
// The texts of the package's standard parts: the walk over defaults/'s own definitions, so one with no Danish entry is known too.
|
|
281
|
+
const standardSet = new Set<string>();
|
|
282
|
+
walkDefinitions(
|
|
283
|
+
translator((shown) => {
|
|
284
|
+
standardSet.add(original.get(shown) ?? shown);
|
|
285
|
+
return shown;
|
|
286
|
+
}),
|
|
287
|
+
standardDefinitions,
|
|
288
|
+
);
|
|
289
|
+
const lacking = new Map<string, Set<string>>();
|
|
228
290
|
const missing = (l: string, own: Record<string, string>, walk: (w: ReturnType<typeof translator>) => void) => {
|
|
229
291
|
const found = new Set<string>();
|
|
230
292
|
walk(
|
|
@@ -234,22 +296,21 @@ export function untranslated(): Record<string, Record<string, string[]>> {
|
|
|
234
296
|
return shown;
|
|
235
297
|
}),
|
|
236
298
|
);
|
|
237
|
-
|
|
299
|
+
// A text of the package's standard parts stops the check, the others (the site's own, an app's) only warn.
|
|
300
|
+
const split = splitMissing(found, standardSet);
|
|
301
|
+
split.standard.forEach((text) => lacking.set(l, (lacking.get(l) ?? new Set()).add(text)));
|
|
302
|
+
return split.own;
|
|
238
303
|
};
|
|
239
|
-
|
|
304
|
+
const gaps = Object.fromEntries(
|
|
240
305
|
locales
|
|
241
306
|
.filter((l) => l !== siteLocale || l !== DEFAULTS_LOCALE)
|
|
242
307
|
.map((l) => {
|
|
243
|
-
const site =
|
|
308
|
+
const site = {
|
|
309
|
+
...(l === 'da-DK' ? inDanish : {}),
|
|
310
|
+
...Object.fromEntries(Object.entries((texts as Texts)[l] ?? {}).filter(([, v]) => v.trim())),
|
|
311
|
+
};
|
|
244
312
|
const files: Record<string, string[]> = {
|
|
245
|
-
'src/site/texts.ts': missing(l, site, (w) =>
|
|
246
|
-
collections.forEach(w.collection);
|
|
247
|
-
[...companyParts, ...partScreens.flatMap((s) => s.parts), ...appearance, ...seoParts, ...blockParts].forEach(w.part);
|
|
248
|
-
partScreens.forEach((s) => w.group({ id: s.id, title: s.label, ids: [] }));
|
|
249
|
-
groups.forEach(w.group);
|
|
250
|
-
colorFields.forEach(w.color);
|
|
251
|
-
colorPairs.forEach(w.pair);
|
|
252
|
-
}),
|
|
313
|
+
'src/site/texts.ts': missing(l, site, (w) => walkDefinitions(w, siteDefinitions)),
|
|
253
314
|
};
|
|
254
315
|
// An app's block's own texts, and the fields of its shared texts (shared.ts), in its texts.ts.
|
|
255
316
|
for (const block of siteBlocks.filter((b) => b.app))
|
|
@@ -266,4 +327,9 @@ export function untranslated(): Record<string, Record<string, string[]>> {
|
|
|
266
327
|
return [l, Object.fromEntries(Object.entries(files).filter(([, list]) => list.length))];
|
|
267
328
|
}),
|
|
268
329
|
);
|
|
330
|
+
// A standard text with no Danish: no entry in defaults/i18n/da-DK/, or a blank one.
|
|
331
|
+
const blank = converted.filter(([, danish]) => !danish.trim()).map(([text]) => text);
|
|
332
|
+
for (const text of blank) lacking.set('da-DK', (lacking.get('da-DK') ?? new Set()).add(text));
|
|
333
|
+
for (const [l, list] of lacking) gaps[l] = { ...gaps[l], 'defaults/i18n/da-DK/index.ts': [...list] };
|
|
334
|
+
return gaps;
|
|
269
335
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Whether the standard drawer shows open: remembered (sidebar.ts), panelOpen (the standard side panel is open), large (window
|
|
2
|
+
* at least `large`), reopened (the menu button opened it while the panel is open). */
|
|
3
|
+
export function drawerShown(remembered: any, panelOpen: any, large: any, reopened: any): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Whether the standard navigation drawer shows open while the standard side panel may be open beside the text (840-1199px:
|
|
2
|
+
// both do not fit). Exposes drawerShown(remembered, panelOpen, large, reopened); used by menuOpen.ts; no dependencies.
|
|
3
|
+
|
|
4
|
+
/** Whether the standard drawer shows open: remembered (sidebar.ts), panelOpen (the standard side panel is open), large (window
|
|
5
|
+
* at least `large`), reopened (the menu button opened it while the panel is open). */
|
|
6
|
+
export function drawerShown(remembered, panelOpen, large, reopened) {
|
|
7
|
+
if (!remembered && !reopened) return false;
|
|
8
|
+
if (panelOpen && !large && !reopened) return false;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tests for drawerYield.mjs: the standard drawer yields to the side panel below large, unless the menu button reopened it.
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { drawerShown } from './drawerYield.mjs';
|
|
5
|
+
|
|
6
|
+
test('the drawer shows as remembered while no panel is open', () => {
|
|
7
|
+
assert.equal(drawerShown(true, false, false, false), true);
|
|
8
|
+
assert.equal(drawerShown(false, false, true, false), false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('the drawer yields while the panel is open below large', () => {
|
|
12
|
+
assert.equal(drawerShown(true, true, false, false), false);
|
|
13
|
+
assert.equal(drawerShown(false, true, false, false), false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('the drawer shows again when the menu button reopened it', () => {
|
|
17
|
+
assert.equal(drawerShown(true, true, false, true), true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('the drawer never yields from large', () => {
|
|
21
|
+
assert.equal(drawerShown(true, true, true, false), true);
|
|
22
|
+
});
|