@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/theme/ui/Search.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A search (m3.material.io/components/search, .search in ../styles/ui.css): the search bar (its leading icon,
|
|
2
2
|
// `leadingIcon`, the theme's magnifying glass if it has none, the placeholder, `label`, and what stands after it,
|
|
3
3
|
// `trailing`: icon buttons, an avatar) and the search view it opens (a <dialog>: over the whole screen on a phone, docked
|
|
4
|
-
// where the bar is from
|
|
4
|
+
// where the bar is from the medium breakpoint), with the button back (`backLabel`), the field (a role="combobox"), the button that
|
|
5
5
|
// empties it (`clearLabel`), and the results (`results`, the theme's list, List and ListItem, as a role="listbox" of
|
|
6
6
|
// links, each its headline, evt. its supporting text and its icon) or, when none fits, `emptyText`. On the site
|
|
7
7
|
// search.script.ts opens and closes it (the back button, Escape, a tap outside), narrows the results to what is
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// A setting's row (.setting-row in ../styles/ui.css): its name (`label`) and a line under it (`supporting`) at the left,
|
|
2
|
+
// and at the right whatever sets it (`children`: the switch's track, a dropdown button), all on one line, centred on it.
|
|
3
|
+
// Switch.tsx and the editor's MenuRow are both made of it, so the two can never drift. `as`: the element, a <label>
|
|
4
|
+
// for a switch (a click anywhere switches it), else a <div>; without a `label` only `children` show; `className` goes on it.
|
|
5
|
+
import type { ElementType, ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
export default function SettingRow({
|
|
8
|
+
label,
|
|
9
|
+
supporting,
|
|
10
|
+
as: Tag = 'div',
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
label: ReactNode;
|
|
15
|
+
supporting?: ReactNode;
|
|
16
|
+
as?: ElementType;
|
|
17
|
+
className?: string;
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<Tag className={['setting-row', className].filter(Boolean).join(' ')}>
|
|
22
|
+
{label != null && (
|
|
23
|
+
<span className="setting-row-text">
|
|
24
|
+
<span className="setting-row-label">{label}</span>
|
|
25
|
+
{supporting && <span className="setting-row-supporting">{supporting}</span>}
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
{children}
|
|
29
|
+
</Tag>
|
|
30
|
+
);
|
|
31
|
+
}
|
package/theme/ui/Switch.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// A switch (m3.material.io/components/switch, .switch in ../styles/ui.css):
|
|
2
|
-
//
|
|
3
|
-
// it; the switch is a native <input type="checkbox" role="switch">, so Space switches it and screen readers say
|
|
1
|
+
// A switch (m3.material.io/components/switch, .switch in ../styles/ui.css): a SettingRow (SettingRow.tsx) with its name
|
|
2
|
+
// (`label`) and a line under it (`supporting`), and after them the track with its handle, all of it one <label>, so a
|
|
3
|
+
// click anywhere switches it; the switch is a native <input type="checkbox" role="switch">, so Space switches it and screen readers say
|
|
4
4
|
// "on"/"off". `icons`: the theme's icons in the handle, 'both' (a check when it is on, a cross when it is off) or
|
|
5
5
|
// 'selected' (only the check); none without it. `checked`: on from the start, or, with `onChange` (the editor), whether
|
|
6
6
|
// it is on now; `disabled`: it cannot be changed. `states`: what it is on and off ("Til", "Fra"), shown before the
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// label. An Astro component gives an element the same classes with switchClass.
|
|
10
10
|
import type { ReactNode } from 'react';
|
|
11
11
|
import { Svg } from '@morphemecms/cms/src/icon';
|
|
12
|
+
import SettingRow from './SettingRow';
|
|
12
13
|
|
|
13
14
|
export type SwitchIcons = 'both' | 'selected';
|
|
14
15
|
|
|
@@ -36,13 +37,12 @@ export default function Switch({
|
|
|
36
37
|
// Changed by the editor (onChange): on as it says; else on from the start, then by the visitor.
|
|
37
38
|
const on = rest.onChange ? { checked: Boolean(checked) } : { defaultChecked: checked };
|
|
38
39
|
return (
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)}
|
|
40
|
+
<SettingRow
|
|
41
|
+
as="label"
|
|
42
|
+
className={[switchClass(icons), className].filter(Boolean).join(' ')}
|
|
43
|
+
label={hideLabel ? null : label}
|
|
44
|
+
supporting={hideLabel ? null : supporting}
|
|
45
|
+
>
|
|
46
46
|
{states && (
|
|
47
47
|
<span className="switch-state" aria-hidden="true">
|
|
48
48
|
{states[checked ? 0 : 1]}
|
|
@@ -55,6 +55,6 @@ export default function Switch({
|
|
|
55
55
|
{icons === 'both' && <Svg name="x" size={16} className="switch-icon switch-icon-off" />}
|
|
56
56
|
</span>
|
|
57
57
|
</span>
|
|
58
|
-
</
|
|
58
|
+
</SettingRow>
|
|
59
59
|
);
|
|
60
60
|
}
|
package/theme/ui/TextField.tsx
CHANGED
|
@@ -196,16 +196,19 @@ export default function TextField({
|
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/** A field that is tapped to choose (the editor's choice from a list, which opens its own sheet
|
|
200
|
-
* with the choices): the text field (its kind and its look) with a button, or a link, in its container
|
|
201
|
-
* chosen (`children`), its name on its edge (`label`; without it, it is named by `aria-label`), an icon at
|
|
202
|
-
* (`trailingIcon`: caret-down, the default, or the arrow to a page), and the line under it (`supporting`) or what
|
|
203
|
-
* wrong (`error`).
|
|
199
|
+
/** A field that is tapped to choose (the editor's choice from a list, which opens its own sheet or a menu under it, or
|
|
200
|
+
* with `href` a page with the choices): the text field (its kind and its look) with a button, or a link, in its container
|
|
201
|
+
* showing what is chosen (`children`), its name on its edge (`label`; without it, it is named by `aria-label`), an icon at
|
|
202
|
+
* its end (`trailingIcon`: caret-down, the default, or the arrow to a page), and the line under it (`supporting`) or what
|
|
203
|
+
* is wrong (`error`). `size="small"`: a small box, as wide as what is chosen, for a row's choice (a setting's, the
|
|
204
|
+
* editor's MenuRow). While its menu is open (aria-expanded on the button) its edge and name are in the accent and the
|
|
205
|
+
* arrow points up. Other attributes (onClick, aria-…) go on the button, and `className` on the field around it. */
|
|
204
206
|
export function TextFieldButton({
|
|
205
207
|
label,
|
|
206
208
|
supporting,
|
|
207
209
|
error,
|
|
208
210
|
kind,
|
|
211
|
+
size,
|
|
209
212
|
href,
|
|
210
213
|
trailingIcon = 'caret-down',
|
|
211
214
|
id,
|
|
@@ -217,6 +220,7 @@ export function TextFieldButton({
|
|
|
217
220
|
supporting?: ReactNode;
|
|
218
221
|
error?: string | boolean;
|
|
219
222
|
kind?: TextFieldKind;
|
|
223
|
+
size?: 'small';
|
|
220
224
|
href?: string;
|
|
221
225
|
trailingIcon?: IconRef;
|
|
222
226
|
id?: string;
|
|
@@ -229,7 +233,14 @@ export function TextFieldButton({
|
|
|
229
233
|
const described = line ? `${field}-supporting` : undefined;
|
|
230
234
|
return (
|
|
231
235
|
<div
|
|
232
|
-
className={[
|
|
236
|
+
className={[
|
|
237
|
+
textFieldClass(kind, !!error),
|
|
238
|
+
label === undefined && 'text-field-without-label',
|
|
239
|
+
size === 'small' && 'text-field-small',
|
|
240
|
+
className,
|
|
241
|
+
]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join(' ')}
|
|
233
244
|
>
|
|
234
245
|
<div className={textFieldContainerClass}>
|
|
235
246
|
{href === undefined ? (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What a standard bottom sheet (BottomSheet.tsx with kind 'standard') does on the site: it can be dragged between its
|
|
2
|
-
// states and tapped
|
|
3
|
-
// (blockScripts, packages/editor/site/blockScripts.ts); without a standard
|
|
2
|
+
// states and tapped to step through them (standardBottomSheet in bottomSheetControl.ts).
|
|
3
|
+
// Run on every page by the theme's Base.astro (blockScripts, packages/editor/site/blockScripts.ts); without a standard
|
|
4
|
+
// bottom sheet on the page it does nothing.
|
|
4
5
|
import { standardBottomSheet } from '@theme/ui/bottomSheetControl';
|
|
5
6
|
|
|
6
7
|
export default function bottomSheets() {
|
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
// What a bottom sheet (BottomSheet.tsx) does in a browser.
|
|
2
|
-
// A modal one (in its <dialog> with bottomSheetClass), for the script that shows it (an app's: Calendly's
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
2
|
+
// A modal one (in its <dialog> with bottomSheetClass), for the script that shows it (an app's: Calendly's Sheet.astro):
|
|
3
|
+
// bottomSheet(dialog) gives it its open() and close(). Native <dialog>: focus handling, Escape and screen readers work
|
|
4
|
+
// out of the box. Motion: on a phone it slides up and down (a state transition); it opens at its start height
|
|
5
|
+
// (BottomSheet's `startAt`, half the screen without it), is pulled up to full height and pulled down to close by its
|
|
6
|
+
// handle, its header or its content (modalSheetSize.ts; the editor's sheets, with bottomSheetFitClass or
|
|
7
|
+
// bottomSheetFullClass, keep their heights and the old handle pull down to close), on a computer (from the medium
|
|
8
|
+
// breakpoint) it follows its data-wide (BottomSheet's `wide`): 'dialog' (the default, a dialog in the middle of the
|
|
9
|
+
// screen) fades, 'side-sheet' (a modal side sheet at the end of the screen) slides in from the end (the left in a
|
|
10
|
+
// language written from the right) and out again, 'sheet' (still a bottom sheet) slides up and down as on a phone;
|
|
11
|
+
// with reduced motion it appears and disappears at once. Its close button, Escape and a tap on the scrim (outside it)
|
|
12
|
+
// close it; the page under it does not scroll while it is open. It moves with the site's motion scheme (motionSpring,
|
|
13
|
+
// @morphemecms/cms/lib/motion.ts).
|
|
14
|
+
// A standard one (kind 'standard', at the bottom of the screen without a scrim; bottomSheet.script.ts on every page, or
|
|
15
|
+
// a block's or an app's script that wants its controls): standardBottomSheet(sheet) lets it be dragged by its handle
|
|
16
|
+
// and its header between the states it rests in: the ones in its data-states (the `states` prop of BottomSheet; without
|
|
17
|
+
// it Material 3's, with 'hidden' when it has a close button; half-expanded only where that is clearly between the
|
|
18
|
+
// others), to the nearest when let go, or on to the next when it is flicked; a tap on the handle (or Enter or Space on
|
|
19
|
+
// it) steps through the states, up one at a time and from the highest back to the lowest (never to hidden), and its
|
|
20
|
+
// close button hides it. Its resize button (`resizeLabel`, [data-bottom-sheet-resize]) does what a tap on the handle
|
|
21
|
+
// does and has aria-expanded true while the sheet is above its lowest stop; its collapse button (`collapseLabel`,
|
|
22
|
+
// [data-bottom-sheet-collapse]) sets the lowest stop, moves focus on to the handle, and shows (ui.css) only under
|
|
23
|
+
// data-full, which is set while the sheet is expanded and as high as its max-height lets it be (also when the window
|
|
24
|
+
// is resized). The decisions are sheet-stops.mjs's (tested); this only measures and applies them.
|
|
25
|
+
// From the medium breakpoint a sheet with data-wide 'side-sheet' (the default; BottomSheet's `wide`) is a side sheet
|
|
26
|
+
// (ui.css) and this does nothing to it: the handle, the header and the buttons do not act. It follows the media query:
|
|
27
|
+
// when the window gets that wide it drops a drag and the height it was dragged to, and when it gets narrower it applies
|
|
28
|
+
// the marks again.
|
|
29
|
+
// How high each state is, is ui.css's (data-state), read without being painted, so a theme sets it there. It gives its
|
|
30
|
+
// state() and set(state) to show it in another (also again after it is hidden). The height follows with the site's
|
|
31
|
+
// motion scheme; with reduced motion it changes at once.
|
|
17
32
|
import { animate } from 'motion';
|
|
33
|
+
import { atLeast } from '@morphemecms/cms/lib/breakpoints.mjs';
|
|
18
34
|
import { motionSpring } from '@morphemecms/cms/lib/motion';
|
|
35
|
+
import { defaultStates, nextOnTap, restAfterDrag, restingStops } from '@morphemecms/cms/lib/sheet-stops.mjs';
|
|
36
|
+
import { modalSheetSize } from '@theme/ui/modalSheetSize';
|
|
19
37
|
import type { BottomSheetState } from '@theme/ui/BottomSheet';
|
|
20
38
|
|
|
21
39
|
export type BottomSheetControls = { open: () => void; close: () => Promise<void> };
|
|
@@ -25,26 +43,42 @@ export function bottomSheet(sheet: HTMLDialogElement): BottomSheetControls {
|
|
|
25
43
|
const panel = sheet.querySelector<HTMLElement>('[data-bottom-sheet-container]')!;
|
|
26
44
|
const handle = sheet.querySelector<HTMLElement>('[data-bottom-sheet-handle]');
|
|
27
45
|
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
28
|
-
//
|
|
29
|
-
|
|
46
|
+
// On a computer (from the medium breakpoint) what data-wide says (ui.css): a dialog in the middle fades, a side sheet
|
|
47
|
+
// slides in from the end, a sheet slides up; the dialog and the side sheet have no handle to drag.
|
|
48
|
+
const wide = matchMedia(atLeast('medium'));
|
|
49
|
+
const kind = () =>
|
|
50
|
+
!wide.matches ? 'sheet' : panel.dataset.wide === 'side-sheet' ? 'side-sheet' : panel.dataset.wide === 'sheet' ? 'sheet' : 'dialog';
|
|
51
|
+
// Out past the end edge: to the right, or to the left in a language written from the right.
|
|
52
|
+
const away = () => (getComputedStyle(sheet).direction === 'rtl' ? '-100%' : '100%');
|
|
53
|
+
const enter = () => {
|
|
54
|
+
const k = kind();
|
|
55
|
+
return k === 'dialog' ? { opacity: [0, 1], scale: [0.98, 1] } : k === 'side-sheet' ? { x: [away(), '0%'] } : { y: ['100%', '0%'] };
|
|
56
|
+
};
|
|
57
|
+
const leave = () => {
|
|
58
|
+
const k = kind();
|
|
59
|
+
return k === 'dialog' ? { opacity: 0, scale: 0.98 } : k === 'side-sheet' ? { x: away() } : { y: '100%' };
|
|
60
|
+
};
|
|
30
61
|
let closing = false;
|
|
62
|
+
// On a phone: its start height and the pull by handle, header and content (not for the editor's sheets).
|
|
63
|
+
const size = modalSheetSize(sheet, panel, () => void close());
|
|
31
64
|
|
|
32
65
|
function open() {
|
|
33
66
|
if (sheet.open) return;
|
|
34
67
|
sheet.showModal();
|
|
35
68
|
document.documentElement.style.overflow = 'hidden';
|
|
69
|
+
size.open();
|
|
36
70
|
if (reduce) return;
|
|
37
|
-
|
|
38
|
-
else animate(panel, { y: ['100%', '0%'] }, motionSpring('spatial'));
|
|
71
|
+
animate(panel, enter(), motionSpring('spatial'));
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
async function close() {
|
|
42
75
|
if (!sheet.open || closing) return;
|
|
43
76
|
closing = true;
|
|
44
|
-
if (!reduce) await animate(panel,
|
|
77
|
+
if (!reduce) await animate(panel, leave(), motionSpring('spatial', 'fast'));
|
|
45
78
|
sheet.close();
|
|
46
79
|
panel.style.transform = '';
|
|
47
80
|
panel.style.opacity = '';
|
|
81
|
+
size.reset();
|
|
48
82
|
document.documentElement.style.overflow = '';
|
|
49
83
|
closing = false;
|
|
50
84
|
}
|
|
@@ -60,10 +94,11 @@ export function bottomSheet(sheet: HTMLDialogElement): BottomSheetControls {
|
|
|
60
94
|
if (e.target === sheet) close();
|
|
61
95
|
});
|
|
62
96
|
|
|
63
|
-
// Drag the handle down to close.
|
|
97
|
+
// Drag the handle down to close (the sheets modalSheetSize does not manage).
|
|
64
98
|
let startY = 0;
|
|
65
99
|
let dragging = false;
|
|
66
100
|
handle?.addEventListener('pointerdown', (e) => {
|
|
101
|
+
if (size.managed()) return;
|
|
67
102
|
dragging = true;
|
|
68
103
|
startY = e.clientY;
|
|
69
104
|
handle.setPointerCapture(e.pointerId);
|
|
@@ -95,8 +130,13 @@ export function standardBottomSheet(sheet: HTMLElement): StandardBottomSheetCont
|
|
|
95
130
|
const handle = sheet.querySelector<HTMLElement>('[data-bottom-sheet-handle]');
|
|
96
131
|
const header = sheet.querySelector<HTMLElement>('[data-bottom-sheet-header]');
|
|
97
132
|
const closeButton = sheet.querySelector<HTMLElement>('[data-bottom-sheet-close]');
|
|
133
|
+
const resizeButton = sheet.querySelector<HTMLElement>('[data-bottom-sheet-resize]');
|
|
134
|
+
const collapseButton = sheet.querySelector<HTMLElement>('[data-bottom-sheet-collapse]');
|
|
98
135
|
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
99
136
|
const state = () => (sheet.dataset.state as BottomSheetState | undefined) ?? 'collapsed';
|
|
137
|
+
// A side sheet from the medium breakpoint (data-wide, ui.css): nothing here acts while it is one.
|
|
138
|
+
const wideQuery = matchMedia(atLeast('medium'));
|
|
139
|
+
const asSideSheet = () => wideQuery.matches && sheet.dataset.wide === 'side-sheet';
|
|
100
140
|
|
|
101
141
|
// How high it is in a state, as ui.css draws it: set, measured and put back before anything is painted.
|
|
102
142
|
function heightIn(s: BottomSheetState) {
|
|
@@ -109,26 +149,33 @@ export function standardBottomSheet(sheet: HTMLElement): StandardBottomSheetCont
|
|
|
109
149
|
sheet.style.height = was.height;
|
|
110
150
|
return px;
|
|
111
151
|
}
|
|
112
|
-
// The states it
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
];
|
|
152
|
+
// The states it allows (data-states, else Material 3's), and the stops they make at the heights measured now.
|
|
153
|
+
const states = (sheet.dataset.states?.split(' ').filter(Boolean) as BottomSheetState[] | undefined) ?? defaultStates(!!closeButton);
|
|
154
|
+
const stops = () =>
|
|
155
|
+
restingStops({ collapsed: heightIn('collapsed'), 'half-expanded': heightIn('half-expanded'), expanded: heightIn('expanded') }, states);
|
|
156
|
+
// The lowest stop that is not 'hidden': where the collapse button goes, and where the resize button's up caret shows.
|
|
157
|
+
const lowest = () => stops().find((stop) => stop.state !== 'hidden')?.state ?? 'collapsed';
|
|
158
|
+
// data-full: expanded, and as high as max-height lets it be (1px of slack).
|
|
159
|
+
function settleFull(s: BottomSheetState) {
|
|
160
|
+
const max = parseFloat(getComputedStyle(sheet).maxHeight);
|
|
161
|
+
sheet.toggleAttribute('data-full', s === 'expanded' && max > 0 && sheet.offsetHeight >= max - 1);
|
|
123
162
|
}
|
|
163
|
+
// The resize button's aria-expanded (and so its caret): true while the sheet is above its lowest stop.
|
|
164
|
+
const settleResize = (s: BottomSheetState) => resizeButton?.setAttribute('aria-expanded', String(s !== 'hidden' && s !== lowest()));
|
|
124
165
|
const shown = (s: BottomSheetState) => {
|
|
125
166
|
sheet.dataset.state = s;
|
|
126
167
|
sheet.style.height = '';
|
|
127
168
|
handle?.setAttribute('aria-expanded', String(s === 'half-expanded' || s === 'expanded'));
|
|
169
|
+
settleResize(s);
|
|
170
|
+
settleFull(s);
|
|
128
171
|
};
|
|
129
172
|
|
|
130
173
|
// From the height it has now (0 when hidden) to the one of `to`, with all of it there while it moves.
|
|
131
174
|
async function set(to: BottomSheetState) {
|
|
175
|
+
if (asSideSheet()) {
|
|
176
|
+
sheet.dataset.state = to;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
132
179
|
const from = state() === 'hidden' ? 0 : sheet.offsetHeight;
|
|
133
180
|
const height = heightIn(to);
|
|
134
181
|
if (reduce || from === height) return shown(to);
|
|
@@ -146,7 +193,7 @@ export function standardBottomSheet(sheet: HTMLElement): StandardBottomSheetCont
|
|
|
146
193
|
let afterDrag = false;
|
|
147
194
|
const down = (e: PointerEvent) => {
|
|
148
195
|
const target = e.target as Element;
|
|
149
|
-
if (e.button !== 0 || (target.closest('button, a') && target.closest('[data-bottom-sheet-handle]') === null)) return;
|
|
196
|
+
if (asSideSheet() || e.button !== 0 || (target.closest('button, a') && target.closest('[data-bottom-sheet-handle]') === null)) return;
|
|
150
197
|
const list = stops();
|
|
151
198
|
drag = {
|
|
152
199
|
y: e.clientY,
|
|
@@ -159,7 +206,7 @@ export function standardBottomSheet(sheet: HTMLElement): StandardBottomSheetCont
|
|
|
159
206
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
160
207
|
};
|
|
161
208
|
const move = (e: PointerEvent) => {
|
|
162
|
-
if (!drag) return;
|
|
209
|
+
if (!drag || asSideSheet()) return;
|
|
163
210
|
const dy = e.clientY - drag.y;
|
|
164
211
|
if (!dragged && Math.abs(dy) < 4) return;
|
|
165
212
|
if (!dragged) {
|
|
@@ -183,14 +230,7 @@ export function standardBottomSheet(sheet: HTMLElement): StandardBottomSheetCont
|
|
|
183
230
|
delete sheet.dataset.moving;
|
|
184
231
|
const height = sheet.offsetHeight;
|
|
185
232
|
const list = stops();
|
|
186
|
-
|
|
187
|
-
const next =
|
|
188
|
-
v > 0.4
|
|
189
|
-
? list.find((s) => s.height > height + 1)
|
|
190
|
-
: v < -0.4
|
|
191
|
-
? list.findLast((s) => s.height < height - 1)
|
|
192
|
-
: list.reduce((a, b) => (Math.abs(b.height - height) < Math.abs(a.height - height) ? b : a));
|
|
193
|
-
set((next ?? list.reduce((a, b) => (Math.abs(b.height - height) < Math.abs(a.height - height) ? b : a))).state);
|
|
233
|
+
set(restAfterDrag(list, height, v));
|
|
194
234
|
};
|
|
195
235
|
for (const el of [handle, header]) {
|
|
196
236
|
el?.addEventListener('pointerdown', down);
|
|
@@ -198,12 +238,57 @@ export function standardBottomSheet(sheet: HTMLElement): StandardBottomSheetCont
|
|
|
198
238
|
el?.addEventListener('pointerup', up);
|
|
199
239
|
el?.addEventListener('pointercancel', up);
|
|
200
240
|
}
|
|
201
|
-
// A tap on the handle (or Enter or Space):
|
|
241
|
+
// A tap on the handle (or Enter or Space): on to the next state; not after it was dragged.
|
|
202
242
|
handle?.addEventListener('click', () => {
|
|
203
|
-
if (afterDrag) return;
|
|
204
|
-
set(
|
|
243
|
+
if (afterDrag || asSideSheet()) return;
|
|
244
|
+
set(nextOnTap(stops(), state()));
|
|
205
245
|
});
|
|
206
|
-
|
|
246
|
+
// The buttons in the header are clicks, not drags (down() leaves them out), like the close button.
|
|
247
|
+
resizeButton?.addEventListener('click', () => asSideSheet() || set(nextOnTap(stops(), state())));
|
|
248
|
+
// The collapse button hides itself with data-full, so focus moves on to the handle (else the resize button, else the
|
|
249
|
+
// sheet) rather than falling to the page.
|
|
250
|
+
collapseButton?.addEventListener('click', async () => {
|
|
251
|
+
if (asSideSheet()) return;
|
|
252
|
+
await set(lowest());
|
|
253
|
+
if (!collapseButton.contains(document.activeElement) && document.activeElement !== document.body) return;
|
|
254
|
+
const next = handle ?? resizeButton ?? sheet;
|
|
255
|
+
if (next === sheet && !sheet.hasAttribute('tabindex')) sheet.tabIndex = -1;
|
|
256
|
+
next.focus();
|
|
257
|
+
});
|
|
258
|
+
closeButton?.addEventListener('click', () => asSideSheet() || set('hidden'));
|
|
259
|
+
// At attach the marks follow the measured stops (the server only guessed), and again when the window is resized
|
|
260
|
+
// (a phone turned while expanded), as the stops move with it; the listener removes itself once the sheet is no longer
|
|
261
|
+
// in the page.
|
|
262
|
+
settleResize(state());
|
|
263
|
+
settleFull(state());
|
|
264
|
+
const onResize = () => {
|
|
265
|
+
if (!sheet.isConnected) {
|
|
266
|
+
removeEventListener('resize', onResize);
|
|
267
|
+
wideQuery.removeEventListener('change', onWide);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (asSideSheet()) return;
|
|
271
|
+
settleResize(state());
|
|
272
|
+
settleFull(state());
|
|
273
|
+
};
|
|
274
|
+
// Becoming a side sheet (or a bottom sheet again): drop a drag in progress and the height it gave the sheet, and on
|
|
275
|
+
// the way back apply the marks as at attach.
|
|
276
|
+
const onWide = () => {
|
|
277
|
+
if (!sheet.isConnected) return onResize();
|
|
278
|
+
drag = undefined;
|
|
279
|
+
dragged = false;
|
|
280
|
+
delete sheet.dataset.moving;
|
|
281
|
+
sheet.style.height = '';
|
|
282
|
+
if (asSideSheet()) {
|
|
283
|
+
sheet.removeAttribute('data-full');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
handle?.setAttribute('aria-expanded', String(state() === 'half-expanded' || state() === 'expanded'));
|
|
287
|
+
settleResize(state());
|
|
288
|
+
settleFull(state());
|
|
289
|
+
};
|
|
290
|
+
addEventListener('resize', onResize);
|
|
291
|
+
wideQuery.addEventListener('change', onWide);
|
|
207
292
|
|
|
208
293
|
const controls = { state, set };
|
|
209
294
|
standardSheets.set(sheet, controls);
|
|
@@ -1,57 +1,73 @@
|
|
|
1
|
-
// What a FAB menu (FabMenu.tsx) does
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
export default function fabMenu() {
|
|
8
|
-
document.querySelectorAll<HTMLElement>('[data-fab-menu]').forEach((root) => {
|
|
9
|
-
const button = root.querySelector<HTMLButtonElement>('.fab-menu-button');
|
|
10
|
-
const list = root.querySelector<HTMLElement>('.fab-menu-list');
|
|
11
|
-
if (!button || !list) return;
|
|
12
|
-
const items = () => [...list.querySelectorAll<HTMLElement>('a[href], button:not(:disabled)')];
|
|
13
|
-
const isOpen = () => button.getAttribute('aria-expanded') === 'true';
|
|
1
|
+
// What a FAB menu (FabMenu.tsx) does: its FAB opens and closes its actions (aria-expanded, the list's hidden), and
|
|
2
|
+
// while they are open its name is the cross's (data-close-label), else its own (data-label). Opened with a key, the
|
|
3
|
+
// focus goes to the first action; the arrow keys go between them, Home and End to the first and the last; Escape
|
|
4
|
+
// closes it and goes back to the FAB, and a choice, a tap outside it or the focus moving out close it. setUpFabMenu does
|
|
5
|
+
// it for one menu (the editor's FabMenu in ui.tsx, which undoes it when the menu goes); the default export for every
|
|
6
|
+
// menu on a site page, run by the theme's Base.astro (blockScripts, packages/editor/site/blockScripts.ts).
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
list!.hidden = !open;
|
|
23
|
-
if (open) document.addEventListener('pointerdown', outside, true);
|
|
24
|
-
else document.removeEventListener('pointerdown', outside, true);
|
|
25
|
-
if (focus === 'button') button!.focus();
|
|
26
|
-
else if (focus) items()[0]?.focus();
|
|
27
|
-
}
|
|
8
|
+
/** Makes one FAB menu (its [data-fab-menu] element) open and close; returns what undoes it (every listener removed). */
|
|
9
|
+
export function setUpFabMenu(root: HTMLElement): () => void {
|
|
10
|
+
const button = root.querySelector<HTMLButtonElement>('.fab-menu-button');
|
|
11
|
+
const list = root.querySelector<HTMLElement>('.fab-menu-list');
|
|
12
|
+
if (!button || !list) return () => {};
|
|
13
|
+
const items = () => [...list.querySelectorAll<HTMLElement>('a[href], button:not(:disabled)')];
|
|
14
|
+
const isOpen = () => button.getAttribute('aria-expanded') === 'true';
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
16
|
+
const outside = (e: PointerEvent) => {
|
|
17
|
+
if (!root.contains(e.target as Node)) set(false);
|
|
18
|
+
};
|
|
19
|
+
function set(open: boolean, focus?: 'first' | 'button') {
|
|
20
|
+
button!.setAttribute('aria-expanded', String(open));
|
|
21
|
+
const name = open ? button!.dataset.closeLabel : button!.dataset.label;
|
|
22
|
+
if (name) button!.setAttribute('aria-label', name);
|
|
23
|
+
list!.hidden = !open;
|
|
24
|
+
if (open) document.addEventListener('pointerdown', outside, true);
|
|
25
|
+
else document.removeEventListener('pointerdown', outside, true);
|
|
26
|
+
if (focus === 'button') button!.focus();
|
|
27
|
+
else if (focus) items()[0]?.focus();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const toggle = (e: MouseEvent) => {
|
|
31
|
+
const open = !isOpen();
|
|
32
|
+
set(open, open && e.detail === 0 ? 'first' : undefined);
|
|
33
|
+
};
|
|
34
|
+
const keys = (e: KeyboardEvent) => {
|
|
35
|
+
if (!isOpen()) return;
|
|
36
|
+
if (e.key === 'Escape') {
|
|
45
37
|
e.preventDefault();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
38
|
+
set(false, 'button');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const all = items();
|
|
42
|
+
const at = all.indexOf(document.activeElement as HTMLElement);
|
|
43
|
+
if (at < 0) return;
|
|
44
|
+
const to = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: all.length - 1 }[e.key];
|
|
45
|
+
if (to === undefined) return;
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
all[(to + all.length) % all.length].focus();
|
|
48
|
+
};
|
|
49
|
+
const choice = (e: MouseEvent) => {
|
|
50
|
+
if ((e.target as HTMLElement).closest('a[href], button')) set(false);
|
|
51
|
+
};
|
|
52
|
+
// The focus moving to something outside it (Tab); a tap outside is `outside`.
|
|
53
|
+
const left = (e: FocusEvent) => {
|
|
54
|
+
const to = e.relatedTarget as Node | null;
|
|
55
|
+
if (isOpen() && to && !root.contains(to)) set(false);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
button.addEventListener('click', toggle);
|
|
59
|
+
root.addEventListener('keydown', keys);
|
|
60
|
+
list.addEventListener('click', choice);
|
|
61
|
+
root.addEventListener('focusout', left);
|
|
62
|
+
return () => {
|
|
63
|
+
button.removeEventListener('click', toggle);
|
|
64
|
+
root.removeEventListener('keydown', keys);
|
|
65
|
+
list.removeEventListener('click', choice);
|
|
66
|
+
root.removeEventListener('focusout', left);
|
|
67
|
+
document.removeEventListener('pointerdown', outside, true);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function fabMenu() {
|
|
72
|
+
document.querySelectorAll<HTMLElement>('[data-fab-menu]').forEach((root) => setUpFabMenu(root));
|
|
57
73
|
}
|