@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
|
@@ -26,7 +26,7 @@ const { page, heading, headingHtml, sections } = Astro.props;
|
|
|
26
26
|
shareTitle={page.data.shareTitle}
|
|
27
27
|
shareDescription={page.data.shareDescription}
|
|
28
28
|
>
|
|
29
|
-
<Container class="pt-8
|
|
29
|
+
<Container class="pt-8 medium:pt-16">
|
|
30
30
|
<h1 class="theme-title max-w-[22ch]">{headingHtml ? <Fragment set:html={headingHtml} /> : heading}</h1>
|
|
31
31
|
</Container>
|
|
32
32
|
{sections.map((s, i) => {
|
|
@@ -25,8 +25,8 @@ const posts = (await getPosts({ unlisted: true })).filter((p) => stateOf(p.data)
|
|
|
25
25
|
<PageHeader title={siteT.theme.newPages} intro={siteT.theme.newPagesIntro} />
|
|
26
26
|
|
|
27
27
|
{rows.length > 0 && (
|
|
28
|
-
<Container as="section" class="mt-12
|
|
29
|
-
<ul class="rounded-panel border border-line px-5
|
|
28
|
+
<Container as="section" class="mt-12 medium:mt-16">
|
|
29
|
+
<ul class="rounded-panel border border-line px-5 medium:px-7">
|
|
30
30
|
{rows.map(({ page, current }, i) => (
|
|
31
31
|
<li id={previewId(page.data.path)} class:list={['scroll-mt-24 py-5', i > 0 && 'border-t border-line']}>
|
|
32
32
|
<p class="type-title-medium">{pageHeading(page)}</p>
|
|
@@ -50,10 +50,10 @@ const posts = (await getPosts({ unlisted: true })).filter((p) => stateOf(p.data)
|
|
|
50
50
|
)}
|
|
51
51
|
|
|
52
52
|
{posts.length > 0 && (
|
|
53
|
-
<Container as="section" class="mt-12 mb-20
|
|
54
|
-
<h2 class="type-headline-small
|
|
53
|
+
<Container as="section" class="mt-12 mb-20 medium:mt-16 medium:mb-28">
|
|
54
|
+
<h2 class="type-headline-small medium:type-headline-medium">{siteT.theme.newPosts}</h2>
|
|
55
55
|
<p class="mt-2 max-w-[60ch] text-muted">{siteT.theme.newPostsIntro}</p>
|
|
56
|
-
<ul class="mt-5 rounded-panel border border-line px-5
|
|
56
|
+
<ul class="mt-5 rounded-panel border border-line px-5 medium:px-7">
|
|
57
57
|
{posts.map((p, i) => (
|
|
58
58
|
<li class:list={['py-4', i > 0 && 'border-t border-line']}>
|
|
59
59
|
<a href={postPath(p)} class="inline-flex items-center gap-1.5 font-medium hover:text-accent">
|
|
@@ -29,13 +29,13 @@ const index: [string, string][] = [
|
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
// Less space above the first section than on the other pages: the suggestion sits between.
|
|
32
|
-
const spaceOf = (i: number) => (i === 0 ? 'mt-6
|
|
32
|
+
const spaceOf = (i: number) => (i === 0 ? 'mt-6 medium:mt-8' : space(i));
|
|
33
33
|
---
|
|
34
34
|
|
|
35
35
|
<Base title={siteT.theme.notFound} noindex>
|
|
36
36
|
<PageHeader title={heading} intro={intro} />
|
|
37
37
|
|
|
38
|
-
<Container as="div" class="mt-8
|
|
38
|
+
<Container as="div" class="mt-8 medium:mt-12">
|
|
39
39
|
<Card href="" id="suggestion" hidden className="group flex max-w-2xl items-center justify-between gap-6">
|
|
40
40
|
<span>
|
|
41
41
|
<span class="type-body-medium block text-muted">{siteT.theme.didYouMean}</span>
|
package/theme/ui/BottomSheet.tsx
CHANGED
|
@@ -9,23 +9,57 @@
|
|
|
9
9
|
// headline. It rests in one of its states (`state`, where it starts: 'collapsed', only the handle and the header;
|
|
10
10
|
// 'half-expanded', at most half the screen's height; 'expanded', as high as what it shows, nearly to the top; or
|
|
11
11
|
// 'hidden'), and its handle is a button (`dragHandleLabel`, what it says to a screen reader) that is dragged between
|
|
12
|
-
// them or tapped to
|
|
12
|
+
// them or tapped to step through them (up one at a time, from the highest back to the lowest); with a close button
|
|
13
|
+
// it can be hidden. `states` (data-states, space-separated) lists the states it may rest in; without it Material 3's
|
|
14
|
+
// (defaultStates in lib/sheet-stops.mjs, 'hidden' first when it has a close button). On the site bottomSheet.script.ts
|
|
13
15
|
// lets it be dragged (standardBottomSheet() in bottomSheetControl.ts, which a block's or an app's script can also use); in
|
|
14
16
|
// the editor it is drawn in its state and dragged here, at the bottom of its frame (which holds it with
|
|
15
17
|
// contain: layout, and is a size container).
|
|
18
|
+
// `wide` is what it becomes from the medium breakpoint (data-wide): a standard sheet's 'side-sheet' (the default: drawn as a
|
|
19
|
+
// standard side sheet at the end of the screen, full height, without handle and buttons, every part shown, and
|
|
20
|
+
// standardBottomSheet() does nothing while the window is that wide) or 'sheet' (stays a bottom sheet, with margins,
|
|
21
|
+
// --bottom-sheet-margin-wide); a modal one's 'dialog' (the default), 'side-sheet' or 'sheet' (TypeScript rejects
|
|
22
|
+
// 'dialog' on a standard sheet). For a modal one 'side-sheet' is a modal side sheet at the end of the screen (full height,
|
|
23
|
+
// its scrim and tokens, without the handle) that bottomSheet() slides in from the end, and 'sheet' stays a bottom sheet
|
|
24
|
+
// with the margins and slides up; a container without the attribute (hand-written) is a 'dialog'.
|
|
25
|
+
// `startAt` (a modal sheet only, data-start; 'half' without it, also for a hand-written container without the attribute,
|
|
26
|
+
// Calendly's) is how high it opens on a phone: 'half', half the screen's height or as high as what is in it if that is
|
|
27
|
+
// less, or 'full', as high as the screen allows below --bottom-sheet-top-gap. It is pulled up to full height and down to
|
|
28
|
+
// close (modalSheetSize.ts); the content scrolls only at full height. The editor's sheets are not touched.
|
|
29
|
+
// `peek` (a standard sheet only) is what it shows even while collapsed (Material 3's preview), between the header and
|
|
30
|
+
// what it shows (.bottom-sheet-peek). A standard sheet is named by its headline or, without one, by `label` (the props are a
|
|
31
|
+
// union: TypeScript rejects a sheet with neither `headline` nor `label`; a modal one's <dialog> is named by the
|
|
32
|
+
// developer who writes it). The drag handle can be left out (`dragHandle={false}`: no handle, so no tap to step
|
|
33
|
+
// through the states; the header, if there is one, still drags it). The header is drawn only when there is a
|
|
34
|
+
// headline or a close button, or a resize or collapse button. `resizeLabel` adds the resize button (Material 3's
|
|
35
|
+
// alternative to dragging the handle for those who use a single pointer: it does what a tap on the handle does, its
|
|
36
|
+
// icon an up caret at the lowest stop and a down caret otherwise, `aria-expanded` saying which); a sheet without a
|
|
37
|
+
// handle that can be resized must have it (the props are a union again). `collapseLabel` adds the collapse button, which
|
|
38
|
+
// shows only while the sheet is as high as the screen allows (data-full, set by the control) and returns it to its
|
|
39
|
+
// lowest stop. `state` defaults to the lowest of `states` that is not 'hidden' ('collapsed' without
|
|
40
|
+
// `states`), so a fixed sheet (`states={['expanded']}`) starts in its only state.
|
|
16
41
|
// The editor's own sheets (choices, fields) are modal ones in a <dialog> with bottomSheetFitClass, as high as what is in
|
|
17
42
|
// them (bottomSheetFullClass: as high as the screen, a whole page in it), with the container and its header
|
|
18
43
|
// (BottomSheetHeader, whose handle is a button there, `onHandle`, named `handleLabel`, and whose close button does
|
|
19
44
|
// `onClose`).
|
|
20
45
|
// How it looks is the theme's (--bottom-sheet-*).
|
|
21
|
-
import { useEffect, useRef, type ReactNode } from 'react';
|
|
46
|
+
import { useEffect, useId, useRef, type ReactNode } from 'react';
|
|
22
47
|
import { useOnSite } from '@morphemecms/cms/src/standard/view';
|
|
23
48
|
import IconButton from '@theme/ui/IconButton';
|
|
49
|
+
import BottomSheetResizeIcon from '@theme/ui/BottomSheetResizeIcon';
|
|
24
50
|
import { standardBottomSheet } from '@theme/ui/bottomSheetControl';
|
|
25
51
|
|
|
26
52
|
/** The kinds of a bottom sheet, and the states a standard one rests in, as Material 3 names them. */
|
|
27
53
|
export type BottomSheetKind = 'modal' | 'standard';
|
|
28
54
|
export type BottomSheetState = 'collapsed' | 'half-expanded' | 'expanded' | 'hidden';
|
|
55
|
+
/** What a sheet becomes from the medium breakpoint; a standard one cannot be a dialog (the props are typed per kind). */
|
|
56
|
+
export type BottomSheetWide = 'side-sheet' | 'dialog' | 'sheet';
|
|
57
|
+
/** Where a modal sheet opens on a phone: at half the screen's height (or as high as what is in it, if that is less), or as
|
|
58
|
+
* high as the screen allows; a standard sheet has its `state` instead (the props are typed per kind). */
|
|
59
|
+
export type BottomSheetStart = 'half' | 'full';
|
|
60
|
+
export type BottomSheetKindWide =
|
|
61
|
+
| { kind?: 'modal'; wide?: BottomSheetWide; startAt?: BottomSheetStart }
|
|
62
|
+
| { kind: 'standard'; wide?: Exclude<BottomSheetWide, 'dialog'>; startAt?: never };
|
|
29
63
|
|
|
30
64
|
/** The class of the <dialog> a modal bottom sheet is in, of what stands in place of what it shows (over a frame while
|
|
31
65
|
* it loads, or instead of it), and a bottom sheet's container's classes. */
|
|
@@ -38,6 +72,26 @@ export const bottomSheetContainerClass = (kind: BottomSheetKind = 'modal') =>
|
|
|
38
72
|
export const bottomSheetFitClass = 'bottom-sheet bottom-sheet-fit';
|
|
39
73
|
export const bottomSheetFullClass = 'bottom-sheet bottom-sheet-full';
|
|
40
74
|
|
|
75
|
+
/** Whether a sheet's header is drawn: it has a headline or a button. */
|
|
76
|
+
const hasHeader = (headline: ReactNode, ...labels: (string | undefined)[]) => headline !== undefined || labels.some(Boolean);
|
|
77
|
+
|
|
78
|
+
/** What names a bottom sheet: a headline, or (without one) a `label`. */
|
|
79
|
+
export type BottomSheetName =
|
|
80
|
+
| { headline: Exclude<ReactNode, undefined | null>; headlineId?: string; label?: never }
|
|
81
|
+
| { label: string; headline?: never; headlineId?: never };
|
|
82
|
+
|
|
83
|
+
/** What a sheet without a drag handle needs to be resizable: a resize button, or states that leave nothing to resize
|
|
84
|
+
* (one other than 'hidden', evt. with 'hidden'). Material 3's states, which omitted `states` means, are resizable. */
|
|
85
|
+
type FixedStates = [Exclude<BottomSheetState, 'hidden'>] | ['hidden', Exclude<BottomSheetState, 'hidden'>];
|
|
86
|
+
export type BottomSheetResize =
|
|
87
|
+
| { dragHandle?: true; resizeLabel?: string }
|
|
88
|
+
| { dragHandle: false; resizeLabel: string }
|
|
89
|
+
| { dragHandle: false; resizeLabel?: undefined; states: FixedStates };
|
|
90
|
+
|
|
91
|
+
/** The state a standard sheet starts in when `state` is not given: the lowest of `states` that is not 'hidden'. */
|
|
92
|
+
const startState = (states?: BottomSheetState[]): BottomSheetState =>
|
|
93
|
+
(['collapsed', 'half-expanded', 'expanded'] as const).find((s) => !states || states.includes(s)) ?? 'collapsed';
|
|
94
|
+
|
|
41
95
|
/** A bottom sheet's drag handle and header: its headline, the supporting text under it and evt. the close button. The
|
|
42
96
|
* handle is a button with `handleLabel` (a standard sheet's, `expanded` saying whether it is up; the editor's, which
|
|
43
97
|
* does `onHandle`); the close button does `onClose` in the editor. */
|
|
@@ -46,25 +100,33 @@ export function BottomSheetHeader({
|
|
|
46
100
|
headlineId,
|
|
47
101
|
supportingText,
|
|
48
102
|
closeLabel,
|
|
103
|
+
resizeLabel,
|
|
104
|
+
collapseLabel,
|
|
49
105
|
handleLabel,
|
|
50
106
|
expanded,
|
|
107
|
+
raised,
|
|
51
108
|
standard,
|
|
109
|
+
dragHandle = true,
|
|
52
110
|
onHandle,
|
|
53
111
|
onClose,
|
|
54
112
|
}: {
|
|
55
|
-
headline
|
|
113
|
+
headline?: ReactNode;
|
|
56
114
|
headlineId?: string;
|
|
57
115
|
supportingText?: ReactNode;
|
|
58
116
|
closeLabel?: string;
|
|
117
|
+
resizeLabel?: string;
|
|
118
|
+
collapseLabel?: string;
|
|
59
119
|
handleLabel?: string;
|
|
60
120
|
expanded?: boolean;
|
|
121
|
+
raised?: boolean;
|
|
61
122
|
standard?: boolean;
|
|
123
|
+
dragHandle?: boolean;
|
|
62
124
|
onHandle?: () => void;
|
|
63
125
|
onClose?: () => void;
|
|
64
126
|
}) {
|
|
65
127
|
return (
|
|
66
128
|
<>
|
|
67
|
-
{handleLabel !== undefined ? (
|
|
129
|
+
{!dragHandle ? null : handleLabel !== undefined ? (
|
|
68
130
|
<button
|
|
69
131
|
type="button"
|
|
70
132
|
className="bottom-sheet-drag-handle"
|
|
@@ -76,44 +138,67 @@ export function BottomSheetHeader({
|
|
|
76
138
|
) : (
|
|
77
139
|
<div className="bottom-sheet-drag-handle" data-bottom-sheet-handle=""></div>
|
|
78
140
|
)}
|
|
79
|
-
|
|
80
|
-
<div className="bottom-sheet-
|
|
81
|
-
<
|
|
82
|
-
{headline
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
{hasHeader(headline, closeLabel, resizeLabel, collapseLabel) && (
|
|
142
|
+
<div className="bottom-sheet-header" data-bottom-sheet-header={standard ? '' : undefined}>
|
|
143
|
+
<div className="bottom-sheet-heading">
|
|
144
|
+
{headline !== undefined && (
|
|
145
|
+
<h2 id={headlineId} className="bottom-sheet-headline">
|
|
146
|
+
{headline}
|
|
147
|
+
</h2>
|
|
148
|
+
)}
|
|
149
|
+
{supportingText && <p className="bottom-sheet-supporting-text">{supportingText}</p>}
|
|
150
|
+
</div>
|
|
151
|
+
{(resizeLabel || collapseLabel || closeLabel) && (
|
|
152
|
+
<div className="bottom-sheet-actions">
|
|
153
|
+
{resizeLabel && (
|
|
154
|
+
<IconButton icon={<BottomSheetResizeIcon />} label={resizeLabel} aria-expanded={!!raised} data-bottom-sheet-resize="" />
|
|
155
|
+
)}
|
|
156
|
+
{collapseLabel && <IconButton icon="caret-down" label={collapseLabel} data-bottom-sheet-collapse="" />}
|
|
157
|
+
{closeLabel && <IconButton icon="x" label={closeLabel} data-bottom-sheet-close="" onClick={onClose} />}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
85
160
|
</div>
|
|
86
|
-
|
|
87
|
-
</div>
|
|
161
|
+
)}
|
|
88
162
|
</>
|
|
89
163
|
);
|
|
90
164
|
}
|
|
91
165
|
|
|
92
166
|
export default function BottomSheet({
|
|
93
167
|
kind = 'modal',
|
|
94
|
-
|
|
168
|
+
wide,
|
|
169
|
+
startAt,
|
|
170
|
+
state,
|
|
171
|
+
states,
|
|
95
172
|
headline,
|
|
96
173
|
headlineId,
|
|
174
|
+
label,
|
|
97
175
|
supportingText,
|
|
98
176
|
closeLabel,
|
|
177
|
+
resizeLabel,
|
|
178
|
+
collapseLabel,
|
|
179
|
+
dragHandle = true,
|
|
99
180
|
dragHandleLabel,
|
|
181
|
+
peek,
|
|
100
182
|
footer,
|
|
101
183
|
height,
|
|
102
184
|
className,
|
|
103
185
|
children,
|
|
104
186
|
}: {
|
|
105
|
-
kind?: BottomSheetKind;
|
|
106
187
|
state?: BottomSheetState;
|
|
107
|
-
|
|
108
|
-
headlineId?: string;
|
|
188
|
+
states?: [BottomSheetState, ...BottomSheetState[]];
|
|
109
189
|
supportingText?: ReactNode;
|
|
110
190
|
closeLabel?: string;
|
|
191
|
+
collapseLabel?: string;
|
|
111
192
|
dragHandleLabel?: string;
|
|
193
|
+
peek?: ReactNode;
|
|
112
194
|
footer?: ReactNode;
|
|
113
195
|
height?: string;
|
|
114
196
|
className?: string;
|
|
115
197
|
children: ReactNode;
|
|
116
|
-
}
|
|
198
|
+
} & BottomSheetKindWide &
|
|
199
|
+
BottomSheetName &
|
|
200
|
+
BottomSheetResize) {
|
|
201
|
+
const shownState = state ?? startState(states);
|
|
117
202
|
const standard = kind === 'standard';
|
|
118
203
|
const onSite = useOnSite();
|
|
119
204
|
const sheet = useRef<HTMLDivElement>(null);
|
|
@@ -121,26 +206,38 @@ export default function BottomSheet({
|
|
|
121
206
|
useEffect(() => {
|
|
122
207
|
if (standard && !onSite && sheet.current) standardBottomSheet(sheet.current);
|
|
123
208
|
}, [standard, onSite]);
|
|
209
|
+
// A standard sheet is named by its headline even when the caller gives it no id.
|
|
210
|
+
const generatedId = useId();
|
|
211
|
+
const titleId = headlineId ?? generatedId;
|
|
124
212
|
const Container = standard ? 'section' : 'div';
|
|
125
213
|
return (
|
|
126
214
|
<Container
|
|
127
215
|
ref={sheet}
|
|
128
216
|
className={[bottomSheetContainerClass(kind), className].filter(Boolean).join(' ')}
|
|
129
|
-
aria-labelledby={standard ?
|
|
217
|
+
aria-labelledby={standard && headline !== undefined ? titleId : undefined}
|
|
218
|
+
aria-label={standard && headline === undefined ? label : undefined}
|
|
130
219
|
data-bottom-sheet-container=""
|
|
131
220
|
data-bottom-sheet-standard={standard ? '' : undefined}
|
|
132
|
-
data-
|
|
221
|
+
data-wide={wide ?? (standard ? 'side-sheet' : 'dialog')}
|
|
222
|
+
data-start={standard ? undefined : (startAt ?? 'half')}
|
|
223
|
+
data-state={standard ? shownState : undefined}
|
|
224
|
+
data-states={standard && states ? states.join(' ') : undefined}
|
|
133
225
|
style={height ? { height } : undefined}
|
|
134
226
|
>
|
|
135
227
|
<BottomSheetHeader
|
|
136
228
|
headline={headline}
|
|
137
|
-
headlineId={headlineId}
|
|
229
|
+
headlineId={standard && headline !== undefined ? titleId : headlineId}
|
|
138
230
|
supportingText={supportingText}
|
|
139
231
|
closeLabel={closeLabel}
|
|
232
|
+
resizeLabel={standard ? resizeLabel : undefined}
|
|
233
|
+
collapseLabel={standard ? collapseLabel : undefined}
|
|
234
|
+
dragHandle={dragHandle}
|
|
140
235
|
handleLabel={standard ? (dragHandleLabel ?? '') : undefined}
|
|
141
|
-
expanded={
|
|
236
|
+
expanded={shownState === 'half-expanded' || shownState === 'expanded'}
|
|
237
|
+
raised={shownState !== 'hidden' && shownState !== startState(states)}
|
|
142
238
|
standard={standard}
|
|
143
239
|
/>
|
|
240
|
+
{standard && peek && <div className="bottom-sheet-peek">{peek}</div>}
|
|
144
241
|
<div className="bottom-sheet-content">{children}</div>
|
|
145
242
|
{footer && <p className="bottom-sheet-footer">{footer}</p>}
|
|
146
243
|
</Container>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The icon of a bottom sheet's resize button (BottomSheet.tsx): the up caret while the sheet is at its lowest stop, the
|
|
2
|
+
// down caret otherwise; both are drawn and ui.css shows the one the button's aria-expanded says, so the control
|
|
3
|
+
// (bottomSheetControl.ts) only changes that attribute. Exposes BottomSheetResizeIcon, an IconRef element for IconButton.
|
|
4
|
+
import { Svg } from '@morphemecms/cms/src/icon';
|
|
5
|
+
|
|
6
|
+
export default function BottomSheetResizeIcon({ size = 22 }: { size?: number; className?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Svg name="caret-up" size={size} className="bottom-sheet-resize-up" />
|
|
10
|
+
<Svg name="caret-down" size={size} className="bottom-sheet-resize-down" />
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
package/theme/ui/Button.tsx
CHANGED
|
@@ -22,9 +22,10 @@ export type ButtonShape = (typeof buttonShapes)[number];
|
|
|
22
22
|
export const buttonClass = (kind: ButtonKind = 'filled', size: ButtonSize = 'medium', shape: ButtonShape = 'round') =>
|
|
23
23
|
['button', `button-${kind}`, size !== 'medium' && `button-${size}`, shape !== 'round' && `button-${shape}`].filter(Boolean).join(' ');
|
|
24
24
|
|
|
25
|
-
/** A toggle's icon (a Button's, an IconButton's): the icon, and the same icon filled while
|
|
26
|
-
* draws it filled (the theme shows the one of the state it is in: .<part>-unselected-icon,
|
|
27
|
-
|
|
25
|
+
/** A toggle's icon (a Button's, an IconButton's, a navigation drawer item's): the icon, and the same icon filled while
|
|
26
|
+
* it is on, where the site draws it filled (the theme shows the one of the state it is in: .<part>-unselected-icon,
|
|
27
|
+
* .<part>-selected-icon). */
|
|
28
|
+
export function ToggleIcon({ part, name, size }: { part: 'button' | 'icon-button' | 'navigation-drawer'; name: string; size: number }) {
|
|
28
29
|
if (!iconFile(name, 'fill')) return <Svg name={name} size={size} />;
|
|
29
30
|
return (
|
|
30
31
|
<>
|
package/theme/ui/Dialog.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// evt. a divider over the actions (`divider`) and the actions at its end (`actions`: buttons, the main one last).
|
|
8
8
|
// - 'full-screen': over the whole screen on a phone, for a task with several steps; in its header the close button
|
|
9
9
|
// (`closeLabel`, data-dialog-close), the headline and the action (`actions`: a text button), evt. a divider under it
|
|
10
|
-
// (`divider`), and under them what it shows (`supportingText` and `children`, scrolling). From
|
|
10
|
+
// (`divider`), and under them what it shows (`supportingText` and `children`, scrolling). From the medium breakpoint, where Material
|
|
11
11
|
// shows it as a dialog, it is in the middle of the screen over the scrim, with the same header.
|
|
12
12
|
// Shown modally it is in a <dialog> with dialogModalClass (a full-screen one with dialogFullScreenModalClass), which
|
|
13
13
|
// dialog() in dialogControl.ts (or the script that shows it: Popup.astro) opens and closes. How it looks is the theme's
|
package/theme/ui/FabMenu.tsx
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// A FAB menu (m3.material.io/components/fab-menu, .fab-menu in ../styles/ui.css): the theme's FAB (its classes,
|
|
2
|
-
// fabClass: `icon`, its name `label`, its size `size`, any of the FAB sizes, and its colour
|
|
3
|
-
// colours, the menu's `kind` filled if it has none) that opens two to six related actions
|
|
4
|
-
// theme's ExtendedFab: its icon and label, with `href` a link), the last nearest the FAB.
|
|
5
|
-
// three colour sets (`kind`: primary, the default, secondary or tertiary): while it is
|
|
6
|
-
// round close button with a cross (its name `closeLabel`) in the set's filled
|
|
7
|
-
// container colour, so they stand apart from it. Where it stands (fixed at the
|
|
8
|
-
// (`className`). `open` draws it open (the editor).
|
|
9
|
-
// attributes (data-…) go on the
|
|
2
|
+
// fabClass: `icon`, a name or an icon, its name `label`, its size `size`, any of the FAB sizes, and its colour
|
|
3
|
+
// `fabKind`, any of the FAB colours, the menu's `kind` filled if it has none) that opens two to six related actions
|
|
4
|
+
// above it (`items`, each the theme's ExtendedFab: its icon and label, with `href` a link), the last nearest the FAB.
|
|
5
|
+
// The menu comes in Material's three colour sets (`kind`: primary, the default, secondary or tertiary): while it is
|
|
6
|
+
// open (aria-expanded) the FAB is a round close button with a cross (its name `closeLabel`) in the set's filled
|
|
7
|
+
// colour, and the actions are in its container colour, so they stand apart from it. Where it stands (fixed at the
|
|
8
|
+
// bottom of the screen) is the component's (`className`). `open` draws it open (the editor). ui/fabMenu.script.ts
|
|
9
|
+
// opens and closes it (on the site, and in the editor through setUpFabMenu). Other attributes (data-…) go on the
|
|
10
|
+
// whole.
|
|
10
11
|
import { useId } from 'react';
|
|
11
|
-
import { Svg } from '@morphemecms/cms/src/icon';
|
|
12
|
+
import { Glyph, Svg, type IconRef } from '@morphemecms/cms/src/icon';
|
|
12
13
|
import { ExtendedFab, fabClass, type FabKind, type FabSize } from '@theme/ui/Fab';
|
|
13
14
|
|
|
14
|
-
export type FabMenuItem = { icon:
|
|
15
|
+
export type FabMenuItem = { icon: IconRef; label: string; href?: string } & Record<string, unknown>;
|
|
15
16
|
export const fabMenuKinds = ['primary', 'secondary', 'tertiary'] as const;
|
|
16
17
|
export type FabMenuKind = (typeof fabMenuKinds)[number];
|
|
17
18
|
|
|
@@ -30,7 +31,7 @@ export default function FabMenu({
|
|
|
30
31
|
className,
|
|
31
32
|
...rest
|
|
32
33
|
}: {
|
|
33
|
-
icon:
|
|
34
|
+
icon: IconRef;
|
|
34
35
|
label: string;
|
|
35
36
|
closeLabel: string;
|
|
36
37
|
items: FabMenuItem[];
|
|
@@ -52,7 +53,7 @@ export default function FabMenu({
|
|
|
52
53
|
data-label={label}
|
|
53
54
|
data-close-label={closeLabel}
|
|
54
55
|
>
|
|
55
|
-
<
|
|
56
|
+
<Glyph icon={icon} size={24} className="fab-menu-icon" />
|
|
56
57
|
<Svg name="x" size={24} className="fab-menu-close-icon" />
|
|
57
58
|
</button>
|
|
58
59
|
<ul id={list} className="fab-menu-list" aria-label={label} hidden={!open}>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ../styles/ui.css): a shape that turns and changes from one of Material's shapes into the next, while something that
|
|
3
3
|
// takes only a moment is on its way (where how far it has come is known, a progress indicator says it instead). By
|
|
4
4
|
// itself, or in its round container (`contained`, where it stands on something else). Screen readers read it as a
|
|
5
|
-
// progress bar without a value, named by `label` (e.g. "Henter
|
|
5
|
+
// progress bar without a value, named by `label` (e.g. "Henter…"). With reduced motion it only turns, slowly. How it
|
|
6
6
|
// looks is the theme's (--loading-indicator-*). An Astro component gives an element the same classes with
|
|
7
7
|
// loadingIndicatorClass, with a .loading-indicator-shape in it.
|
|
8
8
|
|
|
@@ -1,48 +1,149 @@
|
|
|
1
|
-
// A
|
|
2
|
-
// site's pages in a panel over the page (components/MobileMenu.tsx, and a theme's own menu), over its
|
|
3
|
-
// (navigationDrawerScrimClass). Its pages are items (NavigationDrawerItem, `active`: the open page or the one
|
|
4
|
-
// it), an item with sub-pages is a row (NavigationDrawerRow) with the button that unfolds them after it
|
|
1
|
+
// A navigation drawer (m3.material.io/components/navigation-drawer, .navigation-drawer in ../styles/ui.css), at the
|
|
2
|
+
// start edge: the site's pages in a panel over the page (components/MobileMenu.tsx, and a theme's own menu), over its
|
|
3
|
+
// scrim (navigationDrawerScrimClass). Its pages are items (NavigationDrawerItem, `active`: the open page or the one
|
|
4
|
+
// holding it), an item with sub-pages is a row (NavigationDrawerRow) with the button that unfolds them after it
|
|
5
5
|
// (NavigationDrawerToggle, `expanded`), and a sub-page a subitem (NavigationDrawerSubitem). Where it comes from, how it
|
|
6
6
|
// moves and what else is in it (the logo, the buttons) is the menu's own: it gives its own element (a motion.div) the
|
|
7
7
|
// class with navigationDrawerClass; in a <dialog> (the editor's), the dialog has navigationDrawerDialogClass and its
|
|
8
|
-
// scrim is the dialog's.
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// scrim is the dialog's. Its variant (variant): modal, the default, over its scrim, or standard, beside the content at
|
|
9
|
+
// full height without a scrim and without a shadow (navigationDrawerClasses gives the classes to an element of its own);
|
|
10
|
+
// where it stands is the component's own. Links are Link (on the site; in the editor's previews only drawn), an item
|
|
11
|
+
// without `href` a button (the editor's: a part of a form); other attributes (onClick, aria-…) go on them. An item has
|
|
12
|
+
// an icon before its label (icon: a Phosphor name, filled while it is active, or an icon made) and evt. a badge after
|
|
13
|
+
// the label (badge: how many there are, label large). The pages can stand in sections (NavigationDrawerSection, label:
|
|
14
|
+
// its name, title small), a full-width line (the theme's Divider) between them. The arrow keys, Home and End move
|
|
15
|
+
// between the items (NavigationDrawer does it itself; a drawer drawn as a class on an element of its own calls
|
|
16
|
+
// navigationDrawerKeys(root)), and a modal one gives its first item the focus when it opens
|
|
17
|
+
// (focusNavigationDrawerItem(root); navigationDrawerControl.ts). An item can end in an icon (trailing: an arrow to a list
|
|
18
|
+
// it opens), and a drawer in two levels has a back row (NavigationDrawerBack, an arrow and a label) at the top of the
|
|
19
|
+
// second level; it is a button, so the arrow keys reach it like an item.
|
|
20
|
+
import type { KeyboardEvent, ReactNode } from 'react';
|
|
21
|
+
import { Glyph, Svg, type IconRef } from '@morphemecms/cms/src/icon';
|
|
22
|
+
import { ToggleIcon } from '@theme/ui/Button';
|
|
12
23
|
import { Link } from '@morphemecms/cms/src/standard/view';
|
|
24
|
+
import Divider from '@theme/ui/Divider';
|
|
25
|
+
import { handleDrawerKey } from './navigationDrawerControl';
|
|
26
|
+
export { focusNavigationDrawerItem, navigationDrawerKeys } from './navigationDrawerControl';
|
|
27
|
+
|
|
28
|
+
export type NavigationDrawerVariant = 'modal' | 'standard';
|
|
13
29
|
|
|
14
30
|
/** The drawer's class, and its scrim's. */
|
|
15
31
|
export const navigationDrawerClass = 'navigation-drawer';
|
|
16
32
|
export const navigationDrawerScrimClass = 'navigation-drawer-scrim';
|
|
17
|
-
/** The class of a <dialog> the drawer is in: at the
|
|
33
|
+
/** The class of a <dialog> the drawer is in: at the start of the screen, as high as it, over its scrim. */
|
|
18
34
|
export const navigationDrawerDialogClass = 'navigation-drawer-dialog';
|
|
19
35
|
|
|
20
|
-
|
|
36
|
+
/** The drawer's classes with its variant: `modal` (the default) over a scrim, `standard` beside the content, full height. */
|
|
37
|
+
export const navigationDrawerClasses = (variant: NavigationDrawerVariant = 'modal') =>
|
|
38
|
+
[navigationDrawerClass, variant === 'standard' && 'navigation-drawer-standard'].filter(Boolean).join(' ');
|
|
39
|
+
|
|
40
|
+
export function NavigationDrawer({
|
|
41
|
+
variant = 'modal',
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
onKeyDown,
|
|
45
|
+
...rest
|
|
46
|
+
}: {
|
|
47
|
+
variant?: NavigationDrawerVariant;
|
|
48
|
+
className?: string;
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
51
|
+
} & Record<string, unknown>) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className={[navigationDrawerClasses(variant), className].filter(Boolean).join(' ')}
|
|
55
|
+
onKeyDown={(event) => {
|
|
56
|
+
onKeyDown?.(event);
|
|
57
|
+
if (!event.defaultPrevented) handleDrawerKey(event.nativeEvent, event.currentTarget);
|
|
58
|
+
}}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Items under each other in a section, with its name (`label`) over them. Between two sections stands a full-width
|
|
67
|
+
* divider (drawn by the section that has one before it, shown by the theme), never between single items. */
|
|
68
|
+
export function NavigationDrawerSection({ label, children }: { label?: string; children: ReactNode }) {
|
|
21
69
|
return (
|
|
22
|
-
<div className={
|
|
70
|
+
<div className="navigation-drawer-section" role={label ? 'group' : undefined} aria-label={label}>
|
|
71
|
+
<Divider className="navigation-drawer-section-divider" />
|
|
72
|
+
{label && (
|
|
73
|
+
<span className="navigation-drawer-section-label" aria-hidden="true">
|
|
74
|
+
{label}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
23
77
|
{children}
|
|
24
78
|
</div>
|
|
25
79
|
);
|
|
26
80
|
}
|
|
27
81
|
|
|
28
|
-
/**
|
|
82
|
+
/** The icon before an item's label: a Phosphor name is drawn outlined, and filled on the active item where the site draws
|
|
83
|
+
* a filled one (ToggleIcon); an icon made is drawn as it is. */
|
|
84
|
+
function ItemIcon({ icon }: { icon: IconRef }) {
|
|
85
|
+
return (
|
|
86
|
+
<span className="navigation-drawer-icon">
|
|
87
|
+
{typeof icon === 'string' ? <ToggleIcon part="navigation-drawer" name={icon} size={24} /> : <Glyph icon={icon} size={24} />}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A page in the drawer, `active` when it is the open page or holds it; without `href` a button. With an `icon` (24dp,
|
|
93
|
+
* filled when it is active; Material: all items have one or none), evt. a badge (the text at its end: how many there
|
|
94
|
+
* are) and evt. a `trailing` icon at the very end (24dp, outlined; an arrow to the list the item opens). */
|
|
29
95
|
export function NavigationDrawerItem({
|
|
30
96
|
href,
|
|
31
97
|
active,
|
|
98
|
+
icon,
|
|
99
|
+
badge,
|
|
100
|
+
trailing,
|
|
32
101
|
children,
|
|
33
102
|
...rest
|
|
34
|
-
}: {
|
|
103
|
+
}: {
|
|
104
|
+
href?: string;
|
|
105
|
+
active?: boolean;
|
|
106
|
+
icon?: IconRef;
|
|
107
|
+
badge?: ReactNode;
|
|
108
|
+
trailing?: IconRef;
|
|
109
|
+
children: ReactNode;
|
|
110
|
+
} & Record<string, unknown>) {
|
|
111
|
+
const content = (
|
|
112
|
+
<>
|
|
113
|
+
{icon !== undefined && <ItemIcon icon={icon} />}
|
|
114
|
+
<span className="navigation-drawer-label">{children}</span>
|
|
115
|
+
{badge != null && badge !== '' && <span className="navigation-drawer-badge">{badge}</span>}
|
|
116
|
+
{trailing !== undefined && (
|
|
117
|
+
<span className="navigation-drawer-trailing">
|
|
118
|
+
{typeof trailing === 'string' ? <Svg name={trailing} size={24} /> : <Glyph icon={trailing} size={24} />}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
35
123
|
return href === undefined ? (
|
|
36
124
|
<button type="button" className="navigation-drawer-item" data-active={active || undefined} {...rest}>
|
|
37
|
-
{
|
|
125
|
+
{content}
|
|
38
126
|
</button>
|
|
39
127
|
) : (
|
|
40
128
|
<Link href={href} className="navigation-drawer-item" data-active={active || undefined} {...rest}>
|
|
41
|
-
{
|
|
129
|
+
{content}
|
|
42
130
|
</Link>
|
|
43
131
|
);
|
|
44
132
|
}
|
|
45
133
|
|
|
134
|
+
/** The back row at the top of a drawer's second level: an `arrow-left` and the `label` (the first level's name). A
|
|
135
|
+
* button; `onClick` shows the first level. */
|
|
136
|
+
export function NavigationDrawerBack({ label, ...rest }: { label: string } & Record<string, unknown>) {
|
|
137
|
+
return (
|
|
138
|
+
<button type="button" className="navigation-drawer-item navigation-drawer-back" {...rest}>
|
|
139
|
+
<span className="navigation-drawer-icon">
|
|
140
|
+
<Svg name="arrow-left" size={24} />
|
|
141
|
+
</span>
|
|
142
|
+
<span className="navigation-drawer-label">{label}</span>
|
|
143
|
+
</button>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
46
147
|
/** An item and what stands after it (the toggle of its sub-pages). */
|
|
47
148
|
export function NavigationDrawerRow({
|
|
48
149
|
className,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// editor's logo and the site's name). With `onExpandedChange` the rail is expanded and collapsed by whoever draws it (the
|
|
16
16
|
// editor) instead of by its script; a modal one without `scrim` has none (one that expands while the mouse is over it:
|
|
17
17
|
// the editor's).
|
|
18
|
+
// A destination without `href` is a button (the editor's sections, which open their list).
|
|
18
19
|
// Where it stands (beside the content, in a row) is the component's own; how it looks is the theme's. Links are Link (on
|
|
19
20
|
// the site; in the editor only drawn). An Astro component gives its elements the classes with navigationRailClass,
|
|
20
21
|
// navigationRailClasses and navigationRailItemClass.
|
|
@@ -119,7 +120,7 @@ export function NavigationRailSection({ label, children }: { label: string; chil
|
|
|
119
120
|
);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
/** A destination: its icon (a Phosphor name, the theme's icon, or an icon made: IconRef) and its label, `selected` when
|
|
123
|
+
/** A destination, a link (or, without `href`, a button): its icon (a Phosphor name, the theme's icon, or an icon made: IconRef) and its label, `selected` when
|
|
123
124
|
* it is the open page, evt. a badge on the icon, and a text at its end while the rail is expanded. */
|
|
124
125
|
export function NavigationRailItem({
|
|
125
126
|
href,
|
|
@@ -130,25 +131,33 @@ export function NavigationRailItem({
|
|
|
130
131
|
trailingSupporting,
|
|
131
132
|
...rest
|
|
132
133
|
}: {
|
|
133
|
-
href
|
|
134
|
+
href?: string;
|
|
134
135
|
icon: IconRef;
|
|
135
136
|
label: ReactNode;
|
|
136
137
|
selected?: boolean;
|
|
137
138
|
badge?: true | number;
|
|
138
139
|
trailingSupporting?: ReactNode;
|
|
139
140
|
} & Record<string, unknown>) {
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
<span className="navigation-rail-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
{badge !== undefined && <Badge count={badge === true ? undefined : badge} className="navigation-rail-badge" />}
|
|
146
|
-
</span>
|
|
147
|
-
<span className="navigation-rail-label">{label}</span>
|
|
148
|
-
{trailingSupporting != null && trailingSupporting !== '' && (
|
|
149
|
-
<span className="navigation-rail-trailing-supporting">{trailingSupporting}</span>
|
|
150
|
-
)}
|
|
141
|
+
const content = (
|
|
142
|
+
<span className="navigation-rail-indicator">
|
|
143
|
+
<span className="navigation-rail-icon">
|
|
144
|
+
<Glyph icon={icon} size={24} />
|
|
145
|
+
{badge !== undefined && <Badge count={badge === true ? undefined : badge} className="navigation-rail-badge" />}
|
|
151
146
|
</span>
|
|
147
|
+
<span className="navigation-rail-label">{label}</span>
|
|
148
|
+
{trailingSupporting != null && trailingSupporting !== '' && (
|
|
149
|
+
<span className="navigation-rail-trailing-supporting">{trailingSupporting}</span>
|
|
150
|
+
)}
|
|
151
|
+
</span>
|
|
152
|
+
);
|
|
153
|
+
// Without `href` a button (the editor's: a section that opens its list in the expanded rail); `selected` is aria-current.
|
|
154
|
+
return href === undefined ? (
|
|
155
|
+
<button type="button" className={navigationRailItemClass} aria-current={selected ? 'true' : undefined} {...rest}>
|
|
156
|
+
{content}
|
|
157
|
+
</button>
|
|
158
|
+
) : (
|
|
159
|
+
<Link href={href} className={navigationRailItemClass} aria-current={selected ? 'page' : undefined} {...rest}>
|
|
160
|
+
{content}
|
|
152
161
|
</Link>
|
|
153
162
|
);
|
|
154
163
|
}
|