@morphemecms/cms 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -3
- package/astro.d.mts +2 -0
- package/astro.mjs +162 -0
- package/bin/morpheme.mjs +38 -0
- package/client.d.ts +10 -0
- package/defaults/blockTexts.ts +33 -0
- package/defaults/collections.ts +774 -0
- package/defaults/colors.ts +125 -0
- package/defaults/company.ts +20 -0
- package/defaults/editor.ts +178 -0
- package/defaults/internalLinks.ts +30 -0
- package/defaults/opens.ts +18 -0
- package/defaults/popup.tsx +93 -0
- package/defaults/privacy.ts +79 -0
- package/defaults/rules.ts +450 -0
- package/defaults/schema.ts +16 -0
- package/defaults/sitedata.ts +129 -0
- package/defaults/standard.ts +106 -0
- package/defaults/standardPages.ts +87 -0
- package/defaults/texts.ts +481 -0
- package/demo/README.md +26 -0
- package/demo/public/apple-touch-icon.png +0 -0
- package/demo/public/editor/apple-touch-icon.png +0 -0
- package/demo/public/editor/icon-192.png +0 -0
- package/demo/public/editor/icon-512.png +0 -0
- package/demo/public/editor/icon-maskable-512.png +0 -0
- package/demo/public/favicon.ico +0 -0
- package/demo/public/favicon.svg +1 -0
- package/demo/public/images/uploads/2026/10/andromeda.webp +0 -0
- package/demo/public/images/uploads/2026/10/earth-from-space.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-blog.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-colors.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-home.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-page.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-pages.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-seo.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-start.webp +0 -0
- package/demo/public/images/uploads/2026/10/editor-themes.webp +0 -0
- package/demo/public/images/uploads/2026/10/moon.webp +0 -0
- package/demo/public/images/uploads/2026/10/saturn.webp +0 -0
- package/demo/public/logo.svg +1 -0
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -0
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -0
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -0
- package/demo/src/content/blog/every-save-is-a-version.md +21 -0
- package/demo/src/content/blog/schedule-a-post.md +17 -0
- package/demo/src/content/blog/secure-from-day-one.md +17 -0
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -0
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -0
- package/demo/src/content/blog/the-app-store.md +21 -0
- package/demo/src/content/pages/about.md +22 -0
- package/demo/src/content/pages/app-store.md +30 -0
- package/demo/src/content/pages/blocks.md +98 -0
- package/demo/src/content/pages/blog-page.md +11 -0
- package/demo/src/content/pages/contact.md +16 -0
- package/demo/src/content/pages/get-started.md +20 -0
- package/demo/src/content/pages/home.md +26 -0
- package/demo/src/content/pages/page-not-found.md +11 -0
- package/demo/src/content/pages/privacy-policy.md +24 -0
- package/demo/src/data/blocks.json +25 -0
- package/demo/src/data/booking.json +6 -0
- package/demo/src/data/categories/apps.json +6 -0
- package/demo/src/data/categories/design.json +6 -0
- package/demo/src/data/categories/editing.json +6 -0
- package/demo/src/data/categories/security.json +6 -0
- package/demo/src/data/categories/seo.json +6 -0
- package/demo/src/data/colors.json +75 -0
- package/demo/src/data/contact.json +8 -0
- package/demo/src/data/cookies.json +21 -0
- package/demo/src/data/footer.json +27 -0
- package/demo/src/data/menu.json +35 -0
- package/demo/src/data/mobile-menu.json +25 -0
- package/demo/src/data/popups/booking-bar.json +13 -0
- package/demo/src/data/popups/try-the-editor.json +16 -0
- package/demo/src/data/standard-pages.json +8 -0
- package/demo/src/data/testimonials/sample-bakery.json +10 -0
- package/demo/src/data/testimonials/sample-clinic.json +10 -0
- package/demo/src/data/testimonials/sample-studio.json +10 -0
- package/demo/src/site/colors.ts +11 -0
- package/demo/src/site/config.ts +19 -0
- package/icons/lucide/LICENSE +43 -0
- package/icons/lucide/LICENSE-material-design-icons +20 -0
- package/icons/lucide/address-book.svg +1 -0
- package/icons/lucide/app-window.svg +1 -0
- package/icons/lucide/arrow-counter-clockwise.svg +1 -0
- package/icons/lucide/arrow-down.svg +1 -0
- package/icons/lucide/arrow-left.svg +1 -0
- package/icons/lucide/arrow-right.svg +1 -0
- package/icons/lucide/arrow-square-out.svg +1 -0
- package/icons/lucide/arrow-u-up-left.svg +1 -0
- package/icons/lucide/arrow-u-up-right.svg +1 -0
- package/icons/lucide/arrow-up-right.svg +1 -0
- package/icons/lucide/arrow-up.svg +1 -0
- package/icons/lucide/arrows-down-up.svg +1 -0
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -0
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -0
- package/icons/lucide/article.svg +1 -0
- package/icons/lucide/bank.svg +1 -0
- package/icons/lucide/bell.svg +1 -0
- package/icons/lucide/bicycle.svg +1 -0
- package/icons/lucide/binoculars.svg +1 -0
- package/icons/lucide/book-open-text.svg +1 -0
- package/icons/lucide/book-open.svg +1 -0
- package/icons/lucide/browser.svg +1 -0
- package/icons/lucide/buildings.svg +1 -0
- package/icons/lucide/bus.svg +1 -0
- package/icons/lucide/calendar-blank.svg +1 -0
- package/icons/lucide/calendar-check.svg +1 -0
- package/icons/lucide/car.svg +1 -0
- package/icons/lucide/cards.svg +1 -0
- package/icons/lucide/caret-down.svg +1 -0
- package/icons/lucide/caret-left.svg +1 -0
- package/icons/lucide/caret-right.svg +1 -0
- package/icons/lucide/caret-up.svg +1 -0
- package/icons/lucide/certificate.svg +1 -0
- package/icons/lucide/chart-line-up.svg +1 -0
- package/icons/lucide/chats.svg +1 -0
- package/icons/lucide/check-circle.svg +1 -0
- package/icons/lucide/check.svg +1 -0
- package/icons/lucide/circle-half.svg +1 -0
- package/icons/lucide/circle.svg +1 -0
- package/icons/lucide/clock-counter-clockwise.svg +1 -0
- package/icons/lucide/clock.svg +1 -0
- package/icons/lucide/code.svg +1 -0
- package/icons/lucide/coffee.svg +1 -0
- package/icons/lucide/columns.svg +1 -0
- package/icons/lucide/cookie.svg +1 -0
- package/icons/lucide/copy.svg +1 -0
- package/icons/lucide/credit-card.svg +1 -0
- package/icons/lucide/currency-circle-dollar.svg +1 -0
- package/icons/lucide/cursor-click.svg +1 -0
- package/icons/lucide/database.svg +1 -0
- package/icons/lucide/desktop.svg +1 -0
- package/icons/lucide/device-mobile.svg +1 -0
- package/icons/lucide/door-open.svg +1 -0
- package/icons/lucide/download-simple.svg +1 -0
- package/icons/lucide/envelope-simple.svg +1 -0
- package/icons/lucide/eye-slash.svg +1 -0
- package/icons/lucide/eye.svg +1 -0
- package/icons/lucide/file-pdf.svg +1 -0
- package/icons/lucide/file-text.svg +1 -0
- package/icons/lucide/file-video.svg +1 -0
- package/icons/lucide/file-zip.svg +1 -0
- package/icons/lucide/flower-lotus.svg +1 -0
- package/icons/lucide/folder-simple.svg +1 -0
- package/icons/lucide/funnel.svg +1 -0
- package/icons/lucide/gauge.svg +1 -0
- package/icons/lucide/gear.svg +1 -0
- package/icons/lucide/github-logo.svg +1 -0
- package/icons/lucide/globe.svg +1 -0
- package/icons/lucide/google-logo.svg +1 -0
- package/icons/lucide/graduation-cap.svg +1 -0
- package/icons/lucide/hammer.svg +1 -0
- package/icons/lucide/hand-heart.svg +1 -0
- package/icons/lucide/handshake.svg +1 -0
- package/icons/lucide/hard-drives.svg +1 -0
- package/icons/lucide/heart.svg +1 -0
- package/icons/lucide/house.svg +1 -0
- package/icons/lucide/image-square.svg +1 -0
- package/icons/lucide/image.svg +1 -0
- package/icons/lucide/info.svg +1 -0
- package/icons/lucide/keyboard.svg +1 -0
- package/icons/lucide/leaf.svg +1 -0
- package/icons/lucide/lifebuoy.svg +1 -0
- package/icons/lucide/lightbulb.svg +1 -0
- package/icons/lucide/link-break.svg +1 -0
- package/icons/lucide/link-simple.svg +1 -0
- package/icons/lucide/list-bullets.svg +1 -0
- package/icons/lucide/list-checks.svg +1 -0
- package/icons/lucide/list-numbers.svg +1 -0
- package/icons/lucide/list.svg +1 -0
- package/icons/lucide/lock-simple-open.svg +1 -0
- package/icons/lucide/lock-simple.svg +1 -0
- package/icons/lucide/magnifying-glass.svg +1 -0
- package/icons/lucide/map-pin.svg +1 -0
- package/icons/lucide/map-trifold.svg +1 -0
- package/icons/lucide/medal.svg +1 -0
- package/icons/lucide/megaphone.svg +1 -0
- package/icons/lucide/minus.svg +1 -0
- package/icons/lucide/moon.svg +1 -0
- package/icons/lucide/newspaper.svg +1 -0
- package/icons/lucide/pack.json +23 -0
- package/icons/lucide/paint-brush-broad.svg +1 -0
- package/icons/lucide/palette.svg +1 -0
- package/icons/lucide/paper-plane-tilt.svg +1 -0
- package/icons/lucide/pencil-simple.svg +1 -0
- package/icons/lucide/phone.svg +1 -0
- package/icons/lucide/play-fill.svg +1 -0
- package/icons/lucide/plus.svg +1 -0
- package/icons/lucide/prohibit.svg +1 -0
- package/icons/lucide/puzzle-piece.svg +1 -0
- package/icons/lucide/question.svg +1 -0
- package/icons/lucide/quotes.svg +1 -0
- package/icons/lucide/rows.svg +1 -0
- package/icons/lucide/ruler.svg +1 -0
- package/icons/lucide/scan.svg +1 -0
- package/icons/lucide/shapes.svg +1 -0
- package/icons/lucide/share-network.svg +1 -0
- package/icons/lucide/shield-check.svg +1 -0
- package/icons/lucide/sidebar-simple.svg +1 -0
- package/icons/lucide/sign-out.svg +1 -0
- package/icons/lucide/signpost.svg +1 -0
- package/icons/lucide/sliders-horizontal.svg +1 -0
- package/icons/lucide/smiley.svg +1 -0
- package/icons/lucide/sparkle.svg +1 -0
- package/icons/lucide/square-half-bottom.svg +1 -0
- package/icons/lucide/square.svg +1 -0
- package/icons/lucide/squares-four.svg +1 -0
- package/icons/lucide/sticker.svg +1 -0
- package/icons/lucide/storefront.svg +1 -0
- package/icons/lucide/sun.svg +1 -0
- package/icons/lucide/swatches.svg +1 -0
- package/icons/lucide/tag.svg +1 -0
- package/icons/lucide/text-aa.svg +1 -0
- package/icons/lucide/text-align-left.svg +1 -0
- package/icons/lucide/text-b.svg +1 -0
- package/icons/lucide/text-h-one.svg +1 -0
- package/icons/lucide/text-h-three.svg +1 -0
- package/icons/lucide/text-h-two.svg +1 -0
- package/icons/lucide/text-italic.svg +1 -0
- package/icons/lucide/text-t.svg +1 -0
- package/icons/lucide/ticket.svg +1 -0
- package/icons/lucide/toggle-right.svg +1 -0
- package/icons/lucide/train.svg +1 -0
- package/icons/lucide/trash.svg +1 -0
- package/icons/lucide/upload-simple.svg +1 -0
- package/icons/lucide/user-circle.svg +1 -0
- package/icons/lucide/user.svg +1 -0
- package/icons/lucide/users-three.svg +1 -0
- package/icons/lucide/users.svg +1 -0
- package/icons/lucide/video-camera.svg +1 -0
- package/icons/lucide/warning-circle.svg +1 -0
- package/icons/lucide/warning.svg +1 -0
- package/icons/lucide/wave-sine.svg +1 -0
- package/icons/lucide/wheelchair.svg +1 -0
- package/icons/lucide/wrench.svg +1 -0
- package/icons/lucide/x.svg +1 -0
- package/icons/material-symbols/LICENSE +201 -0
- package/icons/material-symbols/LICENSE-material-design-icons +20 -0
- package/icons/material-symbols/address-book.svg +1 -0
- package/icons/material-symbols/app-window.svg +1 -0
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -0
- package/icons/material-symbols/arrow-down.svg +1 -0
- package/icons/material-symbols/arrow-left.svg +1 -0
- package/icons/material-symbols/arrow-right.svg +1 -0
- package/icons/material-symbols/arrow-square-out.svg +1 -0
- package/icons/material-symbols/arrow-u-up-left.svg +1 -0
- package/icons/material-symbols/arrow-u-up-right.svg +1 -0
- package/icons/material-symbols/arrow-up-right.svg +1 -0
- package/icons/material-symbols/arrow-up.svg +1 -0
- package/icons/material-symbols/arrows-down-up.svg +1 -0
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -0
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -0
- package/icons/material-symbols/article.svg +1 -0
- package/icons/material-symbols/bank.svg +1 -0
- package/icons/material-symbols/bell.svg +1 -0
- package/icons/material-symbols/bicycle.svg +1 -0
- package/icons/material-symbols/binoculars.svg +1 -0
- package/icons/material-symbols/book-open-text.svg +1 -0
- package/icons/material-symbols/book-open.svg +1 -0
- package/icons/material-symbols/browser.svg +1 -0
- package/icons/material-symbols/buildings.svg +1 -0
- package/icons/material-symbols/bus.svg +1 -0
- package/icons/material-symbols/calendar-blank.svg +1 -0
- package/icons/material-symbols/calendar-check.svg +1 -0
- package/icons/material-symbols/car.svg +1 -0
- package/icons/material-symbols/cards.svg +1 -0
- package/icons/material-symbols/caret-down.svg +1 -0
- package/icons/material-symbols/caret-left.svg +1 -0
- package/icons/material-symbols/caret-right.svg +1 -0
- package/icons/material-symbols/caret-up.svg +1 -0
- package/icons/material-symbols/certificate.svg +1 -0
- package/icons/material-symbols/chart-line-up.svg +1 -0
- package/icons/material-symbols/chats.svg +1 -0
- package/icons/material-symbols/check-circle.svg +1 -0
- package/icons/material-symbols/check.svg +1 -0
- package/icons/material-symbols/circle-half.svg +1 -0
- package/icons/material-symbols/circle.svg +1 -0
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -0
- package/icons/material-symbols/clock.svg +1 -0
- package/icons/material-symbols/code.svg +1 -0
- package/icons/material-symbols/coffee.svg +1 -0
- package/icons/material-symbols/columns.svg +1 -0
- package/icons/material-symbols/cookie.svg +1 -0
- package/icons/material-symbols/copy.svg +1 -0
- package/icons/material-symbols/credit-card.svg +1 -0
- package/icons/material-symbols/currency-circle-dollar.svg +1 -0
- package/icons/material-symbols/cursor-click.svg +1 -0
- package/icons/material-symbols/database.svg +1 -0
- package/icons/material-symbols/desktop.svg +1 -0
- package/icons/material-symbols/device-mobile.svg +1 -0
- package/icons/material-symbols/door-open.svg +1 -0
- package/icons/material-symbols/download-simple.svg +1 -0
- package/icons/material-symbols/envelope-simple.svg +1 -0
- package/icons/material-symbols/eye-slash.svg +1 -0
- package/icons/material-symbols/eye.svg +1 -0
- package/icons/material-symbols/file-pdf.svg +1 -0
- package/icons/material-symbols/file-text.svg +1 -0
- package/icons/material-symbols/file-video.svg +1 -0
- package/icons/material-symbols/file-zip.svg +1 -0
- package/icons/material-symbols/flower-lotus.svg +1 -0
- package/icons/material-symbols/folder-simple.svg +1 -0
- package/icons/material-symbols/funnel.svg +1 -0
- package/icons/material-symbols/gauge.svg +1 -0
- package/icons/material-symbols/gear.svg +1 -0
- package/icons/material-symbols/github-logo.svg +1 -0
- package/icons/material-symbols/globe.svg +1 -0
- package/icons/material-symbols/google-logo.svg +1 -0
- package/icons/material-symbols/graduation-cap.svg +1 -0
- package/icons/material-symbols/hammer.svg +1 -0
- package/icons/material-symbols/hand-heart.svg +1 -0
- package/icons/material-symbols/handshake.svg +1 -0
- package/icons/material-symbols/hard-drives.svg +1 -0
- package/icons/material-symbols/heart.svg +1 -0
- package/icons/material-symbols/house.svg +1 -0
- package/icons/material-symbols/image-square.svg +1 -0
- package/icons/material-symbols/image.svg +1 -0
- package/icons/material-symbols/info.svg +1 -0
- package/icons/material-symbols/keyboard.svg +1 -0
- package/icons/material-symbols/leaf.svg +1 -0
- package/icons/material-symbols/lifebuoy.svg +1 -0
- package/icons/material-symbols/lightbulb.svg +1 -0
- package/icons/material-symbols/link-break.svg +1 -0
- package/icons/material-symbols/link-simple.svg +1 -0
- package/icons/material-symbols/list-bullets.svg +1 -0
- package/icons/material-symbols/list-checks.svg +1 -0
- package/icons/material-symbols/list-numbers.svg +1 -0
- package/icons/material-symbols/list.svg +1 -0
- package/icons/material-symbols/lock-simple-open.svg +1 -0
- package/icons/material-symbols/lock-simple.svg +1 -0
- package/icons/material-symbols/magnifying-glass.svg +1 -0
- package/icons/material-symbols/map-pin.svg +1 -0
- package/icons/material-symbols/map-trifold.svg +1 -0
- package/icons/material-symbols/medal.svg +1 -0
- package/icons/material-symbols/megaphone.svg +1 -0
- package/icons/material-symbols/minus.svg +1 -0
- package/icons/material-symbols/moon.svg +1 -0
- package/icons/material-symbols/newspaper.svg +1 -0
- package/icons/material-symbols/pack.json +29 -0
- package/icons/material-symbols/paint-brush-broad.svg +1 -0
- package/icons/material-symbols/palette.svg +1 -0
- package/icons/material-symbols/paper-plane-tilt.svg +1 -0
- package/icons/material-symbols/pencil-simple.svg +1 -0
- package/icons/material-symbols/phone.svg +1 -0
- package/icons/material-symbols/play-fill.svg +1 -0
- package/icons/material-symbols/plus.svg +1 -0
- package/icons/material-symbols/prohibit.svg +1 -0
- package/icons/material-symbols/puzzle-piece.svg +1 -0
- package/icons/material-symbols/question.svg +1 -0
- package/icons/material-symbols/quotes.svg +1 -0
- package/icons/material-symbols/rows.svg +1 -0
- package/icons/material-symbols/ruler.svg +1 -0
- package/icons/material-symbols/scan.svg +1 -0
- package/icons/material-symbols/shapes.svg +1 -0
- package/icons/material-symbols/share-network.svg +1 -0
- package/icons/material-symbols/shield-check.svg +1 -0
- package/icons/material-symbols/sidebar-simple.svg +1 -0
- package/icons/material-symbols/sign-out.svg +1 -0
- package/icons/material-symbols/signpost.svg +1 -0
- package/icons/material-symbols/sliders-horizontal.svg +1 -0
- package/icons/material-symbols/smiley.svg +1 -0
- package/icons/material-symbols/sparkle.svg +1 -0
- package/icons/material-symbols/square-half-bottom.svg +1 -0
- package/icons/material-symbols/square.svg +1 -0
- package/icons/material-symbols/squares-four.svg +1 -0
- package/icons/material-symbols/sticker.svg +1 -0
- package/icons/material-symbols/storefront.svg +1 -0
- package/icons/material-symbols/sun.svg +1 -0
- package/icons/material-symbols/swatches.svg +1 -0
- package/icons/material-symbols/tag.svg +1 -0
- package/icons/material-symbols/text-aa.svg +1 -0
- package/icons/material-symbols/text-align-left.svg +1 -0
- package/icons/material-symbols/text-b.svg +1 -0
- package/icons/material-symbols/text-h-one.svg +1 -0
- package/icons/material-symbols/text-h-three.svg +1 -0
- package/icons/material-symbols/text-h-two.svg +1 -0
- package/icons/material-symbols/text-italic.svg +1 -0
- package/icons/material-symbols/text-t.svg +1 -0
- package/icons/material-symbols/ticket.svg +1 -0
- package/icons/material-symbols/toggle-right.svg +1 -0
- package/icons/material-symbols/train.svg +1 -0
- package/icons/material-symbols/trash.svg +1 -0
- package/icons/material-symbols/upload-simple.svg +1 -0
- package/icons/material-symbols/user-circle.svg +1 -0
- package/icons/material-symbols/user.svg +1 -0
- package/icons/material-symbols/users-three.svg +1 -0
- package/icons/material-symbols/users.svg +1 -0
- package/icons/material-symbols/video-camera.svg +1 -0
- package/icons/material-symbols/warning-circle.svg +1 -0
- package/icons/material-symbols/warning.svg +1 -0
- package/icons/material-symbols/wave-sine.svg +1 -0
- package/icons/material-symbols/wheelchair.svg +1 -0
- package/icons/material-symbols/wrench.svg +1 -0
- package/icons/material-symbols/x.svg +1 -0
- package/icons/phosphor/LICENSE +21 -0
- package/icons/phosphor/address-book.svg +1 -0
- package/icons/phosphor/app-window.svg +1 -0
- package/icons/phosphor/arrow-counter-clockwise.svg +1 -0
- package/icons/phosphor/arrow-down.svg +1 -0
- package/icons/phosphor/arrow-left.svg +1 -0
- package/icons/phosphor/arrow-right.svg +1 -0
- package/icons/phosphor/arrow-square-out.svg +1 -0
- package/icons/phosphor/arrow-u-up-left.svg +1 -0
- package/icons/phosphor/arrow-u-up-right.svg +1 -0
- package/icons/phosphor/arrow-up-right.svg +1 -0
- package/icons/phosphor/arrow-up.svg +1 -0
- package/icons/phosphor/arrows-down-up.svg +1 -0
- package/icons/phosphor/arrows-in-line-horizontal.svg +1 -0
- package/icons/phosphor/arrows-out-line-horizontal.svg +1 -0
- package/icons/phosphor/article.svg +1 -0
- package/icons/phosphor/bank.svg +1 -0
- package/icons/phosphor/bell.svg +1 -0
- package/icons/phosphor/bicycle.svg +1 -0
- package/icons/phosphor/binoculars.svg +1 -0
- package/icons/phosphor/book-open-text.svg +1 -0
- package/icons/phosphor/book-open.svg +1 -0
- package/icons/phosphor/browser.svg +1 -0
- package/icons/phosphor/buildings.svg +1 -0
- package/icons/phosphor/bus.svg +1 -0
- package/icons/phosphor/calendar-blank.svg +1 -0
- package/icons/phosphor/calendar-check.svg +1 -0
- package/icons/phosphor/car.svg +1 -0
- package/icons/phosphor/cards.svg +1 -0
- package/icons/phosphor/caret-down.svg +1 -0
- package/icons/phosphor/caret-left.svg +1 -0
- package/icons/phosphor/caret-right.svg +1 -0
- package/icons/phosphor/caret-up.svg +1 -0
- package/icons/phosphor/certificate.svg +1 -0
- package/icons/phosphor/chart-line-up.svg +1 -0
- package/icons/phosphor/chats.svg +1 -0
- package/icons/phosphor/check-circle.svg +1 -0
- package/icons/phosphor/check.svg +1 -0
- package/icons/phosphor/circle-half.svg +1 -0
- package/icons/phosphor/circle.svg +1 -0
- package/icons/phosphor/clock-counter-clockwise.svg +1 -0
- package/icons/phosphor/clock.svg +1 -0
- package/icons/phosphor/code.svg +1 -0
- package/icons/phosphor/coffee.svg +1 -0
- package/icons/phosphor/columns.svg +1 -0
- package/icons/phosphor/cookie.svg +1 -0
- package/icons/phosphor/copy.svg +1 -0
- package/icons/phosphor/credit-card.svg +1 -0
- package/icons/phosphor/currency-circle-dollar.svg +1 -0
- package/icons/phosphor/cursor-click.svg +1 -0
- package/icons/phosphor/database.svg +1 -0
- package/icons/phosphor/desktop.svg +1 -0
- package/icons/phosphor/device-mobile.svg +1 -0
- package/icons/phosphor/door-open.svg +1 -0
- package/icons/phosphor/download-simple.svg +1 -0
- package/icons/phosphor/envelope-simple.svg +1 -0
- package/icons/phosphor/eye-slash.svg +1 -0
- package/icons/phosphor/eye.svg +1 -0
- package/icons/phosphor/file-pdf.svg +1 -0
- package/icons/phosphor/file-text.svg +1 -0
- package/icons/phosphor/file-video.svg +1 -0
- package/icons/phosphor/file-zip.svg +1 -0
- package/icons/phosphor/flower-lotus.svg +1 -0
- package/icons/phosphor/folder-simple.svg +1 -0
- package/icons/phosphor/funnel.svg +1 -0
- package/icons/phosphor/gauge.svg +1 -0
- package/icons/phosphor/gear.svg +1 -0
- package/icons/phosphor/github-logo.svg +1 -0
- package/icons/phosphor/globe.svg +1 -0
- package/icons/phosphor/google-logo.svg +1 -0
- package/icons/phosphor/graduation-cap.svg +1 -0
- package/icons/phosphor/hammer.svg +1 -0
- package/icons/phosphor/hand-heart.svg +1 -0
- package/icons/phosphor/handshake.svg +1 -0
- package/icons/phosphor/hard-drives.svg +1 -0
- package/icons/phosphor/heart.svg +1 -0
- package/icons/phosphor/house.svg +1 -0
- package/icons/phosphor/image-square.svg +1 -0
- package/icons/phosphor/image.svg +1 -0
- package/icons/phosphor/info.svg +1 -0
- package/icons/phosphor/keyboard.svg +1 -0
- package/icons/phosphor/leaf.svg +1 -0
- package/icons/phosphor/lifebuoy.svg +1 -0
- package/icons/phosphor/lightbulb.svg +1 -0
- package/icons/phosphor/link-break.svg +1 -0
- package/icons/phosphor/link-simple.svg +1 -0
- package/icons/phosphor/list-bullets.svg +1 -0
- package/icons/phosphor/list-checks.svg +1 -0
- package/icons/phosphor/list-numbers.svg +1 -0
- package/icons/phosphor/list.svg +1 -0
- package/icons/phosphor/lock-simple-open.svg +1 -0
- package/icons/phosphor/lock-simple.svg +1 -0
- package/icons/phosphor/magnifying-glass.svg +1 -0
- package/icons/phosphor/map-pin.svg +1 -0
- package/icons/phosphor/map-trifold.svg +1 -0
- package/icons/phosphor/medal.svg +1 -0
- package/icons/phosphor/megaphone.svg +1 -0
- package/icons/phosphor/minus.svg +1 -0
- package/icons/phosphor/moon.svg +1 -0
- package/icons/phosphor/newspaper.svg +1 -0
- package/icons/phosphor/pack.json +8 -0
- package/icons/phosphor/paint-brush-broad.svg +1 -0
- package/icons/phosphor/palette.svg +1 -0
- package/icons/phosphor/paper-plane-tilt.svg +1 -0
- package/icons/phosphor/pencil-simple.svg +1 -0
- package/icons/phosphor/phone.svg +1 -0
- package/icons/phosphor/play-fill.svg +1 -0
- package/icons/phosphor/plus.svg +1 -0
- package/icons/phosphor/prohibit.svg +1 -0
- package/icons/phosphor/puzzle-piece.svg +1 -0
- package/icons/phosphor/question.svg +1 -0
- package/icons/phosphor/quotes.svg +1 -0
- package/icons/phosphor/rows.svg +1 -0
- package/icons/phosphor/ruler.svg +1 -0
- package/icons/phosphor/scan.svg +1 -0
- package/icons/phosphor/shapes.svg +1 -0
- package/icons/phosphor/share-network.svg +1 -0
- package/icons/phosphor/shield-check.svg +1 -0
- package/icons/phosphor/sidebar-simple.svg +1 -0
- package/icons/phosphor/sign-out.svg +1 -0
- package/icons/phosphor/signpost.svg +1 -0
- package/icons/phosphor/sliders-horizontal.svg +1 -0
- package/icons/phosphor/smiley.svg +1 -0
- package/icons/phosphor/sparkle.svg +1 -0
- package/icons/phosphor/square-half-bottom.svg +1 -0
- package/icons/phosphor/square.svg +1 -0
- package/icons/phosphor/squares-four.svg +1 -0
- package/icons/phosphor/sticker.svg +1 -0
- package/icons/phosphor/storefront.svg +1 -0
- package/icons/phosphor/sun.svg +1 -0
- package/icons/phosphor/swatches.svg +1 -0
- package/icons/phosphor/tag.svg +1 -0
- package/icons/phosphor/text-aa.svg +1 -0
- package/icons/phosphor/text-align-left.svg +1 -0
- package/icons/phosphor/text-b.svg +1 -0
- package/icons/phosphor/text-h-one.svg +1 -0
- package/icons/phosphor/text-h-three.svg +1 -0
- package/icons/phosphor/text-h-two.svg +1 -0
- package/icons/phosphor/text-italic.svg +1 -0
- package/icons/phosphor/text-t.svg +1 -0
- package/icons/phosphor/ticket.svg +1 -0
- package/icons/phosphor/toggle-right.svg +1 -0
- package/icons/phosphor/train.svg +1 -0
- package/icons/phosphor/trash.svg +1 -0
- package/icons/phosphor/upload-simple.svg +1 -0
- package/icons/phosphor/user-circle.svg +1 -0
- package/icons/phosphor/user.svg +1 -0
- package/icons/phosphor/users-three.svg +1 -0
- package/icons/phosphor/users.svg +1 -0
- package/icons/phosphor/video-camera.svg +1 -0
- package/icons/phosphor/warning-circle.svg +1 -0
- package/icons/phosphor/warning.svg +1 -0
- package/icons/phosphor/wave-sine.svg +1 -0
- package/icons/phosphor/wheelchair.svg +1 -0
- package/icons/phosphor/wrench.svg +1 -0
- package/icons/phosphor/x.svg +1 -0
- package/lib/apps.d.mts +28 -0
- package/lib/apps.mjs +139 -0
- package/lib/blocks.d.mts +23 -0
- package/lib/blocks.mjs +90 -0
- package/lib/colors.ts +86 -0
- package/lib/contactBar.ts +18 -0
- package/lib/cookieCategories.d.mts +17 -0
- package/lib/cookieCategories.mjs +28 -0
- package/lib/crop.d.mts +34 -0
- package/lib/crop.mjs +83 -0
- package/lib/csp.d.mts +22 -0
- package/lib/csp.mjs +130 -0
- package/lib/cspSources.d.mts +24 -0
- package/lib/cspSources.mjs +60 -0
- package/lib/dynamic-colors.ts +426 -0
- package/lib/git.d.mts +24 -0
- package/lib/git.mjs +64 -0
- package/lib/hooks.ts +51 -0
- package/lib/inline.ts +20 -0
- package/lib/license-texts/Apache-2.0.txt +191 -0
- package/lib/license-texts/BSD-2-Clause.txt +24 -0
- package/lib/license-texts/BSD-3-Clause.txt +28 -0
- package/lib/license-texts/ISC.txt +15 -0
- package/lib/license-texts/MIT.txt +21 -0
- package/lib/licenses.d.mts +190 -0
- package/lib/licenses.mjs +683 -0
- package/lib/markdown.ts +42 -0
- package/lib/motion-schemes.d.mts +2 -0
- package/lib/motion-schemes.mjs +5 -0
- package/lib/motion.ts +131 -0
- package/lib/notice.d.mts +3 -0
- package/lib/notice.mjs +34 -0
- package/lib/scheduled.d.mts +15 -0
- package/lib/scheduled.mjs +37 -0
- package/lib/snackbar.ts +149 -0
- package/lib/status.d.mts +7 -0
- package/lib/status.mjs +18 -0
- package/lib/svg.d.mts +22 -0
- package/lib/svg.mjs +231 -0
- package/lib/text-colors.d.mts +11 -0
- package/lib/text-colors.mjs +43 -0
- package/lib/video-id.d.mts +14 -0
- package/lib/video-id.mjs +72 -0
- package/lib/video.d.mts +20 -0
- package/lib/video.mjs +91 -0
- package/package.json +131 -6
- package/pages/404.astro +10 -0
- package/pages/[...slug].astro +51 -0
- package/pages/_headers.txt +31 -0
- package/pages/blog/index.astro +10 -0
- package/pages/blog/kategori/[category].astro +18 -0
- package/pages/editor/code-images.json.js +25 -0
- package/pages/editor/index.astro +75 -0
- package/pages/editor/manifest.webmanifest.js +23 -0
- package/pages/editor/sw.js +33 -0
- package/pages/editor/sw.js.js +4 -0
- package/pages/editor/thumbnails.json.js +21 -0
- package/pages/editor/version.json.js +22 -0
- package/pages/headers.js +4 -0
- package/pages/index.astro +10 -0
- package/pages/ny.astro +6 -0
- package/pages/robots.txt.js +3 -0
- package/pages/rss.xml.js +20 -0
- package/scripts/app-screenshots.mjs +164 -0
- package/scripts/app-workers.mjs +6 -0
- package/scripts/bin.mjs +12 -0
- package/scripts/built-site.mjs +99 -0
- package/scripts/check-consent.mjs +113 -0
- package/scripts/check-content.mjs +149 -0
- package/scripts/check-demo.mjs +24 -0
- package/scripts/check-themes.mjs +45 -0
- package/scripts/check-worker.mjs +569 -0
- package/scripts/compare-html.mjs +81 -0
- package/scripts/demo-site.mjs +42 -0
- package/scripts/editor-icons.mjs +56 -0
- package/scripts/editor-roundtrip.mjs +134 -0
- package/scripts/editor-screens.mjs +101 -0
- package/scripts/git-env.mjs +9 -0
- package/scripts/icon-packs.mjs +151 -0
- package/scripts/link-counts.mjs +22 -0
- package/scripts/page-colors.mjs +42 -0
- package/scripts/releases.d.mts +10 -0
- package/scripts/releases.mjs +80 -0
- package/scripts/responsive-images.mjs +94 -0
- package/scripts/scheduled-list.mjs +15 -0
- package/scripts/theme-screenshots.mjs +88 -0
- package/scripts/types.mjs +21 -0
- package/scripts/video-thumbnails.mjs +89 -0
- package/scripts/worker-modules.mjs +34 -0
- package/site/appCookies.d.mts +9 -0
- package/site/appCookies.mjs +74 -0
- package/site/apps.ts +21 -0
- package/site/autolinks.d.mts +11 -0
- package/site/autolinks.mjs +200 -0
- package/site/blockScripts.ts +31 -0
- package/site/categories.ts +16 -0
- package/site/consent.ts +26 -0
- package/site/content.config.ts +84 -0
- package/site/content.ts +104 -0
- package/site/cookieList.ts +17 -0
- package/site/defineSite.d.mts +142 -0
- package/site/defineSite.mjs +136 -0
- package/site/directives/interact.d.mts +2 -0
- package/site/directives/interact.mjs +31 -0
- package/site/editor-version.d.mts +1 -0
- package/site/editor-version.mjs +26 -0
- package/site/fill.ts +13 -0
- package/site/format.ts +11 -0
- package/site/home.ts +66 -0
- package/site/iconPacks.d.mts +20 -0
- package/site/iconPacks.mjs +43 -0
- package/site/icons.d.mts +693 -0
- package/site/icons.mjs +605 -0
- package/site/images.d.mts +43 -0
- package/site/images.mjs +157 -0
- package/site/motion.ts +13 -0
- package/site/noindex.d.mts +6 -0
- package/site/noindex.mjs +21 -0
- package/site/pageColors.ts +110 -0
- package/site/popups.ts +98 -0
- package/site/schema.ts +189 -0
- package/site/sections.ts +3 -0
- package/site/seo.ts +28 -0
- package/site/site.ts +76 -0
- package/site/siteConfig.d.mts +1 -0
- package/site/siteConfig.mjs +8 -0
- package/site/siteCsp.d.mts +10 -0
- package/site/siteCsp.mjs +75 -0
- package/site/siteModules.d.mts +7 -0
- package/site/siteModules.mjs +27 -0
- package/site/standardPages.ts +59 -0
- package/site/testimonials.ts +48 -0
- package/site/text.ts +43 -0
- package/site/theme.d.mts +83 -0
- package/site/theme.mjs +472 -0
- package/site/tracking.client.ts +225 -0
- package/site/tracking.ts +9 -0
- package/src/ActionParts.tsx +205 -0
- package/src/Advanced.tsx +152 -0
- package/src/App.tsx +904 -0
- package/src/AppList.tsx +1345 -0
- package/src/BlockDialog.tsx +360 -0
- package/src/BlockParts.tsx +652 -0
- package/src/BlockSettings.tsx +533 -0
- package/src/BrandForm.tsx +164 -0
- package/src/Colors.tsx +476 -0
- package/src/CommunicationParts.tsx +199 -0
- package/src/CompanyForm.tsx +209 -0
- package/src/Consents.tsx +175 -0
- package/src/ContainmentParts.tsx +185 -0
- package/src/Crop.tsx +152 -0
- package/src/DataForm.tsx +434 -0
- package/src/Deleted.tsx +177 -0
- package/src/EntryForm.tsx +740 -0
- package/src/FileView.tsx +30 -0
- package/src/FormFrame.tsx +237 -0
- package/src/Keywords.tsx +225 -0
- package/src/Library.tsx +1563 -0
- package/src/Licenses.tsx +336 -0
- package/src/LinkSheet.tsx +138 -0
- package/src/MarkdownEditor.tsx +1075 -0
- package/src/Media.tsx +677 -0
- package/src/NavigationParts.tsx +350 -0
- package/src/NavigationRestParts.tsx +247 -0
- package/src/Notifications.tsx +59 -0
- package/src/PagePicker.tsx +136 -0
- package/src/Parts.tsx +991 -0
- package/src/PartsShown.tsx +168 -0
- package/src/PickerParts.tsx +183 -0
- package/src/Redirects.tsx +450 -0
- package/src/Screen.tsx +184 -0
- package/src/SectionsField.tsx +65 -0
- package/src/SelectionParts.tsx +134 -0
- package/src/SeoScores.tsx +184 -0
- package/src/Sheet.tsx +35 -0
- package/src/ThemeStore.tsx +376 -0
- package/src/Tokens.tsx +346 -0
- package/src/Versions.tsx +214 -0
- package/src/addresses.ts +75 -0
- package/src/allIcons.ts +7 -0
- package/src/appFiles.ts +29 -0
- package/src/apps.ts +281 -0
- package/src/ask.tsx +123 -0
- package/src/blocks.ts +251 -0
- package/src/brand.ts +153 -0
- package/src/cache.ts +42 -0
- package/src/check.ts +285 -0
- package/src/checkSite.ts +256 -0
- package/src/consents.ts +26 -0
- package/src/cookieList.ts +70 -0
- package/src/countries.ts +59 -0
- package/src/deploy.ts +155 -0
- package/src/editor.css +602 -0
- package/src/examples.ts +85 -0
- package/src/fields.tsx +942 -0
- package/src/frontmatter.d.mts +6 -0
- package/src/frontmatter.mjs +42 -0
- package/src/github.ts +376 -0
- package/src/i18n/da-DK/advanced.ts +28 -0
- package/src/i18n/da-DK/app.ts +71 -0
- package/src/i18n/da-DK/apps.ts +90 -0
- package/src/i18n/da-DK/blockSettings.ts +38 -0
- package/src/i18n/da-DK/blocks.ts +408 -0
- package/src/i18n/da-DK/check.ts +25 -0
- package/src/i18n/da-DK/colors.ts +64 -0
- package/src/i18n/da-DK/common.ts +118 -0
- package/src/i18n/da-DK/company.ts +24 -0
- package/src/i18n/da-DK/consents.ts +23 -0
- package/src/i18n/da-DK/forms.ts +116 -0
- package/src/i18n/da-DK/history.ts +35 -0
- package/src/i18n/da-DK/index.ts +49 -0
- package/src/i18n/da-DK/keywords.ts +19 -0
- package/src/i18n/da-DK/library.ts +135 -0
- package/src/i18n/da-DK/licenses.ts +25 -0
- package/src/i18n/da-DK/media.ts +79 -0
- package/src/i18n/da-DK/parts.ts +696 -0
- package/src/i18n/da-DK/preview.ts +13 -0
- package/src/i18n/da-DK/redirects.ts +51 -0
- package/src/i18n/da-DK/seo.ts +20 -0
- package/src/i18n/da-DK/text.ts +35 -0
- package/src/i18n/da-DK/theme.ts +41 -0
- package/src/i18n/en-US/advanced.ts +32 -0
- package/src/i18n/en-US/app.ts +84 -0
- package/src/i18n/en-US/apps.ts +134 -0
- package/src/i18n/en-US/blockSettings.ts +48 -0
- package/src/i18n/en-US/blocks.ts +441 -0
- package/src/i18n/en-US/check.ts +31 -0
- package/src/i18n/en-US/colors.ts +82 -0
- package/src/i18n/en-US/common.ts +144 -0
- package/src/i18n/en-US/company.ts +25 -0
- package/src/i18n/en-US/consents.ts +25 -0
- package/src/i18n/en-US/forms.ts +145 -0
- package/src/i18n/en-US/history.ts +43 -0
- package/src/i18n/en-US/index.ts +51 -0
- package/src/i18n/en-US/keywords.ts +24 -0
- package/src/i18n/en-US/library.ts +176 -0
- package/src/i18n/en-US/licenses.ts +36 -0
- package/src/i18n/en-US/media.ts +80 -0
- package/src/i18n/en-US/parts.ts +718 -0
- package/src/i18n/en-US/preview.ts +15 -0
- package/src/i18n/en-US/redirects.ts +66 -0
- package/src/i18n/en-US/seo.ts +26 -0
- package/src/i18n/en-US/text.ts +41 -0
- package/src/i18n/en-US/theme.ts +41 -0
- package/src/i18n/index.ts +124 -0
- package/src/icon.tsx +160 -0
- package/src/iconPacks.ts +89 -0
- package/src/images.ts +163 -0
- package/src/licenses.ts +79 -0
- package/src/markdown.d.mts +12 -0
- package/src/markdown.mjs +204 -0
- package/src/nav.ts +68 -0
- package/src/notFound.ts +28 -0
- package/src/notifications.ts +223 -0
- package/src/pagePreview.tsx +204 -0
- package/src/r2Caching.ts +55 -0
- package/src/redirects.ts +149 -0
- package/src/registry.tsx +188 -0
- package/src/schema.ts +417 -0
- package/src/sections.d.mts +45 -0
- package/src/sections.mjs +303 -0
- package/src/seo.ts +333 -0
- package/src/shared.ts +44 -0
- package/src/sidebar.ts +31 -0
- package/src/site.ts +317 -0
- package/src/standard/Block.astro +19 -0
- package/src/standard/BlockSite.astro +18 -0
- package/src/standard/accordion/index.tsx +47 -0
- package/src/standard/accordion/load.ts +23 -0
- package/src/standard/accordion/shared.ts +15 -0
- package/src/standard/accordion/view.ts +32 -0
- package/src/standard/buttons/fields.ts +98 -0
- package/src/standard/buttons/index.tsx +23 -0
- package/src/standard/buttons/load.ts +34 -0
- package/src/standard/buttons/shared.ts +12 -0
- package/src/standard/buttons/view.ts +30 -0
- package/src/standard/cards/index.tsx +71 -0
- package/src/standard/cards/load.ts +17 -0
- package/src/standard/cards/shared.ts +27 -0
- package/src/standard/cards/view.ts +29 -0
- package/src/standard/checklist/index.tsx +27 -0
- package/src/standard/checklist/load.ts +5 -0
- package/src/standard/checklist/view.ts +13 -0
- package/src/standard/cookies/index.tsx +17 -0
- package/src/standard/cookies/list.ts +56 -0
- package/src/standard/cookies/load.ts +11 -0
- package/src/standard/cookies/shared.ts +20 -0
- package/src/standard/cookies/view.ts +28 -0
- package/src/standard/faq/index.tsx +29 -0
- package/src/standard/faq/load.ts +14 -0
- package/src/standard/faq/view.ts +21 -0
- package/src/standard/head.ts +23 -0
- package/src/standard/hero/fields.ts +48 -0
- package/src/standard/hero/index.tsx +57 -0
- package/src/standard/hero/load.ts +23 -0
- package/src/standard/hero/view.ts +38 -0
- package/src/standard/links/index.tsx +41 -0
- package/src/standard/links/load.ts +17 -0
- package/src/standard/links/view.ts +25 -0
- package/src/standard/list/index.tsx +50 -0
- package/src/standard/list/load.ts +5 -0
- package/src/standard/list/view.ts +18 -0
- package/src/standard/load.ts +7 -0
- package/src/standard/notice/index.tsx +30 -0
- package/src/standard/notice/load.ts +16 -0
- package/src/standard/notice/view.ts +10 -0
- package/src/standard/points/index.tsx +42 -0
- package/src/standard/points/load.ts +5 -0
- package/src/standard/points/view.ts +12 -0
- package/src/standard/posts/index.tsx +132 -0
- package/src/standard/posts/load.ts +36 -0
- package/src/standard/posts/shared.ts +21 -0
- package/src/standard/posts/view.ts +63 -0
- package/src/standard/pricecards/index.tsx +57 -0
- package/src/standard/pricecards/load.ts +11 -0
- package/src/standard/pricecards/view.ts +50 -0
- package/src/standard/sharedTexts.ts +6 -0
- package/src/standard/site.ts +124 -0
- package/src/standard/siteLooks.ts +19 -0
- package/src/standard/sites.ts +13 -0
- package/src/standard/steps/index.tsx +28 -0
- package/src/standard/steps/load.ts +5 -0
- package/src/standard/steps/view.ts +11 -0
- package/src/standard/testimonials/index.tsx +87 -0
- package/src/standard/testimonials/list.ts +43 -0
- package/src/standard/testimonials/load.ts +58 -0
- package/src/standard/testimonials/shared.ts +13 -0
- package/src/standard/testimonials/view.ts +76 -0
- package/src/standard/themeBlocks.ts +13 -0
- package/src/standard/video/index.tsx +23 -0
- package/src/standard/video/load.ts +10 -0
- package/src/standard/video/view.ts +42 -0
- package/src/standard/view.tsx +105 -0
- package/src/standard/views.ts +37 -0
- package/src/store.ts +499 -0
- package/src/theme.ts +90 -0
- package/src/themeUi.ts +97 -0
- package/src/themes.ts +91 -0
- package/src/ui.tsx +3402 -0
- package/src/virtual.d.ts +5 -0
- package/src/when.ts +22 -0
- package/starter/.githooks/pre-push +4 -0
- package/starter/.nvmrc +1 -0
- package/starter/.prettierignore +11 -0
- package/starter/.prettierrc.json +7 -0
- package/starter/README.md +26 -0
- package/starter/astro.config.mjs +5 -0
- package/starter/package.json +67 -0
- package/starter/public/_redirects +1 -0
- package/starter/public/apple-touch-icon.png +0 -0
- package/starter/public/editor/apple-touch-icon.png +0 -0
- package/starter/public/editor/icon-192.png +0 -0
- package/starter/public/editor/icon-512.png +0 -0
- package/starter/public/editor/icon-maskable-512.png +0 -0
- package/starter/public/favicon.ico +0 -0
- package/starter/public/favicon.svg +1 -0
- package/starter/public/logo.svg +1 -0
- package/starter/src/content/pages/blog.md +8 -0
- package/starter/src/content/pages/forside.md +9 -0
- package/starter/src/content/pages/kontakt.md +7 -0
- package/starter/src/content/pages/privatlivspolitik.md +9 -0
- package/starter/src/content/pages/siden-findes-ikke.md +7 -0
- package/starter/src/content.config.ts +2 -0
- package/starter/src/data/apps.json +1 -0
- package/starter/src/data/block-settings.json +3 -0
- package/starter/src/data/blocks.json +1 -0
- package/starter/src/data/booking.json +6 -0
- package/starter/src/data/colors.json +70 -0
- package/starter/src/data/contact.json +8 -0
- package/starter/src/data/cookies.json +21 -0
- package/starter/src/data/csp.json +1 -0
- package/starter/src/data/footer.json +15 -0
- package/starter/src/data/internal-links.json +4 -0
- package/starter/src/data/menu.json +4 -0
- package/starter/src/data/mobile-menu.json +4 -0
- package/starter/src/data/old-addresses.json +1 -0
- package/starter/src/data/redirects.json +1 -0
- package/starter/src/data/standard-pages.json +8 -0
- package/starter/src/data/theme.json +1 -0
- package/starter/src/env.d.ts +2 -0
- package/starter/src/site/config.ts +13 -0
- package/starter/src/site/worker.js +14 -0
- package/starter/tsconfig.json +17 -0
- package/starter/worker/apps.js +6 -0
- package/starter/wrangler.jsonc +39 -0
- package/theme/LICENSE +21 -0
- package/theme/blocks/ArrowLink.tsx +14 -0
- package/theme/blocks/BlockHead.tsx +26 -0
- package/theme/blocks/Panel.tsx +32 -0
- package/theme/blocks/Person.tsx +26 -0
- package/theme/blocks/accordion/PageRow.tsx +25 -0
- package/theme/blocks/accordion/View.tsx +17 -0
- package/theme/blocks/buttons/Buttons.tsx +28 -0
- package/theme/blocks/buttons/View.tsx +12 -0
- package/theme/blocks/cards/LinkCards.tsx +72 -0
- package/theme/blocks/cards/View.tsx +20 -0
- package/theme/blocks/checklist/Checks.tsx +15 -0
- package/theme/blocks/checklist/View.tsx +14 -0
- package/theme/blocks/cookies/View.tsx +36 -0
- package/theme/blocks/faq/Accordion.tsx +34 -0
- package/theme/blocks/faq/View.tsx +16 -0
- package/theme/blocks/faq/script.ts +56 -0
- package/theme/blocks/hero/View.tsx +44 -0
- package/theme/blocks/links/LinkGroups.tsx +24 -0
- package/theme/blocks/links/View.tsx +17 -0
- package/theme/blocks/list/Rows.tsx +24 -0
- package/theme/blocks/list/View.tsx +14 -0
- package/theme/blocks/notice/View.tsx +13 -0
- package/theme/blocks/points/PointList.tsx +34 -0
- package/theme/blocks/points/View.tsx +14 -0
- package/theme/blocks/posts/View.tsx +26 -0
- package/theme/blocks/pricecards/View.tsx +40 -0
- package/theme/blocks/steps/StepList.tsx +23 -0
- package/theme/blocks/steps/View.tsx +14 -0
- package/theme/blocks/testimonials/List.tsx +46 -0
- package/theme/blocks/testimonials/Slider.tsx +44 -0
- package/theme/blocks/testimonials/Testimonial.tsx +124 -0
- package/theme/blocks/testimonials/View.tsx +21 -0
- package/theme/blocks/testimonials/script.ts +52 -0
- package/theme/blocks/video/View.tsx +41 -0
- package/theme/components/ActionBar.astro +63 -0
- package/theme/components/AppSlot.astro +21 -0
- package/theme/components/ArrowLink.tsx +21 -0
- package/theme/components/BlogIndex.astro +48 -0
- package/theme/components/BlogPosts.astro +18 -0
- package/theme/components/BlogPosts.tsx +45 -0
- package/theme/components/BookButton.tsx +22 -0
- package/theme/components/Container.astro +11 -0
- package/theme/components/CookieConsent.astro +121 -0
- package/theme/components/EndBlock.astro +21 -0
- package/theme/components/FontPreload.astro +6 -0
- package/theme/components/Footer.astro +85 -0
- package/theme/components/Header.astro +186 -0
- package/theme/components/Icon.astro +16 -0
- package/theme/components/Img.astro +29 -0
- package/theme/components/Img.tsx +33 -0
- package/theme/components/Inline.astro +11 -0
- package/theme/components/Logo.astro +23 -0
- package/theme/components/LogoMark.astro +10 -0
- package/theme/components/Markdown.astro +17 -0
- package/theme/components/MobileMenu.tsx +224 -0
- package/theme/components/MobileNav.astro +33 -0
- package/theme/components/PageHeader.astro +22 -0
- package/theme/components/PageSection.astro +24 -0
- package/theme/components/PhoneButton.tsx +11 -0
- package/theme/components/Popup.astro +145 -0
- package/theme/components/PopupButton.tsx +24 -0
- package/theme/components/PopupView.tsx +48 -0
- package/theme/components/PostList.tsx +58 -0
- package/theme/components/PostMeta.tsx +17 -0
- package/theme/components/RichText.astro +28 -0
- package/theme/components/Section.astro +29 -0
- package/theme/components/SectionHeading.tsx +8 -0
- package/theme/components/TextSection.astro +16 -0
- package/theme/components/home/PageBlock.astro +19 -0
- package/theme/components/home/Text.astro +13 -0
- package/theme/components/loadPopup.ts +22 -0
- package/theme/components/loadPosts.ts +16 -0
- package/theme/components/posts.ts +15 -0
- package/theme/editor/looks.mjs +7 -0
- package/theme/editor/previews.tsx +56 -0
- package/theme/layouts/Base.astro +273 -0
- package/theme/layouts/PageLayout.astro +126 -0
- package/theme/layouts/PostLayout.astro +137 -0
- package/theme/screenshots/1-front.jpg +0 -0
- package/theme/screenshots/2-front-dark.jpg +0 -0
- package/theme/screenshots/3-blog.jpg +0 -0
- package/theme/screenshots/4-contact.jpg +0 -0
- package/theme/scripts/motion.ts +29 -0
- package/theme/styles/base.css +320 -0
- package/theme/styles/blocks.css +1808 -0
- package/theme/styles/components.css +206 -0
- package/theme/styles/global.css +1302 -0
- package/theme/styles/ui.css +6544 -0
- package/theme/templates/Blog.astro +38 -0
- package/theme/templates/Category.astro +19 -0
- package/theme/templates/Home.astro +36 -0
- package/theme/templates/New.astro +68 -0
- package/theme/templates/NotFound.astro +117 -0
- package/theme/theme.json +28 -0
- package/theme/ui/Badge.tsx +45 -0
- package/theme/ui/BottomSheet.tsx +148 -0
- package/theme/ui/Button.tsx +79 -0
- package/theme/ui/ButtonGroup.tsx +31 -0
- package/theme/ui/Card.tsx +45 -0
- package/theme/ui/Carousel.tsx +124 -0
- package/theme/ui/Checkbox.tsx +59 -0
- package/theme/ui/Chip.tsx +101 -0
- package/theme/ui/ChipSet.tsx +27 -0
- package/theme/ui/DatePicker.tsx +149 -0
- package/theme/ui/DatePickerContainer.tsx +316 -0
- package/theme/ui/Dialog.tsx +100 -0
- package/theme/ui/Divider.tsx +27 -0
- package/theme/ui/ExposedDropdownMenu.tsx +77 -0
- package/theme/ui/Fab.tsx +88 -0
- package/theme/ui/FabMenu.tsx +67 -0
- package/theme/ui/IconButton.tsx +78 -0
- package/theme/ui/List.tsx +287 -0
- package/theme/ui/LoadingIndicator.tsx +29 -0
- package/theme/ui/Menu.tsx +278 -0
- package/theme/ui/NavigationBar.tsx +62 -0
- package/theme/ui/NavigationDrawer.tsx +80 -0
- package/theme/ui/NavigationRail.tsx +154 -0
- package/theme/ui/ProgressIndicator.tsx +162 -0
- package/theme/ui/RadioButton.tsx +50 -0
- package/theme/ui/Search.tsx +129 -0
- package/theme/ui/SegmentedButton.tsx +49 -0
- package/theme/ui/SideSheet.tsx +54 -0
- package/theme/ui/Slider.tsx +155 -0
- package/theme/ui/Snackbar.tsx +67 -0
- package/theme/ui/SplitButton.tsx +67 -0
- package/theme/ui/Switch.tsx +60 -0
- package/theme/ui/Tabs.tsx +114 -0
- package/theme/ui/TextField.tsx +278 -0
- package/theme/ui/TimePicker.tsx +128 -0
- package/theme/ui/TimePickerContainer.tsx +234 -0
- package/theme/ui/Toolbar.tsx +67 -0
- package/theme/ui/Tooltip.tsx +58 -0
- package/theme/ui/TopAppBar.tsx +77 -0
- package/theme/ui/bottomSheet.script.ts +8 -0
- package/theme/ui/bottomSheet.ts +211 -0
- package/theme/ui/carousel.script.ts +28 -0
- package/theme/ui/carouselLayout.ts +229 -0
- package/theme/ui/checkbox.script.ts +8 -0
- package/theme/ui/datePicker.script.ts +302 -0
- package/theme/ui/dates.ts +185 -0
- package/theme/ui/dialog.ts +48 -0
- package/theme/ui/dropdownMenu.script.ts +166 -0
- package/theme/ui/fabMenu.script.ts +57 -0
- package/theme/ui/navigationRail.script.ts +47 -0
- package/theme/ui/ripple.script.ts +159 -0
- package/theme/ui/search.script.ts +92 -0
- package/theme/ui/sideSheet.ts +48 -0
- package/theme/ui/slider.script.ts +12 -0
- package/theme/ui/sliderUpdate.ts +52 -0
- package/theme/ui/snackbar.script.ts +16 -0
- package/theme/ui/splitButton.script.ts +63 -0
- package/theme/ui/tabs.script.ts +48 -0
- package/theme/ui/textField.script.ts +17 -0
- package/theme/ui/textFieldUpdate.ts +44 -0
- package/theme/ui/timePicker.script.ts +248 -0
- package/theme/ui/toolbar.script.ts +43 -0
- package/theme/ui/tooltip.script.ts +68 -0
- package/worker/consent.js +141 -0
- package/worker/editor-github.js +167 -0
- package/worker/editor-host.js +74 -0
- package/worker/files.js +401 -0
- package/worker/github.js +36 -0
- package/worker/index.js +184 -0
- package/worker/not-found.js +97 -0
- package/worker/oauth.js +95 -0
- package/worker/publish.js +52 -0
- package/worker/push.js +270 -0
- package/worker/r2-status.js +50 -0
- package/worker/session.js +58 -0
package/src/AppList.tsx
ADDED
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
// "Indstillinger" › "Apps" (#apps): the apps installed (src/apps/, apps.ts: the site's own and those from the app store,
|
|
2
|
+
// with one from the store not yet built as "Installeres …"), each with who made it under its name and, from the store,
|
|
3
|
+
// that it has a newer version, with search (by name, who made it and what it does), the order (A–Å, Å–A, kept in this
|
|
4
|
+
// browser) and headings by the first letter, as the lists under "Indhold" have. "Indstillinger" › "Appstore"
|
|
5
|
+
// (#indstillinger/appstore): all apps, those in the app store (store.ts) and the site's own, installed or not, with search (by name, what they
|
|
6
|
+
// do, who made them and the words they are found by), the same order and a funnel by category. An app's page
|
|
7
|
+
// (#apps/<app>) is the same for every app and without examples: its name, who made it, whether it is installed, its
|
|
8
|
+
// version (from the store) and what it does, "Installer" (or "Opdater til …"), its pictures (each opening it whole),
|
|
9
|
+
// what it does more fully ("Om appen"), a row to its settings (installed: on the screen under "Indstillinger" with its
|
|
10
|
+
// kind, "Analytics" or "Promovering", where their fields are saved with "Gem ændringer" in their file,
|
|
11
|
+
// config.apps.settings; its page only installs it), what it adds (its blocks, the lists it brings: "Priser", each opening
|
|
12
|
+
// its list, and its kinds of the site's lists: a Mailchimp popup; installed, each with its switch at the end of its row
|
|
13
|
+
// and opening a sheet with where it is used and the same switch, as under "Blokindstillinger", and a kind's button to a
|
|
14
|
+
// new one, while it is on), what it needs of the site ("Rettigheder", needsOf in store.ts: its code on every page,
|
|
15
|
+
// the addresses it loads from and sends forms to, its part of the Worker, a step in the build), the cookies it sets
|
|
16
|
+
// (and whether the cookie banner asks first), who made it (a row to their page, #apps/udvikler/<name>, with all their
|
|
17
|
+
// apps), and "Afinstaller"; installing, updating and uninstalling are each saved at once in its own commit,
|
|
18
|
+
// as a block turned on or off under "Blokindstillinger". One of the site's own is installed or uninstalled in the site's
|
|
19
|
+
// file of apps; one from the store is copied into the site, or deleted from it, with it (appChanges in store.ts), and
|
|
20
|
+
// its page before it is installed is drawn from the store's catalog (StoreAppPage). Installed, its blocks and kinds are
|
|
21
|
+
// under "Blokindstillinger" (under its name), its blocks under "Indsæt" and its kinds under "+" of their list. An app in
|
|
22
|
+
// use (one of its blocks on a page, an entry of its kind or in its list) cannot be uninstalled, as a block on a page
|
|
23
|
+
// cannot be turned off; its page says where it is used, so uninstalling never changes the site.
|
|
24
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
25
|
+
import {
|
|
26
|
+
ArrowsDownUp,
|
|
27
|
+
Bell,
|
|
28
|
+
CheckCircle,
|
|
29
|
+
Code,
|
|
30
|
+
Cookie,
|
|
31
|
+
Gear,
|
|
32
|
+
Globe,
|
|
33
|
+
Hammer,
|
|
34
|
+
HardDrives,
|
|
35
|
+
Palette,
|
|
36
|
+
PaperPlaneTilt,
|
|
37
|
+
PuzzlePiece,
|
|
38
|
+
ShieldCheck,
|
|
39
|
+
Swatches,
|
|
40
|
+
UserCircle,
|
|
41
|
+
type Icon,
|
|
42
|
+
} from '@phosphor-icons/react';
|
|
43
|
+
import Screen from './Screen';
|
|
44
|
+
import {
|
|
45
|
+
Button,
|
|
46
|
+
InputChip,
|
|
47
|
+
Choice,
|
|
48
|
+
EmptyNote,
|
|
49
|
+
Filters,
|
|
50
|
+
Group,
|
|
51
|
+
GroupTitle,
|
|
52
|
+
ImageCarousel,
|
|
53
|
+
LeadingIcon,
|
|
54
|
+
LinkButton,
|
|
55
|
+
listClass,
|
|
56
|
+
SupportingText,
|
|
57
|
+
SearchRow,
|
|
58
|
+
BottomSheet,
|
|
59
|
+
SheetBody,
|
|
60
|
+
SheetTitle,
|
|
61
|
+
Switch,
|
|
62
|
+
SwitchRow,
|
|
63
|
+
useKept,
|
|
64
|
+
ListItem,
|
|
65
|
+
} from './ui';
|
|
66
|
+
import { BlockSwitch, blockSwitchTexts, KindSwitch, kindSwitchTexts, pagesWith, useOnOff } from './BlockSettings';
|
|
67
|
+
import { ConflictError, type Change, type RepoFile } from './github';
|
|
68
|
+
import { categoryOf } from '../lib/cookieCategories.mjs';
|
|
69
|
+
import { cookieChanges, inCookieList } from './cookieList';
|
|
70
|
+
import type { Env } from './App';
|
|
71
|
+
import { ask, choose } from './ask';
|
|
72
|
+
import { installedApps, kindKey, settingsFile, type SiteApp } from './apps';
|
|
73
|
+
import { AnyIcon } from './icon';
|
|
74
|
+
import {
|
|
75
|
+
appCategories,
|
|
76
|
+
appChanges,
|
|
77
|
+
appFolder,
|
|
78
|
+
breaking,
|
|
79
|
+
changedFiles,
|
|
80
|
+
notesSince,
|
|
81
|
+
recordIn,
|
|
82
|
+
DEVELOPER_ROUTE,
|
|
83
|
+
hasStore,
|
|
84
|
+
newer,
|
|
85
|
+
STORE_ROUTE,
|
|
86
|
+
STORE_THEMES,
|
|
87
|
+
type AppCategory,
|
|
88
|
+
type AppCookie,
|
|
89
|
+
type AppNeeds,
|
|
90
|
+
type ShownApp,
|
|
91
|
+
type StoreAdd,
|
|
92
|
+
type StoreRecord,
|
|
93
|
+
} from './store';
|
|
94
|
+
import { apps, offBlocks, siteBlocks } from './registry';
|
|
95
|
+
import { asWritten, backFrom, collections, companyParts, partScreens } from './site';
|
|
96
|
+
import { byText, slugify, type Part } from './schema';
|
|
97
|
+
import { headed, letterOf } from './Sheet';
|
|
98
|
+
import { config } from '@site/editor';
|
|
99
|
+
import { allThemes, ThemeRow, themeInstalled, useThemeCatalog } from './ThemeStore';
|
|
100
|
+
import { siteT, siteText, t } from './i18n';
|
|
101
|
+
import { OwnerLicenses, StoreLicenseRows } from './Licenses';
|
|
102
|
+
import { appType, setOn, told, useNotificationChoices } from './notifications';
|
|
103
|
+
import { kindAndName } from './deploy';
|
|
104
|
+
|
|
105
|
+
export const appHref = (app: SiteApp) => `#apps/${app.id}`;
|
|
106
|
+
|
|
107
|
+
/** The screen with an app's settings, by its kind, if any: one under "Indstillinger" ("Analytics", partScreens) or a
|
|
108
|
+
* part of "Virksomheden" with parts ("Betaling", companyParts); its address and its name. */
|
|
109
|
+
export function settingsHome(app: SiteApp): { route: string; label: string } | undefined {
|
|
110
|
+
const kind = app.category;
|
|
111
|
+
if (!kind) return undefined;
|
|
112
|
+
const screen = partScreens.find((s) => s.apps?.includes(kind));
|
|
113
|
+
if (screen) return { route: screen.id, label: screen.label };
|
|
114
|
+
const company = companyParts.find((p) => p.parts && p.apps?.includes(kind));
|
|
115
|
+
return company && { route: `virksomheden/${company.id}`, label: company.label };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Where an app's settings are: on the screen with its kind (#maaling/google-analytics, #virksomheden/betaling/mobilepay),
|
|
119
|
+
* else on a screen of their own (#apps/<id>/indstillinger); null without settings. */
|
|
120
|
+
export function settingsRoute(app: SiteApp): string | null {
|
|
121
|
+
if (!app.settings) return null;
|
|
122
|
+
const home = settingsHome(app);
|
|
123
|
+
return home ? `${home.route}/${app.id}` : `apps/${app.id}/indstillinger`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An app's settings as a part, a form of its own (CompanyForm.tsx): its fields in its file, under its name, going back
|
|
127
|
+
* to the screen with its category, or to its page. */
|
|
128
|
+
export function appSettingsPart(app: SiteApp): Part | null {
|
|
129
|
+
if (!app.settings) return null;
|
|
130
|
+
const home = settingsHome(app);
|
|
131
|
+
return {
|
|
132
|
+
id: app.id,
|
|
133
|
+
label: app.label,
|
|
134
|
+
icon: app.icon,
|
|
135
|
+
file: settingsFile(app.id),
|
|
136
|
+
fields: app.settings.fields,
|
|
137
|
+
defaults: app.settings.values,
|
|
138
|
+
commit: siteT.apps.settingsOf(asWritten(app).label),
|
|
139
|
+
back: home ? { href: `#${home.route}`, label: home.label } : { href: appHref(app), label: app.label },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The settings of the apps installed of a screen's kinds (PartScreen.apps, or Part.apps of a part with parts), after
|
|
144
|
+
* its own parts. */
|
|
145
|
+
export function appPartsIn(screen: { apps?: AppCategory[] }, files: RepoFile[]): Part[] {
|
|
146
|
+
if (!screen.apps) return [];
|
|
147
|
+
const installed = installedApps(files);
|
|
148
|
+
return apps
|
|
149
|
+
.filter((a) => !!a.category && screen.apps!.includes(a.category) && installed.includes(a.id))
|
|
150
|
+
.flatMap((a) => appSettingsPart(a) ?? [])
|
|
151
|
+
.sort((a, b) => byText(a.label, b.label));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The entries of an app's kind in a list (a Mailchimp pop-up), by their title, each opening it. */
|
|
155
|
+
export function entriesOf(app: SiteApp, list: string, files: RepoFile[]): { href: string; title: string }[] {
|
|
156
|
+
const c = collections.find((x) => x.id === list);
|
|
157
|
+
if (c?.kind !== 'folder' || !c.kinds) return [];
|
|
158
|
+
return files
|
|
159
|
+
.filter((f) => f.path.startsWith(`${c.folder}/`) && f.path.endsWith('.json'))
|
|
160
|
+
.flatMap((f) => {
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(f.text) as Record<string, unknown>;
|
|
163
|
+
return data[c.kinds!] === app.id ? [{ href: `#${f.path}`, title: c.summary(data) || f.path }] : [];
|
|
164
|
+
} catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The lists an app brings ("Priser"), each with how many entries it has, opening it. */
|
|
171
|
+
export const listsOf = (app: SiteApp, files: RepoFile[]) =>
|
|
172
|
+
(app.lists ?? []).map((l) => ({
|
|
173
|
+
key: l.id,
|
|
174
|
+
icon: l.icon,
|
|
175
|
+
label: l.label,
|
|
176
|
+
href: `#liste/${l.id}`,
|
|
177
|
+
count: files.filter((f) => f.path.startsWith(`${l.folder}/`) && f.path.endsWith('.json')).length,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
/** Where an app is in use: the pages and posts with one of its blocks (also a hidden one), the entries
|
|
181
|
+
* of its kinds (a Mailchimp popup), by their title, and the lists it brings that have entries ("Priser"), each
|
|
182
|
+
* with the icon of what is used there (the first of its blocks on a page) and opening it. */
|
|
183
|
+
export function usesOf(app: SiteApp, files: RepoFile[]): { href: string; title: string; icon: Icon }[] {
|
|
184
|
+
const pages = new Map<string, { title: string; icon: Icon }>();
|
|
185
|
+
for (const b of siteBlocks.filter((x) => x.app === app.id))
|
|
186
|
+
for (const p of pagesWith(b.type, files)) if (!pages.has(p.path)) pages.set(p.path, { title: p.title, icon: b.icon });
|
|
187
|
+
const entries = Object.entries(app.kinds ?? {}).flatMap(([list, kind]) =>
|
|
188
|
+
entriesOf(app, list, files).map((e) => ({ ...e, icon: kind.icon })),
|
|
189
|
+
);
|
|
190
|
+
const lists = listsOf(app, files).flatMap((l) => (l.count ? [{ href: l.href, title: l.label, icon: l.icon }] : []));
|
|
191
|
+
return [...[...pages].map(([path, p]) => ({ href: `#${path}`, ...p })), ...entries, ...lists];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** An app's kinds of the site's lists (a pop-up), each by its own name ("Mailchimp popup", "Calendly bookingark") and
|
|
195
|
+
* opening the list, with the list's id, its name for one and for more. */
|
|
196
|
+
export function kindsOf(app: SiteApp) {
|
|
197
|
+
return Object.entries(app.kinds ?? {}).flatMap(([list, kind]) => {
|
|
198
|
+
const c = collections.find((x) => x.id === list);
|
|
199
|
+
return c
|
|
200
|
+
? [
|
|
201
|
+
{
|
|
202
|
+
key: list,
|
|
203
|
+
icon: kind.icon,
|
|
204
|
+
label: kind.label,
|
|
205
|
+
hint: kind.hint,
|
|
206
|
+
href: `#liste/${c.id}`,
|
|
207
|
+
one: c.singular,
|
|
208
|
+
more: c.label,
|
|
209
|
+
create: c.create,
|
|
210
|
+
},
|
|
211
|
+
]
|
|
212
|
+
: [];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const statusOf = (installed: boolean) => (installed ? t.apps.installed : t.apps.notInstalled);
|
|
217
|
+
|
|
218
|
+
type Sort = 'az' | 'za';
|
|
219
|
+
const SORTS: Sort[] = ['az', 'za'];
|
|
220
|
+
// The order picked last time, in this browser (the same for the apps and the store).
|
|
221
|
+
const SORT_KEY = 'editor-sort-apps';
|
|
222
|
+
|
|
223
|
+
/** The order of a list of apps, A–Å or Å–A, kept in this browser. */
|
|
224
|
+
function useSort(): [Sort, (next: Sort) => void] {
|
|
225
|
+
const [sort, setSort] = useState<Sort>(() => {
|
|
226
|
+
try {
|
|
227
|
+
const saved = localStorage.getItem(SORT_KEY);
|
|
228
|
+
if (SORTS.some((s) => s === saved)) return saved as Sort;
|
|
229
|
+
} catch {}
|
|
230
|
+
return 'az';
|
|
231
|
+
});
|
|
232
|
+
function pickSort(next: Sort) {
|
|
233
|
+
setSort(next);
|
|
234
|
+
try {
|
|
235
|
+
localStorage.setItem(SORT_KEY, next);
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
return [sort, pickSort];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sortChoice = (sort: Sort, pickSort: (next: Sort) => void) => (
|
|
242
|
+
<Choice
|
|
243
|
+
title={t.library.sort}
|
|
244
|
+
icon={ArrowsDownUp}
|
|
245
|
+
value={sort}
|
|
246
|
+
options={SORTS.map((value) => ({ value, label: t.library.sorts[value] }))}
|
|
247
|
+
onChange={pickSort}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
/** The store's catalog (store.ts) for a screen: null while it is read, 'failed' when it could not be; `skip`: not read
|
|
252
|
+
* (the page of one of the site's own apps). */
|
|
253
|
+
function useCatalog(env: Env, skip = false): ShownApp[] | null | 'failed' {
|
|
254
|
+
const [catalog, setCatalog] = useState<ShownApp[] | null | 'failed'>(null);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (skip) return;
|
|
257
|
+
let on = true;
|
|
258
|
+
env.store.catalog().then(
|
|
259
|
+
(apps) => on && setCatalog(apps),
|
|
260
|
+
() => on && setCatalog('failed'),
|
|
261
|
+
);
|
|
262
|
+
return () => {
|
|
263
|
+
on = false;
|
|
264
|
+
};
|
|
265
|
+
}, [skip]);
|
|
266
|
+
return catalog;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// The apps updated while the editor is open, by their id, to their version: their page and the list say so until the
|
|
270
|
+
// site's build has it (and the editor reloads with it, deploy.ts keepCurrent).
|
|
271
|
+
const updated = new Map<string, string>();
|
|
272
|
+
const versionOf = (app: SiteApp) => updated.get(app.id) ?? app.store?.version ?? '';
|
|
273
|
+
|
|
274
|
+
/** The newer version of an app from the store, when the store has one. */
|
|
275
|
+
const updateOf = (app: SiteApp, catalog: ShownApp[] | null | 'failed') => {
|
|
276
|
+
const found = app.store && Array.isArray(catalog) ? catalog.find((a) => a.id === app.id) : undefined;
|
|
277
|
+
return found && newer(found.version, versionOf(app)) ? found : undefined;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** An icon of the store's catalog (Phosphor's name) in front of a row. */
|
|
281
|
+
const storeIcon = (name?: string) => <LeadingIcon>{name && <AnyIcon name={name} size={24} />}</LeadingIcon>;
|
|
282
|
+
|
|
283
|
+
export function Apps({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
284
|
+
const installed = installedApps(files);
|
|
285
|
+
const catalog = useCatalog(env);
|
|
286
|
+
const store = Array.isArray(catalog) ? catalog : [];
|
|
287
|
+
const [query, setQuery] = useKept('apps-soeg', '');
|
|
288
|
+
const [sort, pickSort] = useSort();
|
|
289
|
+
|
|
290
|
+
// The apps installed: the site's own and those from the store; and one from the store in the file of apps that the
|
|
291
|
+
// site's build does not have yet, from the catalog (by its id until it is read). One that is not installed is under
|
|
292
|
+
// "Appstore".
|
|
293
|
+
const rows = [
|
|
294
|
+
...apps
|
|
295
|
+
.filter((a) => installed.includes(a.id))
|
|
296
|
+
.map((a) => ({
|
|
297
|
+
id: a.id,
|
|
298
|
+
label: a.label,
|
|
299
|
+
publisher: a.publisher,
|
|
300
|
+
hint: a.hint,
|
|
301
|
+
icon: <LeadingIcon icon={a.icon} />,
|
|
302
|
+
status: updateOf(a, catalog) ? t.apps.updateFound : '',
|
|
303
|
+
})),
|
|
304
|
+
...installed
|
|
305
|
+
.filter((id) => !apps.some((a) => a.id === id))
|
|
306
|
+
.map((id) => {
|
|
307
|
+
const found = store.find((a) => a.id === id);
|
|
308
|
+
return {
|
|
309
|
+
id,
|
|
310
|
+
label: found?.label ?? id,
|
|
311
|
+
publisher: found?.publisher ?? '',
|
|
312
|
+
hint: found?.hint ?? '',
|
|
313
|
+
icon: storeIcon(found?.icon),
|
|
314
|
+
status: t.apps.pending,
|
|
315
|
+
};
|
|
316
|
+
}),
|
|
317
|
+
];
|
|
318
|
+
// By the name, who made it and what it does.
|
|
319
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
320
|
+
const shown = rows
|
|
321
|
+
.filter((a) => words.every((w) => `${a.label} ${a.publisher} ${a.hint}`.toLowerCase().includes(w)))
|
|
322
|
+
.sort((a, b) => (sort === 'za' ? -1 : 1) * byText(a.label, b.label));
|
|
323
|
+
// Under the first letter of their names (headed, from HEADINGS_FROM apps).
|
|
324
|
+
const groups = headed(shown, (a) => letterOf(a.label));
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<Screen title={t.common.parts.apps} wide back={backFrom('apps')}>
|
|
328
|
+
<SearchRow
|
|
329
|
+
value={query}
|
|
330
|
+
onChange={setQuery}
|
|
331
|
+
label={t.common.searchIn(t.common.parts.apps)}
|
|
332
|
+
sort={sortChoice(sort, pickSort)}
|
|
333
|
+
className="mb-6"
|
|
334
|
+
/>
|
|
335
|
+
{groups.length ? (
|
|
336
|
+
groups.map((g) => (
|
|
337
|
+
<Group key={g.key ?? 'all'} title={g.key ?? undefined}>
|
|
338
|
+
{g.items.map((a) => (
|
|
339
|
+
// Who made it under its name.
|
|
340
|
+
<ListItem
|
|
341
|
+
key={a.id}
|
|
342
|
+
href={`#apps/${a.id}`}
|
|
343
|
+
leading={a.icon}
|
|
344
|
+
headline={a.label}
|
|
345
|
+
supporting={a.publisher}
|
|
346
|
+
count={a.status}
|
|
347
|
+
arrow
|
|
348
|
+
/>
|
|
349
|
+
))}
|
|
350
|
+
</Group>
|
|
351
|
+
))
|
|
352
|
+
) : (
|
|
353
|
+
<p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
|
|
354
|
+
)}
|
|
355
|
+
</Screen>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const backToStore = { href: `#${STORE_ROUTE}`, label: t.apps.find };
|
|
360
|
+
|
|
361
|
+
/** All apps, as "Appstore" lists them: those in the store's catalog, and the site's own (not from the store), drawn
|
|
362
|
+
* from the site's code; each with all its words (for the search) and its icon. */
|
|
363
|
+
function allApps(catalog: ShownApp[] | null | 'failed') {
|
|
364
|
+
const store = Array.isArray(catalog) ? catalog : [];
|
|
365
|
+
return [
|
|
366
|
+
...store.map((a) => ({ ...a, glyph: storeIcon(a.icon) })),
|
|
367
|
+
...apps
|
|
368
|
+
.filter((a) => !a.store && !store.some((s) => s.id === a.id))
|
|
369
|
+
.map((a) => ({
|
|
370
|
+
id: a.id,
|
|
371
|
+
label: a.label,
|
|
372
|
+
hint: a.hint,
|
|
373
|
+
publisher: a.publisher,
|
|
374
|
+
category: a.category,
|
|
375
|
+
words: [a.label, a.hint, a.publisher, ...(a.keywords ?? [])].join(' ').toLowerCase(),
|
|
376
|
+
glyph: <LeadingIcon icon={a.icon} />,
|
|
377
|
+
})),
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The address of a developer's page, by their name. */
|
|
382
|
+
const developerHref = (name: string) => `#${DEVELOPER_ROUTE}/${slugify(name)}`;
|
|
383
|
+
|
|
384
|
+
/** An app's row in "Appstore" and on a developer's page: its icon, its name, what it does, and whether it is installed,
|
|
385
|
+
* opening its page. */
|
|
386
|
+
function AppRow({ app, installed }: { app: ReturnType<typeof allApps>[number]; installed: boolean }) {
|
|
387
|
+
return (
|
|
388
|
+
<ListItem
|
|
389
|
+
href={`#apps/${app.id}`}
|
|
390
|
+
leading={app.glyph}
|
|
391
|
+
headline={app.label}
|
|
392
|
+
supporting={app.hint}
|
|
393
|
+
count={installed && t.apps.installed}
|
|
394
|
+
arrow
|
|
395
|
+
/>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** A developer's page (#apps/udvikler/<name>): their name, and all their apps and themes (in the store and the site's
|
|
400
|
+
* own), each opening its page. */
|
|
401
|
+
export function DeveloperPage({ slug, env, files }: { slug: string; env: Env; files: RepoFile[] }) {
|
|
402
|
+
const installed = installedApps(files);
|
|
403
|
+
const catalog = useCatalog(env);
|
|
404
|
+
const themeCatalog = useThemeCatalog(env);
|
|
405
|
+
const theirs = allApps(catalog)
|
|
406
|
+
.filter((a) => slugify(a.publisher) === slug)
|
|
407
|
+
.sort((a, b) => byText(a.label, b.label));
|
|
408
|
+
const theirThemes = allThemes(themeCatalog)
|
|
409
|
+
.filter((x) => slugify(x.author) === slug)
|
|
410
|
+
.sort((a, b) => byText(a.name, b.name));
|
|
411
|
+
const name = theirs[0]?.publisher ?? theirThemes[0]?.author;
|
|
412
|
+
return (
|
|
413
|
+
<Screen title={name ?? t.apps.developer} wide back={backToStore}>
|
|
414
|
+
{name && (
|
|
415
|
+
<div className="mb-8 flex items-center gap-3 px-4">
|
|
416
|
+
<LeadingIcon icon={UserCircle} />
|
|
417
|
+
<span className="min-w-0 flex-1">
|
|
418
|
+
<span className="block truncate font-medium">{name}</span>
|
|
419
|
+
<span className="block truncate text-sm text-muted">
|
|
420
|
+
{[theirs.length && t.apps.appCount(theirs.length), theirThemes.length && t.library.themeCount(theirThemes.length)]
|
|
421
|
+
.filter(Boolean)
|
|
422
|
+
.join(' · ')}
|
|
423
|
+
</span>
|
|
424
|
+
</span>
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
{theirs.length > 0 && (
|
|
428
|
+
<Group title={t.apps.developerApps}>
|
|
429
|
+
{theirs.map((a) => (
|
|
430
|
+
<AppRow key={a.id} app={a} installed={installed.includes(a.id)} />
|
|
431
|
+
))}
|
|
432
|
+
</Group>
|
|
433
|
+
)}
|
|
434
|
+
{theirThemes.length > 0 && (
|
|
435
|
+
<Group title={t.common.parts.themes}>
|
|
436
|
+
{theirThemes.map((x) => (
|
|
437
|
+
<ThemeRow key={x.id} of={x} />
|
|
438
|
+
))}
|
|
439
|
+
</Group>
|
|
440
|
+
)}
|
|
441
|
+
{!name &&
|
|
442
|
+
(catalog === null || themeCatalog === null ? (
|
|
443
|
+
<EmptyNote busy>{t.apps.loadingStore}</EmptyNote>
|
|
444
|
+
) : (
|
|
445
|
+
<EmptyNote>{t.apps.noDeveloper}</EmptyNote>
|
|
446
|
+
))}
|
|
447
|
+
</Screen>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** What every app's page says about it, from its folder or the store's catalog: its pictures (each opening it whole),
|
|
452
|
+
* what it does more fully, what it needs of the site (needsOf), the cookies it sets (`consent`: the cookie banner asks
|
|
453
|
+
* first), and who made it, with a row to their page. `children`: what stands between its description and what it
|
|
454
|
+
* needs (its settings and what it adds). */
|
|
455
|
+
function AppAbout({
|
|
456
|
+
pictures,
|
|
457
|
+
description,
|
|
458
|
+
news = [],
|
|
459
|
+
needs,
|
|
460
|
+
notifies = [],
|
|
461
|
+
cookies,
|
|
462
|
+
consent,
|
|
463
|
+
publisher,
|
|
464
|
+
licenses,
|
|
465
|
+
children,
|
|
466
|
+
}: {
|
|
467
|
+
pictures: { src: string; label: string }[];
|
|
468
|
+
description?: string;
|
|
469
|
+
news?: { version: string; text: string }[];
|
|
470
|
+
needs?: AppNeeds;
|
|
471
|
+
/** The kinds of notifications it sends (AppNotifications), under "Notifikationer": their names, and installed their
|
|
472
|
+
* types, each with its switch, the same as under "Konto" › "Notifikationer". */
|
|
473
|
+
notifies?: { label: string; type?: string }[];
|
|
474
|
+
cookies: AppCookie[];
|
|
475
|
+
consent: boolean;
|
|
476
|
+
publisher: string;
|
|
477
|
+
/** Its licences, under "Licenser" (OwnerLicenses, or StoreLicenseRows from the catalog). */
|
|
478
|
+
licenses?: ReactNode;
|
|
479
|
+
children?: ReactNode;
|
|
480
|
+
}) {
|
|
481
|
+
const row = (glyph: Icon, text: string, more?: string) => <ListItem key={text} icon={glyph} headline={text} supporting={more} />;
|
|
482
|
+
const needed = needs
|
|
483
|
+
? [
|
|
484
|
+
needs.pages && row(Code, t.apps.needsPages),
|
|
485
|
+
needs.loads.length > 0 && row(Globe, t.apps.needsLoads, needs.loads.join(', ')),
|
|
486
|
+
needs.sends.length > 0 && row(PaperPlaneTilt, t.apps.needsSends, needs.sends.join(', ')),
|
|
487
|
+
needs.server && row(HardDrives, t.apps.needsServer),
|
|
488
|
+
needs.build && row(Hammer, t.apps.needsBuild),
|
|
489
|
+
...(needs.changes ?? []).map((hook) => row(Palette, t.apps.changes[hook])),
|
|
490
|
+
].filter((x) => !!x)
|
|
491
|
+
: [];
|
|
492
|
+
const lasts = (c: AppCookie) => c.lasts && t.apps.cookieLasts(c.lasts);
|
|
493
|
+
// A named cookie's category (lib/cookieCategories.mjs), first under its name.
|
|
494
|
+
const category = (c: AppCookie) => t.blocks.cookies.entries.categories[categoryOf(c)];
|
|
495
|
+
const { off } = useNotificationChoices();
|
|
496
|
+
return (
|
|
497
|
+
<>
|
|
498
|
+
{pictures.length > 0 && (
|
|
499
|
+
<section className="mb-8">
|
|
500
|
+
<GroupTitle>{t.apps.screenshots}</GroupTitle>
|
|
501
|
+
<ImageCarousel label={t.apps.screenshots} images={pictures} />
|
|
502
|
+
</section>
|
|
503
|
+
)}
|
|
504
|
+
{description && (
|
|
505
|
+
<section className="mb-8">
|
|
506
|
+
<GroupTitle>{t.apps.about}</GroupTitle>
|
|
507
|
+
{description.split(/\n\s*\n/).map((p) => (
|
|
508
|
+
<p key={p} className="mb-3 px-4 last:mb-0">
|
|
509
|
+
{p}
|
|
510
|
+
</p>
|
|
511
|
+
))}
|
|
512
|
+
</section>
|
|
513
|
+
)}
|
|
514
|
+
{news.length > 0 && (
|
|
515
|
+
<section className="mb-8">
|
|
516
|
+
<GroupTitle>{t.apps.news}</GroupTitle>
|
|
517
|
+
{news.map((n) => (
|
|
518
|
+
<div key={n.version} className="mb-3 px-4 last:mb-0">
|
|
519
|
+
<h3 className="font-medium">{t.apps.version(n.version)}</h3>
|
|
520
|
+
{n.text.split(/\n\s*\n/).map((p) => (
|
|
521
|
+
<p key={p} className="text-muted">
|
|
522
|
+
{p}
|
|
523
|
+
</p>
|
|
524
|
+
))}
|
|
525
|
+
</div>
|
|
526
|
+
))}
|
|
527
|
+
</section>
|
|
528
|
+
)}
|
|
529
|
+
{children}
|
|
530
|
+
{needs && <Group title={t.apps.needs}>{needed.length ? needed : row(CheckCircle, t.apps.needsNothing)}</Group>}
|
|
531
|
+
{notifies.some((n) => n.type) ? (
|
|
532
|
+
<section className="mb-8">
|
|
533
|
+
<GroupTitle>{t.apps.notifications}</GroupTitle>
|
|
534
|
+
<div className="space-y-5 px-4">
|
|
535
|
+
{notifies.map((n) => (
|
|
536
|
+
<SwitchRow
|
|
537
|
+
key={n.type}
|
|
538
|
+
label={n.label}
|
|
539
|
+
texts={[t.library.on, t.library.off]}
|
|
540
|
+
selected={!off.includes(n.type!)}
|
|
541
|
+
onChange={(yes) => setOn(n.type!, yes)}
|
|
542
|
+
/>
|
|
543
|
+
))}
|
|
544
|
+
</div>
|
|
545
|
+
</section>
|
|
546
|
+
) : (
|
|
547
|
+
<Group title={t.apps.notifications}>
|
|
548
|
+
{notifies.length ? notifies.map((n) => row(Bell, n.label)) : row(Bell, t.apps.notifiesNone)}
|
|
549
|
+
</Group>
|
|
550
|
+
)}
|
|
551
|
+
<Group title={t.apps.cookies}>
|
|
552
|
+
{consent && row(ShieldCheck, t.apps.cookiesAsk)}
|
|
553
|
+
{cookies.map((c) =>
|
|
554
|
+
c.name
|
|
555
|
+
? row(Cookie, c.name, [category(c), c.purpose, lasts(c)].filter(Boolean).join(' · '))
|
|
556
|
+
: row(Cookie, c.purpose, lasts(c) || undefined),
|
|
557
|
+
)}
|
|
558
|
+
{!consent && !cookies.length && row(Cookie, t.apps.noCookies)}
|
|
559
|
+
</Group>
|
|
560
|
+
{licenses}
|
|
561
|
+
<DeveloperRow name={publisher} />
|
|
562
|
+
</>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Who made an app or a theme, under "Udvikler", as a row to their page with all they have made. */
|
|
567
|
+
export function DeveloperRow({ name }: { name: string }) {
|
|
568
|
+
return (
|
|
569
|
+
<Group title={t.apps.developer}>
|
|
570
|
+
<ListItem href={developerHref(name)} icon={UserCircle} headline={name} arrow />
|
|
571
|
+
</Group>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** "Appstore" (#indstillinger/appstore, under "Indstillinger"): in a tab of its own each in the top app bar, all apps,
|
|
576
|
+
* those in the app store and the site's own, each with what it does and whether it is installed, and all themes
|
|
577
|
+
* (#indstillinger/appstore/temaer, ThemeStore.tsx), those in the store
|
|
578
|
+
* and those installed, each with its version and who made it; each opening its page. With search (by name, what it
|
|
579
|
+
* does, who made it and the words it is found by, in both languages), the order and the filters by category (apps)
|
|
580
|
+
* and by whether it is installed, each picked one as a chip. */
|
|
581
|
+
export function StorePage({ env, files, tab }: { env: Env; files: RepoFile[]; tab: 'apps' | 'themes' }) {
|
|
582
|
+
const installed = installedApps(files);
|
|
583
|
+
const catalog = useCatalog(env);
|
|
584
|
+
const themeCatalog = useThemeCatalog(env);
|
|
585
|
+
const [query, setQuery] = useKept('butik-soeg', '');
|
|
586
|
+
const [sort, pickSort] = useSort();
|
|
587
|
+
const [category, setCategory] = useKept<AppCategory | ''>('butik-kategori', '');
|
|
588
|
+
const [state, setState] = useKept<'' | 'installed' | 'notInstalled'>('butik-status', '');
|
|
589
|
+
const apps = tab === 'apps';
|
|
590
|
+
const read = apps ? catalog : themeCatalog;
|
|
591
|
+
|
|
592
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
593
|
+
const order = (a: string, b: string) => (sort === 'za' ? -1 : 1) * byText(a, b);
|
|
594
|
+
const appGroups = headed(
|
|
595
|
+
allApps(catalog)
|
|
596
|
+
.filter((a) => words.every((w) => a.words.includes(w)))
|
|
597
|
+
.filter((a) => !category || a.category === category)
|
|
598
|
+
.filter((a) => !state || installed.includes(a.id) === (state === 'installed'))
|
|
599
|
+
.sort((a, b) => order(a.label, b.label)),
|
|
600
|
+
(a) => letterOf(a.label),
|
|
601
|
+
);
|
|
602
|
+
const themeGroups = headed(
|
|
603
|
+
allThemes(themeCatalog)
|
|
604
|
+
.filter((x) => words.every((w) => x.words.includes(w)))
|
|
605
|
+
.filter((x) => !state || themeInstalled(x.id) === (state === 'installed'))
|
|
606
|
+
.sort((a, b) => order(a.name, b.name)),
|
|
607
|
+
(x) => letterOf(x.name),
|
|
608
|
+
);
|
|
609
|
+
const filters = [
|
|
610
|
+
...(apps
|
|
611
|
+
? [
|
|
612
|
+
{
|
|
613
|
+
title: t.apps.categoryFilter,
|
|
614
|
+
value: category,
|
|
615
|
+
options: [
|
|
616
|
+
{ value: '', label: t.apps.allCategories },
|
|
617
|
+
...appCategories.map((value) => ({ value, label: t.apps.categories[value] })),
|
|
618
|
+
],
|
|
619
|
+
onChange: (v: string) => setCategory(v as AppCategory | ''),
|
|
620
|
+
},
|
|
621
|
+
]
|
|
622
|
+
: []),
|
|
623
|
+
{
|
|
624
|
+
title: t.apps.stateFilter,
|
|
625
|
+
value: state,
|
|
626
|
+
options: [
|
|
627
|
+
{ value: '', label: t.apps.allStates },
|
|
628
|
+
{ value: 'installed', label: t.apps.installed },
|
|
629
|
+
{ value: 'notInstalled', label: t.apps.notInstalled },
|
|
630
|
+
],
|
|
631
|
+
onChange: (v: string) => setState(v as typeof state),
|
|
632
|
+
},
|
|
633
|
+
];
|
|
634
|
+
// Each filter picked, as a chip that removes it.
|
|
635
|
+
const chips = [
|
|
636
|
+
apps && category && { key: 'category', label: t.apps.categories[category], clear: () => setCategory('') },
|
|
637
|
+
state && { key: 'state', label: t.apps[state], clear: () => setState('') },
|
|
638
|
+
].filter((c) => !!c);
|
|
639
|
+
const empty = apps ? !appGroups.length : !themeGroups.length;
|
|
640
|
+
|
|
641
|
+
return (
|
|
642
|
+
<Screen
|
|
643
|
+
title={t.apps.find}
|
|
644
|
+
wide
|
|
645
|
+
back={backFrom('appstore')}
|
|
646
|
+
tabs={{
|
|
647
|
+
label: t.apps.find,
|
|
648
|
+
value: tab,
|
|
649
|
+
options: [
|
|
650
|
+
{ value: 'apps', label: t.common.parts.apps, icon: PuzzlePiece },
|
|
651
|
+
{ value: 'themes', label: t.common.parts.themes, icon: Swatches },
|
|
652
|
+
],
|
|
653
|
+
onChange: (v) => (location.hash = v === 'apps' ? STORE_ROUTE : STORE_THEMES),
|
|
654
|
+
}}
|
|
655
|
+
>
|
|
656
|
+
<SearchRow
|
|
657
|
+
value={query}
|
|
658
|
+
onChange={setQuery}
|
|
659
|
+
label={apps ? t.apps.searchStore : t.library.searchThemes}
|
|
660
|
+
sort={sortChoice(sort, pickSort)}
|
|
661
|
+
filter={<Filters title={t.library.filter} selected={chips.length > 0} groups={filters} />}
|
|
662
|
+
className="mb-6"
|
|
663
|
+
/>
|
|
664
|
+
{chips.length > 0 && (
|
|
665
|
+
<div className="-mt-2 mb-6 flex flex-wrap gap-2">
|
|
666
|
+
{chips.map((c) => (
|
|
667
|
+
<InputChip key={c.key} removeLabel={t.library.removeFilter(c.label)} onRemove={c.clear}>
|
|
668
|
+
{c.label}
|
|
669
|
+
</InputChip>
|
|
670
|
+
))}
|
|
671
|
+
</div>
|
|
672
|
+
)}
|
|
673
|
+
{/* The site's own are shown also when the store cannot be read. */}
|
|
674
|
+
{!hasStore ? (
|
|
675
|
+
<EmptyNote>{t.apps.noStore}</EmptyNote>
|
|
676
|
+
) : read === null ? (
|
|
677
|
+
<EmptyNote busy>{t.apps.loadingStore}</EmptyNote>
|
|
678
|
+
) : read === 'failed' ? (
|
|
679
|
+
<EmptyNote>{t.apps.storeFailed}</EmptyNote>
|
|
680
|
+
) : null}
|
|
681
|
+
{read === null ? null : empty ? (
|
|
682
|
+
<p className="mb-8 px-1 text-muted">{t.common.noMatch}</p>
|
|
683
|
+
) : apps ? (
|
|
684
|
+
appGroups.map((g) => (
|
|
685
|
+
<Group key={g.key ?? 'all'} title={g.key ?? undefined}>
|
|
686
|
+
{g.items.map((a) => (
|
|
687
|
+
<AppRow key={a.id} app={a} installed={installed.includes(a.id)} />
|
|
688
|
+
))}
|
|
689
|
+
</Group>
|
|
690
|
+
))
|
|
691
|
+
) : (
|
|
692
|
+
themeGroups.map((g) => (
|
|
693
|
+
<Group key={g.key ?? 'all'} title={g.key ?? undefined}>
|
|
694
|
+
{g.items.map((x) => (
|
|
695
|
+
<ThemeRow key={x.id} of={x} />
|
|
696
|
+
))}
|
|
697
|
+
</Group>
|
|
698
|
+
))
|
|
699
|
+
)}
|
|
700
|
+
</Screen>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const recordOf = (app: ShownApp): StoreRecord => ({ version: app.version, ref: app.ref, files: app.files });
|
|
705
|
+
|
|
706
|
+
/** The cookies an app sets (its cookies.json among its files from the store), in the site's language; none when it
|
|
707
|
+
* has no such file. */
|
|
708
|
+
function cookiesIn(files: { path: string; content: string | Uint8Array }[]): AppCookie[] {
|
|
709
|
+
const file = files.find((f) => f.path === 'cookies.json');
|
|
710
|
+
try {
|
|
711
|
+
const list = file && JSON.parse(typeof file.content === 'string' ? file.content : new TextDecoder().decode(file.content));
|
|
712
|
+
return Array.isArray(list) ? list : [];
|
|
713
|
+
} catch {
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Saves the changes to an app's folder (appChanges) and to the cookie list (cookieChanges) in one commit, and gives
|
|
719
|
+
* the editor the site's files it changed: the file of apps and the cookie list. `change`: what it changed (deploy.ts). */
|
|
720
|
+
async function saveApp(env: Env, message: string, change: string, changes: Change[], apps: RepoFile | undefined) {
|
|
721
|
+
const oids = await env.commit(message, changes, change);
|
|
722
|
+
const site = changes.filter((c) => c.path === config.apps.installed || inCookieList(c.path));
|
|
723
|
+
const saved = site.flatMap((c) => (typeof c.content === 'string' ? [{ path: c.path, oid: oids[c.path], text: c.content }] : []));
|
|
724
|
+
const removed = site.filter((c) => c.content === null).map((c) => c.path);
|
|
725
|
+
// Only taken out of the list (an update): the file of apps, as it is, with them.
|
|
726
|
+
const [first, ...more] = saved.length ? saved : apps ? [apps] : [];
|
|
727
|
+
if (first) env.onSaved(first, { files: more, removed });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Whether the apps were updated on their own while the editor is open (autoUpdate): once.
|
|
731
|
+
let autoUpdated = false;
|
|
732
|
+
|
|
733
|
+
/** Updates the apps installed from the store on their own, once while the editor is open (App.tsx): each with a newer
|
|
734
|
+
* version in the store, in its own commit, with a message linking to its page, where what is new stands. One whose
|
|
735
|
+
* files were changed in the site is left for "Opdater til …" on its page, which says they are written over. A version
|
|
736
|
+
* with a higher first number (breaking) is not updated to: a message says it is there, once for each such version. */
|
|
737
|
+
export async function autoUpdate(env: Env, files: RepoFile[]) {
|
|
738
|
+
if (autoUpdated) return;
|
|
739
|
+
autoUpdated = true;
|
|
740
|
+
const installed = installedApps(files);
|
|
741
|
+
const file = files.find((f) => f.path === config.apps.installed);
|
|
742
|
+
const catalog = await env.store.catalog().catch(() => [] as ShownApp[]);
|
|
743
|
+
for (const app of apps.filter((a) => a.store && installed.includes(a.id))) {
|
|
744
|
+
const next = catalog.find((a) => a.id === app.id);
|
|
745
|
+
if (!next || !newer(next.version, versionOf(app))) continue;
|
|
746
|
+
try {
|
|
747
|
+
// As the site has it now (another tab, or an update the site's build does not have yet, may have come first).
|
|
748
|
+
const current = await env.folder(appFolder(app.id));
|
|
749
|
+
const record = recordIn(current, appFolder(app.id)) ?? app.store!;
|
|
750
|
+
if (!newer(next.version, record.version)) continue;
|
|
751
|
+
if (breaking(next.version, record.version)) {
|
|
752
|
+
if (!told('updates', `${app.id}@${next.version}`))
|
|
753
|
+
env.notify({
|
|
754
|
+
text: t.apps.breakingFound(next.label, next.version),
|
|
755
|
+
edit: { text: t.apps.whatsNew, to: `apps/${app.id}` },
|
|
756
|
+
kind: 'updates',
|
|
757
|
+
});
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (changedFiles(app.id, record, current).length) continue;
|
|
761
|
+
const wanted = { files: await env.store.files(next), record: recordOf(next) };
|
|
762
|
+
const changes = [...appChanges(app.id, current, wanted, installed, file), ...cookieChanges(app.id, cookiesIn(wanted.files), files)];
|
|
763
|
+
const change = t.app.changes.updated(kindAndName(t.app.changes.app, next.label), next.version);
|
|
764
|
+
await saveApp(env, siteT.apps.commit.update(next.written, next.version), change, changes, file);
|
|
765
|
+
updated.set(app.id, next.version);
|
|
766
|
+
env.notify({
|
|
767
|
+
text: t.apps.autoUpdated(next.label, next.version),
|
|
768
|
+
edit: { text: t.apps.whatsNew, to: `apps/${app.id}` },
|
|
769
|
+
kind: 'updates',
|
|
770
|
+
});
|
|
771
|
+
} catch (e) {
|
|
772
|
+
// Tried again the next time the editor opens.
|
|
773
|
+
console.warn(`${app.id}:`, e);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Installing, updating and uninstalling an app from the store, each saved at once in its own commit: its folder
|
|
779
|
+
* copied from the store, or deleted, with the site's file of apps (appChanges in store.ts). Each says true when it
|
|
780
|
+
* was saved. */
|
|
781
|
+
function useStoreActions(env: Env, files: RepoFile[]) {
|
|
782
|
+
const [saving, setSaving] = useState(false);
|
|
783
|
+
const installed = installedApps(files);
|
|
784
|
+
const file = files.find((f) => f.path === config.apps.installed);
|
|
785
|
+
|
|
786
|
+
async function change(
|
|
787
|
+
id: string,
|
|
788
|
+
// `kind`: the notification it is when it is saved (an update is "Apps opdateret", as one on its own).
|
|
789
|
+
text: { busy: string; done: string; commit: string; change: string; kind?: 'saved' | 'updates' },
|
|
790
|
+
next: () => Promise<Parameters<typeof appChanges>[2]>,
|
|
791
|
+
// Whether to go on, with the app's files in the site now (an update asks, when some were changed).
|
|
792
|
+
go: (current: RepoFile[]) => Promise<boolean> = async () => true,
|
|
793
|
+
): Promise<boolean> {
|
|
794
|
+
setSaving(true);
|
|
795
|
+
env.notify({ text: text.busy });
|
|
796
|
+
try {
|
|
797
|
+
const [current, wanted] = await Promise.all([env.folder(appFolder(id)), next()]);
|
|
798
|
+
if (!(await go(current))) {
|
|
799
|
+
env.notify(null);
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
const cookies = cookieChanges(id, wanted ? cookiesIn(wanted.files) : null, files);
|
|
803
|
+
await saveApp(env, text.commit, text.change, [...appChanges(id, current, wanted, installed, file), ...cookies], file);
|
|
804
|
+
env.notify({ text: text.done, kind: text.kind ?? 'saved' });
|
|
805
|
+
return true;
|
|
806
|
+
} catch (e) {
|
|
807
|
+
env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
808
|
+
return false;
|
|
809
|
+
} finally {
|
|
810
|
+
setSaving(false);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
saving,
|
|
816
|
+
install: (app: ShownApp) =>
|
|
817
|
+
change(
|
|
818
|
+
app.id,
|
|
819
|
+
{
|
|
820
|
+
busy: t.apps.installing,
|
|
821
|
+
done: t.apps.isInstalledSoon(app.label),
|
|
822
|
+
commit: siteT.apps.commit.install(app.written),
|
|
823
|
+
change: t.app.changes.installed(kindAndName(t.app.changes.app, app.label)),
|
|
824
|
+
},
|
|
825
|
+
async () => ({ files: await env.store.files(app), record: recordOf(app) }),
|
|
826
|
+
),
|
|
827
|
+
async update(app: ShownApp, record: StoreRecord) {
|
|
828
|
+
const done = await change(
|
|
829
|
+
app.id,
|
|
830
|
+
{
|
|
831
|
+
busy: t.apps.updating,
|
|
832
|
+
done: t.apps.isUpdatedSoon(app.label, app.version),
|
|
833
|
+
commit: siteT.apps.commit.update(app.written, app.version),
|
|
834
|
+
change: t.app.changes.updated(kindAndName(t.app.changes.app, app.label), app.version),
|
|
835
|
+
kind: 'updates',
|
|
836
|
+
},
|
|
837
|
+
async () => ({ files: await env.store.files(app), record: recordOf(app) }),
|
|
838
|
+
async (current) => {
|
|
839
|
+
env.notify(null);
|
|
840
|
+
return askUpdate(
|
|
841
|
+
app.label,
|
|
842
|
+
record.version,
|
|
843
|
+
app.version,
|
|
844
|
+
notesSince(app.releaseNotes, record.version, app.version),
|
|
845
|
+
changedFiles(app.id, record, current),
|
|
846
|
+
);
|
|
847
|
+
},
|
|
848
|
+
);
|
|
849
|
+
if (done) updated.set(app.id, app.version);
|
|
850
|
+
return done;
|
|
851
|
+
},
|
|
852
|
+
uninstall: (id: string, label: string, written: string) =>
|
|
853
|
+
change(
|
|
854
|
+
id,
|
|
855
|
+
{
|
|
856
|
+
busy: t.apps.uninstalling,
|
|
857
|
+
done: t.apps.isUninstalled(label),
|
|
858
|
+
commit: siteT.apps.commit.uninstall(written),
|
|
859
|
+
change: t.app.changes.uninstalled(kindAndName(t.app.changes.app, label)),
|
|
860
|
+
},
|
|
861
|
+
async () => null,
|
|
862
|
+
),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Before an app or a theme is updated from `from` to `version`: what is new in it since (`notes`, its release notes),
|
|
867
|
+
* that it can break something (a higher first number, breaking), and, when files in its folder were changed in the site
|
|
868
|
+
* (`changed`), that they are written over. Without any of them, it is updated at once. */
|
|
869
|
+
export async function askUpdate(
|
|
870
|
+
name: string,
|
|
871
|
+
from: string,
|
|
872
|
+
version: string,
|
|
873
|
+
notes: { version: string; text: string }[],
|
|
874
|
+
changed: string[],
|
|
875
|
+
) {
|
|
876
|
+
const big = breaking(version, from);
|
|
877
|
+
if (!notes.length && !changed.length && !big) return true;
|
|
878
|
+
const answer = await choose({
|
|
879
|
+
title: t.apps.askUpdate(name, version),
|
|
880
|
+
notes: notes.map((n) => ({ title: t.apps.version(n.version), text: n.text })),
|
|
881
|
+
text: [big && t.apps.breaking, changed.length && t.apps.overwrites].filter(Boolean).join(' '),
|
|
882
|
+
choices: [{ value: 'yes', label: t.apps.updateNow }],
|
|
883
|
+
});
|
|
884
|
+
return !!answer;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** What is new in the last versions up to the one shown (an app's release notes), the newest first, on its page. */
|
|
888
|
+
const newsOf = (notes: Record<string, string> | undefined, version: string) => notesSince(notes, '0', version).slice(0, 3);
|
|
889
|
+
|
|
890
|
+
/** The top of an app's page: its icon, its name, who made it, whether it is installed, its version (from the store),
|
|
891
|
+
* and what it does. */
|
|
892
|
+
function AppHead({
|
|
893
|
+
icon,
|
|
894
|
+
label,
|
|
895
|
+
publisher,
|
|
896
|
+
status,
|
|
897
|
+
version,
|
|
898
|
+
hint,
|
|
899
|
+
}: {
|
|
900
|
+
icon: ReactNode;
|
|
901
|
+
label: string;
|
|
902
|
+
publisher: string;
|
|
903
|
+
status: string;
|
|
904
|
+
version?: string;
|
|
905
|
+
hint: string;
|
|
906
|
+
}) {
|
|
907
|
+
return (
|
|
908
|
+
<div className="mb-8 px-4">
|
|
909
|
+
<div className="flex items-center gap-3">
|
|
910
|
+
{icon}
|
|
911
|
+
<span className="min-w-0 flex-1">
|
|
912
|
+
<span className="block truncate font-medium">{label}</span>
|
|
913
|
+
<span className="block truncate text-sm text-muted">{t.apps.publisher(publisher)}</span>
|
|
914
|
+
<span className="block truncate text-sm text-muted">{status}</span>
|
|
915
|
+
{version && <span className="block truncate text-sm text-muted">{t.apps.version(version)}</span>}
|
|
916
|
+
</span>
|
|
917
|
+
</div>
|
|
918
|
+
<p className="mt-3 text-muted">{hint}</p>
|
|
919
|
+
</div>
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** An app's pictures from the store (readScreenshots), once its entry in the catalog is read; none until then. */
|
|
924
|
+
function useStorePictures(env: Env, app: ShownApp | undefined) {
|
|
925
|
+
const [pictures, setPictures] = useState<{ src: string; label: string }[]>([]);
|
|
926
|
+
useEffect(() => {
|
|
927
|
+
if (!app?.screenshots?.length) return;
|
|
928
|
+
let on = true;
|
|
929
|
+
let made: string[] = [];
|
|
930
|
+
env.store.screenshots(app).then((found) => {
|
|
931
|
+
made = found.map((x) => x.src);
|
|
932
|
+
if (on) setPictures(found);
|
|
933
|
+
else made.forEach((src) => URL.revokeObjectURL(src));
|
|
934
|
+
});
|
|
935
|
+
return () => {
|
|
936
|
+
on = false;
|
|
937
|
+
made.forEach((src) => URL.revokeObjectURL(src));
|
|
938
|
+
};
|
|
939
|
+
}, [app?.id, app?.version]);
|
|
940
|
+
return pictures;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** The page of an app in the store that is not installed, or installed but not yet built (#apps/<app>), from the
|
|
944
|
+
* catalog: as an app's page in the site, what it adds without switches, and "Installer" (or "Afinstaller"). */
|
|
945
|
+
export function StoreAppPage({ id, env, files }: { id: string; env: Env; files: RepoFile[] }) {
|
|
946
|
+
const catalog = useCatalog(env);
|
|
947
|
+
const { saving, install, uninstall } = useStoreActions(env, files);
|
|
948
|
+
const pending = installedApps(files).includes(id);
|
|
949
|
+
const app = Array.isArray(catalog) ? catalog.find((a) => a.id === id) : undefined;
|
|
950
|
+
const pictures = useStorePictures(env, app);
|
|
951
|
+
|
|
952
|
+
const row = (a: StoreAdd, key: string) => <ListItem key={key} leading={storeIcon(a.icon)} headline={a.label} supporting={a.hint} />;
|
|
953
|
+
// Its kinds under the name of the list they are added to ("Promovering").
|
|
954
|
+
const kinds = app?.adds.kinds ?? [];
|
|
955
|
+
const listName = (list: string) => collections.find((c) => c.id === list)?.label ?? list;
|
|
956
|
+
|
|
957
|
+
return (
|
|
958
|
+
<Screen title={app?.label ?? (pending ? id : t.apps.find)} wide back={pending ? backToApps : backToStore}>
|
|
959
|
+
{app && (
|
|
960
|
+
<>
|
|
961
|
+
<AppHead
|
|
962
|
+
icon={storeIcon(app.icon)}
|
|
963
|
+
label={app.label}
|
|
964
|
+
publisher={app.publisher}
|
|
965
|
+
status={pending ? t.apps.pending : t.apps.notInstalled}
|
|
966
|
+
version={app.version}
|
|
967
|
+
hint={app.hint}
|
|
968
|
+
/>
|
|
969
|
+
{!pending && (
|
|
970
|
+
<div className="mb-8">
|
|
971
|
+
<Button wide disabled={saving} onClick={() => install(app)}>
|
|
972
|
+
{t.apps.install}
|
|
973
|
+
</Button>
|
|
974
|
+
</div>
|
|
975
|
+
)}
|
|
976
|
+
<AppAbout
|
|
977
|
+
pictures={pictures}
|
|
978
|
+
description={app.description}
|
|
979
|
+
news={newsOf(app.releaseNotes, app.version)}
|
|
980
|
+
needs={app.needs}
|
|
981
|
+
notifies={app.notifies?.map((label) => ({ label }))}
|
|
982
|
+
cookies={app.cookies ?? []}
|
|
983
|
+
consent={!!app.consent}
|
|
984
|
+
publisher={app.publisher}
|
|
985
|
+
licenses={<StoreLicenseRows licenses={app.licenses} name={app.label} author={app.publisher} version={app.version} />}
|
|
986
|
+
>
|
|
987
|
+
{app.adds.blocks.length > 0 && <Group title={t.apps.blocks}>{app.adds.blocks.map((a, i) => row(a, `${i}`))}</Group>}
|
|
988
|
+
{app.adds.lists.length > 0 && <Group title={t.apps.lists}>{app.adds.lists.map((a, i) => row(a, `${i}`))}</Group>}
|
|
989
|
+
{[...new Set(kinds.map((k) => k.list))].map((list) => (
|
|
990
|
+
<Group key={list} title={listName(list)}>
|
|
991
|
+
{kinds.filter((k) => k.list === list).map((k, i) => row(k, `${i}`))}
|
|
992
|
+
</Group>
|
|
993
|
+
))}
|
|
994
|
+
</AppAbout>
|
|
995
|
+
</>
|
|
996
|
+
)}
|
|
997
|
+
{!app &&
|
|
998
|
+
(catalog === null ? (
|
|
999
|
+
<EmptyNote busy>{t.apps.loadingStore}</EmptyNote>
|
|
1000
|
+
) : (
|
|
1001
|
+
<EmptyNote>{catalog === 'failed' ? t.apps.storeFailed : t.apps.notInStore}</EmptyNote>
|
|
1002
|
+
))}
|
|
1003
|
+
{/* Installed but not yet built, it can be uninstalled again, also without the catalog. */}
|
|
1004
|
+
{pending ? (
|
|
1005
|
+
<div>
|
|
1006
|
+
<Button
|
|
1007
|
+
kind="outlined"
|
|
1008
|
+
|
|
1009
|
+
wide
|
|
1010
|
+
disabled={saving}
|
|
1011
|
+
onClick={async () => {
|
|
1012
|
+
const label = app?.label ?? id;
|
|
1013
|
+
if (await ask(t.apps.askUninstall(label), t.apps.uninstall)) uninstall(id, label, app?.written ?? id);
|
|
1014
|
+
}}
|
|
1015
|
+
>
|
|
1016
|
+
{t.apps.uninstall}
|
|
1017
|
+
</Button>
|
|
1018
|
+
<SupportingText className="mt-2">{t.apps.pendingHint}</SupportingText>
|
|
1019
|
+
</div>
|
|
1020
|
+
) : null}
|
|
1021
|
+
</Screen>
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** What an app adds but its lists, each on its page under "Blokke" or the name of its list ("Promovering") with a sheet: its blocks, and its kinds of the site's
|
|
1026
|
+
* lists (a Mailchimp popup), each with where it is used and its switch; a kind with a button to a new one. */
|
|
1027
|
+
function addsOf(app: SiteApp, files: RepoFile[]) {
|
|
1028
|
+
return [
|
|
1029
|
+
...siteBlocks
|
|
1030
|
+
.filter((b) => b.app === app.id)
|
|
1031
|
+
.map((b) => ({
|
|
1032
|
+
key: b.type,
|
|
1033
|
+
icon: b.icon,
|
|
1034
|
+
label: b.label,
|
|
1035
|
+
hint: b.hint,
|
|
1036
|
+
block: b,
|
|
1037
|
+
create: undefined,
|
|
1038
|
+
uses: pagesWith(b.type, files).map((p) => ({ href: `#${p.path}`, title: p.title })),
|
|
1039
|
+
})),
|
|
1040
|
+
...kindsOf(app).map((k) => ({
|
|
1041
|
+
...k,
|
|
1042
|
+
block: undefined,
|
|
1043
|
+
create: k.create ? { href: `#ny/${k.key}/${app.id}`, label: k.create } : undefined,
|
|
1044
|
+
uses: entriesOf(app, k.key, files),
|
|
1045
|
+
})),
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
type Add = ReturnType<typeof addsOf>[number];
|
|
1050
|
+
|
|
1051
|
+
/** The row of a block (or a kind of a list) an installed app adds: tapped, it opens its sheet; at its end the switch
|
|
1052
|
+
* that turns it on or off, as the one in the sheet ("Aktiveret"), saved at once. Used, it cannot be turned off, and
|
|
1053
|
+
* the switch says why. The only switch in a row (CLAUDE.md). */
|
|
1054
|
+
function SwitchedRow({
|
|
1055
|
+
app,
|
|
1056
|
+
add,
|
|
1057
|
+
env,
|
|
1058
|
+
files,
|
|
1059
|
+
busy,
|
|
1060
|
+
onBusy,
|
|
1061
|
+
onOpen,
|
|
1062
|
+
}: {
|
|
1063
|
+
app: SiteApp;
|
|
1064
|
+
add: Add;
|
|
1065
|
+
env: Env;
|
|
1066
|
+
files: RepoFile[];
|
|
1067
|
+
busy: boolean;
|
|
1068
|
+
onBusy: (busy: boolean) => void;
|
|
1069
|
+
onOpen: () => void;
|
|
1070
|
+
}) {
|
|
1071
|
+
const count = add.uses.length;
|
|
1072
|
+
const id = add.block ? add.block.type : kindKey(add.key, app.id);
|
|
1073
|
+
const texts = add.block ? blockSwitchTexts(add.block, count) : kindSwitchTexts(app, add.key, count);
|
|
1074
|
+
const { on, saving, turn } = useOnOff(id, count, texts, env, files);
|
|
1075
|
+
return (
|
|
1076
|
+
<ListItem
|
|
1077
|
+
aria-haspopup="dialog"
|
|
1078
|
+
onClick={onOpen}
|
|
1079
|
+
icon={add.icon}
|
|
1080
|
+
headline={add.label}
|
|
1081
|
+
supporting={add.hint}
|
|
1082
|
+
actions={
|
|
1083
|
+
<span className="shrink-0 pr-4">
|
|
1084
|
+
<Switch
|
|
1085
|
+
label={t.apps.activeOf(add.label)}
|
|
1086
|
+
selected={on}
|
|
1087
|
+
texts={[t.blockSettings.on, t.blockSettings.off]}
|
|
1088
|
+
disabled={busy || saving}
|
|
1089
|
+
onChange={async (next) => {
|
|
1090
|
+
onBusy(true);
|
|
1091
|
+
try {
|
|
1092
|
+
await turn(next);
|
|
1093
|
+
} finally {
|
|
1094
|
+
onBusy(false);
|
|
1095
|
+
}
|
|
1096
|
+
}}
|
|
1097
|
+
/>
|
|
1098
|
+
</span>
|
|
1099
|
+
}
|
|
1100
|
+
/>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const backToApps = { href: '#apps', label: t.common.parts.apps };
|
|
1105
|
+
|
|
1106
|
+
type AppProps = { app: SiteApp; env: Env; files: RepoFile[] };
|
|
1107
|
+
|
|
1108
|
+
/** An app's page, the same for every app: it installs the app; its settings are on a screen of their own (settingsRoute). */
|
|
1109
|
+
export function AppPage({ app, env, files }: AppProps) {
|
|
1110
|
+
// One that is not installed is found under "Appstore".
|
|
1111
|
+
return (
|
|
1112
|
+
<Screen title={app.label} wide back={installedApps(files).includes(app.id) ? backToApps : backToStore}>
|
|
1113
|
+
<AppContent app={app} env={env} files={files} />
|
|
1114
|
+
</Screen>
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** A screen of an app's own under its page (#apps/<id>/<key>, AppDefinition.screens), while it is installed: its title,
|
|
1119
|
+
* back to the app's page, and what the app shows on it. */
|
|
1120
|
+
export function AppScreenPage({ app, id, env }: { app: SiteApp; id: string; env: Env }) {
|
|
1121
|
+
const screen = app.screens![id];
|
|
1122
|
+
return (
|
|
1123
|
+
<Screen title={siteText(screen.title)} wide back={{ href: `#apps/${app.id}`, label: app.label }}>
|
|
1124
|
+
<div data-app-page={app.id}>
|
|
1125
|
+
<screen.page api={(path, init) => env.appApi(app.id, path, init)} />
|
|
1126
|
+
</div>
|
|
1127
|
+
</Screen>
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/** What every app's page has, in this order: its name, who made it, whether it is installed, its version (from the
|
|
1132
|
+
* store) and what it does; "Installer" (or, from the store, "Opdater" when it has a newer version); its pictures and
|
|
1133
|
+
* what it does more fully; a row to its settings (installed), with where they are; what it adds (its blocks, its kinds
|
|
1134
|
+
* of lists); what it needs, its cookies and who made it (AppAbout); where it is used; and "Afinstaller". */
|
|
1135
|
+
function AppContent({ app, env, files }: AppProps) {
|
|
1136
|
+
const notify = env.notify;
|
|
1137
|
+
const dialog = useRef<HTMLDialogElement>(null);
|
|
1138
|
+
const [toggling, setToggling] = useState(false);
|
|
1139
|
+
const store = useStoreActions(env, files);
|
|
1140
|
+
const saving = toggling || store.saving;
|
|
1141
|
+
const catalog = useCatalog(env);
|
|
1142
|
+
const update = updateOf(app, catalog);
|
|
1143
|
+
// Its release notes as the store has them: also those of a version the site's build does not have yet.
|
|
1144
|
+
const inStore = Array.isArray(catalog) ? catalog.find((a) => a.id === app.id) : undefined;
|
|
1145
|
+
const [openKey, setOpenKey] = useState<string | null>(null);
|
|
1146
|
+
// While one row's switch saves, the others wait.
|
|
1147
|
+
const [switching, setSwitching] = useState(false);
|
|
1148
|
+
const installed = installedApps(files);
|
|
1149
|
+
const on = installed.includes(app.id);
|
|
1150
|
+
// Where it is used: while it is, it cannot be uninstalled.
|
|
1151
|
+
const uses = on ? usesOf(app, files) : [];
|
|
1152
|
+
const stuck = uses.length > 0;
|
|
1153
|
+
const adds = addsOf(app, files);
|
|
1154
|
+
const lists = listsOf(app, files);
|
|
1155
|
+
const open = adds.find((a) => a.key === openKey);
|
|
1156
|
+
const settings = on && settingsRoute(app);
|
|
1157
|
+
|
|
1158
|
+
// Installed or uninstalled, saved at once in its own commit. One in use cannot be uninstalled. One from the store
|
|
1159
|
+
// is deleted from the site (it can be installed again from the store).
|
|
1160
|
+
async function install(yes: boolean) {
|
|
1161
|
+
if (!yes && stuck) return notify({ text: t.apps.stuck, error: true });
|
|
1162
|
+
if (!yes && !(await ask(t.apps.askUninstall(app.label), t.apps.uninstall))) return;
|
|
1163
|
+
if (!yes && app.store) return store.uninstall(app.id, app.label, asWritten(app).label);
|
|
1164
|
+
const file = files.find((f) => f.path === config.apps.installed);
|
|
1165
|
+
const next = installed.filter((id) => id !== app.id).concat(yes ? [app.id] : []);
|
|
1166
|
+
const text = `${JSON.stringify({ installed: [...new Set(next)].sort() }, null, 2)}\n`;
|
|
1167
|
+
if (text === file?.text) return;
|
|
1168
|
+
setToggling(true);
|
|
1169
|
+
notify({ text: yes ? t.apps.installing : t.apps.uninstalling });
|
|
1170
|
+
try {
|
|
1171
|
+
// With its cookies added to the cookie list, or those it added taken out.
|
|
1172
|
+
const changes = [
|
|
1173
|
+
{ path: config.apps.installed, content: text, oid: file?.oid || null },
|
|
1174
|
+
...cookieChanges(app.id, yes ? (asWritten(app).cookieList ?? []) : null, files),
|
|
1175
|
+
];
|
|
1176
|
+
const change = t.app.changes[yes ? 'installed' : 'uninstalled'](kindAndName(t.app.changes.app, app.label));
|
|
1177
|
+
await saveApp(env, siteT.apps.commit[yes ? 'install' : 'uninstall'](asWritten(app).label), change, changes, file);
|
|
1178
|
+
notify({ text: yes ? t.apps.isInstalled(app.label) : t.apps.isUninstalled(app.label), kind: 'saved' });
|
|
1179
|
+
} catch (e) {
|
|
1180
|
+
notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
1181
|
+
} finally {
|
|
1182
|
+
setToggling(false);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// What it adds under a heading. Installed, each row opens its sheet and has its switch at its end.
|
|
1187
|
+
const addsGroup = (title: string, items: typeof adds) =>
|
|
1188
|
+
items.length > 0 && (
|
|
1189
|
+
<Group key={title} title={title}>
|
|
1190
|
+
{items.map((a) =>
|
|
1191
|
+
on ? (
|
|
1192
|
+
<SwitchedRow
|
|
1193
|
+
key={a.key}
|
|
1194
|
+
app={app}
|
|
1195
|
+
add={a}
|
|
1196
|
+
env={env}
|
|
1197
|
+
files={files}
|
|
1198
|
+
busy={switching}
|
|
1199
|
+
onBusy={setSwitching}
|
|
1200
|
+
onOpen={() => {
|
|
1201
|
+
setOpenKey(a.key);
|
|
1202
|
+
dialog.current?.showModal();
|
|
1203
|
+
}}
|
|
1204
|
+
/>
|
|
1205
|
+
) : (
|
|
1206
|
+
<ListItem key={a.key} icon={a.icon} headline={a.label} supporting={a.hint} />
|
|
1207
|
+
),
|
|
1208
|
+
)}
|
|
1209
|
+
</Group>
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
return (
|
|
1213
|
+
<div>
|
|
1214
|
+
<AppHead
|
|
1215
|
+
icon={<LeadingIcon icon={app.icon} selected={on} />}
|
|
1216
|
+
label={app.label}
|
|
1217
|
+
publisher={app.publisher}
|
|
1218
|
+
status={on && update ? t.apps.updateFound : statusOf(on)}
|
|
1219
|
+
version={app.store && versionOf(app)}
|
|
1220
|
+
hint={app.hint}
|
|
1221
|
+
/>
|
|
1222
|
+
{(!on || (update && app.store)) && (
|
|
1223
|
+
<div className="mb-8">
|
|
1224
|
+
{on ? (
|
|
1225
|
+
<Button wide disabled={saving} onClick={() => store.update(update!, app.store!)}>
|
|
1226
|
+
{t.apps.update(update!.version)}
|
|
1227
|
+
</Button>
|
|
1228
|
+
) : (
|
|
1229
|
+
<Button wide disabled={saving} onClick={() => install(true)}>
|
|
1230
|
+
{t.apps.install}
|
|
1231
|
+
</Button>
|
|
1232
|
+
)}
|
|
1233
|
+
</div>
|
|
1234
|
+
)}
|
|
1235
|
+
<AppAbout
|
|
1236
|
+
pictures={app.pictures}
|
|
1237
|
+
description={app.description}
|
|
1238
|
+
news={app.store ? newsOf(inStore?.releaseNotes ?? app.releaseNotes, update?.version ?? versionOf(app)) : []}
|
|
1239
|
+
needs={app.needs}
|
|
1240
|
+
notifies={Object.entries(app.notifications?.types ?? {}).map(([id, label]) => ({
|
|
1241
|
+
label: siteText(label),
|
|
1242
|
+
...(on ? { type: appType(app.id, id) } : {}),
|
|
1243
|
+
}))}
|
|
1244
|
+
cookies={app.cookieList ?? []}
|
|
1245
|
+
consent={!!app.cookies}
|
|
1246
|
+
publisher={app.publisher}
|
|
1247
|
+
licenses={
|
|
1248
|
+
<OwnerLicenses
|
|
1249
|
+
id={`app/${app.id}`}
|
|
1250
|
+
name={app.label}
|
|
1251
|
+
author={app.publisher}
|
|
1252
|
+
version={app.store && versionOf(app)}
|
|
1253
|
+
declared={app}
|
|
1254
|
+
/>
|
|
1255
|
+
}
|
|
1256
|
+
>
|
|
1257
|
+
{settings && (
|
|
1258
|
+
<Group>
|
|
1259
|
+
<ListItem href={`#${settings}`} icon={Gear} headline={t.apps.settings} count={settingsHome(app)?.label} arrow />
|
|
1260
|
+
</Group>
|
|
1261
|
+
)}
|
|
1262
|
+
{/* What the app shows itself (its `page`), while it is installed. */}
|
|
1263
|
+
{on && app.page && (
|
|
1264
|
+
<div data-app-page={app.id}>
|
|
1265
|
+
<app.page api={(path, init) => env.appApi(app.id, path, init)} />
|
|
1266
|
+
</div>
|
|
1267
|
+
)}
|
|
1268
|
+
{/* What it adds: its blocks under "Blokke", the lists it brings under "Lister" (as on "Lister", each opening its
|
|
1269
|
+
list), and its kinds under the name of the list they are added to ("Promovering"). */}
|
|
1270
|
+
{addsGroup(
|
|
1271
|
+
t.apps.blocks,
|
|
1272
|
+
adds.filter((a) => a.block),
|
|
1273
|
+
)}
|
|
1274
|
+
{lists.length > 0 && (
|
|
1275
|
+
<Group title={t.apps.lists}>
|
|
1276
|
+
{lists.map((l) => (
|
|
1277
|
+
// Installed, a row opens its list.
|
|
1278
|
+
<ListItem key={l.key} href={on ? l.href : undefined} icon={l.icon} headline={l.label} count={l.count} arrow={on} />
|
|
1279
|
+
))}
|
|
1280
|
+
</Group>
|
|
1281
|
+
)}
|
|
1282
|
+
{[...new Set(adds.flatMap((a) => (a.block ? [] : [a.more])))].map((title) =>
|
|
1283
|
+
addsGroup(
|
|
1284
|
+
title,
|
|
1285
|
+
adds.filter((a) => !a.block && a.more === title),
|
|
1286
|
+
),
|
|
1287
|
+
)}
|
|
1288
|
+
</AppAbout>
|
|
1289
|
+
{stuck && (
|
|
1290
|
+
<Group title={t.apps.used}>
|
|
1291
|
+
{uses.map((u) => (
|
|
1292
|
+
<ListItem key={u.href} href={u.href} icon={u.icon} headline={u.title} arrow />
|
|
1293
|
+
))}
|
|
1294
|
+
</Group>
|
|
1295
|
+
)}
|
|
1296
|
+
{/* As wide as the fields and the rows above. */}
|
|
1297
|
+
{on && (
|
|
1298
|
+
<div>
|
|
1299
|
+
<Button kind="outlined" wide disabled={saving || stuck} onClick={() => install(false)}>
|
|
1300
|
+
{t.apps.uninstall}
|
|
1301
|
+
</Button>
|
|
1302
|
+
<SupportingText className="mt-2">{stuck ? t.apps.stuck : t.apps.uninstallHint}</SupportingText>
|
|
1303
|
+
</div>
|
|
1304
|
+
)}
|
|
1305
|
+
<BottomSheet
|
|
1306
|
+
ref={dialog}
|
|
1307
|
+
aria-label={open?.label}
|
|
1308
|
+
onClose={(e) => e.target === e.currentTarget && setOpenKey(null)}
|
|
1309
|
+
onClick={(e) => e.target === dialog.current && dialog.current?.close()}
|
|
1310
|
+
>
|
|
1311
|
+
<SheetTitle title={open?.label ?? ''} onClose={() => dialog.current?.close()} />
|
|
1312
|
+
{open && (
|
|
1313
|
+
<SheetBody>
|
|
1314
|
+
<div className="flex items-start gap-3">
|
|
1315
|
+
<LeadingIcon icon={open.icon} />
|
|
1316
|
+
<p className="pt-1.5 text-muted">{open.hint ?? open.label}</p>
|
|
1317
|
+
</div>
|
|
1318
|
+
{open.block ? (
|
|
1319
|
+
<BlockSwitch block={open.block} env={env} files={files} />
|
|
1320
|
+
) : (
|
|
1321
|
+
<KindSwitch app={app} list={open.key} env={env} files={files} />
|
|
1322
|
+
)}
|
|
1323
|
+
{open.create && !offBlocks(files).includes(kindKey(open.key, app.id)) && (
|
|
1324
|
+
<LinkButton href={open.create.href} wide onClick={() => dialog.current?.close()}>
|
|
1325
|
+
{open.create.label}
|
|
1326
|
+
</LinkButton>
|
|
1327
|
+
)}
|
|
1328
|
+
<div>
|
|
1329
|
+
<p className="mb-1 font-medium">{t.apps.used}</p>
|
|
1330
|
+
{open.uses.length ? (
|
|
1331
|
+
<ul className={listClass}>
|
|
1332
|
+
{open.uses.map((u) => (
|
|
1333
|
+
<ListItem key={u.href} href={u.href} onClick={() => dialog.current?.close()} headline={u.title} />
|
|
1334
|
+
))}
|
|
1335
|
+
</ul>
|
|
1336
|
+
) : (
|
|
1337
|
+
<p className="text-muted">{t.apps.notUsed}</p>
|
|
1338
|
+
)}
|
|
1339
|
+
</div>
|
|
1340
|
+
</SheetBody>
|
|
1341
|
+
)}
|
|
1342
|
+
</BottomSheet>
|
|
1343
|
+
</div>
|
|
1344
|
+
);
|
|
1345
|
+
}
|