@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,740 @@
|
|
|
1
|
+
// One blog post or page: its fields and its text, and "Gem", which saves the file as a commit.
|
|
2
|
+
// Only what was changed is written: untouched fields keep their exact text, and the body is only
|
|
3
|
+
// rewritten by Milkdown when the text was edited (packages/editor/scripts/editor-roundtrip.mjs checks that this
|
|
4
|
+
// gives the same page). A new address (Adresse) moves the page or post, with a redirect from the
|
|
5
|
+
// old one (addresses.ts). An earlier version (Versions.tsx) is opened as changes that are not saved,
|
|
6
|
+
// at the address the page or post has now.
|
|
7
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { flushSync } from 'react-dom';
|
|
9
|
+
import { slugify, slugOf, type Field, type MarkdownCollection } from './schema';
|
|
10
|
+
import { config, frontLeads, headingLooks, sectionLooks, sharing, standardPages, standardPaths } from '@site/editor';
|
|
11
|
+
import { asWritten, placeOf, standardPageAt } from './site';
|
|
12
|
+
import { ConflictError, type Change, type RepoFile } from './github';
|
|
13
|
+
import { addressOf, moveRedirects, REDIRECTS, retarget } from './addresses';
|
|
14
|
+
import { changesOf, flatten, readAll, REDIRECT_FILES, setRedirect } from './redirects';
|
|
15
|
+
import { TAB_ROUTES } from './Redirects';
|
|
16
|
+
import { choose } from './ask';
|
|
17
|
+
import { readFields, splitFrontmatter, writeFields } from './frontmatter.mjs';
|
|
18
|
+
import MarkdownEditor, { type MarkdownHandle } from './MarkdownEditor';
|
|
19
|
+
import { useBlockDialog } from './BlockDialog';
|
|
20
|
+
import { blockHtml } from '../lib/blocks.mjs';
|
|
21
|
+
import { sectionBlocks } from './blocks';
|
|
22
|
+
import { looksOf, pageH1 } from './sections.mjs';
|
|
23
|
+
import { shownAt, useUploads } from './images';
|
|
24
|
+
import { useMediaPicker } from './Media';
|
|
25
|
+
import { inUse, problemAt } from './check';
|
|
26
|
+
import { isOver } from '../lib/notice.mjs';
|
|
27
|
+
import { splitPicture } from '../lib/crop.mjs';
|
|
28
|
+
import { asStandardTemplate, blockProblems, notOffered, siteBlocks } from './registry';
|
|
29
|
+
import { FieldInput, Fields, useWrongField, WrongField, type FieldContext } from './fields';
|
|
30
|
+
import { Article, ClockCounterClockwise, Eye, MagnifyingGlass, ShareNetwork, SlidersHorizontal } from '@phosphor-icons/react';
|
|
31
|
+
import {
|
|
32
|
+
CheckList,
|
|
33
|
+
FoldSection,
|
|
34
|
+
FormColumns,
|
|
35
|
+
ModalNavigationDrawer,
|
|
36
|
+
NavigationDrawer,
|
|
37
|
+
NavigationDrawerButton,
|
|
38
|
+
type NavigationItem,
|
|
39
|
+
SharePreview,
|
|
40
|
+
useInView,
|
|
41
|
+
} from './ui';
|
|
42
|
+
import FormFrame, { savedStatus, useSavedAt } from './FormFrame';
|
|
43
|
+
import { usePagePreview } from './pagePreview';
|
|
44
|
+
import { savedOn, VersionList, VersionNotice, WithVersions, type ShownVersion } from './Versions';
|
|
45
|
+
import type { Env } from './App';
|
|
46
|
+
import { kindAndName, savedText } from './deploy';
|
|
47
|
+
import { siteT, siteText, t } from './i18n';
|
|
48
|
+
import { firstPicture, SEO_FIELDS, seoInputOf, seoRating, SHARE_FIELDS, sharedAs, type SeoCheck } from './seo';
|
|
49
|
+
|
|
50
|
+
// At the top: the status (Kladde, Skjult, Udgivet, Planlagt and the time for Planlagt), then the title.
|
|
51
|
+
// On a wide computer screen the status is at the right above the other fields (FormColumns).
|
|
52
|
+
const TOP = ['status', 'publishAt', 'title', 'heading'];
|
|
53
|
+
const STATUS = ['status', 'publishAt'];
|
|
54
|
+
// Under their own headings at the bottom: the Google texts, the keywords and the canonical address; then the picture, title
|
|
55
|
+
// and description when the page is shared.
|
|
56
|
+
const SEO = SEO_FIELDS;
|
|
57
|
+
const SHARE = SHARE_FIELDS;
|
|
58
|
+
// The sections of the form, under each other on one page, each folding in and out under its heading (FoldSection in
|
|
59
|
+
// ui.tsx): the text with the title and heading (open from the start), the status, the other fields, those for search
|
|
60
|
+
// engines, those for social media, and the earlier versions (folded from the start, so the page is short; nothing is remembered till
|
|
61
|
+
// the next time). Its navigation drawer (on a wide computer screen in the right column, narrower from the icon at the
|
|
62
|
+
// right of the top bar) opens one and scrolls to it, and marks the one in view. A field that keeps the form from being
|
|
63
|
+
// saved has its section opened and is scrolled to (FieldInput in fields.tsx).
|
|
64
|
+
type Section = 'content' | 'status' | 'other' | 'seo' | 'share' | 'versions';
|
|
65
|
+
const sectionOf = (path: string): Section => {
|
|
66
|
+
const name = path.split('.')[0];
|
|
67
|
+
return STATUS.includes(name)
|
|
68
|
+
? 'status'
|
|
69
|
+
: TOP.includes(name)
|
|
70
|
+
? 'content'
|
|
71
|
+
: SEO.includes(name)
|
|
72
|
+
? 'seo'
|
|
73
|
+
: SHARE.includes(name)
|
|
74
|
+
? 'share'
|
|
75
|
+
: 'other';
|
|
76
|
+
};
|
|
77
|
+
// The section the next page or post opened starts at: "Søgemaskiner" when it is opened from "SEO-vurdering"
|
|
78
|
+
// (SeoScores.tsx), else the text.
|
|
79
|
+
let opening: Section | null = null;
|
|
80
|
+
export const openAtSeo = () => {
|
|
81
|
+
opening = 'seo';
|
|
82
|
+
};
|
|
83
|
+
// The fields of a section (the other fields are those of none of the others).
|
|
84
|
+
const inSection = (id: Section) => (f: Field) => sectionOf(f.name) === id;
|
|
85
|
+
|
|
86
|
+
// A new file: a blog post gets today's date; a page has the blocks its template
|
|
87
|
+
// starts it with (Template.start), e.g. "Book en tid" at the end like the other pages, but not one that
|
|
88
|
+
// is turned off (off, "Blokindstillinger").
|
|
89
|
+
export const newMarkdown = (collection: MarkdownCollection, off: string[] = []): RepoFile => ({
|
|
90
|
+
path: '',
|
|
91
|
+
oid: '',
|
|
92
|
+
text:
|
|
93
|
+
collection.id === 'blog'
|
|
94
|
+
? `---\ndate: ${new Date().toLocaleDateString('sv')}\n---\n`
|
|
95
|
+
: `---\n---\n${(collection.template?.start ?? [])
|
|
96
|
+
.filter((type) => !off.includes(type))
|
|
97
|
+
.map((type) => `\n${blockHtml(type)}\n`)
|
|
98
|
+
.join('')}`,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// The address of a saved page or post: a page's own `path`, or else /<file name>/ (packages/editor/site/content.ts).
|
|
102
|
+
// '' for a new one.
|
|
103
|
+
// Where the redirects are, as the start screen has it: its group (the site's), or "Indstillinger", › "Søgemaskineoptimering" › "Videresendelser" ›
|
|
104
|
+
// "Alle videresendelser".
|
|
105
|
+
const redirectsPath = () => [placeOf('seo'), t.common.parts.seo, t.common.parts.redirects, t.redirects.all].filter(Boolean).join(' › ');
|
|
106
|
+
|
|
107
|
+
function addressNow(collection: MarkdownCollection, file: RepoFile, data: Record<string, unknown>) {
|
|
108
|
+
if (!file.path) return '';
|
|
109
|
+
return (collection.id === 'pages' && data.path ? String(data.path) : '') || `/${slugOf(file.path, collection.folder)}/`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function valuesOf(collection: MarkdownCollection, file: RepoFile): Record<string, unknown> {
|
|
113
|
+
const data = readFields(splitFrontmatter(file.text).yaml);
|
|
114
|
+
return Object.fromEntries(
|
|
115
|
+
collection.fields.map((f) => {
|
|
116
|
+
const v = data[f.name];
|
|
117
|
+
if (f.name === 'path') return [f.name, addressNow(collection, file, data)];
|
|
118
|
+
if (f.type === 'boolean') return [f.name, v === true];
|
|
119
|
+
if (f.multiple) return [f.name, Array.isArray(v) ? v.map(String) : []];
|
|
120
|
+
return [f.name, v == null ? '' : String(v)];
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Scrolling to a part of the form: smoothly, unless less motion is asked for.
|
|
126
|
+
const scrolling = (): ScrollBehavior => (matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth');
|
|
127
|
+
|
|
128
|
+
const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
|
|
129
|
+
|
|
130
|
+
// The values as they are saved: a field only in use with another's value (Field.only: "Tidspunkt" with
|
|
131
|
+
// "Planlagt") is empty without it, so it leaves the file.
|
|
132
|
+
const inForm = (fields: Field[], values: Record<string, unknown>) =>
|
|
133
|
+
Object.fromEntries(fields.map((f) => [f.name, inUse(f, fields, values) ? values[f.name] : '']));
|
|
134
|
+
|
|
135
|
+
type Props = { collection: MarkdownCollection; file: RepoFile; env: Env };
|
|
136
|
+
|
|
137
|
+
// The form starts again with an earlier version, or with the one saved now.
|
|
138
|
+
export default function EntryForm(props: Props) {
|
|
139
|
+
return (
|
|
140
|
+
<WithVersions notify={props.env.notify}>
|
|
141
|
+
{(version, onVersion) => <Form {...props} version={version} onVersion={onVersion} />}
|
|
142
|
+
</WithVersions>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Form({
|
|
147
|
+
collection,
|
|
148
|
+
file,
|
|
149
|
+
env,
|
|
150
|
+
version,
|
|
151
|
+
onVersion,
|
|
152
|
+
}: Props & { version: ShownVersion | null; onVersion: (v: ShownVersion | null) => void }) {
|
|
153
|
+
// The file as last loaded or saved; what the form compares with and writes on top of.
|
|
154
|
+
const [base, setBase] = useState(file);
|
|
155
|
+
const parts = useMemo(() => splitFrontmatter(base.text), [base]);
|
|
156
|
+
const initial = useMemo(() => valuesOf(collection, base), [collection, base]);
|
|
157
|
+
// An earlier version: its fields and text, at the address there is now.
|
|
158
|
+
const [shown] = useState(() => version && splitFrontmatter(version.text));
|
|
159
|
+
const [values, setValues] = useState(() =>
|
|
160
|
+
version ? { ...valuesOf(collection, { ...base, text: version.text }), path: initial.path } : initial,
|
|
161
|
+
);
|
|
162
|
+
const [bodyChanged, setBodyChanged] = useState(() => !!shown && shown.body !== parts.body);
|
|
163
|
+
const [saving, setSaving] = useState(false);
|
|
164
|
+
const [deleting, setDeleting] = useState(false);
|
|
165
|
+
const text = useRef<MarkdownHandle>(null);
|
|
166
|
+
// Where the page or post is on the site now.
|
|
167
|
+
const url = String(initial.path);
|
|
168
|
+
// Which of the site's blocks to put in the text, and their fields (pages only).
|
|
169
|
+
// What can be put in: what the template allows, but not the blocks turned off under
|
|
170
|
+
// "Blokindstillinger", nor those of an app not installed (those already in the text stay, and are
|
|
171
|
+
// checked by the template alone when saved).
|
|
172
|
+
const off = useMemo(() => notOffered(env.files), [env.files]);
|
|
173
|
+
// A page chosen as a standard page ("Indstillinger" › "Udseende" › "Standardsider", packages/editor/defaults/standardPages.ts) has its
|
|
174
|
+
// rules too (the blog page has "Blogindlæg"). The front page is written as it looks: the heading, text and picture of a
|
|
175
|
+
// block as the text above its card (its lead), and its top ("Hero") first.
|
|
176
|
+
const paths = standardPaths(env.files.find((f) => f.path === config.standardPages)?.text);
|
|
177
|
+
const standard = url ? standardPages.find((p) => paths[p.key] === url) : undefined;
|
|
178
|
+
const front = standard?.key === 'front';
|
|
179
|
+
const template = useMemo(() => asStandardTemplate(collection.template, standard), [standard, collection]);
|
|
180
|
+
const insertable = useMemo(
|
|
181
|
+
() =>
|
|
182
|
+
off.length ? { ...template, blocks: (template?.blocks ?? siteBlocks.map((b) => b.type)).filter((t) => !off.includes(t)) } : template,
|
|
183
|
+
[off, template],
|
|
184
|
+
);
|
|
185
|
+
const [blockAsk, blockDialog] = useBlockDialog(
|
|
186
|
+
useMemo(() => (front ? sectionBlocks(siteBlocks, frontLeads) : undefined), [front]),
|
|
187
|
+
{
|
|
188
|
+
lists: env.lists,
|
|
189
|
+
testimonials: env.testimonials,
|
|
190
|
+
site: env.site,
|
|
191
|
+
relations: env.relations,
|
|
192
|
+
path: url,
|
|
193
|
+
// A standard page's blocks drawn as the theme draws them there (its editor/previews.tsx, by `<file>#<key>`).
|
|
194
|
+
...(standard ? { page: { file: `${config.standardPages}#${standard.key}`, values } } : {}),
|
|
195
|
+
},
|
|
196
|
+
insertable,
|
|
197
|
+
);
|
|
198
|
+
// When the file was last saved, shown in the top bar.
|
|
199
|
+
const [savedAt, setSavedAt] = useSavedAt(env, file.path);
|
|
200
|
+
|
|
201
|
+
const isNew = !base.path;
|
|
202
|
+
const asSaved = useMemo(() => inForm(collection.fields, values), [collection, values]);
|
|
203
|
+
const was = useMemo(() => inForm(collection.fields, initial), [collection, initial]);
|
|
204
|
+
const changed = collection.fields.filter((f) => !same(asSaved[f.name], was[f.name]));
|
|
205
|
+
const dirty = bodyChanged || changed.length > 0;
|
|
206
|
+
|
|
207
|
+
const set = (name: string, value: unknown) => setValues((v) => ({ ...v, [name]: value }));
|
|
208
|
+
const notify = env.notify;
|
|
209
|
+
// A form that cannot be saved: why, as a message, and the field marked (useWrongField in fields.tsx), with its section
|
|
210
|
+
// opened first (unfold, below).
|
|
211
|
+
const wrongField = useWrongField(
|
|
212
|
+
(text) => notify({ text, error: true }),
|
|
213
|
+
(path) => unfold(sectionOf(path)),
|
|
214
|
+
);
|
|
215
|
+
// On a wide computer screen: the navigation drawer beside the form, until its icon puts it away.
|
|
216
|
+
const [drawer, setDrawer] = useState(true);
|
|
217
|
+
const has = (id: Section) => collection.fields.some(inSection(id));
|
|
218
|
+
const sections: NavigationItem[] = [
|
|
219
|
+
{ id: 'content', label: t.forms.sections.content, icon: Article },
|
|
220
|
+
...(has('status') ? [{ id: 'status', label: t.forms.sections.status, icon: Eye }] : []),
|
|
221
|
+
...(has('other') ? [{ id: 'other', label: t.forms.otherFields, icon: SlidersHorizontal }] : []),
|
|
222
|
+
...(has('seo') ? [{ id: 'seo', label: t.forms.seo, icon: MagnifyingGlass }] : []),
|
|
223
|
+
...(has('share') ? [{ id: 'share', label: t.forms.share, icon: ShareNetwork }] : []),
|
|
224
|
+
...(isNew ? [] : [{ id: 'versions', label: t.history.versions, icon: ClockCounterClockwise }]),
|
|
225
|
+
];
|
|
226
|
+
const shows = (id: Section) => sections.some((s) => s.id === id);
|
|
227
|
+
// The section in view, marked in the drawer: while the page scrolls, the last one whose top is above a third of the
|
|
228
|
+
// screen (at the bottom of the page, the last one in view). A tap in the drawer marks its own and scrolls there; the
|
|
229
|
+
// mark stays meanwhile (`steering`, until the scroll ends).
|
|
230
|
+
const [section, setSection] = useState<Section>('content');
|
|
231
|
+
const boxes = useRef<Partial<Record<Section, HTMLElement | null>>>({});
|
|
232
|
+
const order = useRef<Section[]>([]);
|
|
233
|
+
order.current = sections.map((s) => s.id as Section);
|
|
234
|
+
const steering = useRef(0);
|
|
235
|
+
const spy = useRef(() => {});
|
|
236
|
+
const steer = (id: Section) => {
|
|
237
|
+
setSection(id);
|
|
238
|
+
steering.current = performance.now() + 1500;
|
|
239
|
+
addEventListener('scrollend', () => (steering.current = 0), { once: true });
|
|
240
|
+
};
|
|
241
|
+
// The sections that are open; at first only the text (and "Søgemaskiner" when it is opened at it, `opening`).
|
|
242
|
+
const [open, setOpen] = useState<ReadonlySet<Section>>(() => new Set(['content', ...(opening === 'seo' ? ['seo' as const] : [])]));
|
|
243
|
+
const unfold = (id: Section) => setOpen((was) => (was.has(id) ? was : new Set([...was, id])));
|
|
244
|
+
// A section opened by its heading is marked in the drawer; one folded, the one in view now (the page is shorter,
|
|
245
|
+
// without a scroll).
|
|
246
|
+
const toggle = (id: Section) => {
|
|
247
|
+
const opening = !open.has(id);
|
|
248
|
+
setOpen((was) => {
|
|
249
|
+
const next = new Set(was);
|
|
250
|
+
if (opening) next.add(id);
|
|
251
|
+
else next.delete(id);
|
|
252
|
+
return next;
|
|
253
|
+
});
|
|
254
|
+
if (opening) setSection(id);
|
|
255
|
+
else requestAnimationFrame(() => spy.current());
|
|
256
|
+
};
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
let frame = 0;
|
|
259
|
+
spy.current = () => {
|
|
260
|
+
frame = 0;
|
|
261
|
+
if (performance.now() < steering.current) return;
|
|
262
|
+
const bottom = innerHeight + scrollY >= document.documentElement.scrollHeight - 8;
|
|
263
|
+
const line = bottom ? innerHeight - 96 : innerHeight / 3;
|
|
264
|
+
let here: Section | undefined;
|
|
265
|
+
for (const id of order.current) {
|
|
266
|
+
const top = boxes.current[id]?.getBoundingClientRect().top;
|
|
267
|
+
if (top !== undefined && (here === undefined || top <= line)) here = id;
|
|
268
|
+
}
|
|
269
|
+
if (here) setSection(here);
|
|
270
|
+
};
|
|
271
|
+
const onScroll = () => {
|
|
272
|
+
if (!frame) frame = requestAnimationFrame(spy.current);
|
|
273
|
+
};
|
|
274
|
+
addEventListener('scroll', onScroll, { passive: true });
|
|
275
|
+
return () => {
|
|
276
|
+
removeEventListener('scroll', onScroll);
|
|
277
|
+
cancelAnimationFrame(frame);
|
|
278
|
+
};
|
|
279
|
+
}, []);
|
|
280
|
+
// Opens a section and scrolls to it (or to `box` in it: the text), after the drawer on a phone has closed (the page does
|
|
281
|
+
// not scroll while a dialog is open). It is opened at once (flushSync), so it is there to scroll to.
|
|
282
|
+
const textBox = useRef<HTMLDivElement>(null);
|
|
283
|
+
const goTo = (id: Section, box?: HTMLElement | null) => {
|
|
284
|
+
if (!open.has(id)) flushSync(() => unfold(id));
|
|
285
|
+
steer(id);
|
|
286
|
+
requestAnimationFrame(() => (box ?? boxes.current[id])?.scrollIntoView({ block: 'start', behavior: scrolling() }));
|
|
287
|
+
};
|
|
288
|
+
const pick = (id: string) => goTo(id as Section);
|
|
289
|
+
// Opened from "SEO-vurdering" (SeoScores.tsx): straight to "Søgemaskiner" (open from the start, `open`).
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (opening !== 'seo') return;
|
|
292
|
+
opening = null;
|
|
293
|
+
if (shows('seo')) goTo('seo');
|
|
294
|
+
}, []);
|
|
295
|
+
// The field marked as wrong is scrolled to by itself (FieldInput), its section opened (useWrongField) and marked in the
|
|
296
|
+
// drawer.
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (wrongField.field) steer(sectionOf(wrongField.field.path));
|
|
299
|
+
}, [wrongField.field?.at]);
|
|
300
|
+
// A section: where it is, for the drawer, its heading (its name in the drawer), and whether it is open.
|
|
301
|
+
const sectionProps = (id: Section) => ({
|
|
302
|
+
ref: (el: HTMLElement | null) => void (boxes.current[id] = el),
|
|
303
|
+
title: sections.find((s) => s.id === id)!.label,
|
|
304
|
+
open: open.has(id),
|
|
305
|
+
onToggle: () => toggle(id),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// The SEO rating at the top of "Søgemaskiner" (seo.ts), again with every change of a field while that is open and near
|
|
309
|
+
// the screen (`seoBox`, what is in the section, is hidden while it is folded); the same for the preview of a shared link
|
|
310
|
+
// at the top of "Sociale medier" (its first picture in the text). The text is read from the editor when one of them is
|
|
311
|
+
// opened or comes near the screen, and while it is there a moment after the text was last changed: not on every
|
|
312
|
+
// keystroke, nor while they are folded or out of view (then the rating keeps what it had; it is worked out once from
|
|
313
|
+
// the start, so it does not move what is under it when it comes into view).
|
|
314
|
+
const post = collection.id === 'blog';
|
|
315
|
+
const seoBox = useRef<HTMLDivElement>(null);
|
|
316
|
+
const seoNear = useInView(seoBox, '200px');
|
|
317
|
+
const shareBox = useRef<HTMLDivElement>(null);
|
|
318
|
+
const shareNear = useInView(shareBox, '200px');
|
|
319
|
+
const textNear = seoNear || shareNear;
|
|
320
|
+
const near = useRef(textNear);
|
|
321
|
+
near.current = textNear;
|
|
322
|
+
const [body, setBody] = useState(() => shown?.body ?? parts.body);
|
|
323
|
+
const readBody = () => setBody(text.current?.markdown() ?? shown?.body ?? parts.body);
|
|
324
|
+
const reading = useRef(0);
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (textNear && bodyChanged) readBody();
|
|
327
|
+
}, [textNear]);
|
|
328
|
+
useEffect(() => () => clearTimeout(reading.current), []);
|
|
329
|
+
const onText = () => {
|
|
330
|
+
setBodyChanged(true);
|
|
331
|
+
if (!near.current) return;
|
|
332
|
+
clearTimeout(reading.current);
|
|
333
|
+
reading.current = window.setTimeout(readBody, 600);
|
|
334
|
+
};
|
|
335
|
+
const lastRating = useRef<SeoCheck[] | null>(null);
|
|
336
|
+
const rating = useMemo(
|
|
337
|
+
() => (seoNear || !lastRating.current ? seoRating(seoInputOf(values, body, post, env.relations)) : lastRating.current),
|
|
338
|
+
[seoNear, post, body, values, env.relations],
|
|
339
|
+
);
|
|
340
|
+
lastRating.current = rating;
|
|
341
|
+
// The link as it is shared: as Base.astro writes it, from a page's H1 (pageH1) or a post's title, and its picture
|
|
342
|
+
// (a post's "Billede øverst", else the first in the text; a page has its pictures in its text, and the front page shows
|
|
343
|
+
// the site's picture, as Home.astro).
|
|
344
|
+
const card = sharedAs({
|
|
345
|
+
title: (!post && pageH1(body).heading) || String(values.title ?? ''),
|
|
346
|
+
front,
|
|
347
|
+
seoTitle: String(values.seoTitle ?? ''),
|
|
348
|
+
seoDescription: String(values.seoDescription ?? ''),
|
|
349
|
+
shareTitle: String(values.shareTitle ?? ''),
|
|
350
|
+
shareDescription: String(values.shareDescription ?? ''),
|
|
351
|
+
shareImage: String(values.shareImage ?? ''),
|
|
352
|
+
description: String(values.description ?? ''),
|
|
353
|
+
image: front ? undefined : String(values.image ?? '') || firstPicture(body),
|
|
354
|
+
site: sharing,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// A picture is kept in the browser until "Gem", which uploads it with the page.
|
|
358
|
+
const pictures = useUploads(notify);
|
|
359
|
+
const [pickImage, picker] = useMediaPicker(env.media, pictures);
|
|
360
|
+
|
|
361
|
+
const fields: FieldContext = {
|
|
362
|
+
relations: env.relations,
|
|
363
|
+
lists: env.lists,
|
|
364
|
+
testimonials: env.testimonials,
|
|
365
|
+
site: env.site,
|
|
366
|
+
pickImage,
|
|
367
|
+
// A page's address, for what is shown on it (a button that opens a promotion); a new one has none yet.
|
|
368
|
+
page: collection.id === 'pages' && url ? url : undefined,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// A new page or post without a title is called "Ny side 1", "Ny side 2" … (in the site's language,
|
|
372
|
+
// the first that no page or post of its kind has as its title or file name).
|
|
373
|
+
function untitled() {
|
|
374
|
+
const own = env.files.filter((f) => f.path.startsWith(`${collection.folder}/`) && f.path.endsWith('.md'));
|
|
375
|
+
const titles = new Set(own.map((f) => String(readFields(splitFrontmatter(f.text).yaml).title ?? '')));
|
|
376
|
+
const names = new Set(own.map((f) => slugOf(f.path, collection.folder).split('/').at(-1)));
|
|
377
|
+
for (let n = 1; ; n++) {
|
|
378
|
+
const title = `${asWritten(collection).create} ${n}`;
|
|
379
|
+
if (!titles.has(title) && !names.has(slugify(title))) return title;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const named = () => (isNew && !String(values.title ?? '').trim() ? { ...values, title: untitled() } : values);
|
|
383
|
+
|
|
384
|
+
// `other`: an address chosen in the dialog for a taken one (below), saved instead of the one typed.
|
|
385
|
+
async function save(other?: string, values = named()) {
|
|
386
|
+
if (wrongField.stop(problemAt(collection.fields, values))) return;
|
|
387
|
+
// Planlagt, at a time chosen now: one still to come.
|
|
388
|
+
if (
|
|
389
|
+
asSaved.status === 'scheduled' &&
|
|
390
|
+
changed.some((f) => f.name === 'status' || f.name === 'publishAt') &&
|
|
391
|
+
isOver(String(asSaved.publishAt)) &&
|
|
392
|
+
wrongField.stop({ text: t.forms.entry.scheduledPast, path: 'publishAt' })
|
|
393
|
+
)
|
|
394
|
+
return;
|
|
395
|
+
// A standard page is on the site: it cannot be a draft or wait for its time.
|
|
396
|
+
if (
|
|
397
|
+
standard &&
|
|
398
|
+
(asSaved.status === 'draft' || asSaved.status === 'scheduled') &&
|
|
399
|
+
wrongField.stop({ text: t.forms.entry.standardPage(siteText(standard.label), standardPageAt(standard.key)), path: 'status' })
|
|
400
|
+
)
|
|
401
|
+
return;
|
|
402
|
+
const slug = slugify(values.title as string);
|
|
403
|
+
if (isNew && !slug) return notify({ text: t.forms.entry.titleNeedsLetters, error: true });
|
|
404
|
+
// The address: the one typed, or for a new one /<title>/. A post's is its file name; a page's
|
|
405
|
+
// is its file name too when new, and its own `path` when that is another.
|
|
406
|
+
const typed = addressOf(other ?? String(values.path ?? ''), post ? 'post' : 'page');
|
|
407
|
+
const address = typed || (isNew ? `/${slug}/` : url);
|
|
408
|
+
const moved = !isNew && address !== url;
|
|
409
|
+
const name = post || isNew ? address.split('/').at(-2)! : slugOf(base.path, collection.folder);
|
|
410
|
+
const path = isNew || (post && moved) ? `${collection.folder}/${name}.md` : base.path;
|
|
411
|
+
// A new page or post, or a new address: not where another page is, or an address the site sends on
|
|
412
|
+
// elsewhere (the files of the redirects as they are now, redirects.ts).
|
|
413
|
+
let loaded: RepoFile[] = [];
|
|
414
|
+
if (isNew || moved)
|
|
415
|
+
try {
|
|
416
|
+
loaded = await env.load(REDIRECT_FILES);
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return notify({ text: t.forms.entry.redirectsNotLoaded((e as Error).message), error: true });
|
|
419
|
+
}
|
|
420
|
+
const reason = isNew || moved ? env.taken(address, loaded) : null;
|
|
421
|
+
// An address the site sends on elsewhere: the owner is told where to, and chooses to remove the redirect
|
|
422
|
+
// (saved with the page) or another address.
|
|
423
|
+
const freeing = !!reason?.redirect;
|
|
424
|
+
if (reason?.redirect) {
|
|
425
|
+
const { name, via } = reason.redirect;
|
|
426
|
+
const answer = await choose({
|
|
427
|
+
title: t.forms.entry.takenTitle(address),
|
|
428
|
+
text: t.forms.entry.takenText(address, name, via, post),
|
|
429
|
+
choices: [
|
|
430
|
+
{ value: 'free', label: t.forms.entry.freeAndSave },
|
|
431
|
+
{ value: 'other', label: t.forms.entry.pickOther },
|
|
432
|
+
],
|
|
433
|
+
});
|
|
434
|
+
if (!answer) return;
|
|
435
|
+
if (answer.choice === 'other') {
|
|
436
|
+
const next = await choose({
|
|
437
|
+
title: t.forms.entry.otherTitle,
|
|
438
|
+
text: t.forms.entry.otherText(post),
|
|
439
|
+
input: { label: t.forms.address, value: address },
|
|
440
|
+
choices: [{ value: 'save', label: t.common.save }],
|
|
441
|
+
});
|
|
442
|
+
const chosen = next && addressOf(next.text, post ? 'post' : 'page');
|
|
443
|
+
if (!next) return;
|
|
444
|
+
if (!chosen) return notify({ text: t.forms.writeAddress, error: true });
|
|
445
|
+
set('path', chosen);
|
|
446
|
+
return save(chosen);
|
|
447
|
+
}
|
|
448
|
+
} else if (reason)
|
|
449
|
+
return notify({
|
|
450
|
+
text: t.forms.entry.taken(reason.text, !!typed, post),
|
|
451
|
+
error: true,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (
|
|
455
|
+
path !== base.path &&
|
|
456
|
+
env.files.some(
|
|
457
|
+
(f) => f.path !== base.path && slugOf(f.path, collection.folder) === name && f.path.startsWith(`${collection.folder}/`),
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
return notify({ text: t.forms.entry.fileExists(name), error: true });
|
|
461
|
+
|
|
462
|
+
// A new file gets all its filled-in fields in the order of the form; an existing one only the changed. As saved:
|
|
463
|
+
// the values given (a new one without a title has its name, "Ny side 1", named()), not only those in the form.
|
|
464
|
+
const saved = inForm(collection.fields, values);
|
|
465
|
+
const written = (isNew ? collection.fields.filter((f) => !same(saved[f.name], []) && f.type !== 'readonly') : changed).filter(
|
|
466
|
+
(f) => f.name !== 'path',
|
|
467
|
+
);
|
|
468
|
+
const own = !post && (moved || (isNew && address !== `/${name}/`)) ? { path: address } : {};
|
|
469
|
+
const yaml = writeFields(isNew ? '' : parts.yaml, { ...Object.fromEntries(written.map((f) => [f.name, saved[f.name]])), ...own });
|
|
470
|
+
const markdown = bodyChanged ? text.current?.markdown() : null;
|
|
471
|
+
if (isNew && !markdown?.trim()) {
|
|
472
|
+
goTo('content', textBox.current);
|
|
473
|
+
return notify({ text: t.forms.entry.empty(post), error: true });
|
|
474
|
+
}
|
|
475
|
+
const body = markdown == null ? parts.body : `\n${markdown.trim()}\n`;
|
|
476
|
+
// The blocks in the text by their own fields and the template, as the site checks them before it is built.
|
|
477
|
+
const [wrongBlock] = blockProblems(body, template, front ? frontLeads : undefined);
|
|
478
|
+
if (wrongBlock) {
|
|
479
|
+
goTo('content', textBox.current);
|
|
480
|
+
return notify({ text: wrongBlock, error: true });
|
|
481
|
+
}
|
|
482
|
+
let next = `---\n${yaml}\n---\n${body}`;
|
|
483
|
+
|
|
484
|
+
// The redirects (redirects.ts): a freed address no longer sends on (those that went on through it
|
|
485
|
+
// first go straight to where it sent them), the old address sends on to the new one, the new
|
|
486
|
+
// address is the page's (the editor's own redirect from it goes), and none sends on through another.
|
|
487
|
+
let redirectChanges: Change[] = [];
|
|
488
|
+
if (isNew || moved) {
|
|
489
|
+
let all = readAll(loaded);
|
|
490
|
+
if (freeing) {
|
|
491
|
+
const flat = flatten(all);
|
|
492
|
+
if (typeof flat === 'string') return notify({ text: flat, error: true });
|
|
493
|
+
all = setRedirect(flat, address, null);
|
|
494
|
+
}
|
|
495
|
+
const own = moveRedirects(all.own, moved ? url : null, address);
|
|
496
|
+
if (typeof own === 'string') return notify({ text: own, error: true });
|
|
497
|
+
const flat = flatten({ ...all, own });
|
|
498
|
+
if (typeof flat === 'string') return notify({ text: flat, error: true });
|
|
499
|
+
redirectChanges = changesOf(flat, loaded);
|
|
500
|
+
}
|
|
501
|
+
// The links to the old address everywhere, and the posts picked in the blocks on the pages (by their file name).
|
|
502
|
+
const others: RepoFile[] = [];
|
|
503
|
+
if (moved) {
|
|
504
|
+
next = retarget(next, url, address);
|
|
505
|
+
const was = slugOf(base.path, collection.folder);
|
|
506
|
+
for (const f of env.files) {
|
|
507
|
+
if (f.path === base.path || f.path === REDIRECTS || !/\.(md|json)$/.test(f.path)) continue;
|
|
508
|
+
let changedText = retarget(f.text, url, address);
|
|
509
|
+
if (post && f.path.startsWith(`${config.folders.pages}/`)) changedText = pickedRenamed(changedText, was, name);
|
|
510
|
+
if (changedText !== f.text) others.push({ ...f, text: changedText });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (next === base.text && !moved && !redirectChanges.length) {
|
|
514
|
+
setBodyChanged(false);
|
|
515
|
+
return notify({ text: t.forms.nothingToSave });
|
|
516
|
+
}
|
|
517
|
+
// Pictures that were added and are still used.
|
|
518
|
+
const used = pictures.usedIn(next);
|
|
519
|
+
const removed = base.path && path !== base.path ? [base.path] : [];
|
|
520
|
+
const changes: Change[] = [
|
|
521
|
+
{ path, content: next, oid: path === base.path ? base.oid || null : null },
|
|
522
|
+
...removed.map((p) => ({ path: p, content: null, oid: base.oid })),
|
|
523
|
+
...others.map((f) => ({ path: f.path, content: f.text, oid: f.oid })),
|
|
524
|
+
...redirectChanges,
|
|
525
|
+
...used.map((u) => ({ path: u.path, content: u.bytes, oid: null })),
|
|
526
|
+
];
|
|
527
|
+
const what = `"${slugOf(path, collection.folder)}"`;
|
|
528
|
+
const message = moved
|
|
529
|
+
? siteT.forms.commit.move(asWritten(collection).singular, url, address)
|
|
530
|
+
: isNew
|
|
531
|
+
? siteT.forms.commit.create(asWritten(collection).create!, what)
|
|
532
|
+
: version
|
|
533
|
+
? siteT.history.commit.restoreVersion(`${asWritten(collection).singular} ${what}`, savedOn(version))
|
|
534
|
+
: siteT.forms.commit.editOne(asWritten(collection).singular, what);
|
|
535
|
+
const entry = kindAndName(collection.singular, String(values.title ?? '').trim() || name);
|
|
536
|
+
const change = moved
|
|
537
|
+
? t.app.changes.moved(entry, address)
|
|
538
|
+
: isNew
|
|
539
|
+
? t.app.changes.created(entry)
|
|
540
|
+
: version
|
|
541
|
+
? t.app.changes.restored(entry)
|
|
542
|
+
: t.app.changes.edited(entry);
|
|
543
|
+
setSaving(true);
|
|
544
|
+
notify({ text: t.common.saving });
|
|
545
|
+
try {
|
|
546
|
+
const oids = await env.commit(message, changes, change);
|
|
547
|
+
const saved = { path, oid: oids[path], text: next };
|
|
548
|
+
pictures.markSaved(used);
|
|
549
|
+
setBase(saved);
|
|
550
|
+
setValues(valuesOf(collection, saved));
|
|
551
|
+
setBodyChanged(false);
|
|
552
|
+
setSavedAt(new Date());
|
|
553
|
+
env.onSaved(saved, {
|
|
554
|
+
files: [
|
|
555
|
+
...others.map((f) => ({ ...f, oid: oids[f.path] })),
|
|
556
|
+
// The editor's own redirects are among its files (the others are loaded when needed).
|
|
557
|
+
...redirectChanges
|
|
558
|
+
.filter((c) => c.path === REDIRECTS)
|
|
559
|
+
.map((c) => ({ path: REDIRECTS, oid: oids[REDIRECTS], text: String(c.content) })),
|
|
560
|
+
],
|
|
561
|
+
removed,
|
|
562
|
+
});
|
|
563
|
+
notify({ text: savedText(change), kind: 'saved' });
|
|
564
|
+
// The version is now the one saved: the form as usual again.
|
|
565
|
+
if (version) onVersion(null);
|
|
566
|
+
// A new address: the owner is told, in a dialog that stays until it is closed, that the old one now
|
|
567
|
+
// sends on to it, and where the redirects are.
|
|
568
|
+
if (moved) {
|
|
569
|
+
void choose({
|
|
570
|
+
title: t.forms.entry.movedTitle,
|
|
571
|
+
text: t.forms.entry.movedText(address, url, post, redirectsPath()),
|
|
572
|
+
choices: [
|
|
573
|
+
{ value: 'ok', label: t.forms.entry.ok },
|
|
574
|
+
{ value: 'see', label: t.forms.entry.seeRedirects },
|
|
575
|
+
],
|
|
576
|
+
cancel: false,
|
|
577
|
+
}).then((seen) => seen?.choice === 'see' && (location.hash = TAB_ROUTES.redirects));
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
} catch (e) {
|
|
581
|
+
notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
582
|
+
} finally {
|
|
583
|
+
setSaving(false);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// "Forhåndsvis": the page or post with what is changed in the form; a new one, or one whose layout is changed in the
|
|
588
|
+
// form (a field with `layout`, "Hele bredden"), in the page of another one of its kind saved with that layout, with
|
|
589
|
+
// all of the form put in it (pagePreview.tsx).
|
|
590
|
+
const [openPreview, previewSheet] = usePagePreview();
|
|
591
|
+
const showPreview = () =>
|
|
592
|
+
openPreview(() => {
|
|
593
|
+
const layout = collection.fields.filter((f) => f.layout);
|
|
594
|
+
const relaid = !isNew && changed.some((f) => f.layout);
|
|
595
|
+
const sameLayout = (data: Record<string, unknown>) =>
|
|
596
|
+
layout.every((f) => String(data[f.name] || '') === String(values[f.name] || ''));
|
|
597
|
+
const changes = (...names: string[]) => isNew || relaid || changed.some((f) => names.includes(f.name));
|
|
598
|
+
const others =
|
|
599
|
+
isNew || relaid
|
|
600
|
+
? env.files
|
|
601
|
+
.filter((f) => f.path.startsWith(`${collection.folder}/`) && f.path.endsWith('.md') && f.path !== base.path)
|
|
602
|
+
.map((f) => ({ f, data: readFields(splitFrontmatter(f.text).yaml) }))
|
|
603
|
+
.filter(({ data }) => sameLayout(data))
|
|
604
|
+
.map(({ f, data }) => addressNow(collection, f, data))
|
|
605
|
+
.slice(0, 5)
|
|
606
|
+
: [];
|
|
607
|
+
const markdown = bodyChanged ? text.current?.markdown() : null;
|
|
608
|
+
// A page's H1 is at the top of its text (pageH1); a blog post's is its title.
|
|
609
|
+
const h1 = collection.blocks ? pageH1(markdown ?? parts.body) : undefined;
|
|
610
|
+
return {
|
|
611
|
+
shell: isNew || (relaid && others.length) ? others : url,
|
|
612
|
+
heading: h1?.heading ?? (changes('title', 'heading') ? String(values.heading || named().title) : undefined),
|
|
613
|
+
headingHtml: h1?.html,
|
|
614
|
+
// A post's picture at the top (a page has its pictures in its text).
|
|
615
|
+
image:
|
|
616
|
+
collection.fields.some((f) => f.name === 'image') && changes('image', 'imageAlt', 'imageDecorative')
|
|
617
|
+
? values.image
|
|
618
|
+
? { src: String(values.image), alt: values.imageDecorative === true ? '' : String(values.imageAlt ?? '') }
|
|
619
|
+
: null
|
|
620
|
+
: undefined,
|
|
621
|
+
body: isNew || relaid || markdown != null ? (markdown ?? parts.body) : undefined,
|
|
622
|
+
data: { lists: env.lists, testimonials: env.testimonials, site: env.site, relations: env.relations, path: url },
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// A post planned for a time gets that day as its date, so it comes out as the newest.
|
|
627
|
+
const onChange = (f: Field, v: unknown) => {
|
|
628
|
+
set(f.name, v);
|
|
629
|
+
if (f.name === 'publishAt' && /^\d{4}-\d{2}-\d{2}T/.test(String(v)) && collection.fields.some((d) => d.name === 'date'))
|
|
630
|
+
set('date', String(v).slice(0, 10));
|
|
631
|
+
};
|
|
632
|
+
// The fields in use (Field.only).
|
|
633
|
+
const field = (f: Field) =>
|
|
634
|
+
inUse(f, collection.fields, values) && <FieldInput key={f.name} field={f} value={values[f.name]} onChange={(v) => onChange(f, v)} />;
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<Fields.Provider value={fields}>
|
|
638
|
+
<WrongField.Provider value={wrongField.field}>
|
|
639
|
+
<FormFrame
|
|
640
|
+
title={isNew ? collection.create! : String(initial.title || collection.singular)}
|
|
641
|
+
back={{ href: `#liste/${collection.id}`, label: collection.label }}
|
|
642
|
+
status={savedStatus({ savedAt, saving, deleting, isNew })}
|
|
643
|
+
view={isNew ? undefined : url}
|
|
644
|
+
drawer={
|
|
645
|
+
<>
|
|
646
|
+
<div className="contents xl:hidden">
|
|
647
|
+
<ModalNavigationDrawer title={t.forms.sections.title} items={sections} value={section} onChange={pick} />
|
|
648
|
+
</div>
|
|
649
|
+
<div className="hidden xl:contents">
|
|
650
|
+
<NavigationDrawerButton label={t.forms.sections.title} open={drawer} onClick={() => setDrawer(!drawer)} />
|
|
651
|
+
</div>
|
|
652
|
+
</>
|
|
653
|
+
}
|
|
654
|
+
dirty={dirty}
|
|
655
|
+
saving={saving || deleting}
|
|
656
|
+
deleting={deleting}
|
|
657
|
+
onSave={() => save()}
|
|
658
|
+
onPreview={showPreview}
|
|
659
|
+
onDelete={collection.deletable && !isNew ? () => env.remove(collection, base, setDeleting) : undefined}
|
|
660
|
+
deleteLabel={t.forms.deleteOne(collection.singular)}
|
|
661
|
+
panel
|
|
662
|
+
>
|
|
663
|
+
{version && <VersionNotice version={version} onCurrent={() => onVersion(null)} />}
|
|
664
|
+
<FormColumns
|
|
665
|
+
sticky
|
|
666
|
+
wideOnly
|
|
667
|
+
main={
|
|
668
|
+
<>
|
|
669
|
+
<FoldSection {...sectionProps('content')}>
|
|
670
|
+
{collection.fields.filter((f) => TOP.includes(f.name) && !STATUS.includes(f.name)).map(field)}
|
|
671
|
+
<div ref={textBox} className="scroll-mt-[calc(5rem+env(safe-area-inset-top))]">
|
|
672
|
+
<span className="mb-1 block font-medium">{t.forms.text}</span>
|
|
673
|
+
<MarkdownEditor
|
|
674
|
+
ref={text}
|
|
675
|
+
value={shown?.body ?? parts.body}
|
|
676
|
+
onChange={onText}
|
|
677
|
+
addImage={pickImage}
|
|
678
|
+
blocks={collection.blocks ? blockAsk : undefined}
|
|
679
|
+
h1={!!collection.blocks}
|
|
680
|
+
sheet={blockAsk}
|
|
681
|
+
looks={
|
|
682
|
+
front
|
|
683
|
+
? (items) =>
|
|
684
|
+
looksOf(items, { file: `${config.standardPages}#front`, leads: frontLeads, sectionLooks, headingLooks })
|
|
685
|
+
: undefined
|
|
686
|
+
}
|
|
687
|
+
sections={front}
|
|
688
|
+
/>
|
|
689
|
+
</div>
|
|
690
|
+
</FoldSection>
|
|
691
|
+
{shows('status') && (
|
|
692
|
+
<FoldSection {...sectionProps('status')}>
|
|
693
|
+
{collection.fields.filter((f) => STATUS.includes(f.name)).map(field)}
|
|
694
|
+
</FoldSection>
|
|
695
|
+
)}
|
|
696
|
+
{shows('other') && (
|
|
697
|
+
<FoldSection {...sectionProps('other')}>{collection.fields.filter(inSection('other')).map(field)}</FoldSection>
|
|
698
|
+
)}
|
|
699
|
+
{shows('seo') && (
|
|
700
|
+
<FoldSection {...sectionProps('seo')}>
|
|
701
|
+
<div ref={seoBox} className="space-y-6">
|
|
702
|
+
{rating.length > 0 && <CheckList title={t.forms.seoRating.title} items={rating} />}
|
|
703
|
+
{collection.fields.filter(inSection('seo')).map(field)}
|
|
704
|
+
</div>
|
|
705
|
+
</FoldSection>
|
|
706
|
+
)}
|
|
707
|
+
{shows('share') && (
|
|
708
|
+
<FoldSection {...sectionProps('share')}>
|
|
709
|
+
<div ref={shareBox} className="space-y-6">
|
|
710
|
+
<SharePreview
|
|
711
|
+
image={shownAt(splitPicture(card.image).src)}
|
|
712
|
+
domain={config.domain}
|
|
713
|
+
title={card.title}
|
|
714
|
+
description={card.description}
|
|
715
|
+
/>
|
|
716
|
+
{collection.fields.filter(inSection('share')).map(field)}
|
|
717
|
+
</div>
|
|
718
|
+
</FoldSection>
|
|
719
|
+
)}
|
|
720
|
+
{shows('versions') && (
|
|
721
|
+
<FoldSection {...sectionProps('versions')}>
|
|
722
|
+
<VersionList env={env} path={base.path} oid={base.oid} onPick={onVersion} />
|
|
723
|
+
</FoldSection>
|
|
724
|
+
)}
|
|
725
|
+
</>
|
|
726
|
+
}
|
|
727
|
+
side={drawer && <NavigationDrawer title={t.forms.sections.title} items={sections} value={section} onChange={pick} />}
|
|
728
|
+
/>
|
|
729
|
+
</FormFrame>
|
|
730
|
+
{blockDialog}
|
|
731
|
+
{picker}
|
|
732
|
+
{previewSheet}
|
|
733
|
+
</WrongField.Provider>
|
|
734
|
+
</Fields.Provider>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// A page's text with a post's new file name in its blocks with blog posts ("Blogindlæg", `picked`).
|
|
739
|
+
const pickedRenamed = (text: string, was: string, name: string) =>
|
|
740
|
+
text.replace(/^<div data-block="posts".*$/gm, (line) => line.replaceAll(`"${was}"`, `"${name}"`));
|