@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -21
- package/README.md +51 -51
- package/astro.mjs +162 -162
- package/bin/morpheme.mjs +38 -38
- package/client.d.ts +10 -10
- package/defaults/blockTexts.ts +33 -33
- package/defaults/collections.ts +774 -774
- package/defaults/colors.ts +125 -125
- package/defaults/company.ts +20 -20
- package/defaults/editor.ts +178 -178
- package/defaults/internalLinks.ts +30 -30
- package/defaults/opens.ts +18 -18
- package/defaults/popup.tsx +93 -93
- package/defaults/privacy.ts +79 -79
- package/defaults/rules.ts +450 -450
- package/defaults/schema.ts +16 -16
- package/defaults/sitedata.ts +129 -129
- package/defaults/standard.ts +106 -106
- package/defaults/standardPages.ts +87 -87
- package/defaults/texts.ts +481 -481
- package/demo/README.md +26 -26
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
- package/demo/src/content/blog/every-save-is-a-version.md +21 -21
- package/demo/src/content/blog/schedule-a-post.md +17 -17
- package/demo/src/content/blog/secure-from-day-one.md +17 -17
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
- package/demo/src/content/blog/the-app-store.md +21 -21
- package/demo/src/content/pages/about.md +22 -22
- package/demo/src/content/pages/app-store.md +30 -30
- package/demo/src/content/pages/blocks.md +98 -98
- package/demo/src/content/pages/blog-page.md +11 -11
- package/demo/src/content/pages/contact.md +16 -16
- package/demo/src/content/pages/get-started.md +20 -20
- package/demo/src/content/pages/home.md +26 -26
- package/demo/src/content/pages/page-not-found.md +11 -11
- package/demo/src/content/pages/privacy-policy.md +24 -24
- package/demo/src/data/blocks.json +25 -25
- package/demo/src/data/booking.json +6 -6
- package/demo/src/data/categories/apps.json +6 -6
- package/demo/src/data/categories/design.json +6 -6
- package/demo/src/data/categories/editing.json +6 -6
- package/demo/src/data/categories/security.json +6 -6
- package/demo/src/data/categories/seo.json +6 -6
- package/demo/src/data/colors.json +75 -75
- package/demo/src/data/contact.json +8 -8
- package/demo/src/data/cookies.json +21 -21
- package/demo/src/data/footer.json +27 -27
- package/demo/src/data/menu.json +35 -35
- package/demo/src/data/mobile-menu.json +25 -25
- package/demo/src/data/popups/booking-bar.json +13 -13
- package/demo/src/data/popups/try-the-editor.json +16 -16
- package/demo/src/data/standard-pages.json +8 -8
- package/demo/src/data/testimonials/sample-bakery.json +10 -10
- package/demo/src/data/testimonials/sample-clinic.json +10 -10
- package/demo/src/data/testimonials/sample-studio.json +10 -10
- package/demo/src/site/colors.ts +11 -11
- package/demo/src/site/config.ts +19 -19
- package/icons/lucide/LICENSE +43 -43
- package/icons/lucide/address-book.svg +1 -1
- package/icons/lucide/app-window.svg +1 -1
- package/icons/lucide/arrow-counter-clockwise.svg +1 -1
- package/icons/lucide/arrow-down.svg +1 -1
- package/icons/lucide/arrow-left.svg +1 -1
- package/icons/lucide/arrow-right.svg +1 -1
- package/icons/lucide/arrow-square-out.svg +1 -1
- package/icons/lucide/arrow-u-up-left.svg +1 -1
- package/icons/lucide/arrow-u-up-right.svg +1 -1
- package/icons/lucide/arrow-up-right.svg +1 -1
- package/icons/lucide/arrow-up.svg +1 -1
- package/icons/lucide/arrows-down-up.svg +1 -1
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
- package/icons/lucide/article.svg +1 -1
- package/icons/lucide/bank.svg +1 -1
- package/icons/lucide/bell.svg +1 -1
- package/icons/lucide/bicycle.svg +1 -1
- package/icons/lucide/binoculars.svg +1 -1
- package/icons/lucide/book-open-text.svg +1 -1
- package/icons/lucide/book-open.svg +1 -1
- package/icons/lucide/browser.svg +1 -1
- package/icons/lucide/buildings.svg +1 -1
- package/icons/lucide/bus.svg +1 -1
- package/icons/lucide/calendar-blank.svg +1 -1
- package/icons/lucide/calendar-check.svg +1 -1
- package/icons/lucide/car.svg +1 -1
- package/icons/lucide/cards.svg +1 -1
- package/icons/lucide/caret-down.svg +1 -1
- package/icons/lucide/caret-left.svg +1 -1
- package/icons/lucide/caret-right.svg +1 -1
- package/icons/lucide/caret-up.svg +1 -1
- package/icons/lucide/certificate.svg +1 -1
- package/icons/lucide/chart-line-up.svg +1 -1
- package/icons/lucide/chats.svg +1 -1
- package/icons/lucide/check-circle.svg +1 -1
- package/icons/lucide/check.svg +1 -1
- package/icons/lucide/circle-half.svg +1 -1
- package/icons/lucide/circle.svg +1 -1
- package/icons/lucide/clock-counter-clockwise.svg +1 -1
- package/icons/lucide/clock.svg +1 -1
- package/icons/lucide/code.svg +1 -1
- package/icons/lucide/coffee.svg +1 -1
- package/icons/lucide/columns.svg +1 -1
- package/icons/lucide/cookie.svg +1 -1
- package/icons/lucide/copy.svg +1 -1
- package/icons/lucide/credit-card.svg +1 -1
- package/icons/lucide/currency-circle-dollar.svg +1 -1
- package/icons/lucide/cursor-click.svg +1 -1
- package/icons/lucide/database.svg +1 -1
- package/icons/lucide/desktop.svg +1 -1
- package/icons/lucide/device-mobile.svg +1 -1
- package/icons/lucide/door-open.svg +1 -1
- package/icons/lucide/download-simple.svg +1 -1
- package/icons/lucide/envelope-simple.svg +1 -1
- package/icons/lucide/eye-slash.svg +1 -1
- package/icons/lucide/eye.svg +1 -1
- package/icons/lucide/file-pdf.svg +1 -1
- package/icons/lucide/file-text.svg +1 -1
- package/icons/lucide/file-video.svg +1 -1
- package/icons/lucide/file-zip.svg +1 -1
- package/icons/lucide/flower-lotus.svg +1 -1
- package/icons/lucide/folder-simple.svg +1 -1
- package/icons/lucide/funnel.svg +1 -1
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear.svg +1 -1
- package/icons/lucide/github-logo.svg +1 -1
- package/icons/lucide/globe.svg +1 -1
- package/icons/lucide/google-logo.svg +1 -1
- package/icons/lucide/graduation-cap.svg +1 -1
- package/icons/lucide/hammer.svg +1 -1
- package/icons/lucide/hand-heart.svg +1 -1
- package/icons/lucide/handshake.svg +1 -1
- package/icons/lucide/hard-drives.svg +1 -1
- package/icons/lucide/heart.svg +1 -1
- package/icons/lucide/house.svg +1 -1
- package/icons/lucide/image-square.svg +1 -1
- package/icons/lucide/image.svg +1 -1
- package/icons/lucide/info.svg +1 -1
- package/icons/lucide/keyboard.svg +1 -1
- package/icons/lucide/leaf.svg +1 -1
- package/icons/lucide/lifebuoy.svg +1 -1
- package/icons/lucide/lightbulb.svg +1 -1
- package/icons/lucide/link-break.svg +1 -1
- package/icons/lucide/link-simple.svg +1 -1
- package/icons/lucide/list-bullets.svg +1 -1
- package/icons/lucide/list-checks.svg +1 -1
- package/icons/lucide/list-numbers.svg +1 -1
- package/icons/lucide/list.svg +1 -1
- package/icons/lucide/lock-simple-open.svg +1 -1
- package/icons/lucide/lock-simple.svg +1 -1
- package/icons/lucide/magnifying-glass.svg +1 -1
- package/icons/lucide/map-pin.svg +1 -1
- package/icons/lucide/map-trifold.svg +1 -1
- package/icons/lucide/medal.svg +1 -1
- package/icons/lucide/megaphone.svg +1 -1
- package/icons/lucide/minus.svg +1 -1
- package/icons/lucide/moon.svg +1 -1
- package/icons/lucide/newspaper.svg +1 -1
- package/icons/lucide/pack.json +23 -23
- package/icons/lucide/paint-brush-broad.svg +1 -1
- package/icons/lucide/palette.svg +1 -1
- package/icons/lucide/paper-plane-tilt.svg +1 -1
- package/icons/lucide/pencil-simple.svg +1 -1
- package/icons/lucide/phone.svg +1 -1
- package/icons/lucide/play-fill.svg +1 -1
- package/icons/lucide/plus.svg +1 -1
- package/icons/lucide/prohibit.svg +1 -1
- package/icons/lucide/puzzle-piece.svg +1 -1
- package/icons/lucide/question.svg +1 -1
- package/icons/lucide/quotes.svg +1 -1
- package/icons/lucide/rows.svg +1 -1
- package/icons/lucide/ruler.svg +1 -1
- package/icons/lucide/scan.svg +1 -1
- package/icons/lucide/shapes.svg +1 -1
- package/icons/lucide/share-network.svg +1 -1
- package/icons/lucide/shield-check.svg +1 -1
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sign-out.svg +1 -1
- package/icons/lucide/signpost.svg +1 -1
- package/icons/lucide/sliders-horizontal.svg +1 -1
- package/icons/lucide/smiley.svg +1 -1
- package/icons/lucide/sparkle.svg +1 -1
- package/icons/lucide/square-half-bottom.svg +1 -1
- package/icons/lucide/square.svg +1 -1
- package/icons/lucide/squares-four.svg +1 -1
- package/icons/lucide/sticker.svg +1 -1
- package/icons/lucide/storefront.svg +1 -1
- package/icons/lucide/sun.svg +1 -1
- package/icons/lucide/swatches.svg +1 -1
- package/icons/lucide/tag.svg +1 -1
- package/icons/lucide/text-aa.svg +1 -1
- package/icons/lucide/text-align-left.svg +1 -1
- package/icons/lucide/text-b.svg +1 -1
- package/icons/lucide/text-h-one.svg +1 -1
- package/icons/lucide/text-h-three.svg +1 -1
- package/icons/lucide/text-h-two.svg +1 -1
- package/icons/lucide/text-italic.svg +1 -1
- package/icons/lucide/text-t.svg +1 -1
- package/icons/lucide/ticket.svg +1 -1
- package/icons/lucide/toggle-right.svg +1 -1
- package/icons/lucide/train.svg +1 -1
- package/icons/lucide/trash.svg +1 -1
- package/icons/lucide/upload-simple.svg +1 -1
- package/icons/lucide/user-circle.svg +1 -1
- package/icons/lucide/user.svg +1 -1
- package/icons/lucide/users-three.svg +1 -1
- package/icons/lucide/users.svg +1 -1
- package/icons/lucide/video-camera.svg +1 -1
- package/icons/lucide/warning-circle.svg +1 -1
- package/icons/lucide/warning.svg +1 -1
- package/icons/lucide/wave-sine.svg +1 -1
- package/icons/lucide/wheelchair.svg +1 -1
- package/icons/lucide/wrench.svg +1 -1
- package/icons/lucide/x.svg +1 -1
- package/icons/material-symbols/LICENSE +201 -201
- package/icons/material-symbols/address-book.svg +1 -1
- package/icons/material-symbols/app-window.svg +1 -1
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
- package/icons/material-symbols/arrow-down.svg +1 -1
- package/icons/material-symbols/arrow-left.svg +1 -1
- package/icons/material-symbols/arrow-right.svg +1 -1
- package/icons/material-symbols/arrow-square-out.svg +1 -1
- package/icons/material-symbols/arrow-u-up-left.svg +1 -1
- package/icons/material-symbols/arrow-u-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up.svg +1 -1
- package/icons/material-symbols/arrows-down-up.svg +1 -1
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
- package/icons/material-symbols/article.svg +1 -1
- package/icons/material-symbols/bank.svg +1 -1
- package/icons/material-symbols/bell.svg +1 -1
- package/icons/material-symbols/bicycle.svg +1 -1
- package/icons/material-symbols/binoculars.svg +1 -1
- package/icons/material-symbols/book-open-text.svg +1 -1
- package/icons/material-symbols/book-open.svg +1 -1
- package/icons/material-symbols/browser.svg +1 -1
- package/icons/material-symbols/buildings.svg +1 -1
- package/icons/material-symbols/bus.svg +1 -1
- package/icons/material-symbols/calendar-blank.svg +1 -1
- package/icons/material-symbols/calendar-check.svg +1 -1
- package/icons/material-symbols/car.svg +1 -1
- package/icons/material-symbols/cards.svg +1 -1
- package/icons/material-symbols/caret-down.svg +1 -1
- package/icons/material-symbols/caret-left.svg +1 -1
- package/icons/material-symbols/caret-right.svg +1 -1
- package/icons/material-symbols/caret-up.svg +1 -1
- package/icons/material-symbols/certificate.svg +1 -1
- package/icons/material-symbols/chart-line-up.svg +1 -1
- package/icons/material-symbols/chats.svg +1 -1
- package/icons/material-symbols/check-circle.svg +1 -1
- package/icons/material-symbols/check.svg +1 -1
- package/icons/material-symbols/circle-half.svg +1 -1
- package/icons/material-symbols/circle.svg +1 -1
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
- package/icons/material-symbols/clock.svg +1 -1
- package/icons/material-symbols/code.svg +1 -1
- package/icons/material-symbols/coffee.svg +1 -1
- package/icons/material-symbols/columns.svg +1 -1
- package/icons/material-symbols/cookie.svg +1 -1
- package/icons/material-symbols/copy.svg +1 -1
- package/icons/material-symbols/credit-card.svg +1 -1
- package/icons/material-symbols/currency-circle-dollar.svg +1 -1
- package/icons/material-symbols/cursor-click.svg +1 -1
- package/icons/material-symbols/database.svg +1 -1
- package/icons/material-symbols/desktop.svg +1 -1
- package/icons/material-symbols/device-mobile.svg +1 -1
- package/icons/material-symbols/door-open.svg +1 -1
- package/icons/material-symbols/download-simple.svg +1 -1
- package/icons/material-symbols/envelope-simple.svg +1 -1
- package/icons/material-symbols/eye-slash.svg +1 -1
- package/icons/material-symbols/eye.svg +1 -1
- package/icons/material-symbols/file-pdf.svg +1 -1
- package/icons/material-symbols/file-text.svg +1 -1
- package/icons/material-symbols/file-video.svg +1 -1
- package/icons/material-symbols/file-zip.svg +1 -1
- package/icons/material-symbols/flower-lotus.svg +1 -1
- package/icons/material-symbols/folder-simple.svg +1 -1
- package/icons/material-symbols/funnel.svg +1 -1
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear.svg +1 -1
- package/icons/material-symbols/github-logo.svg +1 -1
- package/icons/material-symbols/globe.svg +1 -1
- package/icons/material-symbols/google-logo.svg +1 -1
- package/icons/material-symbols/graduation-cap.svg +1 -1
- package/icons/material-symbols/hammer.svg +1 -1
- package/icons/material-symbols/hand-heart.svg +1 -1
- package/icons/material-symbols/handshake.svg +1 -1
- package/icons/material-symbols/hard-drives.svg +1 -1
- package/icons/material-symbols/heart.svg +1 -1
- package/icons/material-symbols/house.svg +1 -1
- package/icons/material-symbols/image-square.svg +1 -1
- package/icons/material-symbols/image.svg +1 -1
- package/icons/material-symbols/info.svg +1 -1
- package/icons/material-symbols/keyboard.svg +1 -1
- package/icons/material-symbols/leaf.svg +1 -1
- package/icons/material-symbols/lifebuoy.svg +1 -1
- package/icons/material-symbols/lightbulb.svg +1 -1
- package/icons/material-symbols/link-break.svg +1 -1
- package/icons/material-symbols/link-simple.svg +1 -1
- package/icons/material-symbols/list-bullets.svg +1 -1
- package/icons/material-symbols/list-checks.svg +1 -1
- package/icons/material-symbols/list-numbers.svg +1 -1
- package/icons/material-symbols/list.svg +1 -1
- package/icons/material-symbols/lock-simple-open.svg +1 -1
- package/icons/material-symbols/lock-simple.svg +1 -1
- package/icons/material-symbols/magnifying-glass.svg +1 -1
- package/icons/material-symbols/map-pin.svg +1 -1
- package/icons/material-symbols/map-trifold.svg +1 -1
- package/icons/material-symbols/medal.svg +1 -1
- package/icons/material-symbols/megaphone.svg +1 -1
- package/icons/material-symbols/minus.svg +1 -1
- package/icons/material-symbols/moon.svg +1 -1
- package/icons/material-symbols/newspaper.svg +1 -1
- package/icons/material-symbols/pack.json +29 -29
- package/icons/material-symbols/paint-brush-broad.svg +1 -1
- package/icons/material-symbols/palette.svg +1 -1
- package/icons/material-symbols/paper-plane-tilt.svg +1 -1
- package/icons/material-symbols/pencil-simple.svg +1 -1
- package/icons/material-symbols/phone.svg +1 -1
- package/icons/material-symbols/play-fill.svg +1 -1
- package/icons/material-symbols/plus.svg +1 -1
- package/icons/material-symbols/prohibit.svg +1 -1
- package/icons/material-symbols/puzzle-piece.svg +1 -1
- package/icons/material-symbols/question.svg +1 -1
- package/icons/material-symbols/quotes.svg +1 -1
- package/icons/material-symbols/rows.svg +1 -1
- package/icons/material-symbols/ruler.svg +1 -1
- package/icons/material-symbols/scan.svg +1 -1
- package/icons/material-symbols/shapes.svg +1 -1
- package/icons/material-symbols/share-network.svg +1 -1
- package/icons/material-symbols/shield-check.svg +1 -1
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sign-out.svg +1 -1
- package/icons/material-symbols/signpost.svg +1 -1
- package/icons/material-symbols/sliders-horizontal.svg +1 -1
- package/icons/material-symbols/smiley.svg +1 -1
- package/icons/material-symbols/sparkle.svg +1 -1
- package/icons/material-symbols/square-half-bottom.svg +1 -1
- package/icons/material-symbols/square.svg +1 -1
- package/icons/material-symbols/squares-four.svg +1 -1
- package/icons/material-symbols/sticker.svg +1 -1
- package/icons/material-symbols/storefront.svg +1 -1
- package/icons/material-symbols/sun.svg +1 -1
- package/icons/material-symbols/swatches.svg +1 -1
- package/icons/material-symbols/tag.svg +1 -1
- package/icons/material-symbols/text-aa.svg +1 -1
- package/icons/material-symbols/text-align-left.svg +1 -1
- package/icons/material-symbols/text-b.svg +1 -1
- package/icons/material-symbols/text-h-one.svg +1 -1
- package/icons/material-symbols/text-h-three.svg +1 -1
- package/icons/material-symbols/text-h-two.svg +1 -1
- package/icons/material-symbols/text-italic.svg +1 -1
- package/icons/material-symbols/text-t.svg +1 -1
- package/icons/material-symbols/ticket.svg +1 -1
- package/icons/material-symbols/toggle-right.svg +1 -1
- package/icons/material-symbols/train.svg +1 -1
- package/icons/material-symbols/trash.svg +1 -1
- package/icons/material-symbols/upload-simple.svg +1 -1
- package/icons/material-symbols/user-circle.svg +1 -1
- package/icons/material-symbols/user.svg +1 -1
- package/icons/material-symbols/users-three.svg +1 -1
- package/icons/material-symbols/users.svg +1 -1
- package/icons/material-symbols/video-camera.svg +1 -1
- package/icons/material-symbols/warning-circle.svg +1 -1
- package/icons/material-symbols/warning.svg +1 -1
- package/icons/material-symbols/wave-sine.svg +1 -1
- package/icons/material-symbols/wheelchair.svg +1 -1
- package/icons/material-symbols/wrench.svg +1 -1
- package/icons/material-symbols/x.svg +1 -1
- package/icons/phosphor/pack.json +8 -8
- package/lib/apps.mjs +142 -139
- package/lib/apps.test.d.mts +1 -0
- package/lib/apps.test.mjs +50 -0
- package/lib/blocks.mjs +90 -90
- package/lib/caseCollisions.d.mts +4 -0
- package/lib/caseCollisions.mjs +62 -0
- package/lib/caseCollisions.test.d.mts +1 -0
- package/lib/caseCollisions.test.mjs +74 -0
- package/lib/colors.ts +86 -86
- package/lib/contactBar.ts +18 -18
- package/lib/contracts.d.mts +15 -0
- package/lib/contracts.mjs +17 -0
- package/lib/cookieCategories.mjs +28 -28
- package/lib/crop.mjs +83 -83
- package/lib/csp.mjs +130 -130
- package/lib/cspSources.mjs +60 -60
- package/lib/dynamic-colors.ts +426 -426
- package/lib/git.mjs +64 -64
- package/lib/hooks.ts +51 -51
- package/lib/inline.ts +20 -20
- package/lib/license-texts/Apache-2.0.txt +190 -190
- package/lib/license-texts/BSD-2-Clause.txt +24 -24
- package/lib/license-texts/BSD-3-Clause.txt +28 -28
- package/lib/license-texts/ISC.txt +15 -15
- package/lib/license-texts/MIT.txt +21 -21
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.mjs +683 -683
- package/lib/markdown.ts +42 -42
- package/lib/motion-schemes.mjs +5 -5
- package/lib/motion.ts +131 -131
- package/lib/notice.mjs +34 -34
- package/lib/scheduled.mjs +37 -37
- package/lib/snackbar.ts +149 -149
- package/lib/status.mjs +18 -18
- package/lib/storeEntries.d.mts +18 -0
- package/lib/storeEntries.mjs +64 -0
- package/lib/storeEntries.test.d.mts +1 -0
- package/lib/storeEntries.test.mjs +71 -0
- package/lib/svg.mjs +231 -231
- package/lib/text-colors.mjs +43 -43
- package/lib/video-id.mjs +72 -72
- package/lib/video.mjs +91 -91
- package/package.json +136 -131
- package/pages/404.astro +10 -10
- package/pages/[...slug].astro +51 -51
- package/pages/_headers.txt +31 -31
- package/pages/blog/index.astro +10 -10
- package/pages/blog/kategori/[category].astro +18 -18
- package/pages/editor/code-images.json.js +25 -25
- package/pages/editor/index.astro +75 -75
- package/pages/editor/manifest.webmanifest.js +23 -23
- package/pages/editor/sw.js +33 -33
- package/pages/editor/sw.js.js +4 -4
- package/pages/editor/thumbnails.json.js +21 -21
- package/pages/editor/version.json.js +22 -22
- package/pages/headers.js +4 -4
- package/pages/index.astro +10 -10
- package/pages/ny.astro +6 -6
- package/pages/robots.txt.js +3 -3
- package/pages/rss.xml.js +20 -20
- package/scripts/app-screenshots.mjs +164 -164
- package/scripts/app-workers.mjs +6 -6
- package/scripts/bin.mjs +12 -12
- package/scripts/built-site.mjs +134 -99
- package/scripts/check-consent.mjs +113 -113
- package/scripts/check-content.mjs +151 -149
- package/scripts/check-demo.mjs +24 -24
- package/scripts/check-store.mjs +160 -0
- package/scripts/check-themes.mjs +45 -45
- package/scripts/check-worker.mjs +569 -569
- package/scripts/compare-html.mjs +81 -81
- package/scripts/declarations.mjs +10 -0
- package/scripts/declarations.test.mjs +32 -0
- package/scripts/demo-site.mjs +42 -42
- package/scripts/editor-icons.mjs +56 -56
- package/scripts/editor-roundtrip.mjs +134 -134
- package/scripts/editor-screens.mjs +101 -101
- package/scripts/git-env.mjs +9 -9
- package/scripts/icon-packs.mjs +151 -151
- package/scripts/link-counts.mjs +22 -22
- package/scripts/page-colors.mjs +42 -42
- package/scripts/releases.mjs +80 -80
- package/scripts/responsive-images.mjs +94 -94
- package/scripts/scheduled-list.mjs +15 -15
- package/scripts/theme-screenshots.mjs +88 -88
- package/scripts/types.mjs +17 -21
- package/scripts/video-thumbnails.mjs +89 -89
- package/scripts/worker-modules.mjs +34 -34
- package/site/appCookies.mjs +74 -74
- package/site/apps.ts +21 -21
- package/site/autolinks.mjs +200 -200
- package/site/blockScripts.ts +31 -31
- package/site/categories.ts +16 -16
- package/site/consent.ts +26 -26
- package/site/content.config.ts +84 -84
- package/site/content.ts +104 -104
- package/site/cookieList.ts +17 -17
- package/site/defineSite.d.mts +2 -14
- package/site/defineSite.mjs +131 -136
- package/site/directives/interact.mjs +31 -31
- package/site/editor-version.mjs +26 -26
- package/site/fill.ts +13 -13
- package/site/format.ts +11 -11
- package/site/home.ts +66 -66
- package/site/iconPacks.mjs +43 -43
- package/site/icons.mjs +605 -605
- package/site/images.mjs +157 -157
- package/site/motion.ts +13 -13
- package/site/noindex.mjs +21 -21
- package/site/pageColors.ts +110 -110
- package/site/popups.ts +98 -98
- package/site/schema.ts +189 -189
- package/site/sections.ts +3 -3
- package/site/seo.ts +28 -28
- package/site/site.ts +76 -76
- package/site/siteConfig.mjs +8 -8
- package/site/siteCsp.mjs +75 -75
- package/site/siteModules.mjs +27 -27
- package/site/standardPages.ts +59 -59
- package/site/storeTheme.d.mts +6 -0
- package/site/storeTheme.mjs +29 -0
- package/site/storeTheme.test.d.mts +1 -0
- package/site/storeTheme.test.mjs +53 -0
- package/site/testimonials.ts +48 -48
- package/site/text.ts +43 -43
- package/site/theme.mjs +483 -472
- package/site/tracking.client.ts +225 -225
- package/site/tracking.ts +9 -9
- package/src/ActionParts.tsx +205 -205
- package/src/Advanced.tsx +152 -152
- package/src/App.tsx +901 -904
- package/src/AppList.tsx +1362 -1345
- package/src/BlockDialog.tsx +360 -360
- package/src/BlockParts.tsx +652 -652
- package/src/BlockSettings.tsx +533 -533
- package/src/BrandForm.tsx +164 -164
- package/src/Colors.tsx +476 -476
- package/src/CommunicationParts.tsx +199 -199
- package/src/CompanyForm.tsx +209 -209
- package/src/Consents.tsx +175 -175
- package/src/ContainmentParts.tsx +185 -185
- package/src/Crop.tsx +152 -152
- package/src/DataForm.tsx +434 -434
- package/src/Deleted.tsx +177 -177
- package/src/EntryForm.tsx +740 -740
- package/src/FileView.tsx +30 -30
- package/src/FormFrame.tsx +237 -237
- package/src/Keywords.tsx +225 -225
- package/src/Library.tsx +1563 -1563
- package/src/Licenses.tsx +336 -336
- package/src/LinkSheet.tsx +138 -138
- package/src/MarkdownEditor.tsx +1075 -1075
- package/src/Media.tsx +677 -677
- package/src/NavigationParts.tsx +350 -350
- package/src/NavigationRestParts.tsx +247 -247
- package/src/Notifications.tsx +59 -59
- package/src/PagePicker.tsx +136 -136
- package/src/Parts.tsx +991 -991
- package/src/PartsShown.tsx +168 -168
- package/src/PickerParts.tsx +183 -183
- package/src/Redirects.tsx +450 -450
- package/src/Screen.tsx +184 -184
- package/src/SectionsField.tsx +65 -65
- package/src/SelectionParts.tsx +134 -134
- package/src/SeoScores.tsx +184 -184
- package/src/Sheet.tsx +35 -35
- package/src/ThemeStore.tsx +376 -376
- package/src/Tokens.tsx +346 -346
- package/src/Versions.tsx +214 -214
- package/src/addresses.ts +75 -75
- package/src/allIcons.ts +7 -7
- package/src/appFiles.ts +29 -29
- package/src/apps.ts +281 -281
- package/src/ask.tsx +123 -123
- package/src/blocks.ts +251 -251
- package/src/brand.ts +153 -153
- package/src/cache.ts +42 -42
- package/src/check.ts +285 -285
- package/src/checkSite.ts +256 -256
- package/src/{consents.ts → consentData.ts} +26 -26
- package/src/cookieList.ts +70 -70
- package/src/countries.ts +59 -59
- package/src/deploy.ts +155 -155
- package/src/editor.css +602 -602
- package/src/examples.ts +85 -85
- package/src/fields.tsx +942 -942
- package/src/frontmatter.mjs +42 -42
- package/src/github.ts +376 -376
- package/src/i18n/da-DK/advanced.ts +28 -28
- package/src/i18n/da-DK/app.ts +71 -71
- package/src/i18n/da-DK/apps.ts +90 -90
- package/src/i18n/da-DK/blockSettings.ts +38 -38
- package/src/i18n/da-DK/blocks.ts +408 -408
- package/src/i18n/da-DK/check.ts +25 -25
- package/src/i18n/da-DK/colors.ts +64 -64
- package/src/i18n/da-DK/common.ts +118 -118
- package/src/i18n/da-DK/company.ts +24 -24
- package/src/i18n/da-DK/consents.ts +23 -23
- package/src/i18n/da-DK/forms.ts +116 -116
- package/src/i18n/da-DK/history.ts +35 -35
- package/src/i18n/da-DK/index.ts +49 -49
- package/src/i18n/da-DK/keywords.ts +19 -19
- package/src/i18n/da-DK/library.ts +135 -135
- package/src/i18n/da-DK/licenses.ts +25 -25
- package/src/i18n/da-DK/media.ts +79 -79
- package/src/i18n/da-DK/parts.ts +696 -696
- package/src/i18n/da-DK/preview.ts +13 -13
- package/src/i18n/da-DK/redirects.ts +51 -51
- package/src/i18n/da-DK/seo.ts +20 -20
- package/src/i18n/da-DK/text.ts +35 -35
- package/src/i18n/da-DK/theme.ts +41 -41
- package/src/i18n/en-US/advanced.ts +32 -32
- package/src/i18n/en-US/app.ts +84 -84
- package/src/i18n/en-US/apps.ts +134 -134
- package/src/i18n/en-US/blockSettings.ts +48 -48
- package/src/i18n/en-US/blocks.ts +441 -441
- package/src/i18n/en-US/check.ts +31 -31
- package/src/i18n/en-US/colors.ts +82 -82
- package/src/i18n/en-US/common.ts +144 -144
- package/src/i18n/en-US/company.ts +25 -25
- package/src/i18n/en-US/consents.ts +25 -25
- package/src/i18n/en-US/forms.ts +145 -145
- package/src/i18n/en-US/history.ts +43 -43
- package/src/i18n/en-US/index.ts +51 -51
- package/src/i18n/en-US/keywords.ts +24 -24
- package/src/i18n/en-US/library.ts +176 -176
- package/src/i18n/en-US/licenses.ts +36 -36
- package/src/i18n/en-US/media.ts +80 -80
- package/src/i18n/en-US/parts.ts +718 -718
- package/src/i18n/en-US/preview.ts +15 -15
- package/src/i18n/en-US/redirects.ts +66 -66
- package/src/i18n/en-US/seo.ts +26 -26
- package/src/i18n/en-US/text.ts +41 -41
- package/src/i18n/en-US/theme.ts +41 -41
- package/src/i18n/index.ts +124 -124
- package/src/icon.tsx +160 -160
- package/src/iconPacks.ts +89 -89
- package/src/images.ts +163 -163
- package/src/{licenses.ts → licenseData.ts} +79 -79
- package/src/markdown.mjs +204 -204
- package/src/nav.ts +68 -68
- package/src/notFound.ts +28 -28
- package/src/{notifications.ts → notificationData.ts} +223 -223
- package/src/pagePreview.tsx +204 -204
- package/src/r2Caching.ts +55 -55
- package/src/{redirects.ts → redirectData.ts} +149 -149
- package/src/registry.tsx +188 -188
- package/src/schema.ts +417 -417
- package/src/sections.mjs +303 -303
- package/src/seo.ts +333 -333
- package/src/shared.ts +44 -44
- package/src/sidebar.ts +31 -31
- package/src/site.ts +317 -317
- package/src/standard/Block.astro +19 -19
- package/src/standard/BlockSite.astro +18 -18
- package/src/standard/accordion/index.tsx +47 -47
- package/src/standard/accordion/load.ts +23 -23
- package/src/standard/accordion/shared.ts +15 -15
- package/src/standard/accordion/view.ts +32 -32
- package/src/standard/buttons/fields.ts +98 -98
- package/src/standard/buttons/index.tsx +23 -23
- package/src/standard/buttons/load.ts +34 -34
- package/src/standard/buttons/shared.ts +12 -12
- package/src/standard/buttons/view.ts +30 -30
- package/src/standard/cards/index.tsx +71 -71
- package/src/standard/cards/load.ts +17 -17
- package/src/standard/cards/shared.ts +27 -27
- package/src/standard/cards/view.ts +29 -29
- package/src/standard/checklist/index.tsx +27 -27
- package/src/standard/checklist/load.ts +5 -5
- package/src/standard/checklist/view.ts +13 -13
- package/src/standard/cookies/index.tsx +17 -17
- package/src/standard/cookies/list.ts +56 -56
- package/src/standard/cookies/load.ts +11 -11
- package/src/standard/cookies/shared.ts +20 -20
- package/src/standard/cookies/view.ts +28 -28
- package/src/standard/faq/index.tsx +29 -29
- package/src/standard/faq/load.ts +14 -14
- package/src/standard/faq/view.ts +21 -21
- package/src/standard/head.ts +23 -23
- package/src/standard/hero/fields.ts +48 -48
- package/src/standard/hero/index.tsx +57 -57
- package/src/standard/hero/load.ts +23 -23
- package/src/standard/hero/view.ts +38 -38
- package/src/standard/links/index.tsx +41 -41
- package/src/standard/links/load.ts +17 -17
- package/src/standard/links/view.ts +25 -25
- package/src/standard/list/index.tsx +50 -50
- package/src/standard/list/load.ts +5 -5
- package/src/standard/list/view.ts +18 -18
- package/src/standard/load.ts +7 -7
- package/src/standard/notice/index.tsx +30 -30
- package/src/standard/notice/load.ts +16 -16
- package/src/standard/notice/view.ts +10 -10
- package/src/standard/points/index.tsx +42 -42
- package/src/standard/points/load.ts +5 -5
- package/src/standard/points/view.ts +12 -12
- package/src/standard/posts/index.tsx +132 -132
- package/src/standard/posts/load.ts +36 -36
- package/src/standard/posts/shared.ts +21 -21
- package/src/standard/posts/view.ts +63 -63
- package/src/standard/pricecards/index.tsx +57 -57
- package/src/standard/pricecards/load.ts +11 -11
- package/src/standard/pricecards/view.ts +50 -50
- package/src/standard/sharedTexts.ts +6 -6
- package/src/standard/site.ts +124 -124
- package/src/standard/siteLooks.ts +19 -19
- package/src/standard/sites.ts +13 -13
- package/src/standard/steps/index.tsx +28 -28
- package/src/standard/steps/load.ts +5 -5
- package/src/standard/steps/view.ts +11 -11
- package/src/standard/testimonials/index.tsx +87 -87
- package/src/standard/testimonials/list.ts +43 -43
- package/src/standard/testimonials/load.ts +58 -58
- package/src/standard/testimonials/shared.ts +13 -13
- package/src/standard/testimonials/view.ts +76 -76
- package/src/standard/themeBlocks.ts +13 -13
- package/src/standard/video/index.tsx +23 -23
- package/src/standard/video/load.ts +10 -10
- package/src/standard/video/view.ts +42 -42
- package/src/standard/view.tsx +105 -105
- package/src/standard/views.ts +37 -37
- package/src/store.ts +479 -499
- package/src/theme.ts +90 -90
- package/src/themeUi.ts +97 -97
- package/src/themes.ts +91 -91
- package/src/ui.tsx +3402 -3402
- package/src/virtual.d.ts +5 -5
- package/src/when.ts +22 -22
- package/starter/.githooks/pre-push +4 -4
- package/starter/.nvmrc +1 -1
- package/starter/.prettierignore +11 -11
- package/starter/.prettierrc.json +7 -7
- package/starter/README.md +27 -26
- package/starter/astro.config.mjs +5 -5
- package/starter/package.json +67 -67
- package/starter/public/_redirects +1 -1
- package/starter/public/favicon.svg +1 -1
- package/starter/public/logo.svg +1 -1
- package/starter/src/content/pages/blog.md +8 -8
- package/starter/src/content/pages/forside.md +9 -9
- package/starter/src/content/pages/kontakt.md +7 -7
- package/starter/src/content/pages/privatlivspolitik.md +9 -9
- package/starter/src/content/pages/siden-findes-ikke.md +7 -7
- package/starter/src/content.config.ts +2 -2
- package/starter/src/data/apps.json +1 -1
- package/starter/src/data/block-settings.json +3 -3
- package/starter/src/data/blocks.json +1 -1
- package/starter/src/data/booking.json +6 -6
- package/starter/src/data/colors.json +70 -70
- package/starter/src/data/contact.json +8 -8
- package/starter/src/data/cookies.json +21 -21
- package/starter/src/data/csp.json +1 -1
- package/starter/src/data/footer.json +15 -15
- package/starter/src/data/internal-links.json +4 -4
- package/starter/src/data/menu.json +4 -4
- package/starter/src/data/mobile-menu.json +4 -4
- package/starter/src/data/old-addresses.json +1 -1
- package/starter/src/data/redirects.json +1 -1
- package/starter/src/data/standard-pages.json +8 -8
- package/starter/src/data/theme.json +1 -1
- package/starter/src/env.d.ts +2 -2
- package/starter/src/site/config.ts +13 -13
- package/starter/src/site/worker.js +14 -14
- package/starter/tsconfig.json +17 -17
- package/starter/worker/apps.js +6 -6
- package/starter/wrangler.jsonc +39 -39
- package/theme/LICENSE +21 -21
- package/theme/blocks/ArrowLink.tsx +14 -14
- package/theme/blocks/BlockHead.tsx +26 -26
- package/theme/blocks/Panel.tsx +32 -32
- package/theme/blocks/Person.tsx +26 -26
- package/theme/blocks/accordion/PageRow.tsx +25 -25
- package/theme/blocks/accordion/View.tsx +17 -17
- package/theme/blocks/buttons/Buttons.tsx +28 -28
- package/theme/blocks/buttons/View.tsx +12 -12
- package/theme/blocks/cards/LinkCards.tsx +72 -72
- package/theme/blocks/cards/View.tsx +20 -20
- package/theme/blocks/checklist/Checks.tsx +15 -15
- package/theme/blocks/checklist/View.tsx +14 -14
- package/theme/blocks/cookies/View.tsx +36 -36
- package/theme/blocks/faq/Accordion.tsx +34 -34
- package/theme/blocks/faq/View.tsx +16 -16
- package/theme/blocks/faq/script.ts +56 -56
- package/theme/blocks/hero/View.tsx +44 -44
- package/theme/blocks/links/LinkGroups.tsx +24 -24
- package/theme/blocks/links/View.tsx +17 -17
- package/theme/blocks/list/Rows.tsx +24 -24
- package/theme/blocks/list/View.tsx +14 -14
- package/theme/blocks/notice/View.tsx +13 -13
- package/theme/blocks/points/PointList.tsx +34 -34
- package/theme/blocks/points/View.tsx +14 -14
- package/theme/blocks/posts/View.tsx +26 -26
- package/theme/blocks/pricecards/View.tsx +40 -40
- package/theme/blocks/steps/StepList.tsx +23 -23
- package/theme/blocks/steps/View.tsx +14 -14
- package/theme/blocks/testimonials/List.tsx +46 -46
- package/theme/blocks/testimonials/Slider.tsx +44 -44
- package/theme/blocks/testimonials/Testimonial.tsx +124 -124
- package/theme/blocks/testimonials/View.tsx +21 -21
- package/theme/blocks/testimonials/script.ts +52 -52
- package/theme/blocks/video/View.tsx +41 -41
- package/theme/components/ActionBar.astro +63 -63
- package/theme/components/AppSlot.astro +21 -21
- package/theme/components/ArrowLink.tsx +21 -21
- package/theme/components/BlogIndex.astro +48 -48
- package/theme/components/BlogPosts.astro +18 -18
- package/theme/components/BlogPosts.tsx +45 -45
- package/theme/components/BookButton.tsx +22 -22
- package/theme/components/Container.astro +11 -11
- package/theme/components/CookieConsent.astro +121 -121
- package/theme/components/EndBlock.astro +21 -21
- package/theme/components/FontPreload.astro +6 -6
- package/theme/components/Footer.astro +85 -85
- package/theme/components/Header.astro +186 -186
- package/theme/components/Icon.astro +16 -16
- package/theme/components/Img.astro +29 -29
- package/theme/components/Img.tsx +33 -33
- package/theme/components/Inline.astro +11 -11
- package/theme/components/Logo.astro +23 -23
- package/theme/components/LogoMark.astro +10 -10
- package/theme/components/Markdown.astro +17 -17
- package/theme/components/MobileMenu.tsx +224 -224
- package/theme/components/MobileNav.astro +33 -33
- package/theme/components/PageHeader.astro +22 -22
- package/theme/components/PageSection.astro +24 -24
- package/theme/components/PhoneButton.tsx +11 -11
- package/theme/components/Popup.astro +145 -145
- package/theme/components/PopupButton.tsx +24 -24
- package/theme/components/PopupView.tsx +48 -48
- package/theme/components/PostList.tsx +58 -58
- package/theme/components/PostMeta.tsx +17 -17
- package/theme/components/RichText.astro +28 -28
- package/theme/components/Section.astro +29 -29
- package/theme/components/SectionHeading.tsx +8 -8
- package/theme/components/TextSection.astro +16 -16
- package/theme/components/home/PageBlock.astro +19 -19
- package/theme/components/home/Text.astro +13 -13
- package/theme/components/loadPopup.ts +22 -22
- package/theme/components/loadPosts.ts +16 -16
- package/theme/components/posts.ts +15 -15
- package/theme/editor/looks.mjs +7 -7
- package/theme/editor/previews.tsx +56 -56
- package/theme/layouts/Base.astro +273 -273
- package/theme/layouts/PageLayout.astro +126 -126
- package/theme/layouts/PostLayout.astro +137 -137
- package/theme/scripts/motion.ts +29 -29
- package/theme/styles/base.css +320 -320
- package/theme/styles/blocks.css +1808 -1808
- package/theme/styles/components.css +206 -206
- package/theme/styles/global.css +1302 -1302
- package/theme/styles/ui.css +6544 -6544
- package/theme/templates/Blog.astro +38 -38
- package/theme/templates/Category.astro +19 -19
- package/theme/templates/Home.astro +36 -36
- package/theme/templates/New.astro +68 -68
- package/theme/templates/NotFound.astro +117 -117
- package/theme/theme.json +28 -28
- package/theme/ui/Badge.tsx +45 -45
- package/theme/ui/BottomSheet.tsx +148 -148
- package/theme/ui/Button.tsx +79 -79
- package/theme/ui/ButtonGroup.tsx +31 -31
- package/theme/ui/Card.tsx +45 -45
- package/theme/ui/Carousel.tsx +124 -124
- package/theme/ui/Checkbox.tsx +59 -59
- package/theme/ui/Chip.tsx +101 -101
- package/theme/ui/ChipSet.tsx +27 -27
- package/theme/ui/DatePicker.tsx +149 -149
- package/theme/ui/DatePickerContainer.tsx +316 -316
- package/theme/ui/Dialog.tsx +100 -100
- package/theme/ui/Divider.tsx +27 -27
- package/theme/ui/ExposedDropdownMenu.tsx +77 -77
- package/theme/ui/Fab.tsx +88 -88
- package/theme/ui/FabMenu.tsx +67 -67
- package/theme/ui/IconButton.tsx +78 -78
- package/theme/ui/List.tsx +287 -287
- package/theme/ui/LoadingIndicator.tsx +29 -29
- package/theme/ui/Menu.tsx +278 -278
- package/theme/ui/NavigationBar.tsx +62 -62
- package/theme/ui/NavigationDrawer.tsx +80 -80
- package/theme/ui/NavigationRail.tsx +154 -154
- package/theme/ui/ProgressIndicator.tsx +162 -162
- package/theme/ui/RadioButton.tsx +50 -50
- package/theme/ui/Search.tsx +129 -129
- package/theme/ui/SegmentedButton.tsx +49 -49
- package/theme/ui/SideSheet.tsx +54 -54
- package/theme/ui/Slider.tsx +155 -155
- package/theme/ui/Snackbar.tsx +67 -67
- package/theme/ui/SplitButton.tsx +67 -67
- package/theme/ui/Switch.tsx +60 -60
- package/theme/ui/Tabs.tsx +114 -114
- package/theme/ui/TextField.tsx +278 -278
- package/theme/ui/TimePicker.tsx +128 -128
- package/theme/ui/TimePickerContainer.tsx +234 -234
- package/theme/ui/Toolbar.tsx +67 -67
- package/theme/ui/Tooltip.tsx +58 -58
- package/theme/ui/TopAppBar.tsx +77 -77
- package/theme/ui/bottomSheet.script.ts +8 -8
- package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
- package/theme/ui/carousel.script.ts +28 -28
- package/theme/ui/carouselLayout.ts +229 -229
- package/theme/ui/checkbox.script.ts +8 -8
- package/theme/ui/datePicker.script.ts +302 -302
- package/theme/ui/dates.ts +185 -185
- package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
- package/theme/ui/dropdownMenu.script.ts +166 -166
- package/theme/ui/fabMenu.script.ts +57 -57
- package/theme/ui/navigationRail.script.ts +47 -47
- package/theme/ui/ripple.script.ts +159 -159
- package/theme/ui/search.script.ts +92 -92
- package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
- package/theme/ui/slider.script.ts +12 -12
- package/theme/ui/sliderUpdate.ts +52 -52
- package/theme/ui/snackbar.script.ts +16 -16
- package/theme/ui/splitButton.script.ts +63 -63
- package/theme/ui/tabs.script.ts +48 -48
- package/theme/ui/textField.script.ts +17 -17
- package/theme/ui/textFieldUpdate.ts +44 -44
- package/theme/ui/timePicker.script.ts +248 -248
- package/theme/ui/toolbar.script.ts +43 -43
- package/theme/ui/tooltip.script.ts +68 -68
- package/worker/consent.js +141 -141
- package/worker/editor-github.js +167 -167
- package/worker/editor-host.js +74 -74
- package/worker/files.js +401 -401
- package/worker/github.js +36 -36
- package/worker/index.js +188 -184
- package/worker/not-found.js +97 -97
- package/worker/oauth.js +95 -95
- package/worker/publish.js +52 -52
- package/worker/push.js +270 -270
- package/worker/r2-status.js +50 -50
- package/worker/session.js +58 -58
- package/worker/store.js +46 -0
package/src/nav.ts
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
// How the editor goes between its screens (the address after the "#") in the browser's history, so
|
|
2
|
-
// that the back arrow and the phone's back gesture go the same way. The back arrow goes to the screen
|
|
3
|
-
// above (Screen.tsx `back`); when that is the screen before it in the history, it goes back there, as
|
|
4
|
-
// the gesture does, and otherwise the screen above takes the place of this one. No step is added, so
|
|
5
|
-
// the gesture never brings back a screen that was left with the arrow. Every step in the history knows
|
|
6
|
-
// its place and the screen before it (`history.state`).
|
|
7
|
-
|
|
8
|
-
type Step = { at: number; prev: string | null };
|
|
9
|
-
|
|
10
|
-
const step = () => history.state as Step | null;
|
|
11
|
-
|
|
12
|
-
/** The screen of an address ("#liste/sider" → "liste/sider", "#" → ""); one with a broken %-sign is
|
|
13
|
-
* the start screen. */
|
|
14
|
-
export function screenOf(hash: string) {
|
|
15
|
-
try {
|
|
16
|
-
return decodeURIComponent(hash.replace(/^#/, ''));
|
|
17
|
-
} catch {
|
|
18
|
-
return '';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// The place of the screen that is shown, and while a step is undone (arrived), the place it goes back to.
|
|
23
|
-
let at = step()?.at ?? 0;
|
|
24
|
-
let undoing: number | null = null;
|
|
25
|
-
if (!step()) history.replaceState({ at, prev: null } satisfies Step, '');
|
|
26
|
-
|
|
27
|
-
/** Called by the app on every new address, with the screen it came from: null when it is a step back
|
|
28
|
-
* that `undo` asked for, else how to undo the step (back to the screen it came from) and take it again
|
|
29
|
-
* (when the owner leaves without saving). */
|
|
30
|
-
export function arrived(from: string): { undo: () => void; redo: () => void } | null {
|
|
31
|
-
const now = step();
|
|
32
|
-
if (undoing !== null && now?.at === undoing) {
|
|
33
|
-
at = undoing;
|
|
34
|
-
undoing = null;
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
undoing = null;
|
|
38
|
-
const before = at;
|
|
39
|
-
// A step that was there (back, forward): its own place.
|
|
40
|
-
if (now) {
|
|
41
|
-
at = now.at;
|
|
42
|
-
return {
|
|
43
|
-
undo: () => ((undoing = before), history.go(before - now.at)),
|
|
44
|
-
redo: () => history.go(now.at - before),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
// A new step (a link, or the address typed): after this one.
|
|
48
|
-
const href = location.hash;
|
|
49
|
-
at = before + 1;
|
|
50
|
-
history.replaceState({ at, prev: from } satisfies Step, '');
|
|
51
|
-
return {
|
|
52
|
-
undo: () => ((undoing = before), history.back()),
|
|
53
|
-
redo: () => (location.hash = href),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Puts another screen in the place of this one (no new step in the history). */
|
|
58
|
-
export function replace(href: string) {
|
|
59
|
-
history.replaceState({ at, prev: step()?.prev ?? null } satisfies Step, '', href);
|
|
60
|
-
dispatchEvent(new HashChangeEvent('hashchange'));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The back arrow: to the screen above, `href`. Back in the history when that is the screen before,
|
|
64
|
-
* else in the place of this one. */
|
|
65
|
-
export function up(href: string) {
|
|
66
|
-
if (step()?.prev === screenOf(href)) history.back();
|
|
67
|
-
else replace(href);
|
|
68
|
-
}
|
|
1
|
+
// How the editor goes between its screens (the address after the "#") in the browser's history, so
|
|
2
|
+
// that the back arrow and the phone's back gesture go the same way. The back arrow goes to the screen
|
|
3
|
+
// above (Screen.tsx `back`); when that is the screen before it in the history, it goes back there, as
|
|
4
|
+
// the gesture does, and otherwise the screen above takes the place of this one. No step is added, so
|
|
5
|
+
// the gesture never brings back a screen that was left with the arrow. Every step in the history knows
|
|
6
|
+
// its place and the screen before it (`history.state`).
|
|
7
|
+
|
|
8
|
+
type Step = { at: number; prev: string | null };
|
|
9
|
+
|
|
10
|
+
const step = () => history.state as Step | null;
|
|
11
|
+
|
|
12
|
+
/** The screen of an address ("#liste/sider" → "liste/sider", "#" → ""); one with a broken %-sign is
|
|
13
|
+
* the start screen. */
|
|
14
|
+
export function screenOf(hash: string) {
|
|
15
|
+
try {
|
|
16
|
+
return decodeURIComponent(hash.replace(/^#/, ''));
|
|
17
|
+
} catch {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The place of the screen that is shown, and while a step is undone (arrived), the place it goes back to.
|
|
23
|
+
let at = step()?.at ?? 0;
|
|
24
|
+
let undoing: number | null = null;
|
|
25
|
+
if (!step()) history.replaceState({ at, prev: null } satisfies Step, '');
|
|
26
|
+
|
|
27
|
+
/** Called by the app on every new address, with the screen it came from: null when it is a step back
|
|
28
|
+
* that `undo` asked for, else how to undo the step (back to the screen it came from) and take it again
|
|
29
|
+
* (when the owner leaves without saving). */
|
|
30
|
+
export function arrived(from: string): { undo: () => void; redo: () => void } | null {
|
|
31
|
+
const now = step();
|
|
32
|
+
if (undoing !== null && now?.at === undoing) {
|
|
33
|
+
at = undoing;
|
|
34
|
+
undoing = null;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
undoing = null;
|
|
38
|
+
const before = at;
|
|
39
|
+
// A step that was there (back, forward): its own place.
|
|
40
|
+
if (now) {
|
|
41
|
+
at = now.at;
|
|
42
|
+
return {
|
|
43
|
+
undo: () => ((undoing = before), history.go(before - now.at)),
|
|
44
|
+
redo: () => history.go(now.at - before),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// A new step (a link, or the address typed): after this one.
|
|
48
|
+
const href = location.hash;
|
|
49
|
+
at = before + 1;
|
|
50
|
+
history.replaceState({ at, prev: from } satisfies Step, '');
|
|
51
|
+
return {
|
|
52
|
+
undo: () => ((undoing = before), history.back()),
|
|
53
|
+
redo: () => (location.hash = href),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Puts another screen in the place of this one (no new step in the history). */
|
|
58
|
+
export function replace(href: string) {
|
|
59
|
+
history.replaceState({ at, prev: step()?.prev ?? null } satisfies Step, '', href);
|
|
60
|
+
dispatchEvent(new HashChangeEvent('hashchange'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The back arrow: to the screen above, `href`. Back in the history when that is the screen before,
|
|
64
|
+
* else in the place of this one. */
|
|
65
|
+
export function up(href: string) {
|
|
66
|
+
if (step()?.prev === screenOf(href)) history.back();
|
|
67
|
+
else replace(href);
|
|
68
|
+
}
|
package/src/notFound.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
// The addresses visitors came to that do not exist, as the site's Worker keeps them (packages/editor/worker/not-found.js,
|
|
2
|
-
// /api/not-found on the editor's own address), for "Ikke fundet" under "Videresendelser" (Redirects.tsx).
|
|
3
|
-
// Asked with the GitHub sign-in (the Worker's cookie, sent with the request): the Worker lets only someone who can save
|
|
4
|
-
// the site see them.
|
|
5
|
-
|
|
6
|
-
/** An address that does not exist: how many visits, the first and the last (ISO dates), and where the
|
|
7
|
-
* last visitor came from (an address of the site, another site's name, or ''). */
|
|
8
|
-
export type Missing = { path: string; count: number; first: string; last: string; from: string };
|
|
9
|
-
|
|
10
|
-
const API = '/api/not-found';
|
|
11
|
-
|
|
12
|
-
/** The addresses, most visits first; null when the site does not keep them (no Worker, no storage). */
|
|
13
|
-
export async function loadNotFound(): Promise<Missing[] | null> {
|
|
14
|
-
const res = await fetch(API, { cache: 'no-store' });
|
|
15
|
-
if (res.status === 503 || !res.headers.get('content-type')?.includes('application/json')) return null;
|
|
16
|
-
if (!res.ok) throw new Error(String(res.status));
|
|
17
|
-
return ((await res.json()) as { entries: Missing[] }).entries;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Takes the addresses off the list (they now send on, or are not worth it). */
|
|
21
|
-
export async function forgetNotFound(paths: string[]): Promise<void> {
|
|
22
|
-
const res = await fetch(API, {
|
|
23
|
-
method: 'DELETE',
|
|
24
|
-
headers: { 'content-type': 'application/json' },
|
|
25
|
-
body: JSON.stringify({ paths }),
|
|
26
|
-
});
|
|
27
|
-
if (!res.ok) throw new Error(String(res.status));
|
|
28
|
-
}
|
|
1
|
+
// The addresses visitors came to that do not exist, as the site's Worker keeps them (packages/editor/worker/not-found.js,
|
|
2
|
+
// /api/not-found on the editor's own address), for "Ikke fundet" under "Videresendelser" (Redirects.tsx).
|
|
3
|
+
// Asked with the GitHub sign-in (the Worker's cookie, sent with the request): the Worker lets only someone who can save
|
|
4
|
+
// the site see them.
|
|
5
|
+
|
|
6
|
+
/** An address that does not exist: how many visits, the first and the last (ISO dates), and where the
|
|
7
|
+
* last visitor came from (an address of the site, another site's name, or ''). */
|
|
8
|
+
export type Missing = { path: string; count: number; first: string; last: string; from: string };
|
|
9
|
+
|
|
10
|
+
const API = '/api/not-found';
|
|
11
|
+
|
|
12
|
+
/** The addresses, most visits first; null when the site does not keep them (no Worker, no storage). */
|
|
13
|
+
export async function loadNotFound(): Promise<Missing[] | null> {
|
|
14
|
+
const res = await fetch(API, { cache: 'no-store' });
|
|
15
|
+
if (res.status === 503 || !res.headers.get('content-type')?.includes('application/json')) return null;
|
|
16
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
17
|
+
return ((await res.json()) as { entries: Missing[] }).entries;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Takes the addresses off the list (they now send on, or are not worth it). */
|
|
21
|
+
export async function forgetNotFound(paths: string[]): Promise<void> {
|
|
22
|
+
const res = await fetch(API, {
|
|
23
|
+
method: 'DELETE',
|
|
24
|
+
headers: { 'content-type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ paths }),
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
28
|
+
}
|
|
@@ -1,223 +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;
|
|
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;
|