@morphemecms/cms 0.5.0 → 0.7.0
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 +16 -0
- package/astro.mjs +3 -0
- package/bin/morpheme.mjs +3 -1
- package/defaults/blockTexts.ts +2 -2
- package/defaults/collections.ts +10 -0
- package/defaults/editor.ts +57 -53
- package/defaults/privacy.ts +4 -1
- package/defaults/rules.ts +36 -38
- package/defaults/standardPages.ts +0 -3
- package/defaults/texts.ts +11 -2
- package/demo/src/content/pages/about.md +2 -0
- package/icons/lucide/browser-fill.svg +1 -0
- package/icons/lucide/buildings-fill.svg +1 -0
- package/icons/lucide/certificate-fill.svg +1 -0
- package/icons/lucide/chart-line-up-fill.svg +1 -0
- package/icons/lucide/chats-fill.svg +1 -0
- package/icons/lucide/envelope-simple-fill.svg +1 -0
- package/icons/lucide/file-text-fill.svg +1 -0
- package/icons/lucide/folder-simple-fill.svg +1 -0
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear-six.svg +1 -0
- package/icons/lucide/globe-simple.svg +1 -0
- package/icons/lucide/heart-fill.svg +1 -0
- package/icons/lucide/layout.svg +1 -0
- package/icons/lucide/list-checks-fill.svg +1 -0
- package/icons/lucide/magnifying-glass-fill.svg +1 -0
- package/icons/lucide/megaphone-fill.svg +1 -0
- package/icons/lucide/newspaper-fill.svg +1 -0
- package/icons/lucide/paint-brush-broad-fill.svg +1 -0
- package/icons/lucide/puzzle-piece-fill.svg +1 -0
- package/icons/lucide/shield-check-fill.svg +1 -0
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sidebar.svg +1 -0
- package/icons/lucide/square-half-bottom-fill.svg +1 -0
- package/icons/lucide/square-half.svg +1 -0
- package/icons/lucide/squares-four-fill.svg +1 -0
- package/icons/lucide/storefront-fill.svg +1 -0
- package/icons/lucide/tag-fill.svg +1 -0
- package/icons/lucide/toggle-right-fill.svg +1 -0
- package/icons/lucide/trash-fill.svg +1 -0
- package/icons/lucide/user-fill.svg +1 -0
- package/icons/lucide/wrench-fill.svg +1 -0
- package/icons/material-symbols/browser-fill.svg +1 -0
- package/icons/material-symbols/buildings-fill.svg +1 -0
- package/icons/material-symbols/certificate-fill.svg +1 -0
- package/icons/material-symbols/chart-line-up-fill.svg +1 -0
- package/icons/material-symbols/chats-fill.svg +1 -0
- package/icons/material-symbols/envelope-simple-fill.svg +1 -0
- package/icons/material-symbols/file-text-fill.svg +1 -0
- package/icons/material-symbols/folder-simple-fill.svg +1 -0
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear-six.svg +1 -0
- package/icons/material-symbols/globe-simple.svg +1 -0
- package/icons/material-symbols/heart-fill.svg +1 -0
- package/icons/material-symbols/layout.svg +1 -0
- package/icons/material-symbols/list-checks-fill.svg +1 -0
- package/icons/material-symbols/magnifying-glass-fill.svg +1 -0
- package/icons/material-symbols/megaphone-fill.svg +1 -0
- package/icons/material-symbols/newspaper-fill.svg +1 -0
- package/icons/material-symbols/paint-brush-broad-fill.svg +1 -0
- package/icons/material-symbols/puzzle-piece-fill.svg +1 -0
- package/icons/material-symbols/shield-check-fill.svg +1 -0
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sidebar.svg +1 -0
- package/icons/material-symbols/square-half-bottom-fill.svg +1 -0
- package/icons/material-symbols/square-half.svg +1 -0
- package/icons/material-symbols/squares-four-fill.svg +1 -0
- package/icons/material-symbols/storefront-fill.svg +1 -0
- package/icons/material-symbols/tag-fill.svg +1 -0
- package/icons/material-symbols/toggle-right-fill.svg +1 -0
- package/icons/material-symbols/trash-fill.svg +1 -0
- package/icons/material-symbols/user-fill.svg +1 -0
- package/icons/material-symbols/wrench-fill.svg +1 -0
- package/icons/phosphor/browser-fill.svg +1 -0
- package/icons/phosphor/buildings-fill.svg +1 -0
- package/icons/phosphor/certificate-fill.svg +1 -0
- package/icons/phosphor/chart-line-up-fill.svg +1 -0
- package/icons/phosphor/chats-fill.svg +1 -0
- package/icons/phosphor/envelope-simple-fill.svg +1 -0
- package/icons/phosphor/file-text-fill.svg +1 -0
- package/icons/phosphor/folder-simple-fill.svg +1 -0
- package/icons/phosphor/gear-six.svg +1 -0
- package/icons/phosphor/globe-simple.svg +1 -0
- package/icons/phosphor/heart-fill.svg +1 -0
- package/icons/phosphor/layout.svg +1 -0
- package/icons/phosphor/list-checks-fill.svg +1 -0
- package/icons/phosphor/magnifying-glass-fill.svg +1 -0
- package/icons/phosphor/megaphone-fill.svg +1 -0
- package/icons/phosphor/newspaper-fill.svg +1 -0
- package/icons/phosphor/paint-brush-broad-fill.svg +1 -0
- package/icons/phosphor/puzzle-piece-fill.svg +1 -0
- package/icons/phosphor/shield-check-fill.svg +1 -0
- package/icons/phosphor/sidebar.svg +1 -0
- package/icons/phosphor/square-half-bottom-fill.svg +1 -0
- package/icons/phosphor/square-half.svg +1 -0
- package/icons/phosphor/squares-four-fill.svg +1 -0
- package/icons/phosphor/storefront-fill.svg +1 -0
- package/icons/phosphor/tag-fill.svg +1 -0
- package/icons/phosphor/toggle-right-fill.svg +1 -0
- package/icons/phosphor/trash-fill.svg +1 -0
- package/icons/phosphor/user-fill.svg +1 -0
- package/icons/phosphor/wrench-fill.svg +1 -0
- package/lib/breakpoints.d.mts +13 -0
- package/lib/breakpoints.mjs +20 -0
- package/lib/breakpoints.test.mjs +39 -0
- package/lib/build-lane.d.mts +15 -0
- package/lib/build-lane.mjs +23 -0
- package/lib/build-lane.test.mjs +74 -0
- package/lib/build-settings.d.mts +16 -0
- package/lib/build-settings.mjs +39 -0
- package/lib/build-settings.test.mjs +41 -0
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/drawer-swipe.d.mts +10 -0
- package/lib/drawer-swipe.mjs +17 -0
- package/lib/drawer-swipe.test.mjs +33 -0
- package/lib/drawerKeys.d.mts +9 -0
- package/lib/drawerKeys.mjs +26 -0
- package/lib/drawerKeys.test.mjs +21 -0
- package/lib/eyebrow.d.mts +6 -0
- package/lib/eyebrow.mjs +24 -0
- package/lib/eyebrow.test.mjs +20 -0
- package/lib/front-sections.test.mjs +82 -0
- package/lib/heading-levels.d.mts +7 -0
- package/lib/heading-levels.mjs +16 -0
- package/lib/heading-levels.test.mjs +37 -0
- package/lib/inline.ts +3 -2
- package/lib/markdown.ts +2 -0
- package/lib/materialIcon.d.mts +21 -0
- package/lib/materialIcon.mjs +29 -0
- package/lib/materialIcon.test.mjs +45 -0
- package/lib/menu-layout.d.mts +11 -0
- package/lib/menu-layout.mjs +14 -0
- package/lib/menu-layout.test.mjs +23 -0
- package/lib/menu.d.mts +59 -0
- package/lib/menu.mjs +129 -0
- package/lib/menu.test.mjs +291 -0
- package/lib/rehype-emphasize.d.mts +4 -0
- package/lib/rehype-emphasize.mjs +20 -0
- package/lib/rehype-emphasize.test.mjs +39 -0
- package/lib/section-in-view.d.mts +8 -0
- package/lib/section-in-view.mjs +25 -0
- package/lib/section-in-view.test.mjs +35 -0
- package/lib/section-marker.d.mts +5 -0
- package/lib/section-marker.mjs +18 -0
- package/lib/section-marker.test.mjs +22 -0
- package/lib/section-ranges.d.mts +12 -0
- package/lib/section-ranges.mjs +43 -0
- package/lib/section-ranges.test.mjs +61 -0
- package/lib/sheet-stops.d.mts +68 -0
- package/lib/sheet-stops.mjs +198 -0
- package/lib/sheet-stops.test.mjs +155 -0
- package/lib/still-timer.d.mts +5 -0
- package/lib/still-timer.mjs +17 -0
- package/lib/still-timer.test.mjs +42 -0
- package/lib/text-sections.d.mts +10 -0
- package/lib/text-sections.mjs +45 -0
- package/lib/text-sections.test.mjs +78 -0
- package/lib/video.mjs +2 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +7 -0
- package/lib/without-h1.test.mjs +26 -0
- package/lib/word-at.d.mts +6 -0
- package/lib/word-at.mjs +20 -0
- package/lib/word-at.test.mjs +19 -0
- package/package.json +3 -1
- package/pages/editor/version.json.js +22 -2
- package/scripts/already-checked.mjs +11 -0
- package/scripts/already-checked.test.mjs +18 -0
- package/scripts/build-settings-problems.mjs +27 -0
- package/scripts/build-settings-problems.test.mjs +35 -0
- package/scripts/check-content.mjs +42 -4
- package/scripts/check-worker.mjs +96 -4
- package/scripts/ci-build.mjs +85 -0
- package/scripts/ci-build.test.mjs +137 -0
- package/scripts/editor-roundtrip.mjs +62 -3
- package/scripts/editor-screens.mjs +21 -7
- package/scripts/git-env.mjs +6 -0
- package/scripts/step-command.mjs +34 -0
- package/scripts/step-command.test.mjs +50 -0
- package/scripts/text-problems.mjs +83 -0
- package/scripts/text-problems.test.mjs +74 -0
- package/scripts/worker-modules.mjs +11 -1
- package/site/breakpoints.d.mts +6 -0
- package/site/breakpoints.mjs +52 -0
- package/site/breakpoints.test.mjs +36 -0
- package/site/casing.d.mts +3 -0
- package/site/casing.mjs +26 -0
- package/site/content.config.ts +2 -0
- package/site/content.ts +2 -4
- package/site/defineSite.d.mts +1 -0
- package/site/defineSite.mjs +3 -0
- package/site/home.ts +3 -2
- package/site/icons.d.mts +116 -4
- package/site/icons.mjs +62 -6
- package/site/images.mjs +1 -1
- package/site/live-diff.d.mts +22 -0
- package/site/live-diff.mjs +98 -0
- package/site/live-diff.test.mjs +235 -0
- package/site/materialSymbols.d.mts +5 -0
- package/site/materialSymbols.mjs +19 -0
- package/site/sections.ts +1 -1
- package/site/theme.d.mts +1 -1
- package/site/theme.mjs +7 -1
- package/src/ActionParts.tsx +1 -1
- package/src/Advanced.tsx +101 -20
- package/src/App.tsx +47 -121
- package/src/AppList.tsx +1 -1
- package/src/BlockParts.tsx +3 -3
- package/src/Colors.tsx +1 -1
- package/src/CommunicationParts.tsx +1 -1
- package/src/ContainmentParts.tsx +80 -28
- package/src/Deleted.tsx +18 -4
- package/src/EditorFooter.tsx +14 -0
- package/src/EntryForm.tsx +75 -39
- package/src/FormFrame.tsx +3 -8
- package/src/Library.tsx +46 -51
- package/src/Licenses.tsx +27 -11
- package/src/MainDrawerParts.tsx +60 -0
- package/src/MainMenu.tsx +136 -0
- package/src/MarkdownEditor.tsx +175 -32
- package/src/Media.tsx +69 -16
- package/src/MediaTags.tsx +91 -0
- package/src/ModalSheetSample.tsx +47 -0
- package/src/NavigationParts.tsx +67 -21
- package/src/NavigationRestParts.tsx +1 -1
- package/src/Parts.tsx +71 -0
- package/src/PartsShown.tsx +1 -1
- package/src/PickerParts.tsx +2 -2
- package/src/Redirects.tsx +3 -2
- package/src/Screen.tsx +93 -90
- package/src/SelectionParts.tsx +22 -6
- package/src/SiteLogoLink.tsx +14 -0
- package/src/StartFab.tsx +22 -0
- package/src/StartStatus.tsx +1 -1
- package/src/autoTags.d.mts +28 -0
- package/src/autoTags.mjs +68 -0
- package/src/autoTags.test.mjs +109 -0
- package/src/buildSettings.test.mjs +20 -0
- package/src/buildSettings.ts +20 -0
- package/src/checkSite.ts +14 -1
- package/src/deploy.ts +7 -9
- package/src/editor.css +150 -17
- package/src/eyebrow.d.mts +5 -0
- package/src/eyebrow.mjs +53 -0
- package/src/eyebrowState.d.mts +21 -0
- package/src/eyebrowState.mjs +62 -0
- package/src/eyebrowState.test.mjs +78 -0
- package/src/fileRecords.d.mts +115 -0
- package/src/fileRecords.mjs +224 -0
- package/src/fileRecords.test.mjs +224 -0
- package/src/fileTagFilter.d.mts +30 -0
- package/src/fileTagFilter.mjs +44 -0
- package/src/fileTagFilter.test.mjs +57 -0
- package/src/fileTags.d.mts +56 -0
- package/src/fileTags.mjs +100 -0
- package/src/fileTags.test.mjs +127 -0
- package/src/firstHeading.d.mts +6 -0
- package/src/firstHeading.mjs +37 -0
- package/src/i18n/da-DK/advanced.ts +27 -12
- package/src/i18n/da-DK/app.ts +9 -7
- package/src/i18n/da-DK/apps.ts +16 -16
- package/src/i18n/da-DK/blockSettings.ts +9 -9
- package/src/i18n/da-DK/blocks.ts +2 -2
- package/src/i18n/da-DK/check.ts +5 -2
- package/src/i18n/da-DK/cms.ts +14 -12
- package/src/i18n/da-DK/colors.ts +5 -5
- package/src/i18n/da-DK/common.ts +2 -2
- package/src/i18n/da-DK/company.ts +3 -3
- package/src/i18n/da-DK/consents.ts +6 -6
- package/src/i18n/da-DK/forms.ts +6 -6
- package/src/i18n/da-DK/history.ts +6 -6
- package/src/i18n/da-DK/keywords.ts +2 -2
- package/src/i18n/da-DK/library.ts +17 -15
- package/src/i18n/da-DK/licenses.ts +4 -4
- package/src/i18n/da-DK/media.ts +10 -4
- package/src/i18n/da-DK/parts.ts +62 -25
- package/src/i18n/da-DK/preview.ts +5 -6
- package/src/i18n/da-DK/redirects.ts +7 -7
- package/src/i18n/da-DK/seo.ts +1 -1
- package/src/i18n/da-DK/text.ts +2 -0
- package/src/i18n/da-DK/theme.ts +5 -5
- package/src/i18n/en-US/advanced.ts +34 -18
- package/src/i18n/en-US/app.ts +7 -5
- package/src/i18n/en-US/apps.ts +9 -9
- package/src/i18n/en-US/blockSettings.ts +4 -4
- package/src/i18n/en-US/blocks.ts +6 -6
- package/src/i18n/en-US/check.ts +6 -3
- package/src/i18n/en-US/cms.ts +15 -14
- package/src/i18n/en-US/colors.ts +18 -18
- package/src/i18n/en-US/common.ts +3 -3
- package/src/i18n/en-US/consents.ts +4 -4
- package/src/i18n/en-US/forms.ts +5 -5
- package/src/i18n/en-US/history.ts +2 -2
- package/src/i18n/en-US/library.ts +21 -15
- package/src/i18n/en-US/licenses.ts +6 -6
- package/src/i18n/en-US/media.ts +7 -1
- package/src/i18n/en-US/parts.ts +60 -21
- package/src/i18n/en-US/preview.ts +5 -6
- package/src/i18n/en-US/redirects.ts +3 -3
- package/src/i18n/en-US/seo.ts +1 -1
- package/src/i18n/en-US/text.ts +3 -0
- package/src/i18n/en-US/theme.ts +6 -6
- package/src/icon.tsx +90 -27
- package/src/licenseData.test.mjs +28 -0
- package/src/licenseData.ts +12 -1
- package/src/markdown.d.mts +2 -0
- package/src/markdown.mjs +73 -5
- package/src/markdown.test.mjs +74 -0
- package/src/materialIcons.ts +8 -0
- package/src/menuChildren.ts +36 -0
- package/src/menuOpen.ts +34 -0
- package/src/pagePreview.tsx +23 -19
- package/src/r2Caching.ts +14 -0
- package/src/sectionFrame.ts +39 -0
- package/src/sectionState.d.mts +19 -0
- package/src/sectionState.mjs +31 -0
- package/src/sectionState.test.mjs +59 -0
- package/src/sections.d.mts +9 -7
- package/src/sections.mjs +74 -27
- package/src/sections.test.mjs +59 -0
- package/src/sidebar.ts +2 -2
- package/src/site.ts +27 -17
- package/src/stillCursor.d.mts +8 -0
- package/src/stillCursor.mjs +19 -0
- package/src/stillCursor.test.mjs +26 -0
- package/src/tagUses.d.mts +47 -0
- package/src/tagUses.mjs +121 -0
- package/src/tagUses.test.mjs +141 -0
- package/src/themeUi.ts +10 -0
- package/src/topBarSlot.tsx +37 -0
- package/src/ui.tsx +654 -150
- package/src/virtual.d.ts +5 -0
- package/starter/README.md +2 -1
- package/starter/package.json +2 -1
- package/starter/wrangler.jsonc +3 -2
- package/theme/blocks/cards/LinkCards.tsx +2 -1
- package/theme/blocks/hero/View.tsx +2 -1
- package/theme/blocks/video/View.tsx +2 -1
- package/theme/components/BlogIndex.astro +2 -2
- package/theme/components/BlogPosts.astro +1 -1
- package/theme/components/BlogPosts.tsx +9 -6
- package/theme/components/Container.astro +1 -1
- package/theme/components/CookieConsent.astro +1 -1
- package/theme/components/EndBlock.astro +1 -1
- package/theme/components/Footer.astro +5 -5
- package/theme/components/Header.astro +3 -3
- package/theme/components/Icon.astro +2 -2
- package/theme/components/MobileMenu.tsx +72 -61
- package/theme/components/MobileNav.astro +2 -2
- package/theme/components/PageHeader.astro +2 -2
- package/theme/components/PageSection.astro +1 -1
- package/theme/components/PopupButton.tsx +2 -2
- package/theme/components/PopupView.tsx +2 -2
- package/theme/components/PostList.tsx +7 -3
- package/theme/components/RichText.astro +22 -7
- package/theme/components/Section.astro +17 -10
- package/theme/components/SectionHeading.tsx +1 -1
- package/theme/components/home/PageBlock.astro +4 -4
- package/theme/components/home/Text.astro +3 -3
- package/theme/layouts/PageLayout.astro +3 -3
- package/theme/layouts/PostLayout.astro +9 -8
- package/theme/styles/base.css +13 -3
- package/theme/styles/blocks.css +97 -10
- package/theme/styles/components.css +9 -9
- package/theme/styles/elevation.test.mjs +168 -0
- package/theme/styles/global.css +65 -59
- package/theme/styles/ui.css +460 -131
- package/theme/templates/Home.astro +1 -1
- package/theme/templates/New.astro +5 -5
- package/theme/templates/NotFound.astro +2 -2
- package/theme/ui/BottomSheet.tsx +118 -21
- package/theme/ui/BottomSheetResizeIcon.tsx +13 -0
- package/theme/ui/Button.tsx +4 -3
- package/theme/ui/Dialog.tsx +1 -1
- package/theme/ui/FabMenu.tsx +13 -12
- package/theme/ui/LoadingIndicator.tsx +1 -1
- package/theme/ui/NavigationDrawer.tsx +116 -15
- package/theme/ui/NavigationRail.tsx +22 -13
- package/theme/ui/Search.tsx +1 -1
- package/theme/ui/SettingRow.tsx +31 -0
- package/theme/ui/Switch.tsx +11 -11
- package/theme/ui/TextField.tsx +17 -6
- package/theme/ui/bottomSheet.script.ts +3 -2
- package/theme/ui/bottomSheetControl.ts +131 -46
- package/theme/ui/fabMenu.script.ts +69 -53
- package/theme/ui/modalSheetSize.ts +214 -0
- package/theme/ui/navigationDrawerControl.ts +35 -0
- package/theme/ui/search.script.ts +4 -2
- package/worker/index.js +3 -0
- package/worker/plan-status.js +46 -0
- package/worker/push.js +7 -5
- package/worker/r2-status.js +2 -1
- package/icons/lucide/desktop.svg +0 -1
- package/icons/material-symbols/desktop.svg +0 -1
- package/icons/phosphor/desktop.svg +0 -1
package/src/Library.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The start screen of the editor (
|
|
1
|
+
// The start screen of the editor (empty but for its title, status and FAB) and the
|
|
2
2
|
// list of each part: blog posts newest first, the rest by name, prices and testimonials in their order
|
|
3
3
|
// on the site (with arrows to move them), and a search. Lists that are not in an order of their own can
|
|
4
4
|
// be sorted another way (a list of kinds, by its kinds too); the choice is kept in this browser. Pages
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
ClockCounterClockwise,
|
|
16
16
|
CircleHalf,
|
|
17
17
|
FileText,
|
|
18
|
-
Gauge,
|
|
19
18
|
GithubLogo,
|
|
20
19
|
GoogleLogo,
|
|
21
20
|
List as ListIcon,
|
|
@@ -33,6 +32,7 @@ import { byText, entryData, type Collection, type Part, type PartScreen } from '
|
|
|
33
32
|
import { config, countsOf, header, relationsOf, siteName } from '@site/editor';
|
|
34
33
|
import { iconPack, iconPacks } from './iconPacks';
|
|
35
34
|
import { theme, themes, type ThemeInfo } from './themes';
|
|
35
|
+
import { StartFab } from './StartFab';
|
|
36
36
|
import { StartStatus } from './StartStatus';
|
|
37
37
|
import {
|
|
38
38
|
appearance,
|
|
@@ -41,13 +41,11 @@ import {
|
|
|
41
41
|
blockParts,
|
|
42
42
|
collections,
|
|
43
43
|
companyParts,
|
|
44
|
-
groups,
|
|
45
44
|
icons,
|
|
46
45
|
lists,
|
|
47
46
|
listsShown,
|
|
48
47
|
partScreens,
|
|
49
48
|
seoParts,
|
|
50
|
-
settings,
|
|
51
49
|
} from './site';
|
|
52
50
|
import { readFields, splitFrontmatter } from './frontmatter.mjs';
|
|
53
51
|
import { ConflictError, type RepoFile, type Viewer } from './github';
|
|
@@ -108,6 +106,7 @@ import {
|
|
|
108
106
|
useThemeCatalog,
|
|
109
107
|
} from './ThemeStore';
|
|
110
108
|
import { STANDARD_THEME, STORE_ROUTE } from './store';
|
|
109
|
+
import { REDIRECTS_ROUTE } from './Redirects';
|
|
111
110
|
import { MOTION_SCHEMES, schemeInUse, type MotionScheme } from '../lib/motion';
|
|
112
111
|
import { applyMotion } from './theme';
|
|
113
112
|
import { colorsOf } from './Colors';
|
|
@@ -118,12 +117,12 @@ import { offBlocks } from './registry';
|
|
|
118
117
|
import { sideChoicesOf, sideSettingOf } from './SideColumn';
|
|
119
118
|
|
|
120
119
|
// Where a part opens: its list, or the file itself when it is one file (the menu).
|
|
121
|
-
const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
|
|
120
|
+
export const hrefOf = (c: Collection) => (c.kind === 'files' && c.files.length === 1 ? `#${c.files[0].file}` : `#liste/${c.id}`);
|
|
122
121
|
|
|
123
122
|
const countOf = (c: Collection, files: RepoFile[]): number =>
|
|
124
123
|
c.kind === 'files' ? c.files.length : files.filter((f) => f.path.startsWith(`${c.folder}/`)).length;
|
|
125
124
|
|
|
126
|
-
// A part's name, where it opens, and how many entries it has (null for one file).
|
|
125
|
+
// A part's name, where it opens, and how many entries it has (null for one file): for the main drawer (MainMenu.tsx).
|
|
127
126
|
export function partOf(id: string, files: RepoFile[]): { label: string; href: string; count: number | null } {
|
|
128
127
|
const { parts } = t.common;
|
|
129
128
|
if (id === 'virksomheden') return { label: parts.company, href: '#virksomheden', count: null };
|
|
@@ -135,6 +134,26 @@ export function partOf(id: string, files: RepoFile[]): { label: string; href: st
|
|
|
135
134
|
if (id === 'blokvalg') return { label: parts.blockSettings, href: '#blokvalg', count: null };
|
|
136
135
|
if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
|
|
137
136
|
if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
|
|
137
|
+
if (id === 'videresendelser') return { label: parts.redirects, href: `#${REDIRECTS_ROUTE}`, count: null };
|
|
138
|
+
const seoPart = seoParts.find((p) => p.id === id);
|
|
139
|
+
if (seoPart) return { label: seoPart.label, href: `#seo/${id}`, count: null };
|
|
140
|
+
// The screens under "Udseende" (Theme): the ones the editor has, then the site's own (appearance).
|
|
141
|
+
const appearanceLabel = {
|
|
142
|
+
temaer: parts.themes,
|
|
143
|
+
ikonpakker: parts.iconPacks,
|
|
144
|
+
bevaegelse: parts.motion,
|
|
145
|
+
sidekolonne: parts.sideColumn,
|
|
146
|
+
farver: parts.colors,
|
|
147
|
+
logo: parts.brand,
|
|
148
|
+
dele: parts.editorParts,
|
|
149
|
+
}[id];
|
|
150
|
+
const appearancePart = appearanceLabel ?? appearance.find((p) => p.id === id)?.label;
|
|
151
|
+
if (appearancePart) return { label: appearancePart, href: `#udseende/${id}`, count: null };
|
|
152
|
+
// The parts of a screen with parts ("Privatliv"): where each opens, or its screen's address with its id.
|
|
153
|
+
for (const screen of partScreens) {
|
|
154
|
+
const own = screen.parts.find((p) => p.id === id);
|
|
155
|
+
if (own) return { label: own.label, href: own.href ?? `#${screen.id}/${id}`, count: null };
|
|
156
|
+
}
|
|
138
157
|
if (id === 'morpheme') return { label: parts.morpheme, href: `#${CMS_ROUTE}`, count: null };
|
|
139
158
|
if (id === 'avanceret') return { label: parts.advanced, href: '#avanceret', count: null };
|
|
140
159
|
if (id === 'licenser') return { label: parts.licenses, href: '#licenser', count: null };
|
|
@@ -145,7 +164,7 @@ export function partOf(id: string, files: RepoFile[]): { label: string; href: st
|
|
|
145
164
|
return { label: c.label, href: hrefOf(c), count: c.kind === 'files' && c.files.length === 1 ? null : countOf(c, files) };
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
/** The parts of a group on
|
|
167
|
+
/** The parts of a group on a screen with parts (Parts), A–Å by their names in the editor's language. */
|
|
149
168
|
export const partsIn = (ids: string[], files: RepoFile[]) =>
|
|
150
169
|
ids.map((id) => ({ id, ...partOf(id, files) })).sort((a, b) => byText(a.label, b.label));
|
|
151
170
|
|
|
@@ -156,41 +175,18 @@ function PartRow({ id, label, href, count }: { id: string } & ReturnType<typeof
|
|
|
156
175
|
return <ListItem href={href} icon={icons[id]} headline={label} count={count} arrow />;
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
// The start screen,
|
|
160
|
-
//
|
|
161
|
-
export function Start({
|
|
178
|
+
// The start screen: the site's name, the CMS version and when the site was last updated (built on Cloudflare, App.tsx), as StartStatus.tsx shows them, under it, and the FAB
|
|
179
|
+
// menu for a new page or post (StartFab.tsx). The parts of the site are in the main drawer (MainMenu.tsx); the rest is left empty for now.
|
|
180
|
+
export function Start({ builtAt }: { builtAt: Date | null }) {
|
|
162
181
|
return (
|
|
163
182
|
<Screen
|
|
164
183
|
title={siteName}
|
|
165
184
|
wide
|
|
166
185
|
status={<StartStatus builtAt={builtAt} />}
|
|
167
|
-
// The notifications
|
|
186
|
+
// The notifications and the account in the top bar, on a phone too (Screen.tsx).
|
|
168
187
|
home
|
|
169
188
|
>
|
|
170
|
-
<
|
|
171
|
-
{groups.map((g) => (
|
|
172
|
-
<Group key={g.title} title={g.title}>
|
|
173
|
-
{partsIn(g.ids, files).map((p) => (
|
|
174
|
-
<PartRow key={p.id} {...p} />
|
|
175
|
-
))}
|
|
176
|
-
</Group>
|
|
177
|
-
))}
|
|
178
|
-
</div>
|
|
179
|
-
</Screen>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** "Indstillinger" (#indstillinger), which the gear in the top bar opens (Screen.tsx SettingsLink): the settings of the
|
|
184
|
-
* whole site (settings in packages/editor/defaults/editor.ts: "Analytics", "Apps", "Privatliv", "Promovering", "Søgemaskineoptimering",
|
|
185
|
-
* "Udseende"), A–Å as a group of the start screen, each going back here (backFrom). */
|
|
186
|
-
export function Settings({ files }: { files: RepoFile[] }) {
|
|
187
|
-
return (
|
|
188
|
-
<Screen title={t.common.parts.settings} wide back={{ href: '#', label: siteName }}>
|
|
189
|
-
<Group>
|
|
190
|
-
{partsIn(settings, files).map((p) => (
|
|
191
|
-
<PartRow key={p.id} {...p} />
|
|
192
|
-
))}
|
|
193
|
-
</Group>
|
|
189
|
+
<StartFab />
|
|
194
190
|
</Screen>
|
|
195
191
|
);
|
|
196
192
|
}
|
|
@@ -264,7 +260,7 @@ function themeStatus(id: string, files: RepoFile[]): string | undefined {
|
|
|
264
260
|
}
|
|
265
261
|
|
|
266
262
|
/** "Temaer" (#udseende/temaer): the themes installed (src/themes/, packages/editor/src/themes.ts; and one from the store the site's build
|
|
267
|
-
* does not have yet, as "Installeres
|
|
263
|
+
* does not have yet, as "Installeres…"), by name, each with its picture, its version and who made it, and whether it is
|
|
268
264
|
* in use or the store has a newer version, opening its page (ThemePage). More are in "Appstore", under "Indstillinger". */
|
|
269
265
|
export function Themes({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
270
266
|
const catalog = useThemeCatalog(env);
|
|
@@ -528,15 +524,11 @@ export function PartsHome() {
|
|
|
528
524
|
return <Parts title={t.common.parts.editorParts} route="udseende/dele" parts={parts} back={backToAppearance} />;
|
|
529
525
|
}
|
|
530
526
|
|
|
531
|
-
/** "Søgemaskineoptimering" (#seo)
|
|
532
|
-
*
|
|
533
|
-
*
|
|
527
|
+
/** "Søgemaskineoptimering" (#seo), no longer in the menu: "Videresendelser" (Redirects.tsx, #seo/videresendelser) and then
|
|
528
|
+
* the site's own parts (seoParts: "Interne links"), each on a screen of its own (CompanyForm.tsx). "SEO-vurdering"
|
|
529
|
+
* (SeoScores.tsx, #seo/vurdering) is hidden: nothing links to it. */
|
|
534
530
|
export function Seo() {
|
|
535
|
-
const parts: Part[] = [
|
|
536
|
-
{ id: 'vurdering', label: t.seo.title, icon: Gauge },
|
|
537
|
-
{ id: 'videresendelser', label: t.common.parts.redirects, icon: icons.videresendelser },
|
|
538
|
-
...seoParts,
|
|
539
|
-
];
|
|
531
|
+
const parts: Part[] = [{ id: 'videresendelser', label: t.common.parts.redirects, icon: icons.videresendelser }, ...seoParts];
|
|
540
532
|
return <Parts title={t.common.parts.seo} route="seo" parts={parts} back={backFrom('seo')} />;
|
|
541
533
|
}
|
|
542
534
|
|
|
@@ -1342,16 +1334,19 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1342
1334
|
/>
|
|
1343
1335
|
);
|
|
1344
1336
|
};
|
|
1337
|
+
const privacyScreen = partScreens.find((s) => s.id === 'privatliv');
|
|
1345
1338
|
return (
|
|
1346
1339
|
<Screen
|
|
1347
1340
|
title={collection.label}
|
|
1348
1341
|
wide
|
|
1349
1342
|
back={
|
|
1350
|
-
|
|
1351
|
-
? { href:
|
|
1352
|
-
:
|
|
1353
|
-
? { href: '#
|
|
1354
|
-
:
|
|
1343
|
+
collection.id === config.lists.cookies && privacyScreen
|
|
1344
|
+
? { href: `#${privacyScreen.id}`, label: privacyScreen.label }
|
|
1345
|
+
: lists.includes(collection.id)
|
|
1346
|
+
? { href: '#lister', label: t.common.parts.lists }
|
|
1347
|
+
: header.includes(collection.id)
|
|
1348
|
+
? { href: '#header', label: t.common.parts.header }
|
|
1349
|
+
: { href: '#', label: siteName }
|
|
1355
1350
|
}
|
|
1356
1351
|
tabs={
|
|
1357
1352
|
appTabs.length > 0
|
|
@@ -1491,8 +1486,8 @@ export function List({ collection, tab, files, env }: { collection: Collection;
|
|
|
1491
1486
|
function Under({ item, address, children }: { item: Item; address?: boolean; children: ReactNode }) {
|
|
1492
1487
|
return (
|
|
1493
1488
|
<span className="flex gap-6">
|
|
1494
|
-
{address && <span className="hidden min-w-0 flex-1 truncate
|
|
1495
|
-
<span className="min-w-0 truncate
|
|
1489
|
+
{address && <span className="hidden min-w-0 flex-1 truncate expanded:block">{item.path}</span>}
|
|
1490
|
+
<span className="min-w-0 truncate expanded:ml-auto expanded:shrink-0 expanded:text-right">{children}</span>
|
|
1496
1491
|
</span>
|
|
1497
1492
|
);
|
|
1498
1493
|
}
|
package/src/Licenses.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// "Licenser" (#licenser), 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
|
-
// besides its own), then what the editor and the site's own code bring (the npm packages shipped in what is downloaded
|
|
4
|
+
// besides its own), then what the editor and the site's own code bring (the npm packages shipped in what is downloaded,
|
|
5
|
+
// the CMS's own licence "Morpheme CMS itself" first, with its version),
|
|
5
6
|
// then each theme the site is built with and each app installed, under its name: its own licence first, then what it
|
|
6
7
|
// brings (what it declares, its icon packs and what is shipped through them alone). Each row has the work's name, its
|
|
7
8
|
// version, licence and who made it, and what it is and where it is used; one with a text opens it
|
|
@@ -15,9 +16,12 @@ import { backFrom } from './site';
|
|
|
15
16
|
import { apps } from './registry';
|
|
16
17
|
import { themes } from './themes';
|
|
17
18
|
import { byText } from './schema';
|
|
19
|
+
import { PACKAGE } from '../lib/cms-version.mjs';
|
|
18
20
|
import { locale, t } from './i18n';
|
|
19
21
|
import {
|
|
22
|
+
CMS_NAME,
|
|
20
23
|
loadLicenses,
|
|
24
|
+
splitCms,
|
|
21
25
|
UNLICENSED,
|
|
22
26
|
type LicensedWork,
|
|
23
27
|
type LicenseKind,
|
|
@@ -115,18 +119,20 @@ function WorkRow({ work, owners, places, open = true }: { work: LicensedWork; ow
|
|
|
115
119
|
);
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
/** An app's
|
|
122
|
+
/** An app's, a theme's or the CMS's own licence (`name`: whose), first in its group. */
|
|
119
123
|
function OwnRow({
|
|
120
124
|
owner,
|
|
125
|
+
name,
|
|
121
126
|
open = true,
|
|
122
127
|
}: {
|
|
123
128
|
open?: boolean;
|
|
124
|
-
|
|
129
|
+
name: string;
|
|
130
|
+
owner: Pick<LicenseOwner, 'author' | 'license' | 'version'> & { id: string; text?: string | null };
|
|
125
131
|
}) {
|
|
126
132
|
return (
|
|
127
133
|
<LicenseRow
|
|
128
134
|
icon={Certificate}
|
|
129
|
-
name={t.licenses.own(
|
|
135
|
+
name={t.licenses.own(name)}
|
|
130
136
|
lines={[byline(owner.license, owner.author)]}
|
|
131
137
|
version={owner.version}
|
|
132
138
|
href={open && owner.text ? hrefOf(owner.id) : undefined}
|
|
@@ -165,18 +171,23 @@ export default function Licenses() {
|
|
|
165
171
|
const owners = file?.owners ?? [];
|
|
166
172
|
// The system's icon packs (a theme's own are under it, `where` it).
|
|
167
173
|
const packs = file ? sorted(file.works.filter((w) => w.kind === 'icons' && !w.where.some((x) => x.startsWith('tema/')))) : [];
|
|
168
|
-
|
|
174
|
+
// What the editor and the site bring: the CMS's own licence first, the rest A–Å.
|
|
175
|
+
const { cms, rest: system } = splitCms(
|
|
176
|
+
file ? sorted(file.works.filter((w) => w.kind !== 'icons' && w.where.some((x) => x === 'editor' || x === 'site'))) : [],
|
|
177
|
+
);
|
|
178
|
+
const ownCms = cms && (fits(cms) || matches(CMS_NAME)) ? cms : null;
|
|
169
179
|
// The themes the site is built with, then the apps installed, each A–Å.
|
|
170
180
|
const inUse = owners
|
|
171
181
|
.filter((o) => o.inUse)
|
|
172
182
|
.sort((a, b) => (a.kind === b.kind ? byText(ownerName(a), ownerName(b)) : a.kind === 'theme' ? -1 : 1));
|
|
173
183
|
const groups = [
|
|
174
|
-
{ key: 'icons', title: t.licenses.icons, own: null, works: packs.filter(fits) },
|
|
175
|
-
{ key: 'system', title: t.licenses.system, own:
|
|
184
|
+
{ key: 'icons', title: t.licenses.icons, own: null, ownName: '', works: packs.filter(fits) },
|
|
185
|
+
{ key: 'system', title: t.licenses.system, own: ownCms, ownName: CMS_NAME, works: system.filter(fits) },
|
|
176
186
|
...inUse.map((o) => ({
|
|
177
187
|
key: o.id,
|
|
178
188
|
title: o.kind === 'app' ? t.licenses.app(ownerName(o)) : t.licenses.theme(ownerName(o)),
|
|
179
189
|
own: matches(ownerName(o), o.author, o.license, licenseName(o.license)) ? o : null,
|
|
190
|
+
ownName: ownerName(o),
|
|
180
191
|
works: file ? worksOf(o, file).filter(fits) : [],
|
|
181
192
|
})),
|
|
182
193
|
].filter((g) => g.own || g.works.length);
|
|
@@ -193,7 +204,7 @@ export default function Licenses() {
|
|
|
193
204
|
{groups.length ? (
|
|
194
205
|
groups.map((g) => (
|
|
195
206
|
<Group key={g.key} title={g.title}>
|
|
196
|
-
{g.own && <OwnRow owner={g.own} />}
|
|
207
|
+
{g.own && <OwnRow owner={g.own} name={g.ownName} />}
|
|
197
208
|
{g.works.map((w) => (
|
|
198
209
|
<WorkRow key={w.id} work={w} owners={owners} places={g.key === 'system' || g.key === 'icons'} />
|
|
199
210
|
))}
|
|
@@ -228,7 +239,8 @@ export function LicensePage({ id }: { id: string }) {
|
|
|
228
239
|
const file = useLicenses();
|
|
229
240
|
const work = file?.works.find((w) => w.id === id);
|
|
230
241
|
const owner = file?.owners.find((o) => o.id === id);
|
|
231
|
-
const
|
|
242
|
+
const isCms = work?.id === PACKAGE;
|
|
243
|
+
const name = isCms ? t.licenses.own(CMS_NAME) : (work?.name ?? (owner ? t.licenses.own(ownerName(owner)) : t.common.parts.licenses));
|
|
232
244
|
const text = work?.text ?? owner?.text;
|
|
233
245
|
const back = { href: `#${LICENSES_ROUTE}`, label: t.common.parts.licenses };
|
|
234
246
|
return (
|
|
@@ -240,7 +252,11 @@ export function LicensePage({ id }: { id: string }) {
|
|
|
240
252
|
) : (
|
|
241
253
|
<>
|
|
242
254
|
<Group>
|
|
243
|
-
{work
|
|
255
|
+
{work && !isCms ? (
|
|
256
|
+
<WorkRow work={work} owners={file?.owners ?? []} places open={false} />
|
|
257
|
+
) : (
|
|
258
|
+
<OwnRow owner={(work ?? owner)!} name={isCms ? CMS_NAME : ownerName(owner!)} open={false} />
|
|
259
|
+
)}
|
|
244
260
|
</Group>
|
|
245
261
|
<section className="mb-8">
|
|
246
262
|
<GroupTitle>{t.licenses.text}</GroupTitle>
|
|
@@ -292,7 +308,7 @@ export function OwnerLicenses({
|
|
|
292
308
|
};
|
|
293
309
|
return (
|
|
294
310
|
<Group title={t.common.parts.licenses}>
|
|
295
|
-
<OwnRow owner={self} />
|
|
311
|
+
<OwnRow owner={self} name={ownerName(self)} />
|
|
296
312
|
{owner && file
|
|
297
313
|
? worksOf(owner, file).map((w) => <WorkRow key={w.id} work={w} owners={file.owners} places={false} />)
|
|
298
314
|
: (declared.thirdParty ?? []).map((x, i) =>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The main drawer and its menu button on #udseende/dele/editor (Parts.tsx): the modal drawer in its two levels (sections, and
|
|
2
|
+
// a section's list with the back row and a fold-out), opened from a button, so it can be tried at any width, and the menu
|
|
3
|
+
// button open and closed. Exposes MainDrawerSample. Uses MainDrawer and its parts from ui.tsx.
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { icons } from './site';
|
|
6
|
+
import { t } from './i18n';
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
MainDrawer,
|
|
10
|
+
MainDrawerBack,
|
|
11
|
+
MainDrawerFold,
|
|
12
|
+
MainDrawerItem,
|
|
13
|
+
MainDrawerLevel,
|
|
14
|
+
MainDrawerSectionItem,
|
|
15
|
+
MainDrawerSubitem,
|
|
16
|
+
MenuButton,
|
|
17
|
+
} from './ui';
|
|
18
|
+
|
|
19
|
+
const { parts } = t.common;
|
|
20
|
+
|
|
21
|
+
export function MainDrawerSample() {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [section, setSection] = useState<string | null>(null);
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
26
|
+
<Button kind="outlined" onClick={() => setOpen(true)}>
|
|
27
|
+
{t.library.openMenu}
|
|
28
|
+
</Button>
|
|
29
|
+
<MenuButton label={t.library.openMenu} onClick={() => setOpen(true)} />
|
|
30
|
+
<MenuButton label={t.library.closeMenu} open onClick={() => setOpen(false)} />
|
|
31
|
+
<MainDrawer
|
|
32
|
+
label={t.library.navigation}
|
|
33
|
+
open={open}
|
|
34
|
+
onOpenChange={setOpen}
|
|
35
|
+
onEscape={() => {
|
|
36
|
+
const inList = section !== null;
|
|
37
|
+
setSection(null);
|
|
38
|
+
return inList;
|
|
39
|
+
}}
|
|
40
|
+
variant="modal"
|
|
41
|
+
>
|
|
42
|
+
{section === null ? (
|
|
43
|
+
<MainDrawerLevel>
|
|
44
|
+
<MainDrawerItem href="" icon={icons.home} label={parts.company} />
|
|
45
|
+
<MainDrawerSectionItem icon={icons.globalt} label={parts.header} selected onClick={() => setSection('globalt')} />
|
|
46
|
+
<MainDrawerSectionItem icon={icons.udseende} label={parts.appearance} onClick={() => setSection('udseende')} />
|
|
47
|
+
</MainDrawerLevel>
|
|
48
|
+
) : (
|
|
49
|
+
<MainDrawerLevel focus="list">
|
|
50
|
+
<MainDrawerBack label={t.library.mainMenu} onClick={() => setSection(null)} />
|
|
51
|
+
<MainDrawerFold href="" icon={icons.header} label={parts.header} open>
|
|
52
|
+
<MainDrawerSubitem href="" label={parts.apps} selected />
|
|
53
|
+
</MainDrawerFold>
|
|
54
|
+
<MainDrawerItem href="" icon={icons.seo} label={parts.seo} count={3} />
|
|
55
|
+
</MainDrawerLevel>
|
|
56
|
+
)}
|
|
57
|
+
</MainDrawer>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/MainMenu.tsx
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The editor's main menu (App.tsx renders it): the drawer (ui.tsx MainDrawer) in two levels. The first lists Home and the
|
|
2
|
+
// sections; a section opens its list, under a back row, with the open screen marked and its fold-out folded out. Apps lists
|
|
3
|
+
// Appstore and a row per app installed. From medium and wider the first level is also the collapsed navigation rail
|
|
4
|
+
// (MainRailItem), whose sections open the drawer at their list. Exposes MainMenu. The tree and the open place are lib/menu.mjs
|
|
5
|
+
// (menuTree, currentPlace); it only wires them to the drawer.
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { config, header } from '@site/editor';
|
|
8
|
+
import { currentPlace, menuTree } from '../lib/menu.mjs';
|
|
9
|
+
import { STORE_ROUTE } from './store';
|
|
10
|
+
import { collectionOf } from './schema';
|
|
11
|
+
import { apps, collections, groups, icons, lists, partScreens } from './site';
|
|
12
|
+
import { partOf } from './Library';
|
|
13
|
+
import { childrenOf } from './menuChildren';
|
|
14
|
+
import { t } from './i18n';
|
|
15
|
+
import { useMenuOpen } from './menuOpen';
|
|
16
|
+
import {
|
|
17
|
+
MainDrawer,
|
|
18
|
+
MainDrawerBack,
|
|
19
|
+
MainDrawerFold,
|
|
20
|
+
MainDrawerItem,
|
|
21
|
+
MainDrawerLevel,
|
|
22
|
+
MainDrawerSectionItem,
|
|
23
|
+
MainDrawerSubitem,
|
|
24
|
+
MainRailItem,
|
|
25
|
+
MainRailSectionItem,
|
|
26
|
+
} from './ui';
|
|
27
|
+
import type { RepoFile } from './github';
|
|
28
|
+
|
|
29
|
+
export function MainMenu({ route, files }: { route: string; files: RepoFile[] | null }) {
|
|
30
|
+
const { open, setOpen } = useMenuOpen();
|
|
31
|
+
const tree = useMemo(
|
|
32
|
+
() =>
|
|
33
|
+
menuTree(
|
|
34
|
+
groups,
|
|
35
|
+
(id: string) => partOf(id, files ?? []),
|
|
36
|
+
(id: string) => childrenOf(id, files ?? []),
|
|
37
|
+
),
|
|
38
|
+
[files],
|
|
39
|
+
);
|
|
40
|
+
// A file being edited (a page, a post) is open under its list; the same call as wave 3's in App.tsx.
|
|
41
|
+
const collection = collectionOf(collections, route)?.collection;
|
|
42
|
+
const place = currentPlace(route, tree, {
|
|
43
|
+
open: collection?.id,
|
|
44
|
+
lists,
|
|
45
|
+
header,
|
|
46
|
+
partScreens: partScreens.map((s) => s.id),
|
|
47
|
+
cookies: config.lists.cookies,
|
|
48
|
+
store: STORE_ROUTE,
|
|
49
|
+
});
|
|
50
|
+
// The section whose list is shown (null: the first level). It follows the route's section, and the drawer opening.
|
|
51
|
+
const [shown, setShown] = useState<string | null>(place.section);
|
|
52
|
+
const [focus, setFocus] = useState<'list' | 'top' | undefined>();
|
|
53
|
+
const shownRef = useRef(shown);
|
|
54
|
+
shownRef.current = shown;
|
|
55
|
+
// The section a click in the rail asked for: the drawer opens at its list instead of at the route's section.
|
|
56
|
+
const asked = useRef<string | null>(null);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setShown(asked.current ?? place.section);
|
|
59
|
+
setFocus(asked.current ? 'list' : undefined);
|
|
60
|
+
asked.current = null;
|
|
61
|
+
}, [place.section, open]);
|
|
62
|
+
const openAt = (section: string) => {
|
|
63
|
+
asked.current = section;
|
|
64
|
+
setOpen(true);
|
|
65
|
+
};
|
|
66
|
+
const show = (section: string | null) => {
|
|
67
|
+
setFocus(section === null ? 'top' : 'list');
|
|
68
|
+
setShown(section);
|
|
69
|
+
};
|
|
70
|
+
// Escape in a section's list goes to the first level; at the first level it is not used, so the modal drawer closes.
|
|
71
|
+
const onEscape = useCallback(() => {
|
|
72
|
+
if (shownRef.current === null) return false;
|
|
73
|
+
setFocus('top');
|
|
74
|
+
setShown(null);
|
|
75
|
+
return true;
|
|
76
|
+
}, []);
|
|
77
|
+
// Every level stays in the page (the screen check finds the screens from the links); the ones not shown are `hidden`.
|
|
78
|
+
const sections = tree.filter((entry) => entry.href === undefined);
|
|
79
|
+
return (
|
|
80
|
+
<MainDrawer
|
|
81
|
+
label={t.library.navigation}
|
|
82
|
+
open={open}
|
|
83
|
+
onOpenChange={setOpen}
|
|
84
|
+
onEscape={onEscape}
|
|
85
|
+
rail={tree.map((entry) =>
|
|
86
|
+
entry.href !== undefined ? (
|
|
87
|
+
<MainRailItem key={entry.id} href={entry.href} icon={icons[entry.id]} label={entry.label} selected={route === ''} />
|
|
88
|
+
) : (
|
|
89
|
+
<MainRailSectionItem
|
|
90
|
+
key={entry.id}
|
|
91
|
+
icon={icons[entry.id]}
|
|
92
|
+
label={entry.label}
|
|
93
|
+
selected={place.section === entry.id}
|
|
94
|
+
onClick={() => openAt(entry.id)}
|
|
95
|
+
/>
|
|
96
|
+
),
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<MainDrawerLevel shown={shown === null} focus={focus}>
|
|
100
|
+
{tree.map((entry) =>
|
|
101
|
+
entry.href !== undefined ? (
|
|
102
|
+
<MainDrawerItem key={entry.id} href={entry.href} icon={icons[entry.id]} label={entry.label} selected={route === ''} />
|
|
103
|
+
) : (
|
|
104
|
+
<MainDrawerSectionItem
|
|
105
|
+
key={entry.id}
|
|
106
|
+
icon={icons[entry.id]}
|
|
107
|
+
label={entry.label}
|
|
108
|
+
selected={place.section === entry.id}
|
|
109
|
+
onClick={() => show(entry.id)}
|
|
110
|
+
/>
|
|
111
|
+
),
|
|
112
|
+
)}
|
|
113
|
+
</MainDrawerLevel>
|
|
114
|
+
{sections.map((section) => (
|
|
115
|
+
<MainDrawerLevel key={section.id} shown={shown === section.id} focus={focus}>
|
|
116
|
+
<MainDrawerBack label={t.library.mainMenu} onClick={() => show(null)} />
|
|
117
|
+
{section.items.map(({ id, label, href, count, children }) => {
|
|
118
|
+
const here = place.section === section.id && place.item === id;
|
|
119
|
+
const shownCount = files ? count : null;
|
|
120
|
+
// An app's row has the app's icon (an installed app is a row of Apps, lib/menu.mjs).
|
|
121
|
+
const icon = (section.id === 'apps' && apps.find((a) => a.id === id)?.icon) || icons[id];
|
|
122
|
+
return children ? (
|
|
123
|
+
<MainDrawerFold key={id} href={href} icon={icon} label={label} count={shownCount} selected={here && !place.child} open={here}>
|
|
124
|
+
{children.map((child) => (
|
|
125
|
+
<MainDrawerSubitem key={child.id} href={child.href} label={child.label} selected={here && place.child === child.id} />
|
|
126
|
+
))}
|
|
127
|
+
</MainDrawerFold>
|
|
128
|
+
) : (
|
|
129
|
+
<MainDrawerItem key={id} href={href} icon={icon} label={label} count={shownCount} selected={here} />
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</MainDrawerLevel>
|
|
133
|
+
))}
|
|
134
|
+
</MainDrawer>
|
|
135
|
+
);
|
|
136
|
+
}
|