@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,223 @@
|
|
|
1
|
+
// The notifications (#notifikationer, Notifications.tsx): what the editor and the apps have to say that is worth
|
|
2
|
+
// reading again, newest first, with when it came and the link it had. Each has a type: the editor's own (`site`: the
|
|
3
|
+
// site is updated or not, `saved`: something is saved, deleted, restored or installed, `failed`: it could not be,
|
|
4
|
+
// `updates`: an app from the store updated on its own, AppList.tsx autoUpdate), or
|
|
5
|
+
// an app's, which it declares (`notifications` in its index.ts, AppNotifications in apps.ts) as `<app>/<type>`. A
|
|
6
|
+
// message about something going on ("Gemmer…") or about a form that cannot be saved has no type and is only shown at
|
|
7
|
+
// the bottom.
|
|
8
|
+
// Each type can be turned off under "Konto" › "Notifikationer" (Library.tsx Account): then it is not kept here or sent
|
|
9
|
+
// to the phone (the editor's own are still shown at the bottom, as the answer to what was done; an app's not at all).
|
|
10
|
+
// There too "På telefonen": when it is on, a notification comes on the phone or computer as well while the editor is
|
|
11
|
+
// open but not on screen (another app or tab; notifyDevice), and, through the site's Worker (Web Push, packages/editor/worker/push.js),
|
|
12
|
+
// when it is closed: this browser is signed up there with the types turned off and the editor's language (signUp), and
|
|
13
|
+
// after a save the Worker tells it when the site has been built with it (waitOnServer); an app's part of the Worker
|
|
14
|
+
// tells it the app's notifications. The bell in the top bar (Screen.tsx NotificationsLink) has a dot while some are not
|
|
15
|
+
// seen; opening the notifications marks them seen. Kept in this browser: the last 50, the choices and the ids of the
|
|
16
|
+
// apps' notifications already told.
|
|
17
|
+
import { useSyncExternalStore } from 'react';
|
|
18
|
+
import { siteName } from '@site/editor';
|
|
19
|
+
import { locale } from './i18n';
|
|
20
|
+
|
|
21
|
+
export const EDITOR_TYPES = ['site', 'saved', 'failed', 'updates'] as const;
|
|
22
|
+
export type EditorType = (typeof EDITOR_TYPES)[number];
|
|
23
|
+
|
|
24
|
+
/** The type of an app's notification, as it is kept and turned off. */
|
|
25
|
+
export const appType = (app: string, type: string) => `${app}/${type}`;
|
|
26
|
+
|
|
27
|
+
export type Notification = {
|
|
28
|
+
text: string;
|
|
29
|
+
type: string;
|
|
30
|
+
error?: boolean;
|
|
31
|
+
at: number;
|
|
32
|
+
link?: { text: string; href: string };
|
|
33
|
+
};
|
|
34
|
+
type Kept = { items: Notification[]; seen: number; told: string[] };
|
|
35
|
+
/** `off`: the types turned off; `device`: "På telefonen" (not chosen yet: on when the browser allows it). */
|
|
36
|
+
type Choices = { off: string[]; device?: boolean };
|
|
37
|
+
|
|
38
|
+
const KEY = 'editor-notifications';
|
|
39
|
+
const CHOICES = 'editor-notification-settings';
|
|
40
|
+
const MAX = 50;
|
|
41
|
+
const MAX_TOLD = 200;
|
|
42
|
+
|
|
43
|
+
function read<T>(key: string, fallback: T, ok: (value: T) => boolean): T {
|
|
44
|
+
try {
|
|
45
|
+
const saved = JSON.parse(localStorage.getItem(key) ?? 'null') as T | null;
|
|
46
|
+
if (saved && ok(saved)) return { ...fallback, ...saved };
|
|
47
|
+
} catch {}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let kept = read<Kept>(KEY, { items: [], seen: 0, told: [] }, (k) => Array.isArray(k.items));
|
|
52
|
+
let choices = read<Choices>(CHOICES, { off: [] }, (c) => Array.isArray(c.off));
|
|
53
|
+
const listeners = new Set<() => void>();
|
|
54
|
+
|
|
55
|
+
function store(key: string, value: unknown) {
|
|
56
|
+
try {
|
|
57
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
58
|
+
} catch {}
|
|
59
|
+
listeners.forEach((l) => l());
|
|
60
|
+
}
|
|
61
|
+
const set = (next: Kept) => store(KEY, (kept = next));
|
|
62
|
+
const choose = (next: Choices) => store(CHOICES, (choices = next));
|
|
63
|
+
|
|
64
|
+
const subscribe = (changed: () => void) => {
|
|
65
|
+
listeners.add(changed);
|
|
66
|
+
return () => listeners.delete(changed);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Whether notifications of the type are on. */
|
|
70
|
+
export const isOn = (type: string) => !choices.off.includes(type);
|
|
71
|
+
|
|
72
|
+
/** Keeps a notification and sends it to the phone, when its type is on (not `device` when the Worker sends it). */
|
|
73
|
+
export function remember(item: Omit<Notification, 'at'>, device = true) {
|
|
74
|
+
if (!isOn(item.type)) return;
|
|
75
|
+
set({ ...kept, items: [{ ...item, at: Date.now() }, ...kept.items].slice(0, MAX) });
|
|
76
|
+
if (device) notifyDevice(item.text);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether an app's notification (by its own id) has been told before; marks it told. */
|
|
80
|
+
export function told(type: string, id: string) {
|
|
81
|
+
const key = `${type}:${id}`;
|
|
82
|
+
if (kept.told.includes(key)) return true;
|
|
83
|
+
set({ ...kept, told: [key, ...kept.told].slice(0, MAX_TOLD) });
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Marks them all seen (the notifications are open). */
|
|
88
|
+
export function markSeen() {
|
|
89
|
+
if (kept.items.some((n) => n.at > kept.seen)) set({ ...kept, seen: Date.now() });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function clearNotifications() {
|
|
93
|
+
set({ ...kept, items: [], seen: Date.now() });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Turns a type on or off. */
|
|
97
|
+
export function setOn(type: string, on: boolean) {
|
|
98
|
+
choose({ ...choices, off: on ? choices.off.filter((t) => t !== type) : [...choices.off, type] });
|
|
99
|
+
if (signedUp) signUp().catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const allowed = () => typeof Notification !== 'undefined' && Notification.permission === 'granted';
|
|
103
|
+
const deviceOn = () => choices.device ?? allowed();
|
|
104
|
+
|
|
105
|
+
/** Turns "På telefonen" on or off; on asks the browser first. False when the browser does not allow it. */
|
|
106
|
+
export async function setDevice(on: boolean): Promise<boolean> {
|
|
107
|
+
if (on && typeof Notification === 'undefined') return false;
|
|
108
|
+
if (on && Notification.permission === 'default') await Notification.requestPermission().catch(() => {});
|
|
109
|
+
if (on && !allowed()) return false;
|
|
110
|
+
choose({ ...choices, device: on });
|
|
111
|
+
await (on ? signUp() : signOff()).catch(() => {});
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The Worker's notifications (packages/editor/worker/push.js), with the GitHub sign-in (the Worker's cookie, sent with the request): only
|
|
116
|
+
// someone who can save the site may.
|
|
117
|
+
const PUSH = '/api/push';
|
|
118
|
+
// The endpoint this browser is signed up with at the Worker, while it is.
|
|
119
|
+
let signedUp: string | null = null;
|
|
120
|
+
|
|
121
|
+
function api(path: string, method: string, body?: unknown) {
|
|
122
|
+
return fetch(path, {
|
|
123
|
+
method,
|
|
124
|
+
cache: 'no-store',
|
|
125
|
+
headers: { 'content-type': 'application/json' },
|
|
126
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The service worker (registered when the editor opens, deploy.ts registerWorker), once it is running; none when the
|
|
131
|
+
// browser has no push (an iPhone outside the home screen).
|
|
132
|
+
const pushWorker = async (ready = true) =>
|
|
133
|
+
typeof PushManager === 'undefined' || !navigator.serviceWorker
|
|
134
|
+
? undefined
|
|
135
|
+
: ready
|
|
136
|
+
? await navigator.serviceWorker.ready
|
|
137
|
+
: await navigator.serviceWorker.getRegistration('/editor/');
|
|
138
|
+
|
|
139
|
+
const b64url = (key: ArrayBuffer | null | undefined) =>
|
|
140
|
+
key
|
|
141
|
+
? btoa(String.fromCharCode(...new Uint8Array(key)))
|
|
142
|
+
.replace(/\+/g, '-')
|
|
143
|
+
.replace(/\//g, '_')
|
|
144
|
+
.replace(/=+$/, '')
|
|
145
|
+
: '';
|
|
146
|
+
|
|
147
|
+
/** Signs this browser up at the Worker, or tells it the choices again: with the browser's push subscription to the
|
|
148
|
+
* Worker's key (a new one when the key is another), the types turned off and the editor's language. Nothing when the
|
|
149
|
+
* browser cannot (an iPhone outside the home screen) or the site keeps none (a preview of a branch). */
|
|
150
|
+
async function signUp() {
|
|
151
|
+
const worker = await pushWorker();
|
|
152
|
+
if (!worker) return;
|
|
153
|
+
const res = await api(PUSH, 'GET');
|
|
154
|
+
if (!res.ok) return;
|
|
155
|
+
const { key } = (await res.json()) as { key: string };
|
|
156
|
+
let subscription = await worker.pushManager.getSubscription();
|
|
157
|
+
if (subscription && b64url(subscription.options.applicationServerKey) !== key) {
|
|
158
|
+
await subscription.unsubscribe();
|
|
159
|
+
subscription = null;
|
|
160
|
+
}
|
|
161
|
+
subscription ??= await worker.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
|
|
162
|
+
const signed = await api(PUSH, 'PUT', { subscription: subscription.toJSON(), off: choices.off, locale });
|
|
163
|
+
signedUp = signed.ok ? subscription.endpoint : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Signs this browser off at the Worker ("På telefonen" off, "Log ud": before the sign-in goes), and from the push
|
|
167
|
+
* service. */
|
|
168
|
+
export async function signOff() {
|
|
169
|
+
signedUp = null;
|
|
170
|
+
const subscription = await (await pushWorker(false))?.pushManager.getSubscription();
|
|
171
|
+
if (!subscription) return;
|
|
172
|
+
await api(PUSH, 'DELETE', { endpoint: subscription.endpoint }).catch(() => {});
|
|
173
|
+
await subscription.unsubscribe().catch(() => {});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** When the editor opens: signs up again while "På telefonen" is on (the Worker may have a new key, or have forgotten
|
|
177
|
+
* a browser not seen for half a year), else signs off. */
|
|
178
|
+
export function syncDevice() {
|
|
179
|
+
(deviceOn() && allowed() ? signUp() : signOff()).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** After a save: asks the Worker to say on the phone when the site has been built with `commit` (`text`), or that it
|
|
183
|
+
* was not after ten minutes (`failText`). True when it will (this browser is signed up, with the type on). */
|
|
184
|
+
export async function waitOnServer(commit: string, text: string, failText: string): Promise<boolean> {
|
|
185
|
+
if (!signedUp || !isOn('site')) return false;
|
|
186
|
+
const res = await api(`${PUSH}/wait`, 'PUT', { endpoint: signedUp, commit, text, failText }).catch(() => null);
|
|
187
|
+
return !!res?.ok;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Takes the wait back: the editor said it itself. */
|
|
191
|
+
export function stopServerWait() {
|
|
192
|
+
if (signedUp) api(`${PUSH}/wait`, 'DELETE', { endpoint: signedUp }).catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Asks whether the editor may show notifications on the phone, once, while "På telefonen" is not chosen yet.
|
|
196
|
+
// Browsers only ask after a tap, so this is called from "Gem".
|
|
197
|
+
export function askToNotify() {
|
|
198
|
+
if (choices.device === undefined && typeof Notification !== 'undefined' && Notification.permission === 'default')
|
|
199
|
+
setDevice(true).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The message as a notification on the phone or computer (through the service worker, packages/editor/pages/editor/sw.js), when
|
|
203
|
+
// "På telefonen" is on and the editor is not on screen.
|
|
204
|
+
async function notifyDevice(text: string) {
|
|
205
|
+
if (document.visibilityState === 'visible' || !deviceOn() || !allowed()) return;
|
|
206
|
+
const options = { body: text, icon: '/editor/icon-192.png', tag: 'site' };
|
|
207
|
+
try {
|
|
208
|
+
const worker = await navigator.serviceWorker?.getRegistration('/editor/');
|
|
209
|
+
if (worker) await worker.showNotification(siteName, options);
|
|
210
|
+
else new Notification(siteName, options);
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const useNotifications = () => useSyncExternalStore(subscribe, () => kept);
|
|
215
|
+
|
|
216
|
+
/** The choices: the types turned off and whether "På telefonen" is on. */
|
|
217
|
+
export const useNotificationChoices = () => {
|
|
218
|
+
const now = useSyncExternalStore(subscribe, () => choices);
|
|
219
|
+
return { off: now.off, device: now.device ?? allowed() };
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** How many are not seen yet. */
|
|
223
|
+
export const unseen = ({ items, seen }: Kept) => items.filter((n) => n.at > seen).length;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// "Forhåndsvis" (FormFrame.tsx): a page or a blog post as it looks on the site with what is in its form
|
|
2
|
+
// now, at once and without saving, in a sheet, as wide as a phone or a computer.
|
|
3
|
+
//
|
|
4
|
+
// The page as it is on the site gives everything around what the form changes: the header, the footer,
|
|
5
|
+
// the layout and the site's CSS. The site's layouts mark what the form changes (data-editor): the
|
|
6
|
+
// heading, the picture at the top (a blog post's), the text, and the place at the end of the page where the layout
|
|
7
|
+
// puts a block that ends the text (data-editor="end", with its type in data-editor-block). Only what
|
|
8
|
+
// was changed is put in, so the rest is exactly as on the site. The text is the site's Markdown
|
|
9
|
+
// (Astro's pipeline, with the HTML written in it), and its blocks are drawn as in the editor
|
|
10
|
+
// (registry.tsx preview), with the site's classes.
|
|
11
|
+
//
|
|
12
|
+
// The page is shown in a frame without scripts (sandbox), so nothing of the site runs on the editor's
|
|
13
|
+
// address, where its login is kept, and its links lead nowhere. A new page or post is shown in the
|
|
14
|
+
// page of another one. As on the site but for this: a changed text has the pictures themselves (not
|
|
15
|
+
// the sizes the build makes), its words are not yet links to other pages (packages/editor/site/autolinks.mjs), and
|
|
16
|
+
// a video written in it as HTML is the video.
|
|
17
|
+
import { useRef, useState, type ReactNode } from 'react';
|
|
18
|
+
import { createRoot } from 'react-dom/client';
|
|
19
|
+
import { flushSync } from 'react-dom';
|
|
20
|
+
import { DeviceMobile, Desktop } from '@phosphor-icons/react';
|
|
21
|
+
import { parseBlock, splitBlocks } from '../lib/blocks.mjs';
|
|
22
|
+
import { processor } from '../lib/markdown';
|
|
23
|
+
import { splitPicture } from '../lib/crop.mjs';
|
|
24
|
+
import { preview as drawBlock, siteBlocks } from './registry';
|
|
25
|
+
import { shownAt } from './images';
|
|
26
|
+
import { pageH1 } from './sections.mjs';
|
|
27
|
+
import type { BlockData } from './blocks';
|
|
28
|
+
import { PageFrame, BottomSheet, SegmentedButton, SheetTitle } from './ui';
|
|
29
|
+
import { t } from './i18n';
|
|
30
|
+
|
|
31
|
+
/** What a form puts in the page: undefined leaves it as it is on the site. */
|
|
32
|
+
export type PageDraft = {
|
|
33
|
+
/** The page on the site to show it in (its address); for a new one, others like it, the first
|
|
34
|
+
* that can be. */
|
|
35
|
+
shell: string | string[];
|
|
36
|
+
heading?: string;
|
|
37
|
+
/** The heading as HTML with its colours, when a word in it has one (pageH1 in sections.mjs). */
|
|
38
|
+
headingHtml?: string;
|
|
39
|
+
/** The picture at the top (a blog post's); null: none. */
|
|
40
|
+
image?: { src: string; alt: string } | null;
|
|
41
|
+
/** The text, in Markdown with its blocks. */
|
|
42
|
+
body?: string;
|
|
43
|
+
/** What the blocks are drawn with. */
|
|
44
|
+
data: BlockData;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// The text as the site makes it (Astro's Markdown, ../lib/markdown.ts), with the site's plugins (packages/editor/astro.mjs)
|
|
48
|
+
// but those that read the site's files.
|
|
49
|
+
const markdownHtml = async (markdown: string) => String(await processor.process(markdown));
|
|
50
|
+
|
|
51
|
+
// A block drawn as in the editor, as HTML.
|
|
52
|
+
function blockHtml(type: string, props: Record<string, unknown>, data: BlockData) {
|
|
53
|
+
const drawn: ReactNode = drawBlock(type, props, data);
|
|
54
|
+
if (!drawn) return '';
|
|
55
|
+
const box = document.createElement('div');
|
|
56
|
+
const root = createRoot(box);
|
|
57
|
+
flushSync(() => root.render(drawn));
|
|
58
|
+
const html = box.innerHTML;
|
|
59
|
+
root.unmount();
|
|
60
|
+
return html;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A picture made ready in this visit, which the site does not have yet (images.ts shownAt), in the
|
|
64
|
+
// page itself.
|
|
65
|
+
async function inline(src: string) {
|
|
66
|
+
const here = shownAt(src);
|
|
67
|
+
if (here === src) return src;
|
|
68
|
+
const blob = await (await fetch(here)).blob();
|
|
69
|
+
return new Promise<string>((done) => {
|
|
70
|
+
const reader = new FileReader();
|
|
71
|
+
reader.onload = () => done(String(reader.result));
|
|
72
|
+
reader.readAsDataURL(blob);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The page on the site, as a document. From the editor's own address too (EDITOR_HOST), which serves
|
|
77
|
+
// it with this header (packages/editor/worker/editor-host.js).
|
|
78
|
+
async function sitePage(address: string) {
|
|
79
|
+
const res = await fetch(address, { headers: { 'x-editor-preview': '1' } });
|
|
80
|
+
if (!res.ok) throw new Error(t.preview.pageNotLoaded(res.status));
|
|
81
|
+
return new DOMParser().parseFromString(await res.text(), 'text/html');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The page with the draft in it, as HTML for a frame (PageFrame). */
|
|
85
|
+
export async function pageDocument(draft: PageDraft): Promise<string> {
|
|
86
|
+
let doc: Document | null = null;
|
|
87
|
+
let shell = '';
|
|
88
|
+
for (const address of [draft.shell].flat()) {
|
|
89
|
+
const page = await sitePage(address);
|
|
90
|
+
if (page.querySelector('[data-editor="text"]')) {
|
|
91
|
+
doc = page;
|
|
92
|
+
shell = address;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!doc) throw new Error(t.preview.notHere);
|
|
97
|
+
doc.querySelectorAll('script').forEach((s) => s.remove());
|
|
98
|
+
// Its own addresses as on the site; a link opens nothing (target, and no popups in the frame).
|
|
99
|
+
const base = doc.createElement('base');
|
|
100
|
+
base.href = new URL(shell, location.href).href;
|
|
101
|
+
base.target = '_blank';
|
|
102
|
+
doc.head.prepend(base);
|
|
103
|
+
|
|
104
|
+
const heading = doc.querySelector('[data-editor="heading"]');
|
|
105
|
+
if (heading && draft.headingHtml) heading.innerHTML = draft.headingHtml;
|
|
106
|
+
else if (heading && draft.heading !== undefined) heading.textContent = draft.heading;
|
|
107
|
+
|
|
108
|
+
const top = doc.querySelector('[data-editor="image"]');
|
|
109
|
+
if (top && draft.image !== undefined) {
|
|
110
|
+
const { src } = splitPicture(draft.image?.src ?? '');
|
|
111
|
+
// The layout leaves out a picture that is also in the text (heroImage in packages/editor/site/images.mjs).
|
|
112
|
+
if (!src || (draft.body ?? '').includes(src)) top.remove();
|
|
113
|
+
else {
|
|
114
|
+
top.querySelectorAll('source').forEach((s) => s.remove());
|
|
115
|
+
const img = top.querySelector('img');
|
|
116
|
+
if (img) {
|
|
117
|
+
for (const a of ['srcset', 'sizes', 'width', 'height']) img.removeAttribute(a);
|
|
118
|
+
img.src = src;
|
|
119
|
+
img.alt = draft.image!.alt;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (draft.body !== undefined) {
|
|
125
|
+
// A page's H1 at the top of its text is drawn in its own place above (draft.heading), as on the site.
|
|
126
|
+
let html = await markdownHtml(pageH1(draft.body).rest);
|
|
127
|
+
// A block that ends the text, where the layout puts it.
|
|
128
|
+
const end = doc.querySelector('[data-editor="end"]');
|
|
129
|
+
const type = end?.getAttribute('data-editor-block');
|
|
130
|
+
const last = type ? html.match(new RegExp(`<div data-block="${type}"(?: data-props="[^"]*")?\\s*></div>\\s*$`)) : null;
|
|
131
|
+
const block = last && parseBlock(last[0]);
|
|
132
|
+
if (end && block && !block.props.hidden) {
|
|
133
|
+
html = html.slice(0, last!.index);
|
|
134
|
+
end.innerHTML = blockHtml(block.type, block.props, draft.data);
|
|
135
|
+
} else end?.remove();
|
|
136
|
+
const text = doc.querySelector('[data-editor="text"]')!;
|
|
137
|
+
text.innerHTML = splitBlocks(
|
|
138
|
+
html,
|
|
139
|
+
siteBlocks.map((b) => b.type),
|
|
140
|
+
)
|
|
141
|
+
.map((p) => ('html' in p ? p.html : blockHtml(p.type, p.props, draft.data)))
|
|
142
|
+
.join('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await Promise.all(
|
|
146
|
+
[...doc.querySelectorAll('img')].map(async (img) => img.setAttribute('src', await inline(img.getAttribute('src') ?? ''))),
|
|
147
|
+
);
|
|
148
|
+
return `<!doctype html>\n${doc.documentElement.outerHTML}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type Device = 'phone' | 'computer';
|
|
152
|
+
const WIDTH: Record<Device, number> = { phone: 390, computer: 1280 };
|
|
153
|
+
|
|
154
|
+
/** The sheet with the preview, and how to open it with a draft (made when it opens). */
|
|
155
|
+
export function usePagePreview(): [(draft: () => PageDraft | null) => void, ReactNode] {
|
|
156
|
+
const dialog = useRef<HTMLDialogElement>(null);
|
|
157
|
+
const [html, setHtml] = useState<string | null>(null);
|
|
158
|
+
const [problem, setProblem] = useState('');
|
|
159
|
+
const [device, setDevice] = useState<Device>(() => (matchMedia('(min-width: 48rem)').matches ? 'computer' : 'phone'));
|
|
160
|
+
const run = useRef(0);
|
|
161
|
+
|
|
162
|
+
function open(make: () => PageDraft | null) {
|
|
163
|
+
const draft = make();
|
|
164
|
+
if (!draft) return;
|
|
165
|
+
const n = ++run.current;
|
|
166
|
+
setHtml(null);
|
|
167
|
+
setProblem('');
|
|
168
|
+
dialog.current?.showModal();
|
|
169
|
+
pageDocument(draft).then(
|
|
170
|
+
(page) => n === run.current && setHtml(page),
|
|
171
|
+
(e) => n === run.current && setProblem(t.preview.failed((e as Error).message)),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const close = () => dialog.current?.close();
|
|
176
|
+
const sheet = (
|
|
177
|
+
<BottomSheet
|
|
178
|
+
full
|
|
179
|
+
ref={dialog}
|
|
180
|
+
aria-label={t.preview.title}
|
|
181
|
+
onClick={(e) => e.target === dialog.current && close()}
|
|
182
|
+
onClose={() => run.current++}
|
|
183
|
+
>
|
|
184
|
+
<SheetTitle title={t.preview.title} onClose={close} />
|
|
185
|
+
<div className="px-5 pb-3">
|
|
186
|
+
<SegmentedButton
|
|
187
|
+
label={t.preview.device}
|
|
188
|
+
value={device}
|
|
189
|
+
options={[
|
|
190
|
+
{ value: 'phone', label: t.preview.phone, icon: DeviceMobile },
|
|
191
|
+
{ value: 'computer', label: t.preview.computer, icon: Desktop },
|
|
192
|
+
]}
|
|
193
|
+
onChange={setDevice}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
{html ? (
|
|
197
|
+
<PageFrame html={html} width={WIDTH[device]} title={t.preview.title} />
|
|
198
|
+
) : (
|
|
199
|
+
<p className="px-5 text-muted">{problem || t.preview.making}</p>
|
|
200
|
+
)}
|
|
201
|
+
</BottomSheet>
|
|
202
|
+
);
|
|
203
|
+
return [open, sheet];
|
|
204
|
+
}
|
package/src/r2Caching.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// R2 caching: the Worker's copy of the site's files in R2 (packages/editor/worker/files.js), turned on and off under "Indstillinger" ›
|
|
2
|
+
// "Avanceret" (Advanced.tsx). It is the line of its bucket (`"r2_buckets": …`, the binding FILES) in the Worker's settings
|
|
3
|
+
// (config.wrangler): there when it is on, there as a comment ("// …") when it is off, so a deploy never asks for a bucket
|
|
4
|
+
// Cloudflare does not have. Changed as text, so the file's comments stay. It can only be turned on when the Worker says
|
|
5
|
+
// Cloudflare has the bucket (/api/r2, packages/editor/worker/r2-status.js). Where the bucket is kept ("Placering"): placed by Cloudflare,
|
|
6
|
+
// or within a jurisdiction (`"jurisdiction": "eu"` in the line), where it is asked for: Cloudflare has no bucket of that
|
|
7
|
+
// name outside it. It can only be changed while R2 caching is off, since it names another bucket.
|
|
8
|
+
|
|
9
|
+
const LINE = /^([ \t]*)(\/\/[ \t]*)?("r2_buckets"[ \t]*:.*)$/m;
|
|
10
|
+
|
|
11
|
+
/** Whether R2 caching is on in the Worker's settings; null when they have no line for it. */
|
|
12
|
+
export function r2CachingOn(text: string): boolean | null {
|
|
13
|
+
const line = LINE.exec(text);
|
|
14
|
+
return line ? !line[2] : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The Worker's settings with R2 caching on or off. */
|
|
18
|
+
export const withR2Caching = (text: string, on: boolean): string =>
|
|
19
|
+
text.replace(LINE, (_, indent: string, _comment: string | undefined, line: string) => `${indent}${on ? '' : '// '}${line}`);
|
|
20
|
+
|
|
21
|
+
/** The name of the bucket in the line, or null. */
|
|
22
|
+
export const bucketOf = (text: string): string | null => /"bucket_name"\s*:\s*"([^"]+)"/.exec(LINE.exec(text)?.[3] ?? '')?.[1] ?? null;
|
|
23
|
+
|
|
24
|
+
/** The jurisdiction the bucket is kept within (`eu`, `fedramp`), or null when the line names none. */
|
|
25
|
+
export const jurisdictionOf = (text: string): string | null =>
|
|
26
|
+
/"jurisdiction"\s*:\s*"([^"]+)"/.exec(LINE.exec(text)?.[3] ?? '')?.[1] ?? null;
|
|
27
|
+
|
|
28
|
+
/** The jurisdictions the bucket can be kept within, in the editor (Cloudflare also has `fedramp`, for the US government). */
|
|
29
|
+
export const JURISDICTIONS = ['eu'] as const;
|
|
30
|
+
|
|
31
|
+
/** The Worker's settings with the bucket kept within `jurisdiction`, or placed by Cloudflare (null). */
|
|
32
|
+
export const withJurisdiction = (text: string, jurisdiction: string | null): string =>
|
|
33
|
+
text.replace(LINE, (_, indent: string, comment: string | undefined, line: string) => {
|
|
34
|
+
const without = line.replace(/,\s*"jurisdiction"\s*:\s*"[^"]*"/, '');
|
|
35
|
+
const changed = jurisdiction ? without.replace(/("bucket_name"\s*:\s*"[^"]+")/, `$1, "jurisdiction": "${jurisdiction}"`) : without;
|
|
36
|
+
return `${indent}${comment ?? ''}${changed}`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** A jurisdiction's name, as Cloudflare writes it. */
|
|
40
|
+
export const jurisdictionName = (jurisdiction: string): string => ({ eu: 'EU', fedramp: 'FedRAMP' })[jurisdiction] ?? jurisdiction;
|
|
41
|
+
|
|
42
|
+
/** The bucket at Cloudflare, as the Worker says (packages/editor/worker/r2-status.js). */
|
|
43
|
+
export type BucketState = 'ready' | 'no-bucket' | 'no-r2' | 'no-key' | 'refused' | 'failed';
|
|
44
|
+
|
|
45
|
+
/** Whether the Worker has the bucket now (the site is built with R2 caching on), and the bucket at Cloudflare. */
|
|
46
|
+
export async function r2Status(bucket: string, jurisdiction: string | null): Promise<{ bound: boolean; bucket: BucketState }> {
|
|
47
|
+
const query = new URLSearchParams({ bucket, ...(jurisdiction ? { jurisdiction } : {}) });
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`/api/r2?${query}`, { cache: 'no-store' });
|
|
50
|
+
if (!res.ok || !res.headers.get('content-type')?.includes('application/json')) return { bound: false, bucket: 'failed' };
|
|
51
|
+
return await res.json();
|
|
52
|
+
} catch {
|
|
53
|
+
return { bound: false, bucket: 'failed' };
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/redirects.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// All the site's redirects as one: the editor's own (src/data/redirects.json, written when a page
|
|
2
|
+
// moves, addresses.ts), the rules (public/_redirects, "<from> <to> 301", one to a line) and the
|
|
3
|
+
// Worker's map ({ paths, ids }, written with one space of indentation, e.g. by a migration);
|
|
4
|
+
// their files are in config.ts. The Worker sends on by the editor's own first, then by its map, and
|
|
5
|
+
// then the rules are read (packages/editor/worker/index.js); so does lookup here. "Videresendelser" under
|
|
6
|
+
// Indstillinger (Redirects.tsx) shows and changes them, and a page that takes the address of one
|
|
7
|
+
// removes it (EntryForm.tsx). Every change ends with flatten: each redirect sends straight to where
|
|
8
|
+
// it ends, never on through another (no chain, one redirect for the visitor and for Google).
|
|
9
|
+
import { escape, readRedirects, REDIRECTS, writeRedirects, type Redirects } from './addresses';
|
|
10
|
+
import type { Change, RepoFile } from './github';
|
|
11
|
+
import { config } from '@site/config';
|
|
12
|
+
import { locale, t } from './i18n';
|
|
13
|
+
|
|
14
|
+
export const RULES = config.siteRedirects.rules;
|
|
15
|
+
export const MAP = config.siteRedirects.map;
|
|
16
|
+
/** The files of the redirects, loaded when they are needed (Env.load): they change rarely. */
|
|
17
|
+
export const REDIRECT_FILES = [REDIRECTS, RULES, MAP];
|
|
18
|
+
|
|
19
|
+
type WorkerMap = { paths: Redirects; ids: Redirects };
|
|
20
|
+
/** The redirects of the three files; rules: the lines of the rules' file, as they are. */
|
|
21
|
+
export type AllRedirects = { own: Redirects; rules: string[]; map: WorkerMap };
|
|
22
|
+
|
|
23
|
+
// A line of the rules as its addresses; null for a comment or an empty line.
|
|
24
|
+
function ruleOf(line: string): { from: string; to: string } | null {
|
|
25
|
+
const [from, to] = line.trim().split(/\s+/);
|
|
26
|
+
return !from || from.startsWith('#') || !to ? null : { from, to };
|
|
27
|
+
}
|
|
28
|
+
// The line with another target.
|
|
29
|
+
const withTarget = (line: string, to: string, next: string) => line.replace(new RegExp(`(\\s)${escape(to)}(?=\\s|$)`), `$1${next}`);
|
|
30
|
+
|
|
31
|
+
export function readAll(files: RepoFile[]): AllRedirects {
|
|
32
|
+
const text = (path: string) => files.find((f) => f.path === path)?.text;
|
|
33
|
+
const map = text(MAP);
|
|
34
|
+
return {
|
|
35
|
+
own: readRedirects(text(REDIRECTS)),
|
|
36
|
+
rules: (text(RULES) ?? '').split('\n'),
|
|
37
|
+
map: map ? (JSON.parse(map) as WorkerMap) : { paths: {}, ids: {} },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The files that changed, as they are written, to save. */
|
|
42
|
+
export function changesOf(all: AllRedirects, files: RepoFile[]): Change[] {
|
|
43
|
+
const written: [string, string][] = [
|
|
44
|
+
[REDIRECTS, writeRedirects(all.own)],
|
|
45
|
+
[RULES, all.rules.join('\n')],
|
|
46
|
+
[MAP, `${JSON.stringify(all.map, null, 1)}\n`],
|
|
47
|
+
];
|
|
48
|
+
return written.flatMap(([path, content]) => {
|
|
49
|
+
const file = files.find((f) => f.path === path);
|
|
50
|
+
// A file that is not there is written only for the editor's own (a new site has none of the others).
|
|
51
|
+
if (!file && path !== REDIRECTS) return [];
|
|
52
|
+
const was = file?.text ?? writeRedirects({});
|
|
53
|
+
return content === was ? [] : [{ path, content, oid: file?.oid ?? null }];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Where an address is sent on to first, as the Worker does it, or undefined. */
|
|
58
|
+
export function lookup(all: AllRedirects): (address: string) => string | undefined {
|
|
59
|
+
const rules = new Map<string, string>();
|
|
60
|
+
for (const line of all.rules) {
|
|
61
|
+
const r = ruleOf(line);
|
|
62
|
+
if (r && !r.from.includes('*') && !rules.has(r.from)) rules.set(r.from, r.to);
|
|
63
|
+
}
|
|
64
|
+
return (address) => all.own[address] ?? all.map.paths[address] ?? rules.get(address);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Where a target ends after every redirect (keeping its #part when the last has none), and the
|
|
68
|
+
* addresses when they come back to one (a loop). */
|
|
69
|
+
export function endOf(target: string, look: (address: string) => string | undefined): { end: string; loop: string[] | null } {
|
|
70
|
+
const seen: string[] = [];
|
|
71
|
+
let at = target;
|
|
72
|
+
for (;;) {
|
|
73
|
+
const [base, part] = at.split('#');
|
|
74
|
+
const next = look(base);
|
|
75
|
+
if (next === undefined) return { end: at, loop: null };
|
|
76
|
+
if (seen.includes(base)) return { end: at, loop: [...seen, base] };
|
|
77
|
+
seen.push(base);
|
|
78
|
+
at = part !== undefined && !next.includes('#') ? `${next}#${part}` : next;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every redirect straight to where it ends; a message when they would send in a loop. */
|
|
83
|
+
export function flatten(all: AllRedirects): AllRedirects | string {
|
|
84
|
+
const look = lookup(all);
|
|
85
|
+
let loop: string[] | null = null;
|
|
86
|
+
const end = (target: string) => {
|
|
87
|
+
if (!target.startsWith('/') || loop) return target;
|
|
88
|
+
const r = endOf(target, look);
|
|
89
|
+
loop = r.loop;
|
|
90
|
+
return r.end;
|
|
91
|
+
};
|
|
92
|
+
const each = (redirects: Redirects) => Object.fromEntries(Object.entries(redirects).map(([from, to]) => [from, end(to)]));
|
|
93
|
+
const next: AllRedirects = {
|
|
94
|
+
own: each(all.own),
|
|
95
|
+
map: { ...all.map, paths: each(all.map.paths), ids: each(all.map.ids) },
|
|
96
|
+
rules: all.rules.map((line) => {
|
|
97
|
+
const r = ruleOf(line);
|
|
98
|
+
const to = r && end(r.to);
|
|
99
|
+
return r && to !== r.to ? withTarget(line, r.to, to!) : line;
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
return loop ? t.redirects.loop((loop as string[]).join(' → ')) : next;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The redirects with the one from an address sending to `to` instead, or removed (null), in every
|
|
106
|
+
* file it is in; a new one goes in the editor's own. */
|
|
107
|
+
export function setRedirect(all: AllRedirects, from: string, to: string | null): AllRedirects {
|
|
108
|
+
const own = { ...all.own };
|
|
109
|
+
const paths = { ...all.map.paths };
|
|
110
|
+
let found = false;
|
|
111
|
+
for (const redirects of [own, paths])
|
|
112
|
+
if (from in redirects) {
|
|
113
|
+
found = true;
|
|
114
|
+
if (to === null) delete redirects[from];
|
|
115
|
+
else redirects[from] = to;
|
|
116
|
+
}
|
|
117
|
+
const rules = all.rules.flatMap((line) => {
|
|
118
|
+
const r = ruleOf(line);
|
|
119
|
+
if (r?.from !== from) return [line];
|
|
120
|
+
found = true;
|
|
121
|
+
return to === null ? [] : [withTarget(line, r.to, to)];
|
|
122
|
+
});
|
|
123
|
+
if (!found && to !== null) own[from] = to;
|
|
124
|
+
return { own, rules, map: { ...all.map, paths } };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A redirect from the address of a page (not a picture or another file, not a rule with a *),
|
|
128
|
+
* where to, and the files it is in. */
|
|
129
|
+
export type Entry = { from: string; to: string; files: string[] };
|
|
130
|
+
|
|
131
|
+
const isPage = (address: string) => /^\/([^/.*]+\/)*$/.test(address);
|
|
132
|
+
|
|
133
|
+
/** The redirects from addresses of pages, by address; `to` as the Worker sends on (the first file). */
|
|
134
|
+
export function entriesOf(all: AllRedirects): Entry[] {
|
|
135
|
+
const entries = new Map<string, Entry>();
|
|
136
|
+
const add = (from: string, to: string, file: string) => {
|
|
137
|
+
if (!isPage(from)) return;
|
|
138
|
+
const entry = entries.get(from);
|
|
139
|
+
if (entry) entry.files.push(file);
|
|
140
|
+
else entries.set(from, { from, to, files: [file] });
|
|
141
|
+
};
|
|
142
|
+
for (const [from, to] of Object.entries(all.own)) add(from, to, REDIRECTS);
|
|
143
|
+
for (const [from, to] of Object.entries(all.map.paths)) add(from, to, MAP);
|
|
144
|
+
for (const line of all.rules) {
|
|
145
|
+
const r = ruleOf(line);
|
|
146
|
+
if (r) add(r.from, r.to, RULES);
|
|
147
|
+
}
|
|
148
|
+
return [...entries.values()].sort((a, b) => a.from.localeCompare(b.from, locale));
|
|
149
|
+
}
|