@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
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
// What a tooltip (Tooltip.tsx) does on the site: it shows a moment after the mouse comes over its anchor and at once
|
|
2
|
-
// when the anchor gets focus from the keyboard; it stays while the mouse is over the anchor or the tooltip and while
|
|
3
|
-
// focus is in them, and goes when they leave, or with Escape (WCAG 1.4.13: it can be dismissed, hovered and stays).
|
|
4
|
-
// Over its anchor, under it when there is no room above, and moved sideways to stay inside the window. Not on a touch
|
|
5
|
-
// (a tap does what the anchor does). Run on every page by the theme's Base.astro (blockScripts, packages/editor/site/blockScripts.ts);
|
|
6
|
-
// without a tooltip on the page it does nothing.
|
|
7
|
-
const SHOW_MS = 500;
|
|
8
|
-
const HIDE_MS = 150;
|
|
9
|
-
const EDGE = 8;
|
|
10
|
-
|
|
11
|
-
export default function tooltips() {
|
|
12
|
-
document.querySelectorAll<HTMLElement>('[data-tooltip]').forEach((anchor) => {
|
|
13
|
-
const tip = anchor.querySelector<HTMLElement>('.tooltip');
|
|
14
|
-
if (!tip) return;
|
|
15
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
16
|
-
|
|
17
|
-
const place = () => {
|
|
18
|
-
anchor.removeAttribute('data-tooltip-below');
|
|
19
|
-
tip.style.removeProperty('--tooltip-shift');
|
|
20
|
-
const at = anchor.getBoundingClientRect();
|
|
21
|
-
if (at.top - tip.offsetHeight - EDGE < 0) anchor.setAttribute('data-tooltip-below', '');
|
|
22
|
-
const box = tip.getBoundingClientRect();
|
|
23
|
-
const shift = Math.max(EDGE - box.left, Math.min(0, innerWidth - EDGE - box.right));
|
|
24
|
-
if (shift) tip.style.setProperty('--tooltip-shift', `${shift}px`);
|
|
25
|
-
};
|
|
26
|
-
const show = () => {
|
|
27
|
-
clearTimeout(timer);
|
|
28
|
-
if (anchor.hasAttribute('data-tooltip-open')) return;
|
|
29
|
-
anchor.setAttribute('data-tooltip-open', '');
|
|
30
|
-
place();
|
|
31
|
-
};
|
|
32
|
-
const hide = () => {
|
|
33
|
-
clearTimeout(timer);
|
|
34
|
-
anchor.removeAttribute('data-tooltip-open');
|
|
35
|
-
};
|
|
36
|
-
const later = (run: () => void, ms: number) => {
|
|
37
|
-
clearTimeout(timer);
|
|
38
|
-
timer = setTimeout(run, ms);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// Over it with the mouse, and focus in it from the keyboard: it shows while either is so.
|
|
42
|
-
let hovered = false;
|
|
43
|
-
let focused = false;
|
|
44
|
-
anchor.addEventListener('pointerenter', (e) => {
|
|
45
|
-
if (e.pointerType === 'touch') return;
|
|
46
|
-
hovered = true;
|
|
47
|
-
later(show, SHOW_MS);
|
|
48
|
-
});
|
|
49
|
-
anchor.addEventListener('pointerleave', (e) => {
|
|
50
|
-
if (e.pointerType === 'touch') return;
|
|
51
|
-
hovered = false;
|
|
52
|
-
if (!focused) later(hide, HIDE_MS);
|
|
53
|
-
});
|
|
54
|
-
anchor.addEventListener('focusin', (e) => {
|
|
55
|
-
if (!(e.target as HTMLElement).matches(':focus-visible')) return;
|
|
56
|
-
focused = true;
|
|
57
|
-
show();
|
|
58
|
-
});
|
|
59
|
-
anchor.addEventListener('focusout', (e) => {
|
|
60
|
-
if (anchor.contains(e.relatedTarget as Node | null)) return;
|
|
61
|
-
focused = false;
|
|
62
|
-
if (!hovered) later(hide, HIDE_MS);
|
|
63
|
-
});
|
|
64
|
-
// A tap or a click on the anchor does what it does, without the tooltip over it (not one on a rich tooltip's action).
|
|
65
|
-
anchor.addEventListener('pointerdown', (e) => !tip.contains(e.target as Node) && hide());
|
|
66
|
-
addEventListener('keydown', (e) => e.key === 'Escape' && anchor.hasAttribute('data-tooltip-open') && hide());
|
|
67
|
-
});
|
|
68
|
-
}
|
|
1
|
+
// What a tooltip (Tooltip.tsx) does on the site: it shows a moment after the mouse comes over its anchor and at once
|
|
2
|
+
// when the anchor gets focus from the keyboard; it stays while the mouse is over the anchor or the tooltip and while
|
|
3
|
+
// focus is in them, and goes when they leave, or with Escape (WCAG 1.4.13: it can be dismissed, hovered and stays).
|
|
4
|
+
// Over its anchor, under it when there is no room above, and moved sideways to stay inside the window. Not on a touch
|
|
5
|
+
// (a tap does what the anchor does). Run on every page by the theme's Base.astro (blockScripts, packages/editor/site/blockScripts.ts);
|
|
6
|
+
// without a tooltip on the page it does nothing.
|
|
7
|
+
const SHOW_MS = 500;
|
|
8
|
+
const HIDE_MS = 150;
|
|
9
|
+
const EDGE = 8;
|
|
10
|
+
|
|
11
|
+
export default function tooltips() {
|
|
12
|
+
document.querySelectorAll<HTMLElement>('[data-tooltip]').forEach((anchor) => {
|
|
13
|
+
const tip = anchor.querySelector<HTMLElement>('.tooltip');
|
|
14
|
+
if (!tip) return;
|
|
15
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
16
|
+
|
|
17
|
+
const place = () => {
|
|
18
|
+
anchor.removeAttribute('data-tooltip-below');
|
|
19
|
+
tip.style.removeProperty('--tooltip-shift');
|
|
20
|
+
const at = anchor.getBoundingClientRect();
|
|
21
|
+
if (at.top - tip.offsetHeight - EDGE < 0) anchor.setAttribute('data-tooltip-below', '');
|
|
22
|
+
const box = tip.getBoundingClientRect();
|
|
23
|
+
const shift = Math.max(EDGE - box.left, Math.min(0, innerWidth - EDGE - box.right));
|
|
24
|
+
if (shift) tip.style.setProperty('--tooltip-shift', `${shift}px`);
|
|
25
|
+
};
|
|
26
|
+
const show = () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
if (anchor.hasAttribute('data-tooltip-open')) return;
|
|
29
|
+
anchor.setAttribute('data-tooltip-open', '');
|
|
30
|
+
place();
|
|
31
|
+
};
|
|
32
|
+
const hide = () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
anchor.removeAttribute('data-tooltip-open');
|
|
35
|
+
};
|
|
36
|
+
const later = (run: () => void, ms: number) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = setTimeout(run, ms);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Over it with the mouse, and focus in it from the keyboard: it shows while either is so.
|
|
42
|
+
let hovered = false;
|
|
43
|
+
let focused = false;
|
|
44
|
+
anchor.addEventListener('pointerenter', (e) => {
|
|
45
|
+
if (e.pointerType === 'touch') return;
|
|
46
|
+
hovered = true;
|
|
47
|
+
later(show, SHOW_MS);
|
|
48
|
+
});
|
|
49
|
+
anchor.addEventListener('pointerleave', (e) => {
|
|
50
|
+
if (e.pointerType === 'touch') return;
|
|
51
|
+
hovered = false;
|
|
52
|
+
if (!focused) later(hide, HIDE_MS);
|
|
53
|
+
});
|
|
54
|
+
anchor.addEventListener('focusin', (e) => {
|
|
55
|
+
if (!(e.target as HTMLElement).matches(':focus-visible')) return;
|
|
56
|
+
focused = true;
|
|
57
|
+
show();
|
|
58
|
+
});
|
|
59
|
+
anchor.addEventListener('focusout', (e) => {
|
|
60
|
+
if (anchor.contains(e.relatedTarget as Node | null)) return;
|
|
61
|
+
focused = false;
|
|
62
|
+
if (!hovered) later(hide, HIDE_MS);
|
|
63
|
+
});
|
|
64
|
+
// A tap or a click on the anchor does what it does, without the tooltip over it (not one on a rich tooltip's action).
|
|
65
|
+
anchor.addEventListener('pointerdown', (e) => !tip.contains(e.target as Node) && hide());
|
|
66
|
+
addEventListener('keydown', (e) => e.key === 'Escape' && anchor.hasAttribute('data-tooltip-open') && hide());
|
|
67
|
+
});
|
|
68
|
+
}
|
package/worker/consent.js
CHANGED
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
// The visitors' answers in the cookie banner, so the site can show that a visitor said yes (GDPR article 7(1)): the
|
|
2
|
-
// consent log, "Samtykker" under "Privatliv" in the editor (packages/editor/src/
|
|
3
|
-
// Kept in Cloudflare D1 (the binding CONSENT in wrangler.jsonc), never in the repository: one row per answer, with the
|
|
4
|
-
// random consent id the visitor's browser keeps (consentId in src/scripts/tracking.ts, shown in the banner under
|
|
5
|
-
// "Cookieindstillinger"), when, the yes or no to each category the banner asked about, and the banner it was given in
|
|
6
|
-
// (the commit the site was built from, /editor/version.json). No IP address, no user agent: nothing more about the
|
|
7
|
-
// visitor than the answer. An answer older than two years is deleted (forgetOldConsents, each hour); the banner asks
|
|
8
|
-
// again after one. Without the binding (a preview of a branch) nothing is kept and the editor says so.
|
|
9
|
-
// POST /api/consent a visitor's answer, from the site's own pages: { id, choices }
|
|
10
|
-
// GET /api/consent?q=<id> the answers, newest first (at most LIMIT), those of a consent id starting with q
|
|
11
|
-
// GET /api/consent?format=csv all of them, as a file to save
|
|
12
|
-
// DELETE /api/consent { id } deletes the answers of a consent id (a visitor asked for it)
|
|
13
|
-
// Only someone who can save the site (write access to the repository on GitHub) can read or delete them.
|
|
14
|
-
import { isConsentCategory } from '../lib/cookieCategories.mjs';
|
|
15
|
-
import { json } from './not-found.js';
|
|
16
|
-
import { mayEdit } from './session.js';
|
|
17
|
-
|
|
18
|
-
/** How long an answer is kept: two years, the banner asks again after one. */
|
|
19
|
-
export const KEEP_DAYS = 730;
|
|
20
|
-
const LIMIT = 500;
|
|
21
|
-
// A consent id: crypto.randomUUID() in the visitor's browser.
|
|
22
|
-
const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
23
|
-
|
|
24
|
-
// The table, made the first time an isolate needs it (D1 has no migrations on deploy).
|
|
25
|
-
let ready;
|
|
26
|
-
const table = (db) =>
|
|
27
|
-
(ready ??= db
|
|
28
|
-
.batch([
|
|
29
|
-
db.prepare(
|
|
30
|
-
'CREATE TABLE IF NOT EXISTS consents (row INTEGER PRIMARY KEY, id TEXT NOT NULL, at TEXT NOT NULL, choices TEXT NOT NULL, version TEXT NOT NULL)',
|
|
31
|
-
),
|
|
32
|
-
db.prepare('CREATE INDEX IF NOT EXISTS consents_id ON consents (id)'),
|
|
33
|
-
db.prepare('CREATE INDEX IF NOT EXISTS consents_at ON consents (at)'),
|
|
34
|
-
])
|
|
35
|
-
.catch((e) => {
|
|
36
|
-
ready = undefined;
|
|
37
|
-
throw e;
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
// The commit the site was built from (the banner the visitor answered), as the deployed build says it.
|
|
41
|
-
let version;
|
|
42
|
-
async function versionOf(env, url) {
|
|
43
|
-
if (version) return version;
|
|
44
|
-
try {
|
|
45
|
-
const res = await env.ASSETS.fetch(new URL('/editor/version.json', url.origin));
|
|
46
|
-
version = String((await res.json()).commit ?? '');
|
|
47
|
-
} catch {
|
|
48
|
-
return '';
|
|
49
|
-
}
|
|
50
|
-
return version;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The answer as the banner sends it, or null: a consent id and a yes or no to each category asked about. */
|
|
54
|
-
function answerOf(body) {
|
|
55
|
-
if (!body || typeof body !== 'object' || typeof body.id !== 'string' || !ID.test(body.id)) return null;
|
|
56
|
-
const entries = Object.entries(body.choices ?? {});
|
|
57
|
-
if (!entries.length || !entries.every(([category, yes]) => isConsentCategory(category) && typeof yes === 'boolean')) return null;
|
|
58
|
-
return { id: body.id, choices: Object.fromEntries(entries) };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Only the site's own pages send an answer (not another site, not a robot posting from nowhere).
|
|
62
|
-
const fromSite = (request, url) => request.headers.get('origin') === url.origin;
|
|
63
|
-
|
|
64
|
-
export async function handleConsent(request, env) {
|
|
65
|
-
const url = new URL(request.url);
|
|
66
|
-
if (request.method === 'POST') {
|
|
67
|
-
if (!fromSite(request, url)) return json({ error: 'forbidden' }, 403);
|
|
68
|
-
const text = await request.text();
|
|
69
|
-
const answer = text.length <= 2000 ? answerOf(safeJson(text)) : null;
|
|
70
|
-
if (!answer) return json({ error: 'invalid' }, 400);
|
|
71
|
-
if (!env.CONSENT) return new Response(null, { status: 204 });
|
|
72
|
-
await table(env.CONSENT);
|
|
73
|
-
await env.CONSENT.prepare('INSERT INTO consents (id, at, choices, version) VALUES (?, ?, ?, ?)')
|
|
74
|
-
.bind(answer.id, new Date().toISOString(), JSON.stringify(answer.choices), await versionOf(env, url))
|
|
75
|
-
.run();
|
|
76
|
-
return new Response(null, { status: 204 });
|
|
77
|
-
}
|
|
78
|
-
if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
|
|
79
|
-
if (!env.CONSENT) return json({ error: 'unavailable' }, 503);
|
|
80
|
-
await table(env.CONSENT);
|
|
81
|
-
if (request.method === 'GET' && url.searchParams.get('format') === 'csv') return csv(await rows(env.CONSENT, '', 0));
|
|
82
|
-
if (request.method === 'GET') {
|
|
83
|
-
const q = (url.searchParams.get('q') ?? '')
|
|
84
|
-
.trim()
|
|
85
|
-
.toLowerCase()
|
|
86
|
-
.replace(/[^0-9a-f-]/g, '');
|
|
87
|
-
const total = await env.CONSENT.prepare('SELECT COUNT(*) AS n FROM consents').first('n');
|
|
88
|
-
return json({ entries: await rows(env.CONSENT, q, LIMIT), total });
|
|
89
|
-
}
|
|
90
|
-
if (request.method === 'DELETE') {
|
|
91
|
-
const id = safeJson(await request.text())?.id;
|
|
92
|
-
if (typeof id !== 'string' || !ID.test(id)) return json({ error: 'invalid' }, 400);
|
|
93
|
-
const { meta } = await env.CONSENT.prepare('DELETE FROM consents WHERE id = ?').bind(id).run();
|
|
94
|
-
return json({ deleted: meta.changes });
|
|
95
|
-
}
|
|
96
|
-
return json({ error: 'method' }, 405);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Deletes the answers older than KEEP_DAYS (scheduled in packages/editor/worker/index.js). Never fails the cron. */
|
|
100
|
-
export async function forgetOldConsents(env) {
|
|
101
|
-
if (!env.CONSENT) return;
|
|
102
|
-
try {
|
|
103
|
-
await table(env.CONSENT);
|
|
104
|
-
const before = new Date(Date.now() - KEEP_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
105
|
-
await env.CONSENT.prepare('DELETE FROM consents WHERE at < ?').bind(before).run();
|
|
106
|
-
} catch {}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// The answers, newest first: those of a consent id starting with `q`, at most `limit` (0: all).
|
|
110
|
-
async function rows(db, q, limit) {
|
|
111
|
-
const where = q ? 'WHERE id LIKE ?' : '';
|
|
112
|
-
const sql = `SELECT id, at, choices, version FROM consents ${where} ORDER BY at DESC${limit ? ` LIMIT ${limit}` : ''}`;
|
|
113
|
-
const statement = q ? db.prepare(sql).bind(`${q}%`) : db.prepare(sql);
|
|
114
|
-
const { results } = await statement.all();
|
|
115
|
-
return results.map((r) => ({ id: r.id, at: r.at, choices: safeJson(r.choices) ?? {}, version: r.version }));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// All the answers as CSV: one column per category any answer has, true or false, empty when it was not asked.
|
|
119
|
-
function csv(entries) {
|
|
120
|
-
const categories = [...new Set(entries.flatMap((e) => Object.keys(e.choices)))];
|
|
121
|
-
const cell = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
122
|
-
const lines = [
|
|
123
|
-
['id', 'at', ...categories, 'version'].map(cell).join(','),
|
|
124
|
-
...entries.map((e) => [e.id, e.at, ...categories.map((c) => (c in e.choices ? e.choices[c] : '')), e.version].map(cell).join(',')),
|
|
125
|
-
];
|
|
126
|
-
return new Response(`${lines.join('\r\n')}\r\n`, {
|
|
127
|
-
headers: {
|
|
128
|
-
'content-type': 'text/csv; charset=utf-8',
|
|
129
|
-
'content-disposition': `attachment; filename="samtykker-${new Date().toISOString().slice(0, 10)}.csv"`,
|
|
130
|
-
'cache-control': 'no-store',
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function safeJson(text) {
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(text);
|
|
138
|
-
} catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
1
|
+
// The visitors' answers in the cookie banner, so the site can show that a visitor said yes (GDPR article 7(1)): the
|
|
2
|
+
// consent log, "Samtykker" under "Privatliv" in the editor (packages/editor/src/consentData.ts, Consents.tsx).
|
|
3
|
+
// Kept in Cloudflare D1 (the binding CONSENT in wrangler.jsonc), never in the repository: one row per answer, with the
|
|
4
|
+
// random consent id the visitor's browser keeps (consentId in src/scripts/tracking.ts, shown in the banner under
|
|
5
|
+
// "Cookieindstillinger"), when, the yes or no to each category the banner asked about, and the banner it was given in
|
|
6
|
+
// (the commit the site was built from, /editor/version.json). No IP address, no user agent: nothing more about the
|
|
7
|
+
// visitor than the answer. An answer older than two years is deleted (forgetOldConsents, each hour); the banner asks
|
|
8
|
+
// again after one. Without the binding (a preview of a branch) nothing is kept and the editor says so.
|
|
9
|
+
// POST /api/consent a visitor's answer, from the site's own pages: { id, choices }
|
|
10
|
+
// GET /api/consent?q=<id> the answers, newest first (at most LIMIT), those of a consent id starting with q
|
|
11
|
+
// GET /api/consent?format=csv all of them, as a file to save
|
|
12
|
+
// DELETE /api/consent { id } deletes the answers of a consent id (a visitor asked for it)
|
|
13
|
+
// Only someone who can save the site (write access to the repository on GitHub) can read or delete them.
|
|
14
|
+
import { isConsentCategory } from '../lib/cookieCategories.mjs';
|
|
15
|
+
import { json } from './not-found.js';
|
|
16
|
+
import { mayEdit } from './session.js';
|
|
17
|
+
|
|
18
|
+
/** How long an answer is kept: two years, the banner asks again after one. */
|
|
19
|
+
export const KEEP_DAYS = 730;
|
|
20
|
+
const LIMIT = 500;
|
|
21
|
+
// A consent id: crypto.randomUUID() in the visitor's browser.
|
|
22
|
+
const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
23
|
+
|
|
24
|
+
// The table, made the first time an isolate needs it (D1 has no migrations on deploy).
|
|
25
|
+
let ready;
|
|
26
|
+
const table = (db) =>
|
|
27
|
+
(ready ??= db
|
|
28
|
+
.batch([
|
|
29
|
+
db.prepare(
|
|
30
|
+
'CREATE TABLE IF NOT EXISTS consents (row INTEGER PRIMARY KEY, id TEXT NOT NULL, at TEXT NOT NULL, choices TEXT NOT NULL, version TEXT NOT NULL)',
|
|
31
|
+
),
|
|
32
|
+
db.prepare('CREATE INDEX IF NOT EXISTS consents_id ON consents (id)'),
|
|
33
|
+
db.prepare('CREATE INDEX IF NOT EXISTS consents_at ON consents (at)'),
|
|
34
|
+
])
|
|
35
|
+
.catch((e) => {
|
|
36
|
+
ready = undefined;
|
|
37
|
+
throw e;
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// The commit the site was built from (the banner the visitor answered), as the deployed build says it.
|
|
41
|
+
let version;
|
|
42
|
+
async function versionOf(env, url) {
|
|
43
|
+
if (version) return version;
|
|
44
|
+
try {
|
|
45
|
+
const res = await env.ASSETS.fetch(new URL('/editor/version.json', url.origin));
|
|
46
|
+
version = String((await res.json()).commit ?? '');
|
|
47
|
+
} catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
return version;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The answer as the banner sends it, or null: a consent id and a yes or no to each category asked about. */
|
|
54
|
+
function answerOf(body) {
|
|
55
|
+
if (!body || typeof body !== 'object' || typeof body.id !== 'string' || !ID.test(body.id)) return null;
|
|
56
|
+
const entries = Object.entries(body.choices ?? {});
|
|
57
|
+
if (!entries.length || !entries.every(([category, yes]) => isConsentCategory(category) && typeof yes === 'boolean')) return null;
|
|
58
|
+
return { id: body.id, choices: Object.fromEntries(entries) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Only the site's own pages send an answer (not another site, not a robot posting from nowhere).
|
|
62
|
+
const fromSite = (request, url) => request.headers.get('origin') === url.origin;
|
|
63
|
+
|
|
64
|
+
export async function handleConsent(request, env) {
|
|
65
|
+
const url = new URL(request.url);
|
|
66
|
+
if (request.method === 'POST') {
|
|
67
|
+
if (!fromSite(request, url)) return json({ error: 'forbidden' }, 403);
|
|
68
|
+
const text = await request.text();
|
|
69
|
+
const answer = text.length <= 2000 ? answerOf(safeJson(text)) : null;
|
|
70
|
+
if (!answer) return json({ error: 'invalid' }, 400);
|
|
71
|
+
if (!env.CONSENT) return new Response(null, { status: 204 });
|
|
72
|
+
await table(env.CONSENT);
|
|
73
|
+
await env.CONSENT.prepare('INSERT INTO consents (id, at, choices, version) VALUES (?, ?, ?, ?)')
|
|
74
|
+
.bind(answer.id, new Date().toISOString(), JSON.stringify(answer.choices), await versionOf(env, url))
|
|
75
|
+
.run();
|
|
76
|
+
return new Response(null, { status: 204 });
|
|
77
|
+
}
|
|
78
|
+
if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
|
|
79
|
+
if (!env.CONSENT) return json({ error: 'unavailable' }, 503);
|
|
80
|
+
await table(env.CONSENT);
|
|
81
|
+
if (request.method === 'GET' && url.searchParams.get('format') === 'csv') return csv(await rows(env.CONSENT, '', 0));
|
|
82
|
+
if (request.method === 'GET') {
|
|
83
|
+
const q = (url.searchParams.get('q') ?? '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[^0-9a-f-]/g, '');
|
|
87
|
+
const total = await env.CONSENT.prepare('SELECT COUNT(*) AS n FROM consents').first('n');
|
|
88
|
+
return json({ entries: await rows(env.CONSENT, q, LIMIT), total });
|
|
89
|
+
}
|
|
90
|
+
if (request.method === 'DELETE') {
|
|
91
|
+
const id = safeJson(await request.text())?.id;
|
|
92
|
+
if (typeof id !== 'string' || !ID.test(id)) return json({ error: 'invalid' }, 400);
|
|
93
|
+
const { meta } = await env.CONSENT.prepare('DELETE FROM consents WHERE id = ?').bind(id).run();
|
|
94
|
+
return json({ deleted: meta.changes });
|
|
95
|
+
}
|
|
96
|
+
return json({ error: 'method' }, 405);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Deletes the answers older than KEEP_DAYS (scheduled in packages/editor/worker/index.js). Never fails the cron. */
|
|
100
|
+
export async function forgetOldConsents(env) {
|
|
101
|
+
if (!env.CONSENT) return;
|
|
102
|
+
try {
|
|
103
|
+
await table(env.CONSENT);
|
|
104
|
+
const before = new Date(Date.now() - KEEP_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
105
|
+
await env.CONSENT.prepare('DELETE FROM consents WHERE at < ?').bind(before).run();
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The answers, newest first: those of a consent id starting with `q`, at most `limit` (0: all).
|
|
110
|
+
async function rows(db, q, limit) {
|
|
111
|
+
const where = q ? 'WHERE id LIKE ?' : '';
|
|
112
|
+
const sql = `SELECT id, at, choices, version FROM consents ${where} ORDER BY at DESC${limit ? ` LIMIT ${limit}` : ''}`;
|
|
113
|
+
const statement = q ? db.prepare(sql).bind(`${q}%`) : db.prepare(sql);
|
|
114
|
+
const { results } = await statement.all();
|
|
115
|
+
return results.map((r) => ({ id: r.id, at: r.at, choices: safeJson(r.choices) ?? {}, version: r.version }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// All the answers as CSV: one column per category any answer has, true or false, empty when it was not asked.
|
|
119
|
+
function csv(entries) {
|
|
120
|
+
const categories = [...new Set(entries.flatMap((e) => Object.keys(e.choices)))];
|
|
121
|
+
const cell = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
122
|
+
const lines = [
|
|
123
|
+
['id', 'at', ...categories, 'version'].map(cell).join(','),
|
|
124
|
+
...entries.map((e) => [e.id, e.at, ...categories.map((c) => (c in e.choices ? e.choices[c] : '')), e.version].map(cell).join(',')),
|
|
125
|
+
];
|
|
126
|
+
return new Response(`${lines.join('\r\n')}\r\n`, {
|
|
127
|
+
headers: {
|
|
128
|
+
'content-type': 'text/csv; charset=utf-8',
|
|
129
|
+
'content-disposition': `attachment; filename="samtykker-${new Date().toISOString().slice(0, 10)}.csv"`,
|
|
130
|
+
'cache-control': 'no-store',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function safeJson(text) {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(text);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|