@morphemecms/cms 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +51 -3
- package/astro.d.mts +2 -0
- package/astro.mjs +162 -0
- package/bin/morpheme.mjs +38 -0
- package/client.d.ts +10 -0
- package/defaults/blockTexts.ts +33 -0
- package/defaults/collections.ts +774 -0
- package/defaults/colors.ts +125 -0
- package/defaults/company.ts +20 -0
- package/defaults/editor.ts +178 -0
- package/defaults/internalLinks.ts +30 -0
- package/defaults/opens.ts +18 -0
- package/defaults/popup.tsx +93 -0
- package/defaults/privacy.ts +79 -0
- package/defaults/rules.ts +450 -0
- package/defaults/schema.ts +16 -0
- package/defaults/sitedata.ts +129 -0
- package/defaults/standard.ts +106 -0
- package/defaults/standardPages.ts +87 -0
- package/defaults/texts.ts +481 -0
- package/demo/README.md +26 -0
- package/demo/public/apple-touch-icon.png +0 -0
- package/demo/public/editor/apple-touch-icon.png +0 -0
- package/demo/public/editor/icon-192.png +0 -0
- package/demo/public/editor/icon-512.png +0 -0
- package/demo/public/editor/icon-maskable-512.png +0 -0
- package/demo/public/favicon.ico +0 -0
- package/demo/public/favicon.svg +1 -0
- package/demo/public/images/uploads/2026/10/andromeda.webp +0 -0
- package/demo/public/images/uploads/2026/10/earth-from-space.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-blog.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-colors.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-home.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-page.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-pages.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-seo.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-start.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-themes.webp +0 -0
- package/demo/public/images/uploads/2026/10/moon.webp +0 -0
- package/demo/public/images/uploads/2026/10/saturn.webp +0 -0
- package/demo/public/logo.svg +1 -0
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -0
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -0
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -0
- package/demo/src/content/blog/every-save-is-a-version.md +21 -0
- package/demo/src/content/blog/schedule-a-post.md +17 -0
- package/demo/src/content/blog/secure-from-day-one.md +17 -0
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -0
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -0
- package/demo/src/content/blog/the-app-store.md +21 -0
- package/demo/src/content/pages/about.md +22 -0
- package/demo/src/content/pages/app-store.md +30 -0
- package/demo/src/content/pages/blocks.md +98 -0
- package/demo/src/content/pages/blog-page.md +11 -0
- package/demo/src/content/pages/contact.md +16 -0
- package/demo/src/content/pages/get-started.md +20 -0
- package/demo/src/content/pages/home.md +26 -0
- package/demo/src/content/pages/page-not-found.md +11 -0
- package/demo/src/content/pages/privacy-policy.md +24 -0
- package/demo/src/data/blocks.json +25 -0
- package/demo/src/data/booking.json +6 -0
- package/demo/src/data/categories/apps.json +6 -0
- package/demo/src/data/categories/design.json +6 -0
- package/demo/src/data/categories/editing.json +6 -0
- package/demo/src/data/categories/security.json +6 -0
- package/demo/src/data/categories/seo.json +6 -0
- package/demo/src/data/colors.json +75 -0
- package/demo/src/data/contact.json +8 -0
- package/demo/src/data/cookies.json +21 -0
- package/demo/src/data/footer.json +27 -0
- package/demo/src/data/menu.json +35 -0
- package/demo/src/data/mobile-menu.json +25 -0
- package/demo/src/data/popups/booking-bar.json +13 -0
- package/demo/src/data/popups/try-the-editor.json +16 -0
- package/demo/src/data/standard-pages.json +8 -0
- package/demo/src/data/testimonials/sample-bakery.json +10 -0
- package/demo/src/data/testimonials/sample-clinic.json +10 -0
- package/demo/src/data/testimonials/sample-studio.json +10 -0
- package/demo/src/site/colors.ts +11 -0
- package/demo/src/site/config.ts +19 -0
- package/icons/lucide/LICENSE +43 -0
- package/icons/lucide/LICENSE-material-design-icons +20 -0
- package/icons/lucide/address-book.svg +1 -0
- package/icons/lucide/app-window.svg +1 -0
- package/icons/lucide/arrow-counter-clockwise.svg +1 -0
- package/icons/lucide/arrow-down.svg +1 -0
- package/icons/lucide/arrow-left.svg +1 -0
- package/icons/lucide/arrow-right.svg +1 -0
- package/icons/lucide/arrow-square-out.svg +1 -0
- package/icons/lucide/arrow-u-up-left.svg +1 -0
- package/icons/lucide/arrow-u-up-right.svg +1 -0
- package/icons/lucide/arrow-up-right.svg +1 -0
- package/icons/lucide/arrow-up.svg +1 -0
- package/icons/lucide/arrows-down-up.svg +1 -0
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -0
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -0
- package/icons/lucide/article.svg +1 -0
- package/icons/lucide/bank.svg +1 -0
- package/icons/lucide/bell.svg +1 -0
- package/icons/lucide/bicycle.svg +1 -0
- package/icons/lucide/binoculars.svg +1 -0
- package/icons/lucide/book-open-text.svg +1 -0
- package/icons/lucide/book-open.svg +1 -0
- package/icons/lucide/browser.svg +1 -0
- package/icons/lucide/buildings.svg +1 -0
- package/icons/lucide/bus.svg +1 -0
- package/icons/lucide/calendar-blank.svg +1 -0
- package/icons/lucide/calendar-check.svg +1 -0
- package/icons/lucide/car.svg +1 -0
- package/icons/lucide/cards.svg +1 -0
- package/icons/lucide/caret-down.svg +1 -0
- package/icons/lucide/caret-left.svg +1 -0
- package/icons/lucide/caret-right.svg +1 -0
- package/icons/lucide/caret-up.svg +1 -0
- package/icons/lucide/certificate.svg +1 -0
- package/icons/lucide/chart-line-up.svg +1 -0
- package/icons/lucide/chats.svg +1 -0
- package/icons/lucide/check-circle.svg +1 -0
- package/icons/lucide/check.svg +1 -0
- package/icons/lucide/circle-half.svg +1 -0
- package/icons/lucide/circle.svg +1 -0
- package/icons/lucide/clock-counter-clockwise.svg +1 -0
- package/icons/lucide/clock.svg +1 -0
- package/icons/lucide/code.svg +1 -0
- package/icons/lucide/coffee.svg +1 -0
- package/icons/lucide/columns.svg +1 -0
- package/icons/lucide/cookie.svg +1 -0
- package/icons/lucide/copy.svg +1 -0
- package/icons/lucide/credit-card.svg +1 -0
- package/icons/lucide/currency-circle-dollar.svg +1 -0
- package/icons/lucide/cursor-click.svg +1 -0
- package/icons/lucide/database.svg +1 -0
- package/icons/lucide/desktop.svg +1 -0
- package/icons/lucide/device-mobile.svg +1 -0
- package/icons/lucide/door-open.svg +1 -0
- package/icons/lucide/download-simple.svg +1 -0
- package/icons/lucide/envelope-simple.svg +1 -0
- package/icons/lucide/eye-slash.svg +1 -0
- package/icons/lucide/eye.svg +1 -0
- package/icons/lucide/file-pdf.svg +1 -0
- package/icons/lucide/file-text.svg +1 -0
- package/icons/lucide/file-video.svg +1 -0
- package/icons/lucide/file-zip.svg +1 -0
- package/icons/lucide/flower-lotus.svg +1 -0
- package/icons/lucide/folder-simple.svg +1 -0
- package/icons/lucide/funnel.svg +1 -0
- package/icons/lucide/gauge.svg +1 -0
- package/icons/lucide/gear.svg +1 -0
- package/icons/lucide/github-logo.svg +1 -0
- package/icons/lucide/globe.svg +1 -0
- package/icons/lucide/google-logo.svg +1 -0
- package/icons/lucide/graduation-cap.svg +1 -0
- package/icons/lucide/hammer.svg +1 -0
- package/icons/lucide/hand-heart.svg +1 -0
- package/icons/lucide/handshake.svg +1 -0
- package/icons/lucide/hard-drives.svg +1 -0
- package/icons/lucide/heart.svg +1 -0
- package/icons/lucide/house.svg +1 -0
- package/icons/lucide/image-square.svg +1 -0
- package/icons/lucide/image.svg +1 -0
- package/icons/lucide/info.svg +1 -0
- package/icons/lucide/keyboard.svg +1 -0
- package/icons/lucide/leaf.svg +1 -0
- package/icons/lucide/lifebuoy.svg +1 -0
- package/icons/lucide/lightbulb.svg +1 -0
- package/icons/lucide/link-break.svg +1 -0
- package/icons/lucide/link-simple.svg +1 -0
- package/icons/lucide/list-bullets.svg +1 -0
- package/icons/lucide/list-checks.svg +1 -0
- package/icons/lucide/list-numbers.svg +1 -0
- package/icons/lucide/list.svg +1 -0
- package/icons/lucide/lock-simple-open.svg +1 -0
- package/icons/lucide/lock-simple.svg +1 -0
- package/icons/lucide/magnifying-glass.svg +1 -0
- package/icons/lucide/map-pin.svg +1 -0
- package/icons/lucide/map-trifold.svg +1 -0
- package/icons/lucide/medal.svg +1 -0
- package/icons/lucide/megaphone.svg +1 -0
- package/icons/lucide/minus.svg +1 -0
- package/icons/lucide/moon.svg +1 -0
- package/icons/lucide/newspaper.svg +1 -0
- package/icons/lucide/pack.json +23 -0
- package/icons/lucide/paint-brush-broad.svg +1 -0
- package/icons/lucide/palette.svg +1 -0
- package/icons/lucide/paper-plane-tilt.svg +1 -0
- package/icons/lucide/pencil-simple.svg +1 -0
- package/icons/lucide/phone.svg +1 -0
- package/icons/lucide/play-fill.svg +1 -0
- package/icons/lucide/plus.svg +1 -0
- package/icons/lucide/prohibit.svg +1 -0
- package/icons/lucide/puzzle-piece.svg +1 -0
- package/icons/lucide/question.svg +1 -0
- package/icons/lucide/quotes.svg +1 -0
- package/icons/lucide/rows.svg +1 -0
- package/icons/lucide/ruler.svg +1 -0
- package/icons/lucide/scan.svg +1 -0
- package/icons/lucide/shapes.svg +1 -0
- package/icons/lucide/share-network.svg +1 -0
- package/icons/lucide/shield-check.svg +1 -0
- package/icons/lucide/sidebar-simple.svg +1 -0
- package/icons/lucide/sign-out.svg +1 -0
- package/icons/lucide/signpost.svg +1 -0
- package/icons/lucide/sliders-horizontal.svg +1 -0
- package/icons/lucide/smiley.svg +1 -0
- package/icons/lucide/sparkle.svg +1 -0
- package/icons/lucide/square-half-bottom.svg +1 -0
- package/icons/lucide/square.svg +1 -0
- package/icons/lucide/squares-four.svg +1 -0
- package/icons/lucide/sticker.svg +1 -0
- package/icons/lucide/storefront.svg +1 -0
- package/icons/lucide/sun.svg +1 -0
- package/icons/lucide/swatches.svg +1 -0
- package/icons/lucide/tag.svg +1 -0
- package/icons/lucide/text-aa.svg +1 -0
- package/icons/lucide/text-align-left.svg +1 -0
- package/icons/lucide/text-b.svg +1 -0
- package/icons/lucide/text-h-one.svg +1 -0
- package/icons/lucide/text-h-three.svg +1 -0
- package/icons/lucide/text-h-two.svg +1 -0
- package/icons/lucide/text-italic.svg +1 -0
- package/icons/lucide/text-t.svg +1 -0
- package/icons/lucide/ticket.svg +1 -0
- package/icons/lucide/toggle-right.svg +1 -0
- package/icons/lucide/train.svg +1 -0
- package/icons/lucide/trash.svg +1 -0
- package/icons/lucide/upload-simple.svg +1 -0
- package/icons/lucide/user-circle.svg +1 -0
- package/icons/lucide/user.svg +1 -0
- package/icons/lucide/users-three.svg +1 -0
- package/icons/lucide/users.svg +1 -0
- package/icons/lucide/video-camera.svg +1 -0
- package/icons/lucide/warning-circle.svg +1 -0
- package/icons/lucide/warning.svg +1 -0
- package/icons/lucide/wave-sine.svg +1 -0
- package/icons/lucide/wheelchair.svg +1 -0
- package/icons/lucide/wrench.svg +1 -0
- package/icons/lucide/x.svg +1 -0
- package/icons/material-symbols/LICENSE +201 -0
- package/icons/material-symbols/LICENSE-material-design-icons +20 -0
- package/icons/material-symbols/address-book.svg +1 -0
- package/icons/material-symbols/app-window.svg +1 -0
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -0
- package/icons/material-symbols/arrow-down.svg +1 -0
- package/icons/material-symbols/arrow-left.svg +1 -0
- package/icons/material-symbols/arrow-right.svg +1 -0
- package/icons/material-symbols/arrow-square-out.svg +1 -0
- package/icons/material-symbols/arrow-u-up-left.svg +1 -0
- package/icons/material-symbols/arrow-u-up-right.svg +1 -0
- package/icons/material-symbols/arrow-up-right.svg +1 -0
- package/icons/material-symbols/arrow-up.svg +1 -0
- package/icons/material-symbols/arrows-down-up.svg +1 -0
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -0
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -0
- package/icons/material-symbols/article.svg +1 -0
- package/icons/material-symbols/bank.svg +1 -0
- package/icons/material-symbols/bell.svg +1 -0
- package/icons/material-symbols/bicycle.svg +1 -0
- package/icons/material-symbols/binoculars.svg +1 -0
- package/icons/material-symbols/book-open-text.svg +1 -0
- package/icons/material-symbols/book-open.svg +1 -0
- package/icons/material-symbols/browser.svg +1 -0
- package/icons/material-symbols/buildings.svg +1 -0
- package/icons/material-symbols/bus.svg +1 -0
- package/icons/material-symbols/calendar-blank.svg +1 -0
- package/icons/material-symbols/calendar-check.svg +1 -0
- package/icons/material-symbols/car.svg +1 -0
- package/icons/material-symbols/cards.svg +1 -0
- package/icons/material-symbols/caret-down.svg +1 -0
- package/icons/material-symbols/caret-left.svg +1 -0
- package/icons/material-symbols/caret-right.svg +1 -0
- package/icons/material-symbols/caret-up.svg +1 -0
- package/icons/material-symbols/certificate.svg +1 -0
- package/icons/material-symbols/chart-line-up.svg +1 -0
- package/icons/material-symbols/chats.svg +1 -0
- package/icons/material-symbols/check-circle.svg +1 -0
- package/icons/material-symbols/check.svg +1 -0
- package/icons/material-symbols/circle-half.svg +1 -0
- package/icons/material-symbols/circle.svg +1 -0
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -0
- package/icons/material-symbols/clock.svg +1 -0
- package/icons/material-symbols/code.svg +1 -0
- package/icons/material-symbols/coffee.svg +1 -0
- package/icons/material-symbols/columns.svg +1 -0
- package/icons/material-symbols/cookie.svg +1 -0
- package/icons/material-symbols/copy.svg +1 -0
- package/icons/material-symbols/credit-card.svg +1 -0
- package/icons/material-symbols/currency-circle-dollar.svg +1 -0
- package/icons/material-symbols/cursor-click.svg +1 -0
- package/icons/material-symbols/database.svg +1 -0
- package/icons/material-symbols/desktop.svg +1 -0
- package/icons/material-symbols/device-mobile.svg +1 -0
- package/icons/material-symbols/door-open.svg +1 -0
- package/icons/material-symbols/download-simple.svg +1 -0
- package/icons/material-symbols/envelope-simple.svg +1 -0
- package/icons/material-symbols/eye-slash.svg +1 -0
- package/icons/material-symbols/eye.svg +1 -0
- package/icons/material-symbols/file-pdf.svg +1 -0
- package/icons/material-symbols/file-text.svg +1 -0
- package/icons/material-symbols/file-video.svg +1 -0
- package/icons/material-symbols/file-zip.svg +1 -0
- package/icons/material-symbols/flower-lotus.svg +1 -0
- package/icons/material-symbols/folder-simple.svg +1 -0
- package/icons/material-symbols/funnel.svg +1 -0
- package/icons/material-symbols/gauge.svg +1 -0
- package/icons/material-symbols/gear.svg +1 -0
- package/icons/material-symbols/github-logo.svg +1 -0
- package/icons/material-symbols/globe.svg +1 -0
- package/icons/material-symbols/google-logo.svg +1 -0
- package/icons/material-symbols/graduation-cap.svg +1 -0
- package/icons/material-symbols/hammer.svg +1 -0
- package/icons/material-symbols/hand-heart.svg +1 -0
- package/icons/material-symbols/handshake.svg +1 -0
- package/icons/material-symbols/hard-drives.svg +1 -0
- package/icons/material-symbols/heart.svg +1 -0
- package/icons/material-symbols/house.svg +1 -0
- package/icons/material-symbols/image-square.svg +1 -0
- package/icons/material-symbols/image.svg +1 -0
- package/icons/material-symbols/info.svg +1 -0
- package/icons/material-symbols/keyboard.svg +1 -0
- package/icons/material-symbols/leaf.svg +1 -0
- package/icons/material-symbols/lifebuoy.svg +1 -0
- package/icons/material-symbols/lightbulb.svg +1 -0
- package/icons/material-symbols/link-break.svg +1 -0
- package/icons/material-symbols/link-simple.svg +1 -0
- package/icons/material-symbols/list-bullets.svg +1 -0
- package/icons/material-symbols/list-checks.svg +1 -0
- package/icons/material-symbols/list-numbers.svg +1 -0
- package/icons/material-symbols/list.svg +1 -0
- package/icons/material-symbols/lock-simple-open.svg +1 -0
- package/icons/material-symbols/lock-simple.svg +1 -0
- package/icons/material-symbols/magnifying-glass.svg +1 -0
- package/icons/material-symbols/map-pin.svg +1 -0
- package/icons/material-symbols/map-trifold.svg +1 -0
- package/icons/material-symbols/medal.svg +1 -0
- package/icons/material-symbols/megaphone.svg +1 -0
- package/icons/material-symbols/minus.svg +1 -0
- package/icons/material-symbols/moon.svg +1 -0
- package/icons/material-symbols/newspaper.svg +1 -0
- package/icons/material-symbols/pack.json +29 -0
- package/icons/material-symbols/paint-brush-broad.svg +1 -0
- package/icons/material-symbols/palette.svg +1 -0
- package/icons/material-symbols/paper-plane-tilt.svg +1 -0
- package/icons/material-symbols/pencil-simple.svg +1 -0
- package/icons/material-symbols/phone.svg +1 -0
- package/icons/material-symbols/play-fill.svg +1 -0
- package/icons/material-symbols/plus.svg +1 -0
- package/icons/material-symbols/prohibit.svg +1 -0
- package/icons/material-symbols/puzzle-piece.svg +1 -0
- package/icons/material-symbols/question.svg +1 -0
- package/icons/material-symbols/quotes.svg +1 -0
- package/icons/material-symbols/rows.svg +1 -0
- package/icons/material-symbols/ruler.svg +1 -0
- package/icons/material-symbols/scan.svg +1 -0
- package/icons/material-symbols/shapes.svg +1 -0
- package/icons/material-symbols/share-network.svg +1 -0
- package/icons/material-symbols/shield-check.svg +1 -0
- package/icons/material-symbols/sidebar-simple.svg +1 -0
- package/icons/material-symbols/sign-out.svg +1 -0
- package/icons/material-symbols/signpost.svg +1 -0
- package/icons/material-symbols/sliders-horizontal.svg +1 -0
- package/icons/material-symbols/smiley.svg +1 -0
- package/icons/material-symbols/sparkle.svg +1 -0
- package/icons/material-symbols/square-half-bottom.svg +1 -0
- package/icons/material-symbols/square.svg +1 -0
- package/icons/material-symbols/squares-four.svg +1 -0
- package/icons/material-symbols/sticker.svg +1 -0
- package/icons/material-symbols/storefront.svg +1 -0
- package/icons/material-symbols/sun.svg +1 -0
- package/icons/material-symbols/swatches.svg +1 -0
- package/icons/material-symbols/tag.svg +1 -0
- package/icons/material-symbols/text-aa.svg +1 -0
- package/icons/material-symbols/text-align-left.svg +1 -0
- package/icons/material-symbols/text-b.svg +1 -0
- package/icons/material-symbols/text-h-one.svg +1 -0
- package/icons/material-symbols/text-h-three.svg +1 -0
- package/icons/material-symbols/text-h-two.svg +1 -0
- package/icons/material-symbols/text-italic.svg +1 -0
- package/icons/material-symbols/text-t.svg +1 -0
- package/icons/material-symbols/ticket.svg +1 -0
- package/icons/material-symbols/toggle-right.svg +1 -0
- package/icons/material-symbols/train.svg +1 -0
- package/icons/material-symbols/trash.svg +1 -0
- package/icons/material-symbols/upload-simple.svg +1 -0
- package/icons/material-symbols/user-circle.svg +1 -0
- package/icons/material-symbols/user.svg +1 -0
- package/icons/material-symbols/users-three.svg +1 -0
- package/icons/material-symbols/users.svg +1 -0
- package/icons/material-symbols/video-camera.svg +1 -0
- package/icons/material-symbols/warning-circle.svg +1 -0
- package/icons/material-symbols/warning.svg +1 -0
- package/icons/material-symbols/wave-sine.svg +1 -0
- package/icons/material-symbols/wheelchair.svg +1 -0
- package/icons/material-symbols/wrench.svg +1 -0
- package/icons/material-symbols/x.svg +1 -0
- package/icons/phosphor/LICENSE +21 -0
- package/icons/phosphor/address-book.svg +1 -0
- package/icons/phosphor/app-window.svg +1 -0
- package/icons/phosphor/arrow-counter-clockwise.svg +1 -0
- package/icons/phosphor/arrow-down.svg +1 -0
- package/icons/phosphor/arrow-left.svg +1 -0
- package/icons/phosphor/arrow-right.svg +1 -0
- package/icons/phosphor/arrow-square-out.svg +1 -0
- package/icons/phosphor/arrow-u-up-left.svg +1 -0
- package/icons/phosphor/arrow-u-up-right.svg +1 -0
- package/icons/phosphor/arrow-up-right.svg +1 -0
- package/icons/phosphor/arrow-up.svg +1 -0
- package/icons/phosphor/arrows-down-up.svg +1 -0
- package/icons/phosphor/arrows-in-line-horizontal.svg +1 -0
- package/icons/phosphor/arrows-out-line-horizontal.svg +1 -0
- package/icons/phosphor/article.svg +1 -0
- package/icons/phosphor/bank.svg +1 -0
- package/icons/phosphor/bell.svg +1 -0
- package/icons/phosphor/bicycle.svg +1 -0
- package/icons/phosphor/binoculars.svg +1 -0
- package/icons/phosphor/book-open-text.svg +1 -0
- package/icons/phosphor/book-open.svg +1 -0
- package/icons/phosphor/browser.svg +1 -0
- package/icons/phosphor/buildings.svg +1 -0
- package/icons/phosphor/bus.svg +1 -0
- package/icons/phosphor/calendar-blank.svg +1 -0
- package/icons/phosphor/calendar-check.svg +1 -0
- package/icons/phosphor/car.svg +1 -0
- package/icons/phosphor/cards.svg +1 -0
- package/icons/phosphor/caret-down.svg +1 -0
- package/icons/phosphor/caret-left.svg +1 -0
- package/icons/phosphor/caret-right.svg +1 -0
- package/icons/phosphor/caret-up.svg +1 -0
- package/icons/phosphor/certificate.svg +1 -0
- package/icons/phosphor/chart-line-up.svg +1 -0
- package/icons/phosphor/chats.svg +1 -0
- package/icons/phosphor/check-circle.svg +1 -0
- package/icons/phosphor/check.svg +1 -0
- package/icons/phosphor/circle-half.svg +1 -0
- package/icons/phosphor/circle.svg +1 -0
- package/icons/phosphor/clock-counter-clockwise.svg +1 -0
- package/icons/phosphor/clock.svg +1 -0
- package/icons/phosphor/code.svg +1 -0
- package/icons/phosphor/coffee.svg +1 -0
- package/icons/phosphor/columns.svg +1 -0
- package/icons/phosphor/cookie.svg +1 -0
- package/icons/phosphor/copy.svg +1 -0
- package/icons/phosphor/credit-card.svg +1 -0
- package/icons/phosphor/currency-circle-dollar.svg +1 -0
- package/icons/phosphor/cursor-click.svg +1 -0
- package/icons/phosphor/database.svg +1 -0
- package/icons/phosphor/desktop.svg +1 -0
- package/icons/phosphor/device-mobile.svg +1 -0
- package/icons/phosphor/door-open.svg +1 -0
- package/icons/phosphor/download-simple.svg +1 -0
- package/icons/phosphor/envelope-simple.svg +1 -0
- package/icons/phosphor/eye-slash.svg +1 -0
- package/icons/phosphor/eye.svg +1 -0
- package/icons/phosphor/file-pdf.svg +1 -0
- package/icons/phosphor/file-text.svg +1 -0
- package/icons/phosphor/file-video.svg +1 -0
- package/icons/phosphor/file-zip.svg +1 -0
- package/icons/phosphor/flower-lotus.svg +1 -0
- package/icons/phosphor/folder-simple.svg +1 -0
- package/icons/phosphor/funnel.svg +1 -0
- package/icons/phosphor/gauge.svg +1 -0
- package/icons/phosphor/gear.svg +1 -0
- package/icons/phosphor/github-logo.svg +1 -0
- package/icons/phosphor/globe.svg +1 -0
- package/icons/phosphor/google-logo.svg +1 -0
- package/icons/phosphor/graduation-cap.svg +1 -0
- package/icons/phosphor/hammer.svg +1 -0
- package/icons/phosphor/hand-heart.svg +1 -0
- package/icons/phosphor/handshake.svg +1 -0
- package/icons/phosphor/hard-drives.svg +1 -0
- package/icons/phosphor/heart.svg +1 -0
- package/icons/phosphor/house.svg +1 -0
- package/icons/phosphor/image-square.svg +1 -0
- package/icons/phosphor/image.svg +1 -0
- package/icons/phosphor/info.svg +1 -0
- package/icons/phosphor/keyboard.svg +1 -0
- package/icons/phosphor/leaf.svg +1 -0
- package/icons/phosphor/lifebuoy.svg +1 -0
- package/icons/phosphor/lightbulb.svg +1 -0
- package/icons/phosphor/link-break.svg +1 -0
- package/icons/phosphor/link-simple.svg +1 -0
- package/icons/phosphor/list-bullets.svg +1 -0
- package/icons/phosphor/list-checks.svg +1 -0
- package/icons/phosphor/list-numbers.svg +1 -0
- package/icons/phosphor/list.svg +1 -0
- package/icons/phosphor/lock-simple-open.svg +1 -0
- package/icons/phosphor/lock-simple.svg +1 -0
- package/icons/phosphor/magnifying-glass.svg +1 -0
- package/icons/phosphor/map-pin.svg +1 -0
- package/icons/phosphor/map-trifold.svg +1 -0
- package/icons/phosphor/medal.svg +1 -0
- package/icons/phosphor/megaphone.svg +1 -0
- package/icons/phosphor/minus.svg +1 -0
- package/icons/phosphor/moon.svg +1 -0
- package/icons/phosphor/newspaper.svg +1 -0
- package/icons/phosphor/pack.json +8 -0
- package/icons/phosphor/paint-brush-broad.svg +1 -0
- package/icons/phosphor/palette.svg +1 -0
- package/icons/phosphor/paper-plane-tilt.svg +1 -0
- package/icons/phosphor/pencil-simple.svg +1 -0
- package/icons/phosphor/phone.svg +1 -0
- package/icons/phosphor/play-fill.svg +1 -0
- package/icons/phosphor/plus.svg +1 -0
- package/icons/phosphor/prohibit.svg +1 -0
- package/icons/phosphor/puzzle-piece.svg +1 -0
- package/icons/phosphor/question.svg +1 -0
- package/icons/phosphor/quotes.svg +1 -0
- package/icons/phosphor/rows.svg +1 -0
- package/icons/phosphor/ruler.svg +1 -0
- package/icons/phosphor/scan.svg +1 -0
- package/icons/phosphor/shapes.svg +1 -0
- package/icons/phosphor/share-network.svg +1 -0
- package/icons/phosphor/shield-check.svg +1 -0
- package/icons/phosphor/sidebar-simple.svg +1 -0
- package/icons/phosphor/sign-out.svg +1 -0
- package/icons/phosphor/signpost.svg +1 -0
- package/icons/phosphor/sliders-horizontal.svg +1 -0
- package/icons/phosphor/smiley.svg +1 -0
- package/icons/phosphor/sparkle.svg +1 -0
- package/icons/phosphor/square-half-bottom.svg +1 -0
- package/icons/phosphor/square.svg +1 -0
- package/icons/phosphor/squares-four.svg +1 -0
- package/icons/phosphor/sticker.svg +1 -0
- package/icons/phosphor/storefront.svg +1 -0
- package/icons/phosphor/sun.svg +1 -0
- package/icons/phosphor/swatches.svg +1 -0
- package/icons/phosphor/tag.svg +1 -0
- package/icons/phosphor/text-aa.svg +1 -0
- package/icons/phosphor/text-align-left.svg +1 -0
- package/icons/phosphor/text-b.svg +1 -0
- package/icons/phosphor/text-h-one.svg +1 -0
- package/icons/phosphor/text-h-three.svg +1 -0
- package/icons/phosphor/text-h-two.svg +1 -0
- package/icons/phosphor/text-italic.svg +1 -0
- package/icons/phosphor/text-t.svg +1 -0
- package/icons/phosphor/ticket.svg +1 -0
- package/icons/phosphor/toggle-right.svg +1 -0
- package/icons/phosphor/train.svg +1 -0
- package/icons/phosphor/trash.svg +1 -0
- package/icons/phosphor/upload-simple.svg +1 -0
- package/icons/phosphor/user-circle.svg +1 -0
- package/icons/phosphor/user.svg +1 -0
- package/icons/phosphor/users-three.svg +1 -0
- package/icons/phosphor/users.svg +1 -0
- package/icons/phosphor/video-camera.svg +1 -0
- package/icons/phosphor/warning-circle.svg +1 -0
- package/icons/phosphor/warning.svg +1 -0
- package/icons/phosphor/wave-sine.svg +1 -0
- package/icons/phosphor/wheelchair.svg +1 -0
- package/icons/phosphor/wrench.svg +1 -0
- package/icons/phosphor/x.svg +1 -0
- package/lib/apps.d.mts +28 -0
- package/lib/apps.mjs +139 -0
- package/lib/blocks.d.mts +23 -0
- package/lib/blocks.mjs +90 -0
- package/lib/colors.ts +86 -0
- package/lib/contactBar.ts +18 -0
- package/lib/cookieCategories.d.mts +17 -0
- package/lib/cookieCategories.mjs +28 -0
- package/lib/crop.d.mts +34 -0
- package/lib/crop.mjs +83 -0
- package/lib/csp.d.mts +22 -0
- package/lib/csp.mjs +130 -0
- package/lib/cspSources.d.mts +24 -0
- package/lib/cspSources.mjs +60 -0
- package/lib/dynamic-colors.ts +426 -0
- package/lib/git.d.mts +24 -0
- package/lib/git.mjs +64 -0
- package/lib/hooks.ts +51 -0
- package/lib/inline.ts +20 -0
- package/lib/license-texts/Apache-2.0.txt +191 -0
- package/lib/license-texts/BSD-2-Clause.txt +24 -0
- package/lib/license-texts/BSD-3-Clause.txt +28 -0
- package/lib/license-texts/ISC.txt +15 -0
- package/lib/license-texts/MIT.txt +21 -0
- package/lib/licenses.d.mts +190 -0
- package/lib/licenses.mjs +683 -0
- package/lib/markdown.ts +42 -0
- package/lib/motion-schemes.d.mts +2 -0
- package/lib/motion-schemes.mjs +5 -0
- package/lib/motion.ts +131 -0
- package/lib/notice.d.mts +3 -0
- package/lib/notice.mjs +34 -0
- package/lib/scheduled.d.mts +15 -0
- package/lib/scheduled.mjs +37 -0
- package/lib/snackbar.ts +149 -0
- package/lib/status.d.mts +7 -0
- package/lib/status.mjs +18 -0
- package/lib/svg.d.mts +22 -0
- package/lib/svg.mjs +231 -0
- package/lib/text-colors.d.mts +11 -0
- package/lib/text-colors.mjs +43 -0
- package/lib/video-id.d.mts +14 -0
- package/lib/video-id.mjs +72 -0
- package/lib/video.d.mts +20 -0
- package/lib/video.mjs +91 -0
- package/package.json +131 -6
- package/pages/404.astro +10 -0
- package/pages/[...slug].astro +51 -0
- package/pages/_headers.txt +31 -0
- package/pages/blog/index.astro +10 -0
- package/pages/blog/kategori/[category].astro +18 -0
- package/pages/editor/code-images.json.js +25 -0
- package/pages/editor/index.astro +75 -0
- package/pages/editor/manifest.webmanifest.js +23 -0
- package/pages/editor/sw.js +33 -0
- package/pages/editor/sw.js.js +4 -0
- package/pages/editor/thumbnails.json.js +21 -0
- package/pages/editor/version.json.js +22 -0
- package/pages/headers.js +4 -0
- package/pages/index.astro +10 -0
- package/pages/ny.astro +6 -0
- package/pages/robots.txt.js +3 -0
- package/pages/rss.xml.js +20 -0
- package/scripts/app-screenshots.mjs +164 -0
- package/scripts/app-workers.mjs +6 -0
- package/scripts/bin.mjs +12 -0
- package/scripts/built-site.mjs +99 -0
- package/scripts/check-consent.mjs +113 -0
- package/scripts/check-content.mjs +149 -0
- package/scripts/check-demo.mjs +24 -0
- package/scripts/check-themes.mjs +45 -0
- package/scripts/check-worker.mjs +569 -0
- package/scripts/compare-html.mjs +81 -0
- package/scripts/demo-site.mjs +42 -0
- package/scripts/editor-icons.mjs +56 -0
- package/scripts/editor-roundtrip.mjs +134 -0
- package/scripts/editor-screens.mjs +101 -0
- package/scripts/git-env.mjs +9 -0
- package/scripts/icon-packs.mjs +151 -0
- package/scripts/link-counts.mjs +22 -0
- package/scripts/page-colors.mjs +42 -0
- package/scripts/releases.d.mts +10 -0
- package/scripts/releases.mjs +80 -0
- package/scripts/responsive-images.mjs +94 -0
- package/scripts/scheduled-list.mjs +15 -0
- package/scripts/theme-screenshots.mjs +88 -0
- package/scripts/types.mjs +21 -0
- package/scripts/video-thumbnails.mjs +89 -0
- package/scripts/worker-modules.mjs +34 -0
- package/site/appCookies.d.mts +9 -0
- package/site/appCookies.mjs +74 -0
- package/site/apps.ts +21 -0
- package/site/autolinks.d.mts +11 -0
- package/site/autolinks.mjs +200 -0
- package/site/blockScripts.ts +31 -0
- package/site/categories.ts +16 -0
- package/site/consent.ts +26 -0
- package/site/content.config.ts +84 -0
- package/site/content.ts +104 -0
- package/site/cookieList.ts +17 -0
- package/site/defineSite.d.mts +142 -0
- package/site/defineSite.mjs +136 -0
- package/site/directives/interact.d.mts +2 -0
- package/site/directives/interact.mjs +31 -0
- package/site/editor-version.d.mts +1 -0
- package/site/editor-version.mjs +26 -0
- package/site/fill.ts +13 -0
- package/site/format.ts +11 -0
- package/site/home.ts +66 -0
- package/site/iconPacks.d.mts +20 -0
- package/site/iconPacks.mjs +43 -0
- package/site/icons.d.mts +693 -0
- package/site/icons.mjs +605 -0
- package/site/images.d.mts +43 -0
- package/site/images.mjs +157 -0
- package/site/motion.ts +13 -0
- package/site/noindex.d.mts +6 -0
- package/site/noindex.mjs +21 -0
- package/site/pageColors.ts +110 -0
- package/site/popups.ts +98 -0
- package/site/schema.ts +189 -0
- package/site/sections.ts +3 -0
- package/site/seo.ts +28 -0
- package/site/site.ts +76 -0
- package/site/siteConfig.d.mts +1 -0
- package/site/siteConfig.mjs +8 -0
- package/site/siteCsp.d.mts +10 -0
- package/site/siteCsp.mjs +75 -0
- package/site/siteModules.d.mts +7 -0
- package/site/siteModules.mjs +27 -0
- package/site/standardPages.ts +59 -0
- package/site/testimonials.ts +48 -0
- package/site/text.ts +43 -0
- package/site/theme.d.mts +83 -0
- package/site/theme.mjs +472 -0
- package/site/tracking.client.ts +225 -0
- package/site/tracking.ts +9 -0
- package/src/ActionParts.tsx +205 -0
- package/src/Advanced.tsx +152 -0
- package/src/App.tsx +904 -0
- package/src/AppList.tsx +1345 -0
- package/src/BlockDialog.tsx +360 -0
- package/src/BlockParts.tsx +652 -0
- package/src/BlockSettings.tsx +533 -0
- package/src/BrandForm.tsx +164 -0
- package/src/Colors.tsx +476 -0
- package/src/CommunicationParts.tsx +199 -0
- package/src/CompanyForm.tsx +209 -0
- package/src/Consents.tsx +175 -0
- package/src/ContainmentParts.tsx +185 -0
- package/src/Crop.tsx +152 -0
- package/src/DataForm.tsx +434 -0
- package/src/Deleted.tsx +177 -0
- package/src/EntryForm.tsx +740 -0
- package/src/FileView.tsx +30 -0
- package/src/FormFrame.tsx +237 -0
- package/src/Keywords.tsx +225 -0
- package/src/Library.tsx +1563 -0
- package/src/Licenses.tsx +336 -0
- package/src/LinkSheet.tsx +138 -0
- package/src/MarkdownEditor.tsx +1075 -0
- package/src/Media.tsx +677 -0
- package/src/NavigationParts.tsx +350 -0
- package/src/NavigationRestParts.tsx +247 -0
- package/src/Notifications.tsx +59 -0
- package/src/PagePicker.tsx +136 -0
- package/src/Parts.tsx +991 -0
- package/src/PartsShown.tsx +168 -0
- package/src/PickerParts.tsx +183 -0
- package/src/Redirects.tsx +450 -0
- package/src/Screen.tsx +184 -0
- package/src/SectionsField.tsx +65 -0
- package/src/SelectionParts.tsx +134 -0
- package/src/SeoScores.tsx +184 -0
- package/src/Sheet.tsx +35 -0
- package/src/ThemeStore.tsx +376 -0
- package/src/Tokens.tsx +346 -0
- package/src/Versions.tsx +214 -0
- package/src/addresses.ts +75 -0
- package/src/allIcons.ts +7 -0
- package/src/appFiles.ts +29 -0
- package/src/apps.ts +281 -0
- package/src/ask.tsx +123 -0
- package/src/blocks.ts +251 -0
- package/src/brand.ts +153 -0
- package/src/cache.ts +42 -0
- package/src/check.ts +285 -0
- package/src/checkSite.ts +256 -0
- package/src/consents.ts +26 -0
- package/src/cookieList.ts +70 -0
- package/src/countries.ts +59 -0
- package/src/deploy.ts +155 -0
- package/src/editor.css +602 -0
- package/src/examples.ts +85 -0
- package/src/fields.tsx +942 -0
- package/src/frontmatter.d.mts +6 -0
- package/src/frontmatter.mjs +42 -0
- package/src/github.ts +376 -0
- package/src/i18n/da-DK/advanced.ts +28 -0
- package/src/i18n/da-DK/app.ts +71 -0
- package/src/i18n/da-DK/apps.ts +90 -0
- package/src/i18n/da-DK/blockSettings.ts +38 -0
- package/src/i18n/da-DK/blocks.ts +408 -0
- package/src/i18n/da-DK/check.ts +25 -0
- package/src/i18n/da-DK/colors.ts +64 -0
- package/src/i18n/da-DK/common.ts +118 -0
- package/src/i18n/da-DK/company.ts +24 -0
- package/src/i18n/da-DK/consents.ts +23 -0
- package/src/i18n/da-DK/forms.ts +116 -0
- package/src/i18n/da-DK/history.ts +35 -0
- package/src/i18n/da-DK/index.ts +49 -0
- package/src/i18n/da-DK/keywords.ts +19 -0
- package/src/i18n/da-DK/library.ts +135 -0
- package/src/i18n/da-DK/licenses.ts +25 -0
- package/src/i18n/da-DK/media.ts +79 -0
- package/src/i18n/da-DK/parts.ts +696 -0
- package/src/i18n/da-DK/preview.ts +13 -0
- package/src/i18n/da-DK/redirects.ts +51 -0
- package/src/i18n/da-DK/seo.ts +20 -0
- package/src/i18n/da-DK/text.ts +35 -0
- package/src/i18n/da-DK/theme.ts +41 -0
- package/src/i18n/en-US/advanced.ts +32 -0
- package/src/i18n/en-US/app.ts +84 -0
- package/src/i18n/en-US/apps.ts +134 -0
- package/src/i18n/en-US/blockSettings.ts +48 -0
- package/src/i18n/en-US/blocks.ts +441 -0
- package/src/i18n/en-US/check.ts +31 -0
- package/src/i18n/en-US/colors.ts +82 -0
- package/src/i18n/en-US/common.ts +144 -0
- package/src/i18n/en-US/company.ts +25 -0
- package/src/i18n/en-US/consents.ts +25 -0
- package/src/i18n/en-US/forms.ts +145 -0
- package/src/i18n/en-US/history.ts +43 -0
- package/src/i18n/en-US/index.ts +51 -0
- package/src/i18n/en-US/keywords.ts +24 -0
- package/src/i18n/en-US/library.ts +176 -0
- package/src/i18n/en-US/licenses.ts +36 -0
- package/src/i18n/en-US/media.ts +80 -0
- package/src/i18n/en-US/parts.ts +718 -0
- package/src/i18n/en-US/preview.ts +15 -0
- package/src/i18n/en-US/redirects.ts +66 -0
- package/src/i18n/en-US/seo.ts +26 -0
- package/src/i18n/en-US/text.ts +41 -0
- package/src/i18n/en-US/theme.ts +41 -0
- package/src/i18n/index.ts +124 -0
- package/src/icon.tsx +160 -0
- package/src/iconPacks.ts +89 -0
- package/src/images.ts +163 -0
- package/src/licenses.ts +79 -0
- package/src/markdown.d.mts +12 -0
- package/src/markdown.mjs +204 -0
- package/src/nav.ts +68 -0
- package/src/notFound.ts +28 -0
- package/src/notifications.ts +223 -0
- package/src/pagePreview.tsx +204 -0
- package/src/r2Caching.ts +55 -0
- package/src/redirects.ts +149 -0
- package/src/registry.tsx +188 -0
- package/src/schema.ts +417 -0
- package/src/sections.d.mts +45 -0
- package/src/sections.mjs +303 -0
- package/src/seo.ts +333 -0
- package/src/shared.ts +44 -0
- package/src/sidebar.ts +31 -0
- package/src/site.ts +317 -0
- package/src/standard/Block.astro +19 -0
- package/src/standard/BlockSite.astro +18 -0
- package/src/standard/accordion/index.tsx +47 -0
- package/src/standard/accordion/load.ts +23 -0
- package/src/standard/accordion/shared.ts +15 -0
- package/src/standard/accordion/view.ts +32 -0
- package/src/standard/buttons/fields.ts +98 -0
- package/src/standard/buttons/index.tsx +23 -0
- package/src/standard/buttons/load.ts +34 -0
- package/src/standard/buttons/shared.ts +12 -0
- package/src/standard/buttons/view.ts +30 -0
- package/src/standard/cards/index.tsx +71 -0
- package/src/standard/cards/load.ts +17 -0
- package/src/standard/cards/shared.ts +27 -0
- package/src/standard/cards/view.ts +29 -0
- package/src/standard/checklist/index.tsx +27 -0
- package/src/standard/checklist/load.ts +5 -0
- package/src/standard/checklist/view.ts +13 -0
- package/src/standard/cookies/index.tsx +17 -0
- package/src/standard/cookies/list.ts +56 -0
- package/src/standard/cookies/load.ts +11 -0
- package/src/standard/cookies/shared.ts +20 -0
- package/src/standard/cookies/view.ts +28 -0
- package/src/standard/faq/index.tsx +29 -0
- package/src/standard/faq/load.ts +14 -0
- package/src/standard/faq/view.ts +21 -0
- package/src/standard/head.ts +23 -0
- package/src/standard/hero/fields.ts +48 -0
- package/src/standard/hero/index.tsx +57 -0
- package/src/standard/hero/load.ts +23 -0
- package/src/standard/hero/view.ts +38 -0
- package/src/standard/links/index.tsx +41 -0
- package/src/standard/links/load.ts +17 -0
- package/src/standard/links/view.ts +25 -0
- package/src/standard/list/index.tsx +50 -0
- package/src/standard/list/load.ts +5 -0
- package/src/standard/list/view.ts +18 -0
- package/src/standard/load.ts +7 -0
- package/src/standard/notice/index.tsx +30 -0
- package/src/standard/notice/load.ts +16 -0
- package/src/standard/notice/view.ts +10 -0
- package/src/standard/points/index.tsx +42 -0
- package/src/standard/points/load.ts +5 -0
- package/src/standard/points/view.ts +12 -0
- package/src/standard/posts/index.tsx +132 -0
- package/src/standard/posts/load.ts +36 -0
- package/src/standard/posts/shared.ts +21 -0
- package/src/standard/posts/view.ts +63 -0
- package/src/standard/pricecards/index.tsx +57 -0
- package/src/standard/pricecards/load.ts +11 -0
- package/src/standard/pricecards/view.ts +50 -0
- package/src/standard/sharedTexts.ts +6 -0
- package/src/standard/site.ts +124 -0
- package/src/standard/siteLooks.ts +19 -0
- package/src/standard/sites.ts +13 -0
- package/src/standard/steps/index.tsx +28 -0
- package/src/standard/steps/load.ts +5 -0
- package/src/standard/steps/view.ts +11 -0
- package/src/standard/testimonials/index.tsx +87 -0
- package/src/standard/testimonials/list.ts +43 -0
- package/src/standard/testimonials/load.ts +58 -0
- package/src/standard/testimonials/shared.ts +13 -0
- package/src/standard/testimonials/view.ts +76 -0
- package/src/standard/themeBlocks.ts +13 -0
- package/src/standard/video/index.tsx +23 -0
- package/src/standard/video/load.ts +10 -0
- package/src/standard/video/view.ts +42 -0
- package/src/standard/view.tsx +105 -0
- package/src/standard/views.ts +37 -0
- package/src/store.ts +499 -0
- package/src/theme.ts +90 -0
- package/src/themeUi.ts +97 -0
- package/src/themes.ts +91 -0
- package/src/ui.tsx +3402 -0
- package/src/virtual.d.ts +5 -0
- package/src/when.ts +22 -0
- package/starter/.githooks/pre-push +4 -0
- package/starter/.nvmrc +1 -0
- package/starter/.prettierignore +11 -0
- package/starter/.prettierrc.json +7 -0
- package/starter/README.md +26 -0
- package/starter/astro.config.mjs +5 -0
- package/starter/package.json +67 -0
- package/starter/public/_redirects +1 -0
- package/starter/public/apple-touch-icon.png +0 -0
- package/starter/public/editor/apple-touch-icon.png +0 -0
- package/starter/public/editor/icon-192.png +0 -0
- package/starter/public/editor/icon-512.png +0 -0
- package/starter/public/editor/icon-maskable-512.png +0 -0
- package/starter/public/favicon.ico +0 -0
- package/starter/public/favicon.svg +1 -0
- package/starter/public/logo.svg +1 -0
- package/starter/src/content/pages/blog.md +8 -0
- package/starter/src/content/pages/forside.md +9 -0
- package/starter/src/content/pages/kontakt.md +7 -0
- package/starter/src/content/pages/privatlivspolitik.md +9 -0
- package/starter/src/content/pages/siden-findes-ikke.md +7 -0
- package/starter/src/content.config.ts +2 -0
- package/starter/src/data/apps.json +1 -0
- package/starter/src/data/block-settings.json +3 -0
- package/starter/src/data/blocks.json +1 -0
- package/starter/src/data/booking.json +6 -0
- package/starter/src/data/colors.json +70 -0
- package/starter/src/data/contact.json +8 -0
- package/starter/src/data/cookies.json +21 -0
- package/starter/src/data/csp.json +1 -0
- package/starter/src/data/footer.json +15 -0
- package/starter/src/data/internal-links.json +4 -0
- package/starter/src/data/menu.json +4 -0
- package/starter/src/data/mobile-menu.json +4 -0
- package/starter/src/data/old-addresses.json +1 -0
- package/starter/src/data/redirects.json +1 -0
- package/starter/src/data/standard-pages.json +8 -0
- package/starter/src/data/theme.json +1 -0
- package/starter/src/env.d.ts +2 -0
- package/starter/src/site/config.ts +13 -0
- package/starter/src/site/worker.js +14 -0
- package/starter/tsconfig.json +17 -0
- package/starter/worker/apps.js +6 -0
- package/starter/wrangler.jsonc +39 -0
- package/theme/LICENSE +21 -0
- package/theme/blocks/ArrowLink.tsx +14 -0
- package/theme/blocks/BlockHead.tsx +26 -0
- package/theme/blocks/Panel.tsx +32 -0
- package/theme/blocks/Person.tsx +26 -0
- package/theme/blocks/accordion/PageRow.tsx +25 -0
- package/theme/blocks/accordion/View.tsx +17 -0
- package/theme/blocks/buttons/Buttons.tsx +28 -0
- package/theme/blocks/buttons/View.tsx +12 -0
- package/theme/blocks/cards/LinkCards.tsx +72 -0
- package/theme/blocks/cards/View.tsx +20 -0
- package/theme/blocks/checklist/Checks.tsx +15 -0
- package/theme/blocks/checklist/View.tsx +14 -0
- package/theme/blocks/cookies/View.tsx +36 -0
- package/theme/blocks/faq/Accordion.tsx +34 -0
- package/theme/blocks/faq/View.tsx +16 -0
- package/theme/blocks/faq/script.ts +56 -0
- package/theme/blocks/hero/View.tsx +44 -0
- package/theme/blocks/links/LinkGroups.tsx +24 -0
- package/theme/blocks/links/View.tsx +17 -0
- package/theme/blocks/list/Rows.tsx +24 -0
- package/theme/blocks/list/View.tsx +14 -0
- package/theme/blocks/notice/View.tsx +13 -0
- package/theme/blocks/points/PointList.tsx +34 -0
- package/theme/blocks/points/View.tsx +14 -0
- package/theme/blocks/posts/View.tsx +26 -0
- package/theme/blocks/pricecards/View.tsx +40 -0
- package/theme/blocks/steps/StepList.tsx +23 -0
- package/theme/blocks/steps/View.tsx +14 -0
- package/theme/blocks/testimonials/List.tsx +46 -0
- package/theme/blocks/testimonials/Slider.tsx +44 -0
- package/theme/blocks/testimonials/Testimonial.tsx +124 -0
- package/theme/blocks/testimonials/View.tsx +21 -0
- package/theme/blocks/testimonials/script.ts +52 -0
- package/theme/blocks/video/View.tsx +41 -0
- package/theme/components/ActionBar.astro +63 -0
- package/theme/components/AppSlot.astro +21 -0
- package/theme/components/ArrowLink.tsx +21 -0
- package/theme/components/BlogIndex.astro +48 -0
- package/theme/components/BlogPosts.astro +18 -0
- package/theme/components/BlogPosts.tsx +45 -0
- package/theme/components/BookButton.tsx +22 -0
- package/theme/components/Container.astro +11 -0
- package/theme/components/CookieConsent.astro +121 -0
- package/theme/components/EndBlock.astro +21 -0
- package/theme/components/FontPreload.astro +6 -0
- package/theme/components/Footer.astro +85 -0
- package/theme/components/Header.astro +186 -0
- package/theme/components/Icon.astro +16 -0
- package/theme/components/Img.astro +29 -0
- package/theme/components/Img.tsx +33 -0
- package/theme/components/Inline.astro +11 -0
- package/theme/components/Logo.astro +23 -0
- package/theme/components/LogoMark.astro +10 -0
- package/theme/components/Markdown.astro +17 -0
- package/theme/components/MobileMenu.tsx +224 -0
- package/theme/components/MobileNav.astro +33 -0
- package/theme/components/PageHeader.astro +22 -0
- package/theme/components/PageSection.astro +24 -0
- package/theme/components/PhoneButton.tsx +11 -0
- package/theme/components/Popup.astro +145 -0
- package/theme/components/PopupButton.tsx +24 -0
- package/theme/components/PopupView.tsx +48 -0
- package/theme/components/PostList.tsx +58 -0
- package/theme/components/PostMeta.tsx +17 -0
- package/theme/components/RichText.astro +28 -0
- package/theme/components/Section.astro +29 -0
- package/theme/components/SectionHeading.tsx +8 -0
- package/theme/components/TextSection.astro +16 -0
- package/theme/components/home/PageBlock.astro +19 -0
- package/theme/components/home/Text.astro +13 -0
- package/theme/components/loadPopup.ts +22 -0
- package/theme/components/loadPosts.ts +16 -0
- package/theme/components/posts.ts +15 -0
- package/theme/editor/looks.mjs +7 -0
- package/theme/editor/previews.tsx +56 -0
- package/theme/layouts/Base.astro +273 -0
- package/theme/layouts/PageLayout.astro +126 -0
- package/theme/layouts/PostLayout.astro +137 -0
- package/theme/screenshots/1-front.jpg +0 -0
- package/theme/screenshots/2-front-dark.jpg +0 -0
- package/theme/screenshots/3-blog.jpg +0 -0
- package/theme/screenshots/4-contact.jpg +0 -0
- package/theme/scripts/motion.ts +29 -0
- package/theme/styles/base.css +320 -0
- package/theme/styles/blocks.css +1808 -0
- package/theme/styles/components.css +206 -0
- package/theme/styles/global.css +1302 -0
- package/theme/styles/ui.css +6544 -0
- package/theme/templates/Blog.astro +38 -0
- package/theme/templates/Category.astro +19 -0
- package/theme/templates/Home.astro +36 -0
- package/theme/templates/New.astro +68 -0
- package/theme/templates/NotFound.astro +117 -0
- package/theme/theme.json +28 -0
- package/theme/ui/Badge.tsx +45 -0
- package/theme/ui/BottomSheet.tsx +148 -0
- package/theme/ui/Button.tsx +79 -0
- package/theme/ui/ButtonGroup.tsx +31 -0
- package/theme/ui/Card.tsx +45 -0
- package/theme/ui/Carousel.tsx +124 -0
- package/theme/ui/Checkbox.tsx +59 -0
- package/theme/ui/Chip.tsx +101 -0
- package/theme/ui/ChipSet.tsx +27 -0
- package/theme/ui/DatePicker.tsx +149 -0
- package/theme/ui/DatePickerContainer.tsx +316 -0
- package/theme/ui/Dialog.tsx +100 -0
- package/theme/ui/Divider.tsx +27 -0
- package/theme/ui/ExposedDropdownMenu.tsx +77 -0
- package/theme/ui/Fab.tsx +88 -0
- package/theme/ui/FabMenu.tsx +67 -0
- package/theme/ui/IconButton.tsx +78 -0
- package/theme/ui/List.tsx +287 -0
- package/theme/ui/LoadingIndicator.tsx +29 -0
- package/theme/ui/Menu.tsx +278 -0
- package/theme/ui/NavigationBar.tsx +62 -0
- package/theme/ui/NavigationDrawer.tsx +80 -0
- package/theme/ui/NavigationRail.tsx +154 -0
- package/theme/ui/ProgressIndicator.tsx +162 -0
- package/theme/ui/RadioButton.tsx +50 -0
- package/theme/ui/Search.tsx +129 -0
- package/theme/ui/SegmentedButton.tsx +49 -0
- package/theme/ui/SideSheet.tsx +54 -0
- package/theme/ui/Slider.tsx +155 -0
- package/theme/ui/Snackbar.tsx +67 -0
- package/theme/ui/SplitButton.tsx +67 -0
- package/theme/ui/Switch.tsx +60 -0
- package/theme/ui/Tabs.tsx +114 -0
- package/theme/ui/TextField.tsx +278 -0
- package/theme/ui/TimePicker.tsx +128 -0
- package/theme/ui/TimePickerContainer.tsx +234 -0
- package/theme/ui/Toolbar.tsx +67 -0
- package/theme/ui/Tooltip.tsx +58 -0
- package/theme/ui/TopAppBar.tsx +77 -0
- package/theme/ui/bottomSheet.script.ts +8 -0
- package/theme/ui/bottomSheet.ts +211 -0
- package/theme/ui/carousel.script.ts +28 -0
- package/theme/ui/carouselLayout.ts +229 -0
- package/theme/ui/checkbox.script.ts +8 -0
- package/theme/ui/datePicker.script.ts +302 -0
- package/theme/ui/dates.ts +185 -0
- package/theme/ui/dialog.ts +48 -0
- package/theme/ui/dropdownMenu.script.ts +166 -0
- package/theme/ui/fabMenu.script.ts +57 -0
- package/theme/ui/navigationRail.script.ts +47 -0
- package/theme/ui/ripple.script.ts +159 -0
- package/theme/ui/search.script.ts +92 -0
- package/theme/ui/sideSheet.ts +48 -0
- package/theme/ui/slider.script.ts +12 -0
- package/theme/ui/sliderUpdate.ts +52 -0
- package/theme/ui/snackbar.script.ts +16 -0
- package/theme/ui/splitButton.script.ts +63 -0
- package/theme/ui/tabs.script.ts +48 -0
- package/theme/ui/textField.script.ts +17 -0
- package/theme/ui/textFieldUpdate.ts +44 -0
- package/theme/ui/timePicker.script.ts +248 -0
- package/theme/ui/toolbar.script.ts +43 -0
- package/theme/ui/tooltip.script.ts +68 -0
- package/worker/consent.js +141 -0
- package/worker/editor-github.js +167 -0
- package/worker/editor-host.js +74 -0
- package/worker/files.js +401 -0
- package/worker/github.js +36 -0
- package/worker/index.js +184 -0
- package/worker/not-found.js +97 -0
- package/worker/oauth.js +95 -0
- package/worker/publish.js +52 -0
- package/worker/push.js +270 -0
- package/worker/r2-status.js +50 -0
- package/worker/session.js +58 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Days and times as the date and time pickers (DatePicker.tsx, TimePicker.tsx and their scripts) hold them: a day as a
|
|
2
|
+
// form saves it (ISO, 2026-11-02), a month as its first day's year and month (2026-11), and a time of day as hh:mm
|
|
3
|
+
// (24-hour). Written out and read back in a language (`locale`, BCP 47: the site's on the site), with Intl, so the
|
|
4
|
+
// names of the months and the weekdays, the order of day, month and year and the week's first day are the language's
|
|
5
|
+
// (in Danish: 02.11.2026, november, man., the week from Monday). No React and no texts here: the scripts load it at once.
|
|
6
|
+
|
|
7
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
8
|
+
|
|
9
|
+
/** A day (2026-11-02) as a date at noon, so no change of time moves it a day; null when it is not one. */
|
|
10
|
+
export function dateOf(day: string): Date | null {
|
|
11
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
|
|
12
|
+
if (!m) return null;
|
|
13
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12);
|
|
14
|
+
return d.getMonth() === Number(m[2]) - 1 ? d : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A date's day, as a form saves it (2026-11-02). */
|
|
18
|
+
export const dayOf = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
19
|
+
|
|
20
|
+
/** Today, as a day. */
|
|
21
|
+
export const today = () => dayOf(new Date());
|
|
22
|
+
|
|
23
|
+
/** The month a day is in (2026-11). */
|
|
24
|
+
export const monthOfDay = (day: string) => day.slice(0, 7);
|
|
25
|
+
|
|
26
|
+
/** A month (2026-11) moved by a number of months. */
|
|
27
|
+
export function addMonths(month: string, months: number): string {
|
|
28
|
+
const [y, m] = month.split('-').map(Number);
|
|
29
|
+
return dayOf(new Date(y, m - 1 + months, 1, 12)).slice(0, 7);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A day moved by a number of days. */
|
|
33
|
+
export function addDays(day: string, days: number): string {
|
|
34
|
+
const d = dateOf(day)!;
|
|
35
|
+
d.setDate(d.getDate() + days);
|
|
36
|
+
return dayOf(d);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A day moved by a number of months, on the same day of the month or, in a shorter month, its last. */
|
|
40
|
+
export function addMonthsToDay(day: string, months: number): string {
|
|
41
|
+
const d = dateOf(day)!;
|
|
42
|
+
const month = addMonths(monthOfDay(day), months);
|
|
43
|
+
const [y, m] = month.split('-').map(Number);
|
|
44
|
+
const last = new Date(y, m, 0).getDate();
|
|
45
|
+
return `${month}-${pad(Math.min(d.getDate(), last))}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A day within the days that can be picked (`min` and `max`, either of them '' for none). */
|
|
49
|
+
export const clampDay = (day: string, min = '', max = '') => (min && day < min ? min : max && day > max ? max : day);
|
|
50
|
+
|
|
51
|
+
/** The day a week starts with in a language (1 Monday … 7 Sunday), where Intl knows it; else Monday. */
|
|
52
|
+
export function weekStart(locale: string): number {
|
|
53
|
+
try {
|
|
54
|
+
const l = new Intl.Locale(locale) as Intl.Locale & { getWeekInfo?: () => { firstDay: number }; weekInfo?: { firstDay: number } };
|
|
55
|
+
return (l.getWeekInfo?.() ?? l.weekInfo)?.firstDay ?? 1;
|
|
56
|
+
} catch {
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A month's weeks, each seven days from the week's first day: a day, or '' for the days of the months before and
|
|
62
|
+
* after. */
|
|
63
|
+
export function weeksOf(month: string, locale: string): string[][] {
|
|
64
|
+
const [y, m] = month.split('-').map(Number);
|
|
65
|
+
const first = new Date(y, m - 1, 1, 12);
|
|
66
|
+
const lead = (first.getDay() - (weekStart(locale) % 7) + 7) % 7;
|
|
67
|
+
const length = new Date(y, m, 0).getDate();
|
|
68
|
+
const cells = [...Array.from({ length: lead }, () => ''), ...Array.from({ length }, (_, i) => `${month}-${pad(i + 1)}`)];
|
|
69
|
+
while (cells.length % 7) cells.push('');
|
|
70
|
+
return Array.from({ length: cells.length / 7 }, (_, i) => cells.slice(i * 7, i * 7 + 7));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The weekdays from the one the week starts with, each its short and its full name (M and mandag). */
|
|
74
|
+
export function weekdaysOf(locale: string): { short: string; long: string }[] {
|
|
75
|
+
const start = weekStart(locale);
|
|
76
|
+
// 2024-01-01 was a Monday.
|
|
77
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
78
|
+
const d = new Date(2024, 0, ((start - 1 + i) % 7) + 1, 12);
|
|
79
|
+
return { short: d.toLocaleDateString(locale, { weekday: 'narrow' }), long: d.toLocaleDateString(locale, { weekday: 'long' }) };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const firstUp = (text: string, locale: string) => text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
|
|
84
|
+
|
|
85
|
+
/** A month's name and year (November 2026), its name alone (November), or its short name (nov.). */
|
|
86
|
+
export function monthName(month: string, locale: string, style: 'year' | 'long' | 'short' = 'year'): string {
|
|
87
|
+
const d = dateOf(`${month}-01`)!;
|
|
88
|
+
return firstUp(
|
|
89
|
+
style === 'year' ? d.toLocaleDateString(locale, { month: 'long', year: 'numeric' }) : d.toLocaleDateString(locale, { month: style }),
|
|
90
|
+
locale,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A day written out in full (mandag 2. november 2026), for screen readers. */
|
|
95
|
+
export const dayName = (day: string, locale: string) =>
|
|
96
|
+
dateOf(day)!.toLocaleDateString(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
97
|
+
|
|
98
|
+
/** A day written short (Man. 2. nov.), as a date picker's headline. */
|
|
99
|
+
export const dayHeadline = (day: string, locale: string) =>
|
|
100
|
+
firstUp(dateOf(day)!.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }), locale);
|
|
101
|
+
|
|
102
|
+
/** A day as it is typed in a field, in the language's order (02.11.2026). */
|
|
103
|
+
export const dayTyped = (day: string, locale: string) =>
|
|
104
|
+
dateOf(day)!.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
105
|
+
|
|
106
|
+
/** A day typed in a field, in the language's order of day, month and year (2.11.2026, 02-11-2026), as a day; null
|
|
107
|
+
* when it is not one. */
|
|
108
|
+
export function dayFromTyped(text: string, locale: string): string | null {
|
|
109
|
+
const numbers = text.match(/\d+/g);
|
|
110
|
+
if (!numbers || numbers.length !== 3 || text.replace(/[\d\s./-]/g, '')) return null;
|
|
111
|
+
const order = new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
|
112
|
+
.formatToParts(new Date(2024, 0, 1, 12))
|
|
113
|
+
.map((p) => p.type)
|
|
114
|
+
.filter((t) => t === 'day' || t === 'month' || t === 'year');
|
|
115
|
+
const value = Object.fromEntries(order.map((t, i) => [t, numbers[i]]));
|
|
116
|
+
if (value.year?.length !== 4) return null;
|
|
117
|
+
const day = `${value.year}-${pad(Number(value.month))}-${pad(Number(value.day))}`;
|
|
118
|
+
return dateOf(day) ? day : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Days typed in a field, one or two (02.11.2026 – 08.11.2026), each a day or null. */
|
|
122
|
+
export function daysFromTyped(text: string, locale: string): (string | null)[] {
|
|
123
|
+
return text
|
|
124
|
+
.split(/\s[–-]\s|\s*–\s*/)
|
|
125
|
+
.map((part) => part.trim())
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map((part) => dayFromTyped(part, locale));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A time of day (14:05) as hours and minutes; null when it is not one. */
|
|
131
|
+
export function timeOf(time: string): { hour: number; minute: number } | null {
|
|
132
|
+
const m = /^(\d{2}):(\d{2})$/.exec(time);
|
|
133
|
+
if (!m) return null;
|
|
134
|
+
const hour = Number(m[1]);
|
|
135
|
+
const minute = Number(m[2]);
|
|
136
|
+
return hour <= 23 && minute <= 59 ? { hour, minute } : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Hours and minutes as a form saves them (14:05). */
|
|
140
|
+
export const timeString = (hour: number, minute: number) => `${pad(hour)}:${pad(minute)}`;
|
|
141
|
+
|
|
142
|
+
/** Two digits (05). */
|
|
143
|
+
export const twoDigits = pad;
|
|
144
|
+
|
|
145
|
+
/** A time of day as the language writes it, 24-hour (14.05 in Danish), or on a 12-hour clock (`twelve`: 2.05 PM). */
|
|
146
|
+
export const timeTyped = (time: string, locale: string, twelve = false) => {
|
|
147
|
+
const t = timeOf(time)!;
|
|
148
|
+
return new Date(2024, 0, 1, t.hour, t.minute).toLocaleTimeString(
|
|
149
|
+
locale,
|
|
150
|
+
twelve ? { hour: 'numeric', minute: '2-digit', hourCycle: 'h12' } : { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' },
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** What the language calls the morning and the afternoon on a 12-hour clock (AM and PM). */
|
|
155
|
+
export const dayPeriods = (locale: string): [string, string] =>
|
|
156
|
+
[9, 21].map(
|
|
157
|
+
(hour) =>
|
|
158
|
+
new Intl.DateTimeFormat(locale, { hour: 'numeric', hourCycle: 'h12' })
|
|
159
|
+
.formatToParts(new Date(2024, 0, 1, hour))
|
|
160
|
+
.find((p) => p.type === 'dayPeriod')?.value ?? (hour < 12 ? 'AM' : 'PM'),
|
|
161
|
+
) as [string, string];
|
|
162
|
+
|
|
163
|
+
/** What the language writes between the hours and the minutes (. in Danish). */
|
|
164
|
+
export const timeSeparator = (locale: string) =>
|
|
165
|
+
new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
|
166
|
+
.formatToParts(new Date(2024, 0, 1, 14, 5))
|
|
167
|
+
.find((p) => p.type === 'literal')
|
|
168
|
+
?.value.trim() || ':';
|
|
169
|
+
|
|
170
|
+
/** A time typed in a field (14.05, 14:05, 1405, 9.30; with `periods`, the language's AM and PM, also 2.05 PM or
|
|
171
|
+
* 2:05 pm, the hour 1–12), as a time; null when it is not one. */
|
|
172
|
+
export function timeFromTyped(text: string, periods?: [string, string]): string | null {
|
|
173
|
+
const m = /^\s*(\d{1,2})\s*[.:\s]?\s*(\d{2})\s*(\S*)\s*$/.exec(text);
|
|
174
|
+
if (!m) return null;
|
|
175
|
+
let hour = Number(m[1]);
|
|
176
|
+
const said = m[3].toLowerCase().replace(/\./g, '');
|
|
177
|
+
if (said) {
|
|
178
|
+
const at = (periods ?? ['AM', 'PM']).findIndex((p) => p.toLowerCase().replace(/\./g, '') === said);
|
|
179
|
+
const pm = at >= 0 ? at === 1 : said === 'pm' || said === 'p' ? true : said === 'am' || said === 'a' ? false : null;
|
|
180
|
+
if (pm === null || !periods || hour < 1 || hour > 12) return null;
|
|
181
|
+
hour = (hour % 12) + (pm ? 12 : 0);
|
|
182
|
+
}
|
|
183
|
+
const time = timeString(hour, Number(m[2]));
|
|
184
|
+
return timeOf(time) ? time : null;
|
|
185
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// What a modal dialog (Dialog.tsx in its <dialog> with dialogModalClass, or a full-screen one's with
|
|
2
|
+
// dialogFullScreenModalClass) does in a browser, for the script that shows it (a block's or an app's): dialog(el)
|
|
3
|
+
// gives it its open() and close(). Native <dialog>: focus moves into it and back, Escape closes it, and screen readers
|
|
4
|
+
// know it is modal. Motion: it comes up a little as it opens (ui.css) and goes down and fades as it closes; with reduced
|
|
5
|
+
// motion it appears and disappears at once. Its close button (data-dialog-close: a full-screen one's cross, or a
|
|
6
|
+
// button among a basic one's actions) and Escape close it, and a tap on the scrim (outside it) closes a basic one (a
|
|
7
|
+
// full-screen one holds a task that is not done, so only its buttons close it); the page under it does not scroll
|
|
8
|
+
// while it is open. It moves with the site's motion scheme (motionSpring, @morphemecms/cms/lib/motion.ts).
|
|
9
|
+
import { animate } from 'motion';
|
|
10
|
+
import { motionSpring } from '@morphemecms/cms/lib/motion';
|
|
11
|
+
|
|
12
|
+
export type DialogControls = { open: () => void; close: () => Promise<void> };
|
|
13
|
+
|
|
14
|
+
export function dialog(el: HTMLDialogElement): DialogControls {
|
|
15
|
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
16
|
+
const fullScreen = el.classList.contains('dialog-modal-full-screen');
|
|
17
|
+
let closing = false;
|
|
18
|
+
|
|
19
|
+
function open() {
|
|
20
|
+
if (el.open) return;
|
|
21
|
+
el.showModal();
|
|
22
|
+
document.documentElement.style.overflow = 'hidden';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function close() {
|
|
26
|
+
if (!el.open || closing) return;
|
|
27
|
+
closing = true;
|
|
28
|
+
if (!reduce) await animate(el, { opacity: 0, y: 16 }, motionSpring('spatial', 'fast'));
|
|
29
|
+
el.close();
|
|
30
|
+
el.style.transform = '';
|
|
31
|
+
el.style.opacity = '';
|
|
32
|
+
document.documentElement.style.overflow = '';
|
|
33
|
+
closing = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
el.querySelectorAll('[data-dialog-close]').forEach((b) => b.addEventListener('click', close));
|
|
37
|
+
// Escape: animate out instead of the dialog's instant close.
|
|
38
|
+
el.addEventListener('cancel', (e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
close();
|
|
41
|
+
});
|
|
42
|
+
// A tap on the scrim (outside the box) closes a basic dialog.
|
|
43
|
+
el.addEventListener('click', (e) => {
|
|
44
|
+
if (e.target === el && !fullScreen) close();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { open, close };
|
|
48
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// What a dropdown menu (DropdownMenu in Menu.tsx) does on the site, on top of what its popover does by itself (a tap on
|
|
2
|
+
// the button opens and closes it, Escape and a tap outside close it): WAI-ARIA's menu button (aria-expanded on the
|
|
3
|
+
// button; Enter, Space and the down arrow open it on its first item, the up arrow on its last). In the menu the arrow
|
|
4
|
+
// keys go between its items (round), Home and End to the first and the last, and a letter to the next item starting
|
|
5
|
+
// with it; the right arrow, Enter and Space open a submenu on its first item and the left arrow closes it again; Escape
|
|
6
|
+
// closes the menu it is in and goes back to what opened it, and Tab closes them all. The items are one stop for Tab
|
|
7
|
+
// (tabindex -1, so the focus moves with the keys). A submenu also opens when the mouse rests on its item. Choosing an
|
|
8
|
+
// item closes the menu and goes back to the button, ticking a menuitemcheckbox on or off and making a menuitemradio
|
|
9
|
+
// the chosen one of its group first. Where the browser has no CSS anchor positioning, it places each menu at what
|
|
10
|
+
// opened it. Run on every page by the theme's Base.astro (blockScripts, packages/editor/site/blockScripts.ts); without a dropdown menu
|
|
11
|
+
// on the page it does nothing.
|
|
12
|
+
const gap = 4;
|
|
13
|
+
const edge = 8;
|
|
14
|
+
|
|
15
|
+
// A menu's own items, not its submenus', that can be chosen.
|
|
16
|
+
const itemsOf = (menu: HTMLElement) =>
|
|
17
|
+
[...menu.querySelectorAll<HTMLElement>('.menu-item')].filter(
|
|
18
|
+
(i) => i.parentElement?.closest('.menu') === menu && !i.matches(':disabled, [aria-disabled="true"]'),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// The menu an item opens.
|
|
22
|
+
const submenuOf = (item: Element) =>
|
|
23
|
+
item.getAttribute('popovertarget') ? document.getElementById(item.getAttribute('popovertarget')!) : null;
|
|
24
|
+
|
|
25
|
+
/** Where a menu goes without anchor positioning (a fixed one, by its left and top): under the button (above it when
|
|
26
|
+
* there is no room below), or beside its item (on the other side when there is no room); inside the screen. The
|
|
27
|
+
* editor's menus (Menu in packages/editor/src/ui.tsx) are placed by it too. */
|
|
28
|
+
export function placeMenu(menu: HTMLElement, opener: HTMLElement, beside = false) {
|
|
29
|
+
const at = opener.getBoundingClientRect();
|
|
30
|
+
const { offsetWidth: w, offsetHeight: h } = menu;
|
|
31
|
+
let left = beside ? at.right : at.left;
|
|
32
|
+
let top = beside ? at.top : at.bottom + gap;
|
|
33
|
+
if (left + w > innerWidth - edge) left = beside ? at.left - w : at.right - w;
|
|
34
|
+
if (top + h > innerHeight - edge) top = beside ? at.bottom - h : at.top - gap - h;
|
|
35
|
+
menu.style.left = `${Math.max(edge, left)}px`;
|
|
36
|
+
menu.style.top = `${Math.max(edge, top)}px`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function dropdownMenu() {
|
|
40
|
+
const anchored = CSS.supports('position-area', 'block-end');
|
|
41
|
+
document.querySelectorAll<HTMLElement>('[data-dropdown-menu]').forEach((root) => {
|
|
42
|
+
const button = root.querySelector<HTMLElement>('[data-dropdown-menu-button]');
|
|
43
|
+
const menu = root.querySelector<HTMLElement>('[data-dropdown-menu-menu]');
|
|
44
|
+
if (!button || !menu?.popover) return;
|
|
45
|
+
const menus = [menu, ...menu.querySelectorAll<HTMLElement>('.menu-submenu[popover]')];
|
|
46
|
+
// Whether a menu that opens takes the focus to its first item (not a submenu the mouse opened, nor one a key
|
|
47
|
+
// opened, which openOn gives the focus itself).
|
|
48
|
+
let focusOnOpen: 'first' | null = 'first';
|
|
49
|
+
let resting: number | undefined;
|
|
50
|
+
const openers = new Map<HTMLElement, HTMLElement>();
|
|
51
|
+
// Opens a menu (with a key) and puts the focus on its first or last item at once.
|
|
52
|
+
const openOn = (m: HTMLElement, which: 'first' | 'last') => {
|
|
53
|
+
focusOnOpen = null;
|
|
54
|
+
if (!m.matches(':popover-open')) m.showPopover();
|
|
55
|
+
focusOnOpen = 'first';
|
|
56
|
+
const items = itemsOf(m);
|
|
57
|
+
items[which === 'first' ? 0 : items.length - 1]?.focus();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const m of menus) {
|
|
61
|
+
const opener = m === menu ? button : root.querySelector<HTMLElement>(`[popovertarget="${CSS.escape(m.id)}"]`);
|
|
62
|
+
if (!opener) continue;
|
|
63
|
+
openers.set(m, opener);
|
|
64
|
+
opener.setAttribute('aria-expanded', 'false');
|
|
65
|
+
itemsOf(m).forEach((i) => (i.tabIndex = -1));
|
|
66
|
+
m.addEventListener('toggle', (e) => {
|
|
67
|
+
const open = (e as ToggleEvent).newState === 'open';
|
|
68
|
+
opener.setAttribute('aria-expanded', String(open));
|
|
69
|
+
if (open) {
|
|
70
|
+
if (!anchored) placeMenu(m, opener, m !== menu);
|
|
71
|
+
const items = itemsOf(m);
|
|
72
|
+
if (focusOnOpen && !m.contains(document.activeElement)) items[0]?.focus();
|
|
73
|
+
focusOnOpen = 'first';
|
|
74
|
+
} else if (m.contains(document.activeElement)) opener.focus();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!anchored) {
|
|
78
|
+
const replace = () => openers.forEach((opener, m) => m.matches(':popover-open') && placeMenu(m, opener, m !== menu));
|
|
79
|
+
addEventListener('scroll', replace, { capture: true, passive: true });
|
|
80
|
+
addEventListener('resize', replace, { passive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
button.addEventListener('keydown', (e) => {
|
|
84
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
openOn(menu, e.key === 'ArrowDown' ? 'first' : 'last');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
menu.addEventListener('keydown', (e) => {
|
|
90
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('.menu-item');
|
|
91
|
+
const m = item?.parentElement?.closest<HTMLElement>('.menu');
|
|
92
|
+
if (!item || !m) return;
|
|
93
|
+
const items = itemsOf(m);
|
|
94
|
+
const at = items.indexOf(item);
|
|
95
|
+
const sub = submenuOf(item);
|
|
96
|
+
if (e.key === 'Tab') {
|
|
97
|
+
menu.hidePopover();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (e.key === 'ArrowRight' && sub) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
openOn(sub, 'first');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (e.key === 'ArrowLeft' && m !== menu) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
m.hidePopover();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let to = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: items.length - 1 }[e.key];
|
|
111
|
+
// A letter: the next item whose label starts with it.
|
|
112
|
+
if (to === undefined && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey && e.key.trim()) {
|
|
113
|
+
const key = e.key.toLocaleLowerCase();
|
|
114
|
+
const starts = (i: HTMLElement) => i.querySelector('.menu-item-label')?.textContent?.trim().toLocaleLowerCase().startsWith(key);
|
|
115
|
+
const next = [...items.slice(at + 1), ...items.slice(0, at + 1)].find(starts);
|
|
116
|
+
if (next) to = items.indexOf(next);
|
|
117
|
+
}
|
|
118
|
+
if (to === undefined) return;
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
items[(to + items.length) % items.length]?.focus();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// A choice: tick it, or make it the chosen one of its group, then close the menu (a link is followed).
|
|
124
|
+
menu.addEventListener('click', (e) => {
|
|
125
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('.menu-item');
|
|
126
|
+
if (!item || submenuOf(item) || item.matches(':disabled, [aria-disabled="true"]')) return;
|
|
127
|
+
const role = item.getAttribute('role');
|
|
128
|
+
if (role === 'menuitemcheckbox') item.setAttribute('aria-checked', String(item.getAttribute('aria-checked') !== 'true'));
|
|
129
|
+
if (role === 'menuitemradio') {
|
|
130
|
+
const group = item.closest('[role="group"], [role="menu"]');
|
|
131
|
+
group?.querySelectorAll('[role="menuitemradio"]').forEach((r) => {
|
|
132
|
+
if (r.closest('[role="group"], [role="menu"]') === group) r.setAttribute('aria-checked', String(r === item));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
menu.hidePopover();
|
|
136
|
+
button.focus();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// The mouse resting on an item with a submenu opens it (without taking the focus); on another item of the same
|
|
140
|
+
// menu, an open submenu there closes.
|
|
141
|
+
menu.addEventListener('pointerover', (e) => {
|
|
142
|
+
if (e.pointerType !== 'mouse') return;
|
|
143
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('.menu-item');
|
|
144
|
+
const m = item?.parentElement?.closest<HTMLElement>('.menu');
|
|
145
|
+
if (!item || !m) return;
|
|
146
|
+
clearTimeout(resting);
|
|
147
|
+
const sub = submenuOf(item);
|
|
148
|
+
resting = window.setTimeout(() => {
|
|
149
|
+
m.querySelectorAll<HTMLElement>('.menu-submenu:popover-open').forEach((s) => {
|
|
150
|
+
if (s !== sub && s.parentElement?.closest('.menu') === m) s.hidePopover();
|
|
151
|
+
});
|
|
152
|
+
if (sub && !sub.matches(':popover-open') && !item.matches(':disabled')) {
|
|
153
|
+
focusOnOpen = null;
|
|
154
|
+
sub.showPopover();
|
|
155
|
+
}
|
|
156
|
+
}, 150);
|
|
157
|
+
});
|
|
158
|
+
menu.addEventListener('pointerleave', () => clearTimeout(resting));
|
|
159
|
+
|
|
160
|
+
// The focus moving out of the button and its menus (a tap on something else that takes it) closes them.
|
|
161
|
+
root.addEventListener('focusout', (e) => {
|
|
162
|
+
const to = e.relatedTarget as Node | null;
|
|
163
|
+
if (to && !root.contains(to) && menu.matches(':popover-open')) menu.hidePopover();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// What a FAB menu (FabMenu.tsx) does on the site: its FAB opens and closes its actions (aria-expanded, the list's
|
|
2
|
+
// hidden), and while they are open its name is the cross's (data-close-label), else its own (data-label). Opened with a
|
|
3
|
+
// key, the focus goes to the first action; the arrow keys go between them, Home and End to the first and the last;
|
|
4
|
+
// Escape closes it and goes back to the FAB, and a choice, a tap outside it or the focus moving out close it. Run on
|
|
5
|
+
// every page by the theme's Base.astro (blockScripts, packages/editor/site/blockScripts.ts); without a FAB menu on the page it does
|
|
6
|
+
// nothing.
|
|
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';
|
|
14
|
+
|
|
15
|
+
const outside = (e: PointerEvent) => {
|
|
16
|
+
if (!root.contains(e.target as Node)) set(false);
|
|
17
|
+
};
|
|
18
|
+
function set(open: boolean, focus?: 'first' | 'button') {
|
|
19
|
+
button!.setAttribute('aria-expanded', String(open));
|
|
20
|
+
const name = open ? button!.dataset.closeLabel : button!.dataset.label;
|
|
21
|
+
if (name) button!.setAttribute('aria-label', name);
|
|
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
|
+
}
|
|
28
|
+
|
|
29
|
+
button.addEventListener('click', (e) => {
|
|
30
|
+
const open = !isOpen();
|
|
31
|
+
set(open, open && e.detail === 0 ? 'first' : undefined);
|
|
32
|
+
});
|
|
33
|
+
root.addEventListener('keydown', (e) => {
|
|
34
|
+
if (!isOpen()) return;
|
|
35
|
+
if (e.key === 'Escape') {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
set(false, 'button');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const all = items();
|
|
41
|
+
const at = all.indexOf(document.activeElement as HTMLElement);
|
|
42
|
+
if (at < 0) return;
|
|
43
|
+
const to = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: all.length - 1 }[e.key];
|
|
44
|
+
if (to === undefined) return;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
all[(to + all.length) % all.length].focus();
|
|
47
|
+
});
|
|
48
|
+
list.addEventListener('click', (e) => {
|
|
49
|
+
if ((e.target as HTMLElement).closest('a[href], button')) set(false);
|
|
50
|
+
});
|
|
51
|
+
// The focus moving to something outside it (Tab); a tap outside is `outside`.
|
|
52
|
+
root.addEventListener('focusout', (e) => {
|
|
53
|
+
const to = e.relatedTarget as Node | null;
|
|
54
|
+
if (isOpen() && to && !root.contains(to)) set(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// What a navigation rail (NavigationRail.tsx) does on the site: its menu button, and any other button that opens it
|
|
2
|
+
// (data-navigation-rail-menu with aria-controls, the rail's id: for a rail that hides while it is collapsed), expands
|
|
3
|
+
// and collapses it (data-expanded on the rail, aria-expanded on the buttons), and its FAB shows its label only while it
|
|
4
|
+
// is expanded (data-collapsed). A modal one (.navigation-rail-modal) shows its scrim while it is expanded, and the scrim,
|
|
5
|
+
// Escape and following one of its links collapse it again. When it collapses with the focus in it and it hides
|
|
6
|
+
// (.navigation-rail-hide), the focus goes back to the button that opened it. Run on every page by the theme's Base.astro
|
|
7
|
+
// (blockScripts, packages/editor/site/blockScripts.ts); without a rail it does nothing.
|
|
8
|
+
export default function navigationRail() {
|
|
9
|
+
document.querySelectorAll<HTMLElement>('[data-navigation-rail]').forEach((rail) => {
|
|
10
|
+
const scrim = rail.previousElementSibling?.matches('[data-navigation-rail-scrim]')
|
|
11
|
+
? (rail.previousElementSibling as HTMLElement)
|
|
12
|
+
: null;
|
|
13
|
+
const modal = rail.classList.contains('navigation-rail-modal');
|
|
14
|
+
const hides = rail.classList.contains('navigation-rail-hide');
|
|
15
|
+
const inside = rail.querySelector<HTMLButtonElement>('[data-navigation-rail-menu]');
|
|
16
|
+
const outside = rail.id
|
|
17
|
+
? [...document.querySelectorAll<HTMLButtonElement>(`[data-navigation-rail-menu][aria-controls="${CSS.escape(rail.id)}"]`)]
|
|
18
|
+
: [];
|
|
19
|
+
const menus = [inside, ...outside].filter((menu): menu is HTMLButtonElement => !!menu);
|
|
20
|
+
let opener: HTMLButtonElement | null = null;
|
|
21
|
+
|
|
22
|
+
const set = (expanded: boolean) => {
|
|
23
|
+
rail.toggleAttribute('data-expanded', expanded);
|
|
24
|
+
for (const menu of menus) menu.setAttribute('aria-expanded', String(expanded));
|
|
25
|
+
rail.querySelectorAll('.navigation-rail-header > .extended-fab').forEach((fab) => fab.toggleAttribute('data-collapsed', !expanded));
|
|
26
|
+
if (scrim) scrim.hidden = !expanded;
|
|
27
|
+
if (!expanded && hides && rail.contains(document.activeElement)) (opener ?? outside[0])?.focus();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (const menu of menus)
|
|
31
|
+
menu.addEventListener('click', () => {
|
|
32
|
+
const expanded = !rail.hasAttribute('data-expanded');
|
|
33
|
+
opener = expanded && menu !== inside ? menu : opener;
|
|
34
|
+
set(expanded);
|
|
35
|
+
// Opened from outside, the focus moves into it, to its own menu button.
|
|
36
|
+
if (expanded && menu !== inside) inside?.focus();
|
|
37
|
+
});
|
|
38
|
+
if (!modal) return;
|
|
39
|
+
scrim?.addEventListener('click', () => set(false));
|
|
40
|
+
document.addEventListener('keydown', (e) => {
|
|
41
|
+
if (e.key === 'Escape' && rail.hasAttribute('data-expanded')) set(false);
|
|
42
|
+
});
|
|
43
|
+
rail.addEventListener('click', (e) => {
|
|
44
|
+
if ((e.target as HTMLElement).closest('.navigation-rail-item')) set(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The ripple, Material 3's pressed state (m3.material.io/foundations/interaction/states/applying-states, .ripple in
|
|
2
|
+
// ../styles/ui.css): pressed, a part grows a wave of its content's colour from where it was pressed (from its middle with
|
|
3
|
+
// the keyboard) until the wave covers it, and the wave fades when it is let go. On the parts that are pressed (PARTS),
|
|
4
|
+
// in the part's shape or, on a navigation item, its active indicator's; not on one that cannot be used. A touch waits a
|
|
5
|
+
// moment first, so a finger that scrolls the page does not ripple. One listener on the document, so it also works on
|
|
6
|
+
// what is drawn later (the editor's screens). Run on every page by the theme's Base.astro (blockScripts,
|
|
7
|
+
// packages/editor/site/blockScripts.ts) and by the editor (App.tsx). How it looks is the theme's (--ripple-*); with
|
|
8
|
+
// reduced motion the wave does not grow, it only fades in and out.
|
|
9
|
+
|
|
10
|
+
/** The parts that ripple when pressed: the buttons, icon buttons and FABs, chips, menu items, tabs and segments, the
|
|
11
|
+
* navigation's items, list items and cards that link or are tapped, and the pickers' days, years and buttons. */
|
|
12
|
+
const PARTS = [
|
|
13
|
+
'.button',
|
|
14
|
+
'.icon-button',
|
|
15
|
+
'.fab',
|
|
16
|
+
'.extended-fab',
|
|
17
|
+
'a.chip',
|
|
18
|
+
'button.chip',
|
|
19
|
+
'.chip-action',
|
|
20
|
+
'.chip-remove',
|
|
21
|
+
'.menu-item',
|
|
22
|
+
'.tab',
|
|
23
|
+
'.segmented-button-segment',
|
|
24
|
+
'.navigation-bar-item',
|
|
25
|
+
'.navigation-rail-item',
|
|
26
|
+
'.navigation-drawer-item',
|
|
27
|
+
'.navigation-drawer-subitem',
|
|
28
|
+
'.navigation-drawer-toggle',
|
|
29
|
+
':is(a, button, label, [role="button"]).list-item-container',
|
|
30
|
+
'.list-item-with-control',
|
|
31
|
+
'a.card',
|
|
32
|
+
'button.card',
|
|
33
|
+
'.date-picker-day',
|
|
34
|
+
'.date-picker-year',
|
|
35
|
+
'.date-picker-menu-item',
|
|
36
|
+
'.time-picker-period-button',
|
|
37
|
+
].join(', ');
|
|
38
|
+
|
|
39
|
+
// Material's: the wave starts at a fifth of the part's longer side and grows past its corners with a soft edge, and
|
|
40
|
+
// stays at least this long, so a quick tap is seen.
|
|
41
|
+
const START = 0.2;
|
|
42
|
+
const SOFT_EDGE = 0.35;
|
|
43
|
+
const SOFT_EDGE_MIN = 75;
|
|
44
|
+
const PADDING = 10;
|
|
45
|
+
const MINIMUM_PRESS_MS = 225;
|
|
46
|
+
// How long a touch waits before it ripples: a scroll that starts within it cancels the touch.
|
|
47
|
+
const TOUCH_DELAY_MS = 150;
|
|
48
|
+
// How long a wave stays after it is let go, when it has no transition to wait for.
|
|
49
|
+
const FADE_MS = 1000;
|
|
50
|
+
|
|
51
|
+
let started = false;
|
|
52
|
+
|
|
53
|
+
export default function ripples() {
|
|
54
|
+
if (started) return;
|
|
55
|
+
started = true;
|
|
56
|
+
document.addEventListener('pointerdown', pointerDown, { capture: true, passive: true });
|
|
57
|
+
document.addEventListener('click', keyboardClick, true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The part that is pressed at `target`, the nearest one (an icon button in a list item); none when it cannot be used. */
|
|
61
|
+
function partAt(target: EventTarget | null) {
|
|
62
|
+
const part = target instanceof Element ? target.closest<HTMLElement>(PARTS) : null;
|
|
63
|
+
if (!part) return null;
|
|
64
|
+
if (part.matches(':disabled, .list-item-with-control:has(:disabled)')) return null;
|
|
65
|
+
if (part.closest('[aria-disabled="true"], .list-item-disabled, fieldset:disabled')) return null;
|
|
66
|
+
return part;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Where a part's ripple is drawn: an input chip's on the whole chip, a navigation item's on its active indicator (the
|
|
70
|
+
* icon's, where the indicator is only the box of the icon and the label), else on the part. */
|
|
71
|
+
function surfaceOf(part: HTMLElement) {
|
|
72
|
+
if (part.matches('.chip-action')) return part.parentElement ?? part;
|
|
73
|
+
const indicator = part.querySelector<HTMLElement>(':scope > :is(.navigation-bar-indicator, .navigation-rail-indicator)');
|
|
74
|
+
if (!indicator) return part;
|
|
75
|
+
if (getComputedStyle(indicator).display !== 'contents') return indicator;
|
|
76
|
+
return indicator.querySelector<HTMLElement>(':scope > :is(.navigation-bar-icon, .navigation-rail-icon)') ?? part;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Starts a wave on `surface`, from the point pressed (else its middle), and gives what lets it go. */
|
|
80
|
+
function press(surface: HTMLElement, x?: number, y?: number) {
|
|
81
|
+
const box = surface.getBoundingClientRect();
|
|
82
|
+
const longer = Math.max(box.width, box.height);
|
|
83
|
+
if (!longer) return () => {};
|
|
84
|
+
const size = Math.max(Math.floor(longer * START), 1);
|
|
85
|
+
const scale = (Math.hypot(box.width, box.height) + PADDING + Math.max(longer * SOFT_EDGE, SOFT_EDGE_MIN)) / size;
|
|
86
|
+
const fromX = x === undefined ? box.width / 2 : x - box.left;
|
|
87
|
+
const fromY = y === undefined ? box.height / 2 : y - box.top;
|
|
88
|
+
|
|
89
|
+
let layer = surface.querySelector<HTMLElement>(':scope > .ripple');
|
|
90
|
+
if (!layer) {
|
|
91
|
+
layer = document.createElement('span');
|
|
92
|
+
layer.className = 'ripple';
|
|
93
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
94
|
+
surface.append(layer);
|
|
95
|
+
}
|
|
96
|
+
const wave = document.createElement('span');
|
|
97
|
+
wave.className = 'ripple-wave';
|
|
98
|
+
wave.style.setProperty('--ripple-size', `${size}px`);
|
|
99
|
+
wave.style.setProperty('--ripple-from', `${fromX - size / 2}px, ${fromY - size / 2}px`);
|
|
100
|
+
wave.style.setProperty('--ripple-to', `${box.width / 2 - size / 2}px, ${box.height / 2 - size / 2}px`);
|
|
101
|
+
wave.style.setProperty('--ripple-scale', `${scale}`);
|
|
102
|
+
layer.append(wave);
|
|
103
|
+
|
|
104
|
+
const pressed = performance.now();
|
|
105
|
+
const shown = layer;
|
|
106
|
+
return () => {
|
|
107
|
+
setTimeout(() => letGo(wave, shown), Math.max(0, MINIMUM_PRESS_MS - (performance.now() - pressed)));
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Fades a wave out and takes it away, and its layer with the last one, so the part is as it was. */
|
|
112
|
+
function letGo(wave: HTMLElement, layer: HTMLElement) {
|
|
113
|
+
const gone = () => {
|
|
114
|
+
wave.remove();
|
|
115
|
+
if (!layer.childElementCount) layer.remove();
|
|
116
|
+
};
|
|
117
|
+
wave.addEventListener('transitionend', gone, { once: true });
|
|
118
|
+
setTimeout(gone, FADE_MS);
|
|
119
|
+
wave.dataset.released = '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pointerDown(down: PointerEvent) {
|
|
123
|
+
if (!down.isPrimary || down.button !== 0) return;
|
|
124
|
+
const part = partAt(down.target);
|
|
125
|
+
if (!part) return;
|
|
126
|
+
const surface = surfaceOf(part);
|
|
127
|
+
let release: (() => void) | undefined;
|
|
128
|
+
const start = () => {
|
|
129
|
+
release ??= press(surface, down.clientX, down.clientY);
|
|
130
|
+
};
|
|
131
|
+
const touch = down.pointerType === 'touch';
|
|
132
|
+
const timer = touch ? setTimeout(start, TOUCH_DELAY_MS) : undefined;
|
|
133
|
+
if (!touch) start();
|
|
134
|
+
|
|
135
|
+
const end = (e: Event) => {
|
|
136
|
+
if (e instanceof PointerEvent && e.pointerId !== down.pointerId) return;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
removeEventListener('pointerup', end, true);
|
|
139
|
+
removeEventListener('pointercancel', end, true);
|
|
140
|
+
removeEventListener('contextmenu', end, true);
|
|
141
|
+
part.removeEventListener('pointerleave', end);
|
|
142
|
+
// A tap let go within the delay still ripples; a scroll (pointercancel) does not.
|
|
143
|
+
if (e.type === 'pointerup') start();
|
|
144
|
+
release?.();
|
|
145
|
+
};
|
|
146
|
+
addEventListener('pointerup', end, true);
|
|
147
|
+
addEventListener('pointercancel', end, true);
|
|
148
|
+
addEventListener('contextmenu', end, true);
|
|
149
|
+
if (!touch) part.addEventListener('pointerleave', end);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A part used from the keyboard (Enter, Space) ripples from its middle. A click from a pointer has its detail, and one
|
|
153
|
+
// from a script or a <label> is on a part without the keyboard's focus.
|
|
154
|
+
function keyboardClick(e: MouseEvent) {
|
|
155
|
+
if (e.detail !== 0) return;
|
|
156
|
+
const part = partAt(e.target);
|
|
157
|
+
if (!part?.matches(':focus-visible, :has(:focus-visible)')) return;
|
|
158
|
+
press(surfaceOf(part))();
|
|
159
|
+
}
|