@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -21
- package/README.md +51 -51
- package/astro.mjs +162 -162
- package/bin/morpheme.mjs +38 -38
- package/client.d.ts +10 -10
- package/defaults/blockTexts.ts +33 -33
- package/defaults/collections.ts +774 -774
- package/defaults/colors.ts +125 -125
- package/defaults/company.ts +20 -20
- package/defaults/editor.ts +178 -178
- package/defaults/internalLinks.ts +30 -30
- package/defaults/opens.ts +18 -18
- package/defaults/popup.tsx +93 -93
- package/defaults/privacy.ts +79 -79
- package/defaults/rules.ts +450 -450
- package/defaults/schema.ts +16 -16
- package/defaults/sitedata.ts +129 -129
- package/defaults/standard.ts +106 -106
- package/defaults/standardPages.ts +87 -87
- package/defaults/texts.ts +481 -481
- package/demo/README.md +26 -26
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
- package/demo/src/content/blog/every-save-is-a-version.md +21 -21
- package/demo/src/content/blog/schedule-a-post.md +17 -17
- package/demo/src/content/blog/secure-from-day-one.md +17 -17
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
- package/demo/src/content/blog/the-app-store.md +21 -21
- package/demo/src/content/pages/about.md +22 -22
- package/demo/src/content/pages/app-store.md +30 -30
- package/demo/src/content/pages/blocks.md +98 -98
- package/demo/src/content/pages/blog-page.md +11 -11
- package/demo/src/content/pages/contact.md +16 -16
- package/demo/src/content/pages/get-started.md +20 -20
- package/demo/src/content/pages/home.md +26 -26
- package/demo/src/content/pages/page-not-found.md +11 -11
- package/demo/src/content/pages/privacy-policy.md +24 -24
- package/demo/src/data/blocks.json +25 -25
- package/demo/src/data/booking.json +6 -6
- package/demo/src/data/categories/apps.json +6 -6
- package/demo/src/data/categories/design.json +6 -6
- package/demo/src/data/categories/editing.json +6 -6
- package/demo/src/data/categories/security.json +6 -6
- package/demo/src/data/categories/seo.json +6 -6
- package/demo/src/data/colors.json +75 -75
- package/demo/src/data/contact.json +8 -8
- package/demo/src/data/cookies.json +21 -21
- package/demo/src/data/footer.json +27 -27
- package/demo/src/data/menu.json +35 -35
- package/demo/src/data/mobile-menu.json +25 -25
- package/demo/src/data/popups/booking-bar.json +13 -13
- package/demo/src/data/popups/try-the-editor.json +16 -16
- package/demo/src/data/standard-pages.json +8 -8
- package/demo/src/data/testimonials/sample-bakery.json +10 -10
- package/demo/src/data/testimonials/sample-clinic.json +10 -10
- package/demo/src/data/testimonials/sample-studio.json +10 -10
- package/demo/src/site/colors.ts +11 -11
- package/demo/src/site/config.ts +19 -19
- package/icons/lucide/LICENSE +43 -43
- package/icons/lucide/address-book.svg +1 -1
- package/icons/lucide/app-window.svg +1 -1
- package/icons/lucide/arrow-counter-clockwise.svg +1 -1
- package/icons/lucide/arrow-down.svg +1 -1
- package/icons/lucide/arrow-left.svg +1 -1
- package/icons/lucide/arrow-right.svg +1 -1
- package/icons/lucide/arrow-square-out.svg +1 -1
- package/icons/lucide/arrow-u-up-left.svg +1 -1
- package/icons/lucide/arrow-u-up-right.svg +1 -1
- package/icons/lucide/arrow-up-right.svg +1 -1
- package/icons/lucide/arrow-up.svg +1 -1
- package/icons/lucide/arrows-down-up.svg +1 -1
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
- package/icons/lucide/article.svg +1 -1
- package/icons/lucide/bank.svg +1 -1
- package/icons/lucide/bell.svg +1 -1
- package/icons/lucide/bicycle.svg +1 -1
- package/icons/lucide/binoculars.svg +1 -1
- package/icons/lucide/book-open-text.svg +1 -1
- package/icons/lucide/book-open.svg +1 -1
- package/icons/lucide/browser.svg +1 -1
- package/icons/lucide/buildings.svg +1 -1
- package/icons/lucide/bus.svg +1 -1
- package/icons/lucide/calendar-blank.svg +1 -1
- package/icons/lucide/calendar-check.svg +1 -1
- package/icons/lucide/car.svg +1 -1
- package/icons/lucide/cards.svg +1 -1
- package/icons/lucide/caret-down.svg +1 -1
- package/icons/lucide/caret-left.svg +1 -1
- package/icons/lucide/caret-right.svg +1 -1
- package/icons/lucide/caret-up.svg +1 -1
- package/icons/lucide/certificate.svg +1 -1
- package/icons/lucide/chart-line-up.svg +1 -1
- package/icons/lucide/chats.svg +1 -1
- package/icons/lucide/check-circle.svg +1 -1
- package/icons/lucide/check.svg +1 -1
- package/icons/lucide/circle-half.svg +1 -1
- package/icons/lucide/circle.svg +1 -1
- package/icons/lucide/clock-counter-clockwise.svg +1 -1
- package/icons/lucide/clock.svg +1 -1
- package/icons/lucide/code.svg +1 -1
- package/icons/lucide/coffee.svg +1 -1
- package/icons/lucide/columns.svg +1 -1
- package/icons/lucide/cookie.svg +1 -1
- package/icons/lucide/copy.svg +1 -1
- package/icons/lucide/credit-card.svg +1 -1
- package/icons/lucide/currency-circle-dollar.svg +1 -1
- package/icons/lucide/cursor-click.svg +1 -1
- package/icons/lucide/database.svg +1 -1
- package/icons/lucide/desktop.svg +1 -1
- package/icons/lucide/device-mobile.svg +1 -1
- package/icons/lucide/door-open.svg +1 -1
- package/icons/lucide/download-simple.svg +1 -1
- package/icons/lucide/envelope-simple.svg +1 -1
- package/icons/lucide/eye-slash.svg +1 -1
- package/icons/lucide/eye.svg +1 -1
- package/icons/lucide/file-pdf.svg +1 -1
- package/icons/lucide/file-text.svg +1 -1
- package/icons/lucide/file-video.svg +1 -1
- package/icons/lucide/file-zip.svg +1 -1
- package/icons/lucide/flower-lotus.svg +1 -1
- package/icons/lucide/folder-simple.svg +1 -1
- package/icons/lucide/funnel.svg +1 -1
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear.svg +1 -1
- package/icons/lucide/github-logo.svg +1 -1
- package/icons/lucide/globe.svg +1 -1
- package/icons/lucide/google-logo.svg +1 -1
- package/icons/lucide/graduation-cap.svg +1 -1
- package/icons/lucide/hammer.svg +1 -1
- package/icons/lucide/hand-heart.svg +1 -1
- package/icons/lucide/handshake.svg +1 -1
- package/icons/lucide/hard-drives.svg +1 -1
- package/icons/lucide/heart.svg +1 -1
- package/icons/lucide/house.svg +1 -1
- package/icons/lucide/image-square.svg +1 -1
- package/icons/lucide/image.svg +1 -1
- package/icons/lucide/info.svg +1 -1
- package/icons/lucide/keyboard.svg +1 -1
- package/icons/lucide/leaf.svg +1 -1
- package/icons/lucide/lifebuoy.svg +1 -1
- package/icons/lucide/lightbulb.svg +1 -1
- package/icons/lucide/link-break.svg +1 -1
- package/icons/lucide/link-simple.svg +1 -1
- package/icons/lucide/list-bullets.svg +1 -1
- package/icons/lucide/list-checks.svg +1 -1
- package/icons/lucide/list-numbers.svg +1 -1
- package/icons/lucide/list.svg +1 -1
- package/icons/lucide/lock-simple-open.svg +1 -1
- package/icons/lucide/lock-simple.svg +1 -1
- package/icons/lucide/magnifying-glass.svg +1 -1
- package/icons/lucide/map-pin.svg +1 -1
- package/icons/lucide/map-trifold.svg +1 -1
- package/icons/lucide/medal.svg +1 -1
- package/icons/lucide/megaphone.svg +1 -1
- package/icons/lucide/minus.svg +1 -1
- package/icons/lucide/moon.svg +1 -1
- package/icons/lucide/newspaper.svg +1 -1
- package/icons/lucide/pack.json +23 -23
- package/icons/lucide/paint-brush-broad.svg +1 -1
- package/icons/lucide/palette.svg +1 -1
- package/icons/lucide/paper-plane-tilt.svg +1 -1
- package/icons/lucide/pencil-simple.svg +1 -1
- package/icons/lucide/phone.svg +1 -1
- package/icons/lucide/play-fill.svg +1 -1
- package/icons/lucide/plus.svg +1 -1
- package/icons/lucide/prohibit.svg +1 -1
- package/icons/lucide/puzzle-piece.svg +1 -1
- package/icons/lucide/question.svg +1 -1
- package/icons/lucide/quotes.svg +1 -1
- package/icons/lucide/rows.svg +1 -1
- package/icons/lucide/ruler.svg +1 -1
- package/icons/lucide/scan.svg +1 -1
- package/icons/lucide/shapes.svg +1 -1
- package/icons/lucide/share-network.svg +1 -1
- package/icons/lucide/shield-check.svg +1 -1
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sign-out.svg +1 -1
- package/icons/lucide/signpost.svg +1 -1
- package/icons/lucide/sliders-horizontal.svg +1 -1
- package/icons/lucide/smiley.svg +1 -1
- package/icons/lucide/sparkle.svg +1 -1
- package/icons/lucide/square-half-bottom.svg +1 -1
- package/icons/lucide/square.svg +1 -1
- package/icons/lucide/squares-four.svg +1 -1
- package/icons/lucide/sticker.svg +1 -1
- package/icons/lucide/storefront.svg +1 -1
- package/icons/lucide/sun.svg +1 -1
- package/icons/lucide/swatches.svg +1 -1
- package/icons/lucide/tag.svg +1 -1
- package/icons/lucide/text-aa.svg +1 -1
- package/icons/lucide/text-align-left.svg +1 -1
- package/icons/lucide/text-b.svg +1 -1
- package/icons/lucide/text-h-one.svg +1 -1
- package/icons/lucide/text-h-three.svg +1 -1
- package/icons/lucide/text-h-two.svg +1 -1
- package/icons/lucide/text-italic.svg +1 -1
- package/icons/lucide/text-t.svg +1 -1
- package/icons/lucide/ticket.svg +1 -1
- package/icons/lucide/toggle-right.svg +1 -1
- package/icons/lucide/train.svg +1 -1
- package/icons/lucide/trash.svg +1 -1
- package/icons/lucide/upload-simple.svg +1 -1
- package/icons/lucide/user-circle.svg +1 -1
- package/icons/lucide/user.svg +1 -1
- package/icons/lucide/users-three.svg +1 -1
- package/icons/lucide/users.svg +1 -1
- package/icons/lucide/video-camera.svg +1 -1
- package/icons/lucide/warning-circle.svg +1 -1
- package/icons/lucide/warning.svg +1 -1
- package/icons/lucide/wave-sine.svg +1 -1
- package/icons/lucide/wheelchair.svg +1 -1
- package/icons/lucide/wrench.svg +1 -1
- package/icons/lucide/x.svg +1 -1
- package/icons/material-symbols/LICENSE +201 -201
- package/icons/material-symbols/address-book.svg +1 -1
- package/icons/material-symbols/app-window.svg +1 -1
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
- package/icons/material-symbols/arrow-down.svg +1 -1
- package/icons/material-symbols/arrow-left.svg +1 -1
- package/icons/material-symbols/arrow-right.svg +1 -1
- package/icons/material-symbols/arrow-square-out.svg +1 -1
- package/icons/material-symbols/arrow-u-up-left.svg +1 -1
- package/icons/material-symbols/arrow-u-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up.svg +1 -1
- package/icons/material-symbols/arrows-down-up.svg +1 -1
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
- package/icons/material-symbols/article.svg +1 -1
- package/icons/material-symbols/bank.svg +1 -1
- package/icons/material-symbols/bell.svg +1 -1
- package/icons/material-symbols/bicycle.svg +1 -1
- package/icons/material-symbols/binoculars.svg +1 -1
- package/icons/material-symbols/book-open-text.svg +1 -1
- package/icons/material-symbols/book-open.svg +1 -1
- package/icons/material-symbols/browser.svg +1 -1
- package/icons/material-symbols/buildings.svg +1 -1
- package/icons/material-symbols/bus.svg +1 -1
- package/icons/material-symbols/calendar-blank.svg +1 -1
- package/icons/material-symbols/calendar-check.svg +1 -1
- package/icons/material-symbols/car.svg +1 -1
- package/icons/material-symbols/cards.svg +1 -1
- package/icons/material-symbols/caret-down.svg +1 -1
- package/icons/material-symbols/caret-left.svg +1 -1
- package/icons/material-symbols/caret-right.svg +1 -1
- package/icons/material-symbols/caret-up.svg +1 -1
- package/icons/material-symbols/certificate.svg +1 -1
- package/icons/material-symbols/chart-line-up.svg +1 -1
- package/icons/material-symbols/chats.svg +1 -1
- package/icons/material-symbols/check-circle.svg +1 -1
- package/icons/material-symbols/check.svg +1 -1
- package/icons/material-symbols/circle-half.svg +1 -1
- package/icons/material-symbols/circle.svg +1 -1
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
- package/icons/material-symbols/clock.svg +1 -1
- package/icons/material-symbols/code.svg +1 -1
- package/icons/material-symbols/coffee.svg +1 -1
- package/icons/material-symbols/columns.svg +1 -1
- package/icons/material-symbols/cookie.svg +1 -1
- package/icons/material-symbols/copy.svg +1 -1
- package/icons/material-symbols/credit-card.svg +1 -1
- package/icons/material-symbols/currency-circle-dollar.svg +1 -1
- package/icons/material-symbols/cursor-click.svg +1 -1
- package/icons/material-symbols/database.svg +1 -1
- package/icons/material-symbols/desktop.svg +1 -1
- package/icons/material-symbols/device-mobile.svg +1 -1
- package/icons/material-symbols/door-open.svg +1 -1
- package/icons/material-symbols/download-simple.svg +1 -1
- package/icons/material-symbols/envelope-simple.svg +1 -1
- package/icons/material-symbols/eye-slash.svg +1 -1
- package/icons/material-symbols/eye.svg +1 -1
- package/icons/material-symbols/file-pdf.svg +1 -1
- package/icons/material-symbols/file-text.svg +1 -1
- package/icons/material-symbols/file-video.svg +1 -1
- package/icons/material-symbols/file-zip.svg +1 -1
- package/icons/material-symbols/flower-lotus.svg +1 -1
- package/icons/material-symbols/folder-simple.svg +1 -1
- package/icons/material-symbols/funnel.svg +1 -1
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear.svg +1 -1
- package/icons/material-symbols/github-logo.svg +1 -1
- package/icons/material-symbols/globe.svg +1 -1
- package/icons/material-symbols/google-logo.svg +1 -1
- package/icons/material-symbols/graduation-cap.svg +1 -1
- package/icons/material-symbols/hammer.svg +1 -1
- package/icons/material-symbols/hand-heart.svg +1 -1
- package/icons/material-symbols/handshake.svg +1 -1
- package/icons/material-symbols/hard-drives.svg +1 -1
- package/icons/material-symbols/heart.svg +1 -1
- package/icons/material-symbols/house.svg +1 -1
- package/icons/material-symbols/image-square.svg +1 -1
- package/icons/material-symbols/image.svg +1 -1
- package/icons/material-symbols/info.svg +1 -1
- package/icons/material-symbols/keyboard.svg +1 -1
- package/icons/material-symbols/leaf.svg +1 -1
- package/icons/material-symbols/lifebuoy.svg +1 -1
- package/icons/material-symbols/lightbulb.svg +1 -1
- package/icons/material-symbols/link-break.svg +1 -1
- package/icons/material-symbols/link-simple.svg +1 -1
- package/icons/material-symbols/list-bullets.svg +1 -1
- package/icons/material-symbols/list-checks.svg +1 -1
- package/icons/material-symbols/list-numbers.svg +1 -1
- package/icons/material-symbols/list.svg +1 -1
- package/icons/material-symbols/lock-simple-open.svg +1 -1
- package/icons/material-symbols/lock-simple.svg +1 -1
- package/icons/material-symbols/magnifying-glass.svg +1 -1
- package/icons/material-symbols/map-pin.svg +1 -1
- package/icons/material-symbols/map-trifold.svg +1 -1
- package/icons/material-symbols/medal.svg +1 -1
- package/icons/material-symbols/megaphone.svg +1 -1
- package/icons/material-symbols/minus.svg +1 -1
- package/icons/material-symbols/moon.svg +1 -1
- package/icons/material-symbols/newspaper.svg +1 -1
- package/icons/material-symbols/pack.json +29 -29
- package/icons/material-symbols/paint-brush-broad.svg +1 -1
- package/icons/material-symbols/palette.svg +1 -1
- package/icons/material-symbols/paper-plane-tilt.svg +1 -1
- package/icons/material-symbols/pencil-simple.svg +1 -1
- package/icons/material-symbols/phone.svg +1 -1
- package/icons/material-symbols/play-fill.svg +1 -1
- package/icons/material-symbols/plus.svg +1 -1
- package/icons/material-symbols/prohibit.svg +1 -1
- package/icons/material-symbols/puzzle-piece.svg +1 -1
- package/icons/material-symbols/question.svg +1 -1
- package/icons/material-symbols/quotes.svg +1 -1
- package/icons/material-symbols/rows.svg +1 -1
- package/icons/material-symbols/ruler.svg +1 -1
- package/icons/material-symbols/scan.svg +1 -1
- package/icons/material-symbols/shapes.svg +1 -1
- package/icons/material-symbols/share-network.svg +1 -1
- package/icons/material-symbols/shield-check.svg +1 -1
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sign-out.svg +1 -1
- package/icons/material-symbols/signpost.svg +1 -1
- package/icons/material-symbols/sliders-horizontal.svg +1 -1
- package/icons/material-symbols/smiley.svg +1 -1
- package/icons/material-symbols/sparkle.svg +1 -1
- package/icons/material-symbols/square-half-bottom.svg +1 -1
- package/icons/material-symbols/square.svg +1 -1
- package/icons/material-symbols/squares-four.svg +1 -1
- package/icons/material-symbols/sticker.svg +1 -1
- package/icons/material-symbols/storefront.svg +1 -1
- package/icons/material-symbols/sun.svg +1 -1
- package/icons/material-symbols/swatches.svg +1 -1
- package/icons/material-symbols/tag.svg +1 -1
- package/icons/material-symbols/text-aa.svg +1 -1
- package/icons/material-symbols/text-align-left.svg +1 -1
- package/icons/material-symbols/text-b.svg +1 -1
- package/icons/material-symbols/text-h-one.svg +1 -1
- package/icons/material-symbols/text-h-three.svg +1 -1
- package/icons/material-symbols/text-h-two.svg +1 -1
- package/icons/material-symbols/text-italic.svg +1 -1
- package/icons/material-symbols/text-t.svg +1 -1
- package/icons/material-symbols/ticket.svg +1 -1
- package/icons/material-symbols/toggle-right.svg +1 -1
- package/icons/material-symbols/train.svg +1 -1
- package/icons/material-symbols/trash.svg +1 -1
- package/icons/material-symbols/upload-simple.svg +1 -1
- package/icons/material-symbols/user-circle.svg +1 -1
- package/icons/material-symbols/user.svg +1 -1
- package/icons/material-symbols/users-three.svg +1 -1
- package/icons/material-symbols/users.svg +1 -1
- package/icons/material-symbols/video-camera.svg +1 -1
- package/icons/material-symbols/warning-circle.svg +1 -1
- package/icons/material-symbols/warning.svg +1 -1
- package/icons/material-symbols/wave-sine.svg +1 -1
- package/icons/material-symbols/wheelchair.svg +1 -1
- package/icons/material-symbols/wrench.svg +1 -1
- package/icons/material-symbols/x.svg +1 -1
- package/icons/phosphor/pack.json +8 -8
- package/lib/apps.mjs +142 -139
- package/lib/apps.test.d.mts +1 -0
- package/lib/apps.test.mjs +50 -0
- package/lib/blocks.mjs +90 -90
- package/lib/caseCollisions.d.mts +4 -0
- package/lib/caseCollisions.mjs +62 -0
- package/lib/caseCollisions.test.d.mts +1 -0
- package/lib/caseCollisions.test.mjs +74 -0
- package/lib/colors.ts +86 -86
- package/lib/contactBar.ts +18 -18
- package/lib/contracts.d.mts +15 -0
- package/lib/contracts.mjs +17 -0
- package/lib/cookieCategories.mjs +28 -28
- package/lib/crop.mjs +83 -83
- package/lib/csp.mjs +130 -130
- package/lib/cspSources.mjs +60 -60
- package/lib/dynamic-colors.ts +426 -426
- package/lib/git.mjs +64 -64
- package/lib/hooks.ts +51 -51
- package/lib/inline.ts +20 -20
- package/lib/license-texts/Apache-2.0.txt +190 -190
- package/lib/license-texts/BSD-2-Clause.txt +24 -24
- package/lib/license-texts/BSD-3-Clause.txt +28 -28
- package/lib/license-texts/ISC.txt +15 -15
- package/lib/license-texts/MIT.txt +21 -21
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.mjs +683 -683
- package/lib/markdown.ts +42 -42
- package/lib/motion-schemes.mjs +5 -5
- package/lib/motion.ts +131 -131
- package/lib/notice.mjs +34 -34
- package/lib/scheduled.mjs +37 -37
- package/lib/snackbar.ts +149 -149
- package/lib/status.mjs +18 -18
- package/lib/storeEntries.d.mts +18 -0
- package/lib/storeEntries.mjs +64 -0
- package/lib/storeEntries.test.d.mts +1 -0
- package/lib/storeEntries.test.mjs +71 -0
- package/lib/svg.mjs +231 -231
- package/lib/text-colors.mjs +43 -43
- package/lib/video-id.mjs +72 -72
- package/lib/video.mjs +91 -91
- package/package.json +136 -131
- package/pages/404.astro +10 -10
- package/pages/[...slug].astro +51 -51
- package/pages/_headers.txt +31 -31
- package/pages/blog/index.astro +10 -10
- package/pages/blog/kategori/[category].astro +18 -18
- package/pages/editor/code-images.json.js +25 -25
- package/pages/editor/index.astro +75 -75
- package/pages/editor/manifest.webmanifest.js +23 -23
- package/pages/editor/sw.js +33 -33
- package/pages/editor/sw.js.js +4 -4
- package/pages/editor/thumbnails.json.js +21 -21
- package/pages/editor/version.json.js +22 -22
- package/pages/headers.js +4 -4
- package/pages/index.astro +10 -10
- package/pages/ny.astro +6 -6
- package/pages/robots.txt.js +3 -3
- package/pages/rss.xml.js +20 -20
- package/scripts/app-screenshots.mjs +164 -164
- package/scripts/app-workers.mjs +6 -6
- package/scripts/bin.mjs +12 -12
- package/scripts/built-site.mjs +134 -99
- package/scripts/check-consent.mjs +113 -113
- package/scripts/check-content.mjs +151 -149
- package/scripts/check-demo.mjs +24 -24
- package/scripts/check-store.mjs +160 -0
- package/scripts/check-themes.mjs +45 -45
- package/scripts/check-worker.mjs +569 -569
- package/scripts/compare-html.mjs +81 -81
- package/scripts/declarations.mjs +10 -0
- package/scripts/declarations.test.mjs +32 -0
- package/scripts/demo-site.mjs +42 -42
- package/scripts/editor-icons.mjs +56 -56
- package/scripts/editor-roundtrip.mjs +134 -134
- package/scripts/editor-screens.mjs +101 -101
- package/scripts/git-env.mjs +9 -9
- package/scripts/icon-packs.mjs +151 -151
- package/scripts/link-counts.mjs +22 -22
- package/scripts/page-colors.mjs +42 -42
- package/scripts/releases.mjs +80 -80
- package/scripts/responsive-images.mjs +94 -94
- package/scripts/scheduled-list.mjs +15 -15
- package/scripts/theme-screenshots.mjs +88 -88
- package/scripts/types.mjs +17 -21
- package/scripts/video-thumbnails.mjs +89 -89
- package/scripts/worker-modules.mjs +34 -34
- package/site/appCookies.mjs +74 -74
- package/site/apps.ts +21 -21
- package/site/autolinks.mjs +200 -200
- package/site/blockScripts.ts +31 -31
- package/site/categories.ts +16 -16
- package/site/consent.ts +26 -26
- package/site/content.config.ts +84 -84
- package/site/content.ts +104 -104
- package/site/cookieList.ts +17 -17
- package/site/defineSite.d.mts +2 -14
- package/site/defineSite.mjs +131 -136
- package/site/directives/interact.mjs +31 -31
- package/site/editor-version.mjs +26 -26
- package/site/fill.ts +13 -13
- package/site/format.ts +11 -11
- package/site/home.ts +66 -66
- package/site/iconPacks.mjs +43 -43
- package/site/icons.mjs +605 -605
- package/site/images.mjs +157 -157
- package/site/motion.ts +13 -13
- package/site/noindex.mjs +21 -21
- package/site/pageColors.ts +110 -110
- package/site/popups.ts +98 -98
- package/site/schema.ts +189 -189
- package/site/sections.ts +3 -3
- package/site/seo.ts +28 -28
- package/site/site.ts +76 -76
- package/site/siteConfig.mjs +8 -8
- package/site/siteCsp.mjs +75 -75
- package/site/siteModules.mjs +27 -27
- package/site/standardPages.ts +59 -59
- package/site/storeTheme.d.mts +6 -0
- package/site/storeTheme.mjs +29 -0
- package/site/storeTheme.test.d.mts +1 -0
- package/site/storeTheme.test.mjs +53 -0
- package/site/testimonials.ts +48 -48
- package/site/text.ts +43 -43
- package/site/theme.mjs +483 -472
- package/site/tracking.client.ts +225 -225
- package/site/tracking.ts +9 -9
- package/src/ActionParts.tsx +205 -205
- package/src/Advanced.tsx +152 -152
- package/src/App.tsx +901 -904
- package/src/AppList.tsx +1362 -1345
- package/src/BlockDialog.tsx +360 -360
- package/src/BlockParts.tsx +652 -652
- package/src/BlockSettings.tsx +533 -533
- package/src/BrandForm.tsx +164 -164
- package/src/Colors.tsx +476 -476
- package/src/CommunicationParts.tsx +199 -199
- package/src/CompanyForm.tsx +209 -209
- package/src/Consents.tsx +175 -175
- package/src/ContainmentParts.tsx +185 -185
- package/src/Crop.tsx +152 -152
- package/src/DataForm.tsx +434 -434
- package/src/Deleted.tsx +177 -177
- package/src/EntryForm.tsx +740 -740
- package/src/FileView.tsx +30 -30
- package/src/FormFrame.tsx +237 -237
- package/src/Keywords.tsx +225 -225
- package/src/Library.tsx +1563 -1563
- package/src/Licenses.tsx +336 -336
- package/src/LinkSheet.tsx +138 -138
- package/src/MarkdownEditor.tsx +1075 -1075
- package/src/Media.tsx +677 -677
- package/src/NavigationParts.tsx +350 -350
- package/src/NavigationRestParts.tsx +247 -247
- package/src/Notifications.tsx +59 -59
- package/src/PagePicker.tsx +136 -136
- package/src/Parts.tsx +991 -991
- package/src/PartsShown.tsx +168 -168
- package/src/PickerParts.tsx +183 -183
- package/src/Redirects.tsx +450 -450
- package/src/Screen.tsx +184 -184
- package/src/SectionsField.tsx +65 -65
- package/src/SelectionParts.tsx +134 -134
- package/src/SeoScores.tsx +184 -184
- package/src/Sheet.tsx +35 -35
- package/src/ThemeStore.tsx +376 -376
- package/src/Tokens.tsx +346 -346
- package/src/Versions.tsx +214 -214
- package/src/addresses.ts +75 -75
- package/src/allIcons.ts +7 -7
- package/src/appFiles.ts +29 -29
- package/src/apps.ts +281 -281
- package/src/ask.tsx +123 -123
- package/src/blocks.ts +251 -251
- package/src/brand.ts +153 -153
- package/src/cache.ts +42 -42
- package/src/check.ts +285 -285
- package/src/checkSite.ts +256 -256
- package/src/{consents.ts → consentData.ts} +26 -26
- package/src/cookieList.ts +70 -70
- package/src/countries.ts +59 -59
- package/src/deploy.ts +155 -155
- package/src/editor.css +602 -602
- package/src/examples.ts +85 -85
- package/src/fields.tsx +942 -942
- package/src/frontmatter.mjs +42 -42
- package/src/github.ts +376 -376
- package/src/i18n/da-DK/advanced.ts +28 -28
- package/src/i18n/da-DK/app.ts +71 -71
- package/src/i18n/da-DK/apps.ts +90 -90
- package/src/i18n/da-DK/blockSettings.ts +38 -38
- package/src/i18n/da-DK/blocks.ts +408 -408
- package/src/i18n/da-DK/check.ts +25 -25
- package/src/i18n/da-DK/colors.ts +64 -64
- package/src/i18n/da-DK/common.ts +118 -118
- package/src/i18n/da-DK/company.ts +24 -24
- package/src/i18n/da-DK/consents.ts +23 -23
- package/src/i18n/da-DK/forms.ts +116 -116
- package/src/i18n/da-DK/history.ts +35 -35
- package/src/i18n/da-DK/index.ts +49 -49
- package/src/i18n/da-DK/keywords.ts +19 -19
- package/src/i18n/da-DK/library.ts +135 -135
- package/src/i18n/da-DK/licenses.ts +25 -25
- package/src/i18n/da-DK/media.ts +79 -79
- package/src/i18n/da-DK/parts.ts +696 -696
- package/src/i18n/da-DK/preview.ts +13 -13
- package/src/i18n/da-DK/redirects.ts +51 -51
- package/src/i18n/da-DK/seo.ts +20 -20
- package/src/i18n/da-DK/text.ts +35 -35
- package/src/i18n/da-DK/theme.ts +41 -41
- package/src/i18n/en-US/advanced.ts +32 -32
- package/src/i18n/en-US/app.ts +84 -84
- package/src/i18n/en-US/apps.ts +134 -134
- package/src/i18n/en-US/blockSettings.ts +48 -48
- package/src/i18n/en-US/blocks.ts +441 -441
- package/src/i18n/en-US/check.ts +31 -31
- package/src/i18n/en-US/colors.ts +82 -82
- package/src/i18n/en-US/common.ts +144 -144
- package/src/i18n/en-US/company.ts +25 -25
- package/src/i18n/en-US/consents.ts +25 -25
- package/src/i18n/en-US/forms.ts +145 -145
- package/src/i18n/en-US/history.ts +43 -43
- package/src/i18n/en-US/index.ts +51 -51
- package/src/i18n/en-US/keywords.ts +24 -24
- package/src/i18n/en-US/library.ts +176 -176
- package/src/i18n/en-US/licenses.ts +36 -36
- package/src/i18n/en-US/media.ts +80 -80
- package/src/i18n/en-US/parts.ts +718 -718
- package/src/i18n/en-US/preview.ts +15 -15
- package/src/i18n/en-US/redirects.ts +66 -66
- package/src/i18n/en-US/seo.ts +26 -26
- package/src/i18n/en-US/text.ts +41 -41
- package/src/i18n/en-US/theme.ts +41 -41
- package/src/i18n/index.ts +124 -124
- package/src/icon.tsx +160 -160
- package/src/iconPacks.ts +89 -89
- package/src/images.ts +163 -163
- package/src/{licenses.ts → licenseData.ts} +79 -79
- package/src/markdown.mjs +204 -204
- package/src/nav.ts +68 -68
- package/src/notFound.ts +28 -28
- package/src/{notifications.ts → notificationData.ts} +223 -223
- package/src/pagePreview.tsx +204 -204
- package/src/r2Caching.ts +55 -55
- package/src/{redirects.ts → redirectData.ts} +149 -149
- package/src/registry.tsx +188 -188
- package/src/schema.ts +417 -417
- package/src/sections.mjs +303 -303
- package/src/seo.ts +333 -333
- package/src/shared.ts +44 -44
- package/src/sidebar.ts +31 -31
- package/src/site.ts +317 -317
- package/src/standard/Block.astro +19 -19
- package/src/standard/BlockSite.astro +18 -18
- package/src/standard/accordion/index.tsx +47 -47
- package/src/standard/accordion/load.ts +23 -23
- package/src/standard/accordion/shared.ts +15 -15
- package/src/standard/accordion/view.ts +32 -32
- package/src/standard/buttons/fields.ts +98 -98
- package/src/standard/buttons/index.tsx +23 -23
- package/src/standard/buttons/load.ts +34 -34
- package/src/standard/buttons/shared.ts +12 -12
- package/src/standard/buttons/view.ts +30 -30
- package/src/standard/cards/index.tsx +71 -71
- package/src/standard/cards/load.ts +17 -17
- package/src/standard/cards/shared.ts +27 -27
- package/src/standard/cards/view.ts +29 -29
- package/src/standard/checklist/index.tsx +27 -27
- package/src/standard/checklist/load.ts +5 -5
- package/src/standard/checklist/view.ts +13 -13
- package/src/standard/cookies/index.tsx +17 -17
- package/src/standard/cookies/list.ts +56 -56
- package/src/standard/cookies/load.ts +11 -11
- package/src/standard/cookies/shared.ts +20 -20
- package/src/standard/cookies/view.ts +28 -28
- package/src/standard/faq/index.tsx +29 -29
- package/src/standard/faq/load.ts +14 -14
- package/src/standard/faq/view.ts +21 -21
- package/src/standard/head.ts +23 -23
- package/src/standard/hero/fields.ts +48 -48
- package/src/standard/hero/index.tsx +57 -57
- package/src/standard/hero/load.ts +23 -23
- package/src/standard/hero/view.ts +38 -38
- package/src/standard/links/index.tsx +41 -41
- package/src/standard/links/load.ts +17 -17
- package/src/standard/links/view.ts +25 -25
- package/src/standard/list/index.tsx +50 -50
- package/src/standard/list/load.ts +5 -5
- package/src/standard/list/view.ts +18 -18
- package/src/standard/load.ts +7 -7
- package/src/standard/notice/index.tsx +30 -30
- package/src/standard/notice/load.ts +16 -16
- package/src/standard/notice/view.ts +10 -10
- package/src/standard/points/index.tsx +42 -42
- package/src/standard/points/load.ts +5 -5
- package/src/standard/points/view.ts +12 -12
- package/src/standard/posts/index.tsx +132 -132
- package/src/standard/posts/load.ts +36 -36
- package/src/standard/posts/shared.ts +21 -21
- package/src/standard/posts/view.ts +63 -63
- package/src/standard/pricecards/index.tsx +57 -57
- package/src/standard/pricecards/load.ts +11 -11
- package/src/standard/pricecards/view.ts +50 -50
- package/src/standard/sharedTexts.ts +6 -6
- package/src/standard/site.ts +124 -124
- package/src/standard/siteLooks.ts +19 -19
- package/src/standard/sites.ts +13 -13
- package/src/standard/steps/index.tsx +28 -28
- package/src/standard/steps/load.ts +5 -5
- package/src/standard/steps/view.ts +11 -11
- package/src/standard/testimonials/index.tsx +87 -87
- package/src/standard/testimonials/list.ts +43 -43
- package/src/standard/testimonials/load.ts +58 -58
- package/src/standard/testimonials/shared.ts +13 -13
- package/src/standard/testimonials/view.ts +76 -76
- package/src/standard/themeBlocks.ts +13 -13
- package/src/standard/video/index.tsx +23 -23
- package/src/standard/video/load.ts +10 -10
- package/src/standard/video/view.ts +42 -42
- package/src/standard/view.tsx +105 -105
- package/src/standard/views.ts +37 -37
- package/src/store.ts +479 -499
- package/src/theme.ts +90 -90
- package/src/themeUi.ts +97 -97
- package/src/themes.ts +91 -91
- package/src/ui.tsx +3402 -3402
- package/src/virtual.d.ts +5 -5
- package/src/when.ts +22 -22
- package/starter/.githooks/pre-push +4 -4
- package/starter/.nvmrc +1 -1
- package/starter/.prettierignore +11 -11
- package/starter/.prettierrc.json +7 -7
- package/starter/README.md +27 -26
- package/starter/astro.config.mjs +5 -5
- package/starter/package.json +67 -67
- package/starter/public/_redirects +1 -1
- package/starter/public/favicon.svg +1 -1
- package/starter/public/logo.svg +1 -1
- package/starter/src/content/pages/blog.md +8 -8
- package/starter/src/content/pages/forside.md +9 -9
- package/starter/src/content/pages/kontakt.md +7 -7
- package/starter/src/content/pages/privatlivspolitik.md +9 -9
- package/starter/src/content/pages/siden-findes-ikke.md +7 -7
- package/starter/src/content.config.ts +2 -2
- package/starter/src/data/apps.json +1 -1
- package/starter/src/data/block-settings.json +3 -3
- package/starter/src/data/blocks.json +1 -1
- package/starter/src/data/booking.json +6 -6
- package/starter/src/data/colors.json +70 -70
- package/starter/src/data/contact.json +8 -8
- package/starter/src/data/cookies.json +21 -21
- package/starter/src/data/csp.json +1 -1
- package/starter/src/data/footer.json +15 -15
- package/starter/src/data/internal-links.json +4 -4
- package/starter/src/data/menu.json +4 -4
- package/starter/src/data/mobile-menu.json +4 -4
- package/starter/src/data/old-addresses.json +1 -1
- package/starter/src/data/redirects.json +1 -1
- package/starter/src/data/standard-pages.json +8 -8
- package/starter/src/data/theme.json +1 -1
- package/starter/src/env.d.ts +2 -2
- package/starter/src/site/config.ts +13 -13
- package/starter/src/site/worker.js +14 -14
- package/starter/tsconfig.json +17 -17
- package/starter/worker/apps.js +6 -6
- package/starter/wrangler.jsonc +39 -39
- package/theme/LICENSE +21 -21
- package/theme/blocks/ArrowLink.tsx +14 -14
- package/theme/blocks/BlockHead.tsx +26 -26
- package/theme/blocks/Panel.tsx +32 -32
- package/theme/blocks/Person.tsx +26 -26
- package/theme/blocks/accordion/PageRow.tsx +25 -25
- package/theme/blocks/accordion/View.tsx +17 -17
- package/theme/blocks/buttons/Buttons.tsx +28 -28
- package/theme/blocks/buttons/View.tsx +12 -12
- package/theme/blocks/cards/LinkCards.tsx +72 -72
- package/theme/blocks/cards/View.tsx +20 -20
- package/theme/blocks/checklist/Checks.tsx +15 -15
- package/theme/blocks/checklist/View.tsx +14 -14
- package/theme/blocks/cookies/View.tsx +36 -36
- package/theme/blocks/faq/Accordion.tsx +34 -34
- package/theme/blocks/faq/View.tsx +16 -16
- package/theme/blocks/faq/script.ts +56 -56
- package/theme/blocks/hero/View.tsx +44 -44
- package/theme/blocks/links/LinkGroups.tsx +24 -24
- package/theme/blocks/links/View.tsx +17 -17
- package/theme/blocks/list/Rows.tsx +24 -24
- package/theme/blocks/list/View.tsx +14 -14
- package/theme/blocks/notice/View.tsx +13 -13
- package/theme/blocks/points/PointList.tsx +34 -34
- package/theme/blocks/points/View.tsx +14 -14
- package/theme/blocks/posts/View.tsx +26 -26
- package/theme/blocks/pricecards/View.tsx +40 -40
- package/theme/blocks/steps/StepList.tsx +23 -23
- package/theme/blocks/steps/View.tsx +14 -14
- package/theme/blocks/testimonials/List.tsx +46 -46
- package/theme/blocks/testimonials/Slider.tsx +44 -44
- package/theme/blocks/testimonials/Testimonial.tsx +124 -124
- package/theme/blocks/testimonials/View.tsx +21 -21
- package/theme/blocks/testimonials/script.ts +52 -52
- package/theme/blocks/video/View.tsx +41 -41
- package/theme/components/ActionBar.astro +63 -63
- package/theme/components/AppSlot.astro +21 -21
- package/theme/components/ArrowLink.tsx +21 -21
- package/theme/components/BlogIndex.astro +48 -48
- package/theme/components/BlogPosts.astro +18 -18
- package/theme/components/BlogPosts.tsx +45 -45
- package/theme/components/BookButton.tsx +22 -22
- package/theme/components/Container.astro +11 -11
- package/theme/components/CookieConsent.astro +121 -121
- package/theme/components/EndBlock.astro +21 -21
- package/theme/components/FontPreload.astro +6 -6
- package/theme/components/Footer.astro +85 -85
- package/theme/components/Header.astro +186 -186
- package/theme/components/Icon.astro +16 -16
- package/theme/components/Img.astro +29 -29
- package/theme/components/Img.tsx +33 -33
- package/theme/components/Inline.astro +11 -11
- package/theme/components/Logo.astro +23 -23
- package/theme/components/LogoMark.astro +10 -10
- package/theme/components/Markdown.astro +17 -17
- package/theme/components/MobileMenu.tsx +224 -224
- package/theme/components/MobileNav.astro +33 -33
- package/theme/components/PageHeader.astro +22 -22
- package/theme/components/PageSection.astro +24 -24
- package/theme/components/PhoneButton.tsx +11 -11
- package/theme/components/Popup.astro +145 -145
- package/theme/components/PopupButton.tsx +24 -24
- package/theme/components/PopupView.tsx +48 -48
- package/theme/components/PostList.tsx +58 -58
- package/theme/components/PostMeta.tsx +17 -17
- package/theme/components/RichText.astro +28 -28
- package/theme/components/Section.astro +29 -29
- package/theme/components/SectionHeading.tsx +8 -8
- package/theme/components/TextSection.astro +16 -16
- package/theme/components/home/PageBlock.astro +19 -19
- package/theme/components/home/Text.astro +13 -13
- package/theme/components/loadPopup.ts +22 -22
- package/theme/components/loadPosts.ts +16 -16
- package/theme/components/posts.ts +15 -15
- package/theme/editor/looks.mjs +7 -7
- package/theme/editor/previews.tsx +56 -56
- package/theme/layouts/Base.astro +273 -273
- package/theme/layouts/PageLayout.astro +126 -126
- package/theme/layouts/PostLayout.astro +137 -137
- package/theme/scripts/motion.ts +29 -29
- package/theme/styles/base.css +320 -320
- package/theme/styles/blocks.css +1808 -1808
- package/theme/styles/components.css +206 -206
- package/theme/styles/global.css +1302 -1302
- package/theme/styles/ui.css +6544 -6544
- package/theme/templates/Blog.astro +38 -38
- package/theme/templates/Category.astro +19 -19
- package/theme/templates/Home.astro +36 -36
- package/theme/templates/New.astro +68 -68
- package/theme/templates/NotFound.astro +117 -117
- package/theme/theme.json +28 -28
- package/theme/ui/Badge.tsx +45 -45
- package/theme/ui/BottomSheet.tsx +148 -148
- package/theme/ui/Button.tsx +79 -79
- package/theme/ui/ButtonGroup.tsx +31 -31
- package/theme/ui/Card.tsx +45 -45
- package/theme/ui/Carousel.tsx +124 -124
- package/theme/ui/Checkbox.tsx +59 -59
- package/theme/ui/Chip.tsx +101 -101
- package/theme/ui/ChipSet.tsx +27 -27
- package/theme/ui/DatePicker.tsx +149 -149
- package/theme/ui/DatePickerContainer.tsx +316 -316
- package/theme/ui/Dialog.tsx +100 -100
- package/theme/ui/Divider.tsx +27 -27
- package/theme/ui/ExposedDropdownMenu.tsx +77 -77
- package/theme/ui/Fab.tsx +88 -88
- package/theme/ui/FabMenu.tsx +67 -67
- package/theme/ui/IconButton.tsx +78 -78
- package/theme/ui/List.tsx +287 -287
- package/theme/ui/LoadingIndicator.tsx +29 -29
- package/theme/ui/Menu.tsx +278 -278
- package/theme/ui/NavigationBar.tsx +62 -62
- package/theme/ui/NavigationDrawer.tsx +80 -80
- package/theme/ui/NavigationRail.tsx +154 -154
- package/theme/ui/ProgressIndicator.tsx +162 -162
- package/theme/ui/RadioButton.tsx +50 -50
- package/theme/ui/Search.tsx +129 -129
- package/theme/ui/SegmentedButton.tsx +49 -49
- package/theme/ui/SideSheet.tsx +54 -54
- package/theme/ui/Slider.tsx +155 -155
- package/theme/ui/Snackbar.tsx +67 -67
- package/theme/ui/SplitButton.tsx +67 -67
- package/theme/ui/Switch.tsx +60 -60
- package/theme/ui/Tabs.tsx +114 -114
- package/theme/ui/TextField.tsx +278 -278
- package/theme/ui/TimePicker.tsx +128 -128
- package/theme/ui/TimePickerContainer.tsx +234 -234
- package/theme/ui/Toolbar.tsx +67 -67
- package/theme/ui/Tooltip.tsx +58 -58
- package/theme/ui/TopAppBar.tsx +77 -77
- package/theme/ui/bottomSheet.script.ts +8 -8
- package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
- package/theme/ui/carousel.script.ts +28 -28
- package/theme/ui/carouselLayout.ts +229 -229
- package/theme/ui/checkbox.script.ts +8 -8
- package/theme/ui/datePicker.script.ts +302 -302
- package/theme/ui/dates.ts +185 -185
- package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
- package/theme/ui/dropdownMenu.script.ts +166 -166
- package/theme/ui/fabMenu.script.ts +57 -57
- package/theme/ui/navigationRail.script.ts +47 -47
- package/theme/ui/ripple.script.ts +159 -159
- package/theme/ui/search.script.ts +92 -92
- package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
- package/theme/ui/slider.script.ts +12 -12
- package/theme/ui/sliderUpdate.ts +52 -52
- package/theme/ui/snackbar.script.ts +16 -16
- package/theme/ui/splitButton.script.ts +63 -63
- package/theme/ui/tabs.script.ts +48 -48
- package/theme/ui/textField.script.ts +17 -17
- package/theme/ui/textFieldUpdate.ts +44 -44
- package/theme/ui/timePicker.script.ts +248 -248
- package/theme/ui/toolbar.script.ts +43 -43
- package/theme/ui/tooltip.script.ts +68 -68
- package/worker/consent.js +141 -141
- package/worker/editor-github.js +167 -167
- package/worker/editor-host.js +74 -74
- package/worker/files.js +401 -401
- package/worker/github.js +36 -36
- package/worker/index.js +188 -184
- package/worker/not-found.js +97 -97
- package/worker/oauth.js +95 -95
- package/worker/publish.js +52 -52
- package/worker/push.js +270 -270
- package/worker/r2-status.js +50 -50
- package/worker/session.js +58 -58
- package/worker/store.js +46 -0
package/src/Colors.tsx
CHANGED
|
@@ -1,476 +1,476 @@
|
|
|
1
|
-
// "Udseende" › "Farver" (#udseende/farver): the site's colours, for light and dark mode, in the site's file of
|
|
2
|
-
// colours (config.colors, packages/editor/lib/colors.ts). The first tab, "Generator", where the screen starts, or one
|
|
3
|
-
// mode at a time, picked with the tabs after it, and the editor is shown in the mode while the screen is open
|
|
4
|
-
// (theme.ts holdTheme). Each mode has a little page drawn in its colours, the pairs that are too hard to
|
|
5
|
-
// read (colorPairs in @site/colors, WCAG AA) and a field for each colour (colorFields). Colours
|
|
6
|
-
// that are hard to read together cannot be saved; "Gem" then shows the mode they are in. "Brug
|
|
7
|
-
// standardfarverne" puts back the ones the site was designed with (defaultColors). The generator is dynamic colour for
|
|
8
|
-
// both modes at once, with its three base colours at the top (ColorTiles) and the little page in each mode under the
|
|
9
|
-
// fields: it
|
|
10
|
-
// makes all of them, in both modes, from one source colour, Material 3's way (dynamic colour,
|
|
11
|
-
// lib/dynamic-colors.ts, each colour by its role, colorRoles): the source colour picked or taken from a
|
|
12
|
-
// picture, or dragged on a colour wheel (ColorWheel), a colour harmony (HARMONIES: where the secondary and tertiary
|
|
13
|
-
// colours are on the wheel from it) and a contrast level, kept in the file with the colours. Choosing one of them makes
|
|
14
|
-
// the colours from them; changing a colour by hand turns it off. Material's other source of dynamic colour, each page's own picture, is an app ("Indholdsbaserede
|
|
15
|
-
// farver", src/apps/content-colors/: pageColors in packages/editor/site/pageColors.ts). While an app installed changes the pages'
|
|
16
|
-
// colours (hooks/page-colors.ts), each mode shows the pages' colours instead (PageColors), as the site was last built
|
|
17
|
-
// (/editor/page-colors.json, packages/editor/scripts/page-colors.mjs): first a notice that the apps give them, with a button to the
|
|
18
|
-
// site's own, which are only the fallback ("Reservefarver", #udseende/farver/reservefarver, a page of the same form, so
|
|
19
|
-
// what is unsaved stays), then the apps, each to its settings, the pages with colours from a picture (the picture, the
|
|
20
|
-
// page and its colours in the mode open), and those with the site's own. Saved with "Gem ændringer" in one commit; the
|
|
21
|
-
// editor shows the new colours at once (theme.ts applyColors), the site when it is built again.
|
|
22
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
23
|
-
import { Image, Moon, Palette, Sun } from '@phosphor-icons/react';
|
|
24
|
-
import { ConflictError, type RepoFile } from './github';
|
|
25
|
-
import FormFrame, { useFormPages } from './FormFrame';
|
|
26
|
-
import {
|
|
27
|
-
Button,
|
|
28
|
-
ChipSet,
|
|
29
|
-
ColorField,
|
|
30
|
-
ColorSwatches,
|
|
31
|
-
ColorTiles,
|
|
32
|
-
ColorWheel,
|
|
33
|
-
ColorsPreview,
|
|
34
|
-
EmptyNote,
|
|
35
|
-
FilterChip,
|
|
36
|
-
FormColumns,
|
|
37
|
-
FormField,
|
|
38
|
-
Group,
|
|
39
|
-
Label,
|
|
40
|
-
LeadingImage,
|
|
41
|
-
LinkButton,
|
|
42
|
-
Notice,
|
|
43
|
-
SupportingText,
|
|
44
|
-
SegmentedButton,
|
|
45
|
-
ProblemNote,
|
|
46
|
-
ListItem,
|
|
47
|
-
} from './ui';
|
|
48
|
-
import type { Env } from './App';
|
|
49
|
-
import { savedText } from './deploy';
|
|
50
|
-
import { colorProblems, lowContrast, MODES, type Colors as ColorsData, type Mode } from '../lib/colors';
|
|
51
|
-
import {
|
|
52
|
-
completeColors,
|
|
53
|
-
CONTRASTS,
|
|
54
|
-
dynamicColors,
|
|
55
|
-
dynamicProblems,
|
|
56
|
-
BASE_COLORS,
|
|
57
|
-
HARMONIES,
|
|
58
|
-
canSetBaseColor,
|
|
59
|
-
withBaseColor,
|
|
60
|
-
wheelColor,
|
|
61
|
-
wheelColors,
|
|
62
|
-
wheelDots,
|
|
63
|
-
type Contrast,
|
|
64
|
-
type Dynamic,
|
|
65
|
-
} from '../lib/dynamic-colors';
|
|
66
|
-
import { colorRoles, config, defaultColors } from '@site/editor';
|
|
67
|
-
import { colorsInPicture, pickImage, shownAt } from './images';
|
|
68
|
-
import { holdTheme, shownMode } from './theme';
|
|
69
|
-
import { locale, siteT, t } from './i18n';
|
|
70
|
-
import { colorFields, colorPairs, apps as siteApps } from './site';
|
|
71
|
-
import { appSettings, installedApps, type SiteApp } from './apps';
|
|
72
|
-
import { settingsRoute } from './AppList';
|
|
73
|
-
import { splitPicture } from '../lib/crop.mjs';
|
|
74
|
-
|
|
75
|
-
const names = colorFields.map((f) => f.name);
|
|
76
|
-
|
|
77
|
-
/** The site's colours as the editor has them (with those a file written before them does not have made, completeColors),
|
|
78
|
-
* or the ones it was designed with when the file is missing or cannot be read (`unreadable`). */
|
|
79
|
-
export function colorsOf(files: RepoFile[]): ColorsData & { unreadable?: boolean } {
|
|
80
|
-
const file = files.find((f) => f.path === config.colors);
|
|
81
|
-
try {
|
|
82
|
-
const colors = completeColors(JSON.parse(file?.text ?? ''), colorRoles);
|
|
83
|
-
if (!colorProblems(colors, names).length && (colors.dynamic === undefined || !dynamicProblems(colors.dynamic).length)) return colors;
|
|
84
|
-
} catch {}
|
|
85
|
-
return { ...defaultColors, unreadable: true };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const fileOf = (colors: ColorsData) => {
|
|
89
|
-
const sets = Object.fromEntries(MODES.map((m) => [m, Object.fromEntries(names.map((n) => [n, colors[m][n]]))]));
|
|
90
|
-
const d = colors.dynamic;
|
|
91
|
-
const dynamic = d && {
|
|
92
|
-
source: d.source,
|
|
93
|
-
...(d.harmony ? { harmony: d.harmony } : { variant: d.variant }),
|
|
94
|
-
contrast: d.contrast,
|
|
95
|
-
...(d.sourceLocked && { sourceLocked: true }),
|
|
96
|
-
};
|
|
97
|
-
return `${JSON.stringify({ ...sets, ...(dynamic && { dynamic }) }, null, 2)}\n`;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/** The colours made from a choice of dynamic colour, with the choice. */
|
|
101
|
-
const madeFrom = (dynamic: Dynamic): Pick<ColorsData, 'light' | 'dark' | 'dynamic'> => ({ ...dynamicColors(dynamic, colorRoles), dynamic });
|
|
102
|
-
|
|
103
|
-
// The colour wheel's edge and middle, the same for every choice.
|
|
104
|
-
const wheel = wheelColors();
|
|
105
|
-
|
|
106
|
-
const ratio = (n: number) => n.toLocaleString(locale, { maximumFractionDigits: 1 });
|
|
107
|
-
|
|
108
|
-
// What a save of the colours changed (deploy.ts).
|
|
109
|
-
const CHANGE = t.app.changes.edited(t.app.changes.colors);
|
|
110
|
-
|
|
111
|
-
/** The screen's tabs: a mode's colours, or the generator (dynamic colour), for both. */
|
|
112
|
-
type Tab = Mode | 'generator';
|
|
113
|
-
|
|
114
|
-
/** The page with the site's own colours, while an app changes the pages' colours: the fallback. */
|
|
115
|
-
export const FALLBACK_ROUTE = 'udseende/farver/reservefarver';
|
|
116
|
-
|
|
117
|
-
/** The apps installed that change the pages' colours (hooks/page-colors.ts). */
|
|
118
|
-
const colorApps = (files: RepoFile[]) => {
|
|
119
|
-
const installed = installedApps(files);
|
|
120
|
-
return siteApps.filter((a) => installed.includes(a.id) && a.needs.changes?.includes('page-colors'));
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/** `fallback`: the page with the site's own colours is open (FALLBACK_ROUTE). */
|
|
124
|
-
export default function Colors({ env, files, fallback }: { env: Env; files: RepoFile[]; fallback?: boolean }) {
|
|
125
|
-
const notify = env.notify;
|
|
126
|
-
const loaded = useMemo(() => colorsOf(files), [files]);
|
|
127
|
-
const [colors, setColors] = useState<ColorsData>(loaded);
|
|
128
|
-
const [saving, setSaving] = useState(false);
|
|
129
|
-
// It starts on the generator, or, on the page with the site's own colours (the fallback), in the mode the editor is
|
|
130
|
-
// shown in.
|
|
131
|
-
const [tab, setTab] = useState<Tab>(fallback ? shownMode : 'generator');
|
|
132
|
-
// The mode the editor is shown in: the tab's, or on the generator the last one open (at first the one it is in).
|
|
133
|
-
const [mode, setMode] = useState<Mode>(shownMode);
|
|
134
|
-
useEffect(() => holdTheme(mode), [mode]);
|
|
135
|
-
const show = (tab: Tab) => {
|
|
136
|
-
setTab(tab);
|
|
137
|
-
if (tab !== 'generator') setMode(tab);
|
|
138
|
-
};
|
|
139
|
-
const changing = useMemo(() => colorApps(files), [files]);
|
|
140
|
-
useEffect(() => () => holdTheme(null), []);
|
|
141
|
-
// While an app changes the pages' colours, the modes show the pages', and the site's own are on a page of the form.
|
|
142
|
-
useFormPages(changing.length ? ['udseende/farver', FALLBACK_ROUTE] : []);
|
|
143
|
-
const ownShown = fallback || !changing.length;
|
|
144
|
-
const appNames = new Intl.ListFormat(locale).format(changing.map((a) => a.label));
|
|
145
|
-
const file = files.find((f) => f.path === config.colors);
|
|
146
|
-
const text = fileOf(colors);
|
|
147
|
-
// Not changed while they are the file's (with the colours one written before them did not have, which a save adds).
|
|
148
|
-
const dirty = text !== file?.text && (!!loaded.unreadable || text !== fileOf(loaded));
|
|
149
|
-
const low = Object.fromEntries(MODES.map((m) => [m, lowContrast(colors[m], colorPairs)])) as Record<Mode, ReturnType<typeof lowContrast>>;
|
|
150
|
-
const isDefault = text === fileOf(defaultColors);
|
|
151
|
-
// The colours to pick from in a colour's sheet: the site's own, in both modes, and the ones it was made with.
|
|
152
|
-
const swatches = [...new Set([colors, defaultColors].flatMap((c) => MODES.flatMap((m) => names.map((n) => c[m][n].toLowerCase()))))];
|
|
153
|
-
const dynamic = colors.dynamic;
|
|
154
|
-
const d = t.colors.dynamic;
|
|
155
|
-
|
|
156
|
-
// A colour changed by hand: they are no longer made from the source colour.
|
|
157
|
-
const set = (mode: Mode, name: string, value: string) => {
|
|
158
|
-
if (dynamic) notify({ text: d.turnedOff });
|
|
159
|
-
setColors(({ dynamic: _, ...c }) => ({ ...c, [mode]: { ...c[mode], [name]: value } }));
|
|
160
|
-
};
|
|
161
|
-
// The generator's choice: the one the colours were made from, else where it starts (the accent colour in light mode,
|
|
162
|
-
// the first harmony, Material's standard contrast). Choosing makes the colours from it.
|
|
163
|
-
const choiceOf = (c: ColorsData): Dynamic => c.dynamic ?? { source: c.light.accent, harmony: HARMONIES[0], contrast: 'standard' };
|
|
164
|
-
const choice = choiceOf(colors);
|
|
165
|
-
const update = (change: (d: Dynamic) => Dynamic) => setColors((c) => ({ ...c, ...madeFrom(change(choiceOf(c))) }));
|
|
166
|
-
const setDynamic = (change: Partial<Dynamic>) => update((d) => ({ ...d, ...change }));
|
|
167
|
-
|
|
168
|
-
async function fromPicture() {
|
|
169
|
-
const file = await pickImage();
|
|
170
|
-
if (!file) return;
|
|
171
|
-
try {
|
|
172
|
-
const found = await colorsInPicture(file);
|
|
173
|
-
if (!found.length) return notify({ text: d.noColorInPicture, error: true });
|
|
174
|
-
setDynamic({ source: found[0] });
|
|
175
|
-
notify({ text: d.fromPictureDone });
|
|
176
|
-
} catch {
|
|
177
|
-
notify({ text: t.app.pictureNotOpened, error: true });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function save() {
|
|
182
|
-
// The colours that are hard to read are shown with the mode they are in.
|
|
183
|
-
const hard = MODES.find((m) => low[m].length);
|
|
184
|
-
if (hard) {
|
|
185
|
-
show(hard);
|
|
186
|
-
if (!ownShown) location.hash = `#${FALLBACK_ROUTE}`;
|
|
187
|
-
return notify({ text: t.colors.cannotSave, error: true });
|
|
188
|
-
}
|
|
189
|
-
const path = config.colors;
|
|
190
|
-
const changes = [{ path, content: text, oid: file?.oid || null }];
|
|
191
|
-
setSaving(true);
|
|
192
|
-
notify({ text: t.common.saving });
|
|
193
|
-
try {
|
|
194
|
-
const oids = await env.commit(siteT.colors.commit, changes, CHANGE);
|
|
195
|
-
env.onSaved({ path, oid: oids[path], text });
|
|
196
|
-
notify({ text: savedText(CHANGE), kind: 'saved' });
|
|
197
|
-
return true;
|
|
198
|
-
} catch (e) {
|
|
199
|
-
notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
200
|
-
} finally {
|
|
201
|
-
setSaving(false);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<FormFrame
|
|
207
|
-
title={changing.length && fallback ? t.colors.pages.fallback : t.common.parts.colors}
|
|
208
|
-
back={
|
|
209
|
-
changing.length && fallback
|
|
210
|
-
? { href: '#udseende/farver', label: t.common.parts.colors, guard: (go) => go() }
|
|
211
|
-
: { href: '#udseende', label: t.common.parts.appearance }
|
|
212
|
-
}
|
|
213
|
-
dirty={dirty}
|
|
214
|
-
saving={saving}
|
|
215
|
-
onSave={save}
|
|
216
|
-
panel
|
|
217
|
-
tabs={{
|
|
218
|
-
label: t.colors.modes,
|
|
219
|
-
value: tab,
|
|
220
|
-
options: [
|
|
221
|
-
{ value: 'generator', label: d.tab, icon: Palette },
|
|
222
|
-
{ value: 'light', label: t.colors.light, icon: Sun },
|
|
223
|
-
{ value: 'dark', label: t.colors.dark, icon: Moon },
|
|
224
|
-
],
|
|
225
|
-
onChange: show,
|
|
226
|
-
}}
|
|
227
|
-
>
|
|
228
|
-
{tab === 'generator' ? (
|
|
229
|
-
generator()
|
|
230
|
-
) : ownShown ? (
|
|
231
|
-
modeColors()
|
|
232
|
-
) : (
|
|
233
|
-
<>
|
|
234
|
-
{/* The pages' colours come from the apps: said first, with the way to the site's own, the fallback. */}
|
|
235
|
-
<Notice
|
|
236
|
-
title={t.colors.pages.title}
|
|
237
|
-
action={
|
|
238
|
-
<LinkButton kind="outlined" size="small" href={`#${FALLBACK_ROUTE}`}>
|
|
239
|
-
{t.colors.pages.showFallback}
|
|
240
|
-
</LinkButton>
|
|
241
|
-
}
|
|
242
|
-
>
|
|
243
|
-
{t.colors.pages.byApps(appNames)}
|
|
244
|
-
</Notice>
|
|
245
|
-
<PageColors env={env} files={files} apps={changing} mode={mode} />
|
|
246
|
-
</>
|
|
247
|
-
)}
|
|
248
|
-
</FormFrame>
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
// A mode's little page and the pairs in it that are too hard to read.
|
|
252
|
-
function preview(mode: Mode) {
|
|
253
|
-
return (
|
|
254
|
-
<section className="space-y-4">
|
|
255
|
-
<ColorsPreview colors={colors[mode]} title={t.colors.preview.title(t.colors[mode])} />
|
|
256
|
-
{low[mode].length > 0 && (
|
|
257
|
-
<ProblemNote alert>
|
|
258
|
-
<ul className="space-y-1">
|
|
259
|
-
{low[mode].map((pair) => (
|
|
260
|
-
<li key={`${pair.text}-${pair.on}`}>{t.colors.tooLow(pair.what, ratio(pair.ratio), ratio(pair.min))}</li>
|
|
261
|
-
))}
|
|
262
|
-
</ul>
|
|
263
|
-
</ProblemNote>
|
|
264
|
-
)}
|
|
265
|
-
</section>
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// "Generator": dynamic colour, for both modes: the three base colours, the colour wheel with the harmonies, the
|
|
270
|
-
// source colour and the contrast, and the little page in each mode beside them (on a phone under them).
|
|
271
|
-
function generator() {
|
|
272
|
-
const dots = wheelDots(choice);
|
|
273
|
-
return (
|
|
274
|
-
<>
|
|
275
|
-
{loaded.unreadable && file && <ProblemNote>{t.colors.unreadable}</ProblemNote>}
|
|
276
|
-
<FormColumns
|
|
277
|
-
cards={false}
|
|
278
|
-
sticky
|
|
279
|
-
main={
|
|
280
|
-
<section className="space-y-5">
|
|
281
|
-
<FormField label={d.base}>
|
|
282
|
-
<div className="space-y-3">
|
|
283
|
-
<ColorTiles
|
|
284
|
-
label={d.base}
|
|
285
|
-
colors={BASE_COLORS.map((which, i) => ({
|
|
286
|
-
color: dots[i].color,
|
|
287
|
-
name: [d.source, t.colors.preview.secondary, t.colors.preview.tertiary][i],
|
|
288
|
-
disabled: !canSetBaseColor(choice, which),
|
|
289
|
-
onChange: (color, lock) => update((c) => withBaseColor(c, which, color, lock)),
|
|
290
|
-
// Only the source colour has a lock.
|
|
291
|
-
...(which === 'source' && {
|
|
292
|
-
locked: !!choice.sourceLocked,
|
|
293
|
-
onLock: (locked: boolean) => update(({ sourceLocked: _, ...c }) => (locked ? { ...c, sourceLocked: true } : c)),
|
|
294
|
-
}),
|
|
295
|
-
}))}
|
|
296
|
-
/>
|
|
297
|
-
<Button kind="outlined" size="small" icon={Image} onClick={fromPicture}>
|
|
298
|
-
{d.fromPicture}
|
|
299
|
-
</Button>
|
|
300
|
-
</div>
|
|
301
|
-
</FormField>
|
|
302
|
-
{/* Named by the harmony chosen, else (a file with a variant) "Harmoni". */}
|
|
303
|
-
<FormField label={choice.harmony ? d.harmonies[choice.harmony] : d.harmony} hint={d.harmonyHint}>
|
|
304
|
-
<div className="space-y-4">
|
|
305
|
-
<ChipSet label={d.harmony} scroll>
|
|
306
|
-
{HARMONIES.map((h) => (
|
|
307
|
-
<FilterChip key={h} selected={choice.harmony === h} onClick={() => setDynamic({ harmony: h, variant: undefined })}>
|
|
308
|
-
{d.harmonies[h]}
|
|
309
|
-
</FilterChip>
|
|
310
|
-
))}
|
|
311
|
-
</ChipSet>
|
|
312
|
-
<ColorWheel
|
|
313
|
-
edge={wheel.edge}
|
|
314
|
-
middle={wheel.middle}
|
|
315
|
-
dots={dots.map((dot, i) => ({ ...dot, locked: !i && !!choice.sourceLocked }))}
|
|
316
|
-
label={d.wheel}
|
|
317
|
-
valueText={choice.source}
|
|
318
|
-
disabled={choice.sourceLocked}
|
|
319
|
-
onMove={(x, y) =>
|
|
320
|
-
// A harmony from the wheel on, as the dots are its (a variant's palettes do not follow the chroma).
|
|
321
|
-
update((c) => ({ ...c, source: wheelColor(x, y), harmony: c.harmony ?? HARMONIES[0], variant: undefined }))
|
|
322
|
-
}
|
|
323
|
-
/>
|
|
324
|
-
</div>
|
|
325
|
-
</FormField>
|
|
326
|
-
<FormField label={d.contrast} hint={d.contrastHint}>
|
|
327
|
-
<SegmentedButton<Contrast>
|
|
328
|
-
label={d.contrast}
|
|
329
|
-
value={choice.contrast}
|
|
330
|
-
options={(Object.keys(CONTRASTS) as Contrast[]).map((c) => ({ value: c, label: d.contrasts[c] }))}
|
|
331
|
-
onChange={(contrast) => setDynamic({ contrast })}
|
|
332
|
-
/>
|
|
333
|
-
</FormField>
|
|
334
|
-
</section>
|
|
335
|
-
}
|
|
336
|
-
side={
|
|
337
|
-
<div className="space-y-6">
|
|
338
|
-
{MODES.map((m) => (
|
|
339
|
-
<div key={m}>{preview(m)}</div>
|
|
340
|
-
))}
|
|
341
|
-
</div>
|
|
342
|
-
}
|
|
343
|
-
/>
|
|
344
|
-
</>
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// A mode's colours: the preview and the fields (a function, not a component, so the fields keep their focus).
|
|
349
|
-
function modeColors() {
|
|
350
|
-
return (
|
|
351
|
-
<>
|
|
352
|
-
<p className="text-muted">{changing.length ? t.colors.pages.fallbackIntro(appNames) : t.colors.intro}</p>
|
|
353
|
-
{loaded.unreadable && file && <ProblemNote>{t.colors.unreadable}</ProblemNote>}
|
|
354
|
-
{/* On a wide computer screen the colours in the wide column, and the preview beside them, in view
|
|
355
|
-
while they are changed. */}
|
|
356
|
-
<FormColumns
|
|
357
|
-
cards={false}
|
|
358
|
-
sticky
|
|
359
|
-
first={preview(mode)}
|
|
360
|
-
main={
|
|
361
|
-
<>
|
|
362
|
-
<div className="grid gap-5 sm:grid-cols-2">
|
|
363
|
-
{colorFields.map((field) => {
|
|
364
|
-
const id = `color-${mode}-${field.name}`;
|
|
365
|
-
return (
|
|
366
|
-
<div key={field.name}>
|
|
367
|
-
<Label htmlFor={id} hint={field.hint}>
|
|
368
|
-
{field.label}
|
|
369
|
-
</Label>
|
|
370
|
-
<ColorField
|
|
371
|
-
id={id}
|
|
372
|
-
label={field.label}
|
|
373
|
-
value={colors[mode][field.name]}
|
|
374
|
-
swatches={swatches}
|
|
375
|
-
onChange={(v) => set(mode, field.name, v)}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
);
|
|
379
|
-
})}
|
|
380
|
-
</div>
|
|
381
|
-
<section className="space-y-2">
|
|
382
|
-
<Button kind="outlined" size="small" disabled={isDefault || saving} onClick={() => setColors(defaultColors)}>
|
|
383
|
-
{t.colors.standard}
|
|
384
|
-
</Button>
|
|
385
|
-
<SupportingText>{t.colors.standardHint}</SupportingText>
|
|
386
|
-
<SupportingText>{t.colors.icons}</SupportingText>
|
|
387
|
-
</section>
|
|
388
|
-
</>
|
|
389
|
-
}
|
|
390
|
-
/>
|
|
391
|
-
</>
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** A page's colours as the site was last built (packages/editor/scripts/page-colors.mjs): its address, its colours in both modes, and,
|
|
397
|
-
* when an app changed them, the app (its id) and the picture they came from. */
|
|
398
|
-
type BuiltPage = { path: string; light: Record<string, string>; dark: Record<string, string>; app?: string; picture?: string };
|
|
399
|
-
|
|
400
|
-
// The colours shown of a page, by their names: the accent colour, the light colour, the background and the text.
|
|
401
|
-
const SHOWN = ['accent', 'tint', 'bg', 'ink'];
|
|
402
|
-
|
|
403
|
-
/** "Sider": the apps that change the pages' colours, each to its settings, and each page's colours as the site was last
|
|
404
|
-
* built, those from a picture first. */
|
|
405
|
-
function PageColors({ env, files, apps, mode }: { env: Env; files: RepoFile[]; apps: SiteApp[]; mode: Mode }) {
|
|
406
|
-
const p = t.colors.pages;
|
|
407
|
-
// undefined while they are fetched, null when the site has none (not built since, or not built at all).
|
|
408
|
-
const [pages, setPages] = useState<BuiltPage[] | null>();
|
|
409
|
-
useEffect(() => {
|
|
410
|
-
let current = true;
|
|
411
|
-
fetch('/editor/page-colors.json', { cache: 'no-store' })
|
|
412
|
-
.then((r) => (r.ok ? r.json() : null))
|
|
413
|
-
.then((json) => (Array.isArray(json?.pages) ? (json.pages as BuiltPage[]) : null))
|
|
414
|
-
.catch(() => null)
|
|
415
|
-
.then((found) => current && setPages(found));
|
|
416
|
-
return () => {
|
|
417
|
-
current = false;
|
|
418
|
-
};
|
|
419
|
-
}, []);
|
|
420
|
-
const settings = useMemo(() => appSettings(files, apps), [files, apps]);
|
|
421
|
-
const { relations } = env;
|
|
422
|
-
// A page's name, as the editor has it, else its address.
|
|
423
|
-
const nameAt = (path: string) =>
|
|
424
|
-
[...relations.fixed, ...relations.pages].find((o) => o.value === path)?.label ??
|
|
425
|
-
relations.blog.find((o) => `/${o.value}/` === path)?.label ??
|
|
426
|
-
relations.categories.find((o) => `/blog/kategori/${o.value}/` === path)?.label ??
|
|
427
|
-
path;
|
|
428
|
-
// An app's settings chosen in a list ("Kontrast: Standard · Variant: Som indholdet").
|
|
429
|
-
const chosen = (app: SiteApp) =>
|
|
430
|
-
(app.settings?.fields ?? [])
|
|
431
|
-
.flatMap((f) => {
|
|
432
|
-
const option = f.options?.find((o) => o.value === settings[app.id]?.[f.name]);
|
|
433
|
-
return option ? [`${f.label}: ${option.label}`] : [];
|
|
434
|
-
})
|
|
435
|
-
.join(' · ');
|
|
436
|
-
const fromApp = pages?.filter((page) => page.app) ?? [];
|
|
437
|
-
const own = pages?.filter((page) => !page.app) ?? [];
|
|
438
|
-
const row = (page: BuiltPage) => (
|
|
439
|
-
<ListItem
|
|
440
|
-
key={page.path}
|
|
441
|
-
leading={page.picture && <LeadingImage src={shownAt(splitPicture(page.picture).src)} />}
|
|
442
|
-
headline={nameAt(page.path)}
|
|
443
|
-
supporting={page.path}
|
|
444
|
-
truncate
|
|
445
|
-
trailing={<ColorSwatches colors={SHOWN.map((n) => page[mode][n])} label={p.swatches(t.colors[mode])} />}
|
|
446
|
-
/>
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
return (
|
|
450
|
-
<>
|
|
451
|
-
<p className="text-muted">{p.intro}</p>
|
|
452
|
-
<Group title={p.apps}>
|
|
453
|
-
{apps.map((app) => (
|
|
454
|
-
<ListItem
|
|
455
|
-
key={app.id}
|
|
456
|
-
href={`#${settingsRoute(app) ?? `apps/${app.id}`}`}
|
|
457
|
-
icon={app.icon}
|
|
458
|
-
headline={app.label}
|
|
459
|
-
supporting={chosen(app)}
|
|
460
|
-
arrow
|
|
461
|
-
/>
|
|
462
|
-
))}
|
|
463
|
-
</Group>
|
|
464
|
-
{pages === undefined ? (
|
|
465
|
-
<EmptyNote busy>{p.loading}</EmptyNote>
|
|
466
|
-
) : pages === null ? (
|
|
467
|
-
<EmptyNote>{p.missing}</EmptyNote>
|
|
468
|
-
) : (
|
|
469
|
-
<>
|
|
470
|
-
{fromApp.length > 0 && <Group title={p.fromApp}>{fromApp.map(row)}</Group>}
|
|
471
|
-
{own.length > 0 && <Group title={p.own}>{own.map(row)}</Group>}
|
|
472
|
-
</>
|
|
473
|
-
)}
|
|
474
|
-
</>
|
|
475
|
-
);
|
|
476
|
-
}
|
|
1
|
+
// "Udseende" › "Farver" (#udseende/farver): the site's colours, for light and dark mode, in the site's file of
|
|
2
|
+
// colours (config.colors, packages/editor/lib/colors.ts). The first tab, "Generator", where the screen starts, or one
|
|
3
|
+
// mode at a time, picked with the tabs after it, and the editor is shown in the mode while the screen is open
|
|
4
|
+
// (theme.ts holdTheme). Each mode has a little page drawn in its colours, the pairs that are too hard to
|
|
5
|
+
// read (colorPairs in @site/colors, WCAG AA) and a field for each colour (colorFields). Colours
|
|
6
|
+
// that are hard to read together cannot be saved; "Gem" then shows the mode they are in. "Brug
|
|
7
|
+
// standardfarverne" puts back the ones the site was designed with (defaultColors). The generator is dynamic colour for
|
|
8
|
+
// both modes at once, with its three base colours at the top (ColorTiles) and the little page in each mode under the
|
|
9
|
+
// fields: it
|
|
10
|
+
// makes all of them, in both modes, from one source colour, Material 3's way (dynamic colour,
|
|
11
|
+
// lib/dynamic-colors.ts, each colour by its role, colorRoles): the source colour picked or taken from a
|
|
12
|
+
// picture, or dragged on a colour wheel (ColorWheel), a colour harmony (HARMONIES: where the secondary and tertiary
|
|
13
|
+
// colours are on the wheel from it) and a contrast level, kept in the file with the colours. Choosing one of them makes
|
|
14
|
+
// the colours from them; changing a colour by hand turns it off. Material's other source of dynamic colour, each page's own picture, is an app ("Indholdsbaserede
|
|
15
|
+
// farver", src/apps/content-colors/: pageColors in packages/editor/site/pageColors.ts). While an app installed changes the pages'
|
|
16
|
+
// colours (hooks/page-colors.ts), each mode shows the pages' colours instead (PageColors), as the site was last built
|
|
17
|
+
// (/editor/page-colors.json, packages/editor/scripts/page-colors.mjs): first a notice that the apps give them, with a button to the
|
|
18
|
+
// site's own, which are only the fallback ("Reservefarver", #udseende/farver/reservefarver, a page of the same form, so
|
|
19
|
+
// what is unsaved stays), then the apps, each to its settings, the pages with colours from a picture (the picture, the
|
|
20
|
+
// page and its colours in the mode open), and those with the site's own. Saved with "Gem ændringer" in one commit; the
|
|
21
|
+
// editor shows the new colours at once (theme.ts applyColors), the site when it is built again.
|
|
22
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
23
|
+
import { Image, Moon, Palette, Sun } from '@phosphor-icons/react';
|
|
24
|
+
import { ConflictError, type RepoFile } from './github';
|
|
25
|
+
import FormFrame, { useFormPages } from './FormFrame';
|
|
26
|
+
import {
|
|
27
|
+
Button,
|
|
28
|
+
ChipSet,
|
|
29
|
+
ColorField,
|
|
30
|
+
ColorSwatches,
|
|
31
|
+
ColorTiles,
|
|
32
|
+
ColorWheel,
|
|
33
|
+
ColorsPreview,
|
|
34
|
+
EmptyNote,
|
|
35
|
+
FilterChip,
|
|
36
|
+
FormColumns,
|
|
37
|
+
FormField,
|
|
38
|
+
Group,
|
|
39
|
+
Label,
|
|
40
|
+
LeadingImage,
|
|
41
|
+
LinkButton,
|
|
42
|
+
Notice,
|
|
43
|
+
SupportingText,
|
|
44
|
+
SegmentedButton,
|
|
45
|
+
ProblemNote,
|
|
46
|
+
ListItem,
|
|
47
|
+
} from './ui';
|
|
48
|
+
import type { Env } from './App';
|
|
49
|
+
import { savedText } from './deploy';
|
|
50
|
+
import { colorProblems, lowContrast, MODES, type Colors as ColorsData, type Mode } from '../lib/colors';
|
|
51
|
+
import {
|
|
52
|
+
completeColors,
|
|
53
|
+
CONTRASTS,
|
|
54
|
+
dynamicColors,
|
|
55
|
+
dynamicProblems,
|
|
56
|
+
BASE_COLORS,
|
|
57
|
+
HARMONIES,
|
|
58
|
+
canSetBaseColor,
|
|
59
|
+
withBaseColor,
|
|
60
|
+
wheelColor,
|
|
61
|
+
wheelColors,
|
|
62
|
+
wheelDots,
|
|
63
|
+
type Contrast,
|
|
64
|
+
type Dynamic,
|
|
65
|
+
} from '../lib/dynamic-colors';
|
|
66
|
+
import { colorRoles, config, defaultColors } from '@site/editor';
|
|
67
|
+
import { colorsInPicture, pickImage, shownAt } from './images';
|
|
68
|
+
import { holdTheme, shownMode } from './theme';
|
|
69
|
+
import { locale, siteT, t } from './i18n';
|
|
70
|
+
import { colorFields, colorPairs, apps as siteApps } from './site';
|
|
71
|
+
import { appSettings, installedApps, type SiteApp } from './apps';
|
|
72
|
+
import { settingsRoute } from './AppList';
|
|
73
|
+
import { splitPicture } from '../lib/crop.mjs';
|
|
74
|
+
|
|
75
|
+
const names = colorFields.map((f) => f.name);
|
|
76
|
+
|
|
77
|
+
/** The site's colours as the editor has them (with those a file written before them does not have made, completeColors),
|
|
78
|
+
* or the ones it was designed with when the file is missing or cannot be read (`unreadable`). */
|
|
79
|
+
export function colorsOf(files: RepoFile[]): ColorsData & { unreadable?: boolean } {
|
|
80
|
+
const file = files.find((f) => f.path === config.colors);
|
|
81
|
+
try {
|
|
82
|
+
const colors = completeColors(JSON.parse(file?.text ?? ''), colorRoles);
|
|
83
|
+
if (!colorProblems(colors, names).length && (colors.dynamic === undefined || !dynamicProblems(colors.dynamic).length)) return colors;
|
|
84
|
+
} catch {}
|
|
85
|
+
return { ...defaultColors, unreadable: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fileOf = (colors: ColorsData) => {
|
|
89
|
+
const sets = Object.fromEntries(MODES.map((m) => [m, Object.fromEntries(names.map((n) => [n, colors[m][n]]))]));
|
|
90
|
+
const d = colors.dynamic;
|
|
91
|
+
const dynamic = d && {
|
|
92
|
+
source: d.source,
|
|
93
|
+
...(d.harmony ? { harmony: d.harmony } : { variant: d.variant }),
|
|
94
|
+
contrast: d.contrast,
|
|
95
|
+
...(d.sourceLocked && { sourceLocked: true }),
|
|
96
|
+
};
|
|
97
|
+
return `${JSON.stringify({ ...sets, ...(dynamic && { dynamic }) }, null, 2)}\n`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** The colours made from a choice of dynamic colour, with the choice. */
|
|
101
|
+
const madeFrom = (dynamic: Dynamic): Pick<ColorsData, 'light' | 'dark' | 'dynamic'> => ({ ...dynamicColors(dynamic, colorRoles), dynamic });
|
|
102
|
+
|
|
103
|
+
// The colour wheel's edge and middle, the same for every choice.
|
|
104
|
+
const wheel = wheelColors();
|
|
105
|
+
|
|
106
|
+
const ratio = (n: number) => n.toLocaleString(locale, { maximumFractionDigits: 1 });
|
|
107
|
+
|
|
108
|
+
// What a save of the colours changed (deploy.ts).
|
|
109
|
+
const CHANGE = t.app.changes.edited(t.app.changes.colors);
|
|
110
|
+
|
|
111
|
+
/** The screen's tabs: a mode's colours, or the generator (dynamic colour), for both. */
|
|
112
|
+
type Tab = Mode | 'generator';
|
|
113
|
+
|
|
114
|
+
/** The page with the site's own colours, while an app changes the pages' colours: the fallback. */
|
|
115
|
+
export const FALLBACK_ROUTE = 'udseende/farver/reservefarver';
|
|
116
|
+
|
|
117
|
+
/** The apps installed that change the pages' colours (hooks/page-colors.ts). */
|
|
118
|
+
const colorApps = (files: RepoFile[]) => {
|
|
119
|
+
const installed = installedApps(files);
|
|
120
|
+
return siteApps.filter((a) => installed.includes(a.id) && a.needs.changes?.includes('page-colors'));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** `fallback`: the page with the site's own colours is open (FALLBACK_ROUTE). */
|
|
124
|
+
export default function Colors({ env, files, fallback }: { env: Env; files: RepoFile[]; fallback?: boolean }) {
|
|
125
|
+
const notify = env.notify;
|
|
126
|
+
const loaded = useMemo(() => colorsOf(files), [files]);
|
|
127
|
+
const [colors, setColors] = useState<ColorsData>(loaded);
|
|
128
|
+
const [saving, setSaving] = useState(false);
|
|
129
|
+
// It starts on the generator, or, on the page with the site's own colours (the fallback), in the mode the editor is
|
|
130
|
+
// shown in.
|
|
131
|
+
const [tab, setTab] = useState<Tab>(fallback ? shownMode : 'generator');
|
|
132
|
+
// The mode the editor is shown in: the tab's, or on the generator the last one open (at first the one it is in).
|
|
133
|
+
const [mode, setMode] = useState<Mode>(shownMode);
|
|
134
|
+
useEffect(() => holdTheme(mode), [mode]);
|
|
135
|
+
const show = (tab: Tab) => {
|
|
136
|
+
setTab(tab);
|
|
137
|
+
if (tab !== 'generator') setMode(tab);
|
|
138
|
+
};
|
|
139
|
+
const changing = useMemo(() => colorApps(files), [files]);
|
|
140
|
+
useEffect(() => () => holdTheme(null), []);
|
|
141
|
+
// While an app changes the pages' colours, the modes show the pages', and the site's own are on a page of the form.
|
|
142
|
+
useFormPages(changing.length ? ['udseende/farver', FALLBACK_ROUTE] : []);
|
|
143
|
+
const ownShown = fallback || !changing.length;
|
|
144
|
+
const appNames = new Intl.ListFormat(locale).format(changing.map((a) => a.label));
|
|
145
|
+
const file = files.find((f) => f.path === config.colors);
|
|
146
|
+
const text = fileOf(colors);
|
|
147
|
+
// Not changed while they are the file's (with the colours one written before them did not have, which a save adds).
|
|
148
|
+
const dirty = text !== file?.text && (!!loaded.unreadable || text !== fileOf(loaded));
|
|
149
|
+
const low = Object.fromEntries(MODES.map((m) => [m, lowContrast(colors[m], colorPairs)])) as Record<Mode, ReturnType<typeof lowContrast>>;
|
|
150
|
+
const isDefault = text === fileOf(defaultColors);
|
|
151
|
+
// The colours to pick from in a colour's sheet: the site's own, in both modes, and the ones it was made with.
|
|
152
|
+
const swatches = [...new Set([colors, defaultColors].flatMap((c) => MODES.flatMap((m) => names.map((n) => c[m][n].toLowerCase()))))];
|
|
153
|
+
const dynamic = colors.dynamic;
|
|
154
|
+
const d = t.colors.dynamic;
|
|
155
|
+
|
|
156
|
+
// A colour changed by hand: they are no longer made from the source colour.
|
|
157
|
+
const set = (mode: Mode, name: string, value: string) => {
|
|
158
|
+
if (dynamic) notify({ text: d.turnedOff });
|
|
159
|
+
setColors(({ dynamic: _, ...c }) => ({ ...c, [mode]: { ...c[mode], [name]: value } }));
|
|
160
|
+
};
|
|
161
|
+
// The generator's choice: the one the colours were made from, else where it starts (the accent colour in light mode,
|
|
162
|
+
// the first harmony, Material's standard contrast). Choosing makes the colours from it.
|
|
163
|
+
const choiceOf = (c: ColorsData): Dynamic => c.dynamic ?? { source: c.light.accent, harmony: HARMONIES[0], contrast: 'standard' };
|
|
164
|
+
const choice = choiceOf(colors);
|
|
165
|
+
const update = (change: (d: Dynamic) => Dynamic) => setColors((c) => ({ ...c, ...madeFrom(change(choiceOf(c))) }));
|
|
166
|
+
const setDynamic = (change: Partial<Dynamic>) => update((d) => ({ ...d, ...change }));
|
|
167
|
+
|
|
168
|
+
async function fromPicture() {
|
|
169
|
+
const file = await pickImage();
|
|
170
|
+
if (!file) return;
|
|
171
|
+
try {
|
|
172
|
+
const found = await colorsInPicture(file);
|
|
173
|
+
if (!found.length) return notify({ text: d.noColorInPicture, error: true });
|
|
174
|
+
setDynamic({ source: found[0] });
|
|
175
|
+
notify({ text: d.fromPictureDone });
|
|
176
|
+
} catch {
|
|
177
|
+
notify({ text: t.app.pictureNotOpened, error: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function save() {
|
|
182
|
+
// The colours that are hard to read are shown with the mode they are in.
|
|
183
|
+
const hard = MODES.find((m) => low[m].length);
|
|
184
|
+
if (hard) {
|
|
185
|
+
show(hard);
|
|
186
|
+
if (!ownShown) location.hash = `#${FALLBACK_ROUTE}`;
|
|
187
|
+
return notify({ text: t.colors.cannotSave, error: true });
|
|
188
|
+
}
|
|
189
|
+
const path = config.colors;
|
|
190
|
+
const changes = [{ path, content: text, oid: file?.oid || null }];
|
|
191
|
+
setSaving(true);
|
|
192
|
+
notify({ text: t.common.saving });
|
|
193
|
+
try {
|
|
194
|
+
const oids = await env.commit(siteT.colors.commit, changes, CHANGE);
|
|
195
|
+
env.onSaved({ path, oid: oids[path], text });
|
|
196
|
+
notify({ text: savedText(CHANGE), kind: 'saved' });
|
|
197
|
+
return true;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
200
|
+
} finally {
|
|
201
|
+
setSaving(false);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<FormFrame
|
|
207
|
+
title={changing.length && fallback ? t.colors.pages.fallback : t.common.parts.colors}
|
|
208
|
+
back={
|
|
209
|
+
changing.length && fallback
|
|
210
|
+
? { href: '#udseende/farver', label: t.common.parts.colors, guard: (go) => go() }
|
|
211
|
+
: { href: '#udseende', label: t.common.parts.appearance }
|
|
212
|
+
}
|
|
213
|
+
dirty={dirty}
|
|
214
|
+
saving={saving}
|
|
215
|
+
onSave={save}
|
|
216
|
+
panel
|
|
217
|
+
tabs={{
|
|
218
|
+
label: t.colors.modes,
|
|
219
|
+
value: tab,
|
|
220
|
+
options: [
|
|
221
|
+
{ value: 'generator', label: d.tab, icon: Palette },
|
|
222
|
+
{ value: 'light', label: t.colors.light, icon: Sun },
|
|
223
|
+
{ value: 'dark', label: t.colors.dark, icon: Moon },
|
|
224
|
+
],
|
|
225
|
+
onChange: show,
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
{tab === 'generator' ? (
|
|
229
|
+
generator()
|
|
230
|
+
) : ownShown ? (
|
|
231
|
+
modeColors()
|
|
232
|
+
) : (
|
|
233
|
+
<>
|
|
234
|
+
{/* The pages' colours come from the apps: said first, with the way to the site's own, the fallback. */}
|
|
235
|
+
<Notice
|
|
236
|
+
title={t.colors.pages.title}
|
|
237
|
+
action={
|
|
238
|
+
<LinkButton kind="outlined" size="small" href={`#${FALLBACK_ROUTE}`}>
|
|
239
|
+
{t.colors.pages.showFallback}
|
|
240
|
+
</LinkButton>
|
|
241
|
+
}
|
|
242
|
+
>
|
|
243
|
+
{t.colors.pages.byApps(appNames)}
|
|
244
|
+
</Notice>
|
|
245
|
+
<PageColors env={env} files={files} apps={changing} mode={mode} />
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
</FormFrame>
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// A mode's little page and the pairs in it that are too hard to read.
|
|
252
|
+
function preview(mode: Mode) {
|
|
253
|
+
return (
|
|
254
|
+
<section className="space-y-4">
|
|
255
|
+
<ColorsPreview colors={colors[mode]} title={t.colors.preview.title(t.colors[mode])} />
|
|
256
|
+
{low[mode].length > 0 && (
|
|
257
|
+
<ProblemNote alert>
|
|
258
|
+
<ul className="space-y-1">
|
|
259
|
+
{low[mode].map((pair) => (
|
|
260
|
+
<li key={`${pair.text}-${pair.on}`}>{t.colors.tooLow(pair.what, ratio(pair.ratio), ratio(pair.min))}</li>
|
|
261
|
+
))}
|
|
262
|
+
</ul>
|
|
263
|
+
</ProblemNote>
|
|
264
|
+
)}
|
|
265
|
+
</section>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// "Generator": dynamic colour, for both modes: the three base colours, the colour wheel with the harmonies, the
|
|
270
|
+
// source colour and the contrast, and the little page in each mode beside them (on a phone under them).
|
|
271
|
+
function generator() {
|
|
272
|
+
const dots = wheelDots(choice);
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
{loaded.unreadable && file && <ProblemNote>{t.colors.unreadable}</ProblemNote>}
|
|
276
|
+
<FormColumns
|
|
277
|
+
cards={false}
|
|
278
|
+
sticky
|
|
279
|
+
main={
|
|
280
|
+
<section className="space-y-5">
|
|
281
|
+
<FormField label={d.base}>
|
|
282
|
+
<div className="space-y-3">
|
|
283
|
+
<ColorTiles
|
|
284
|
+
label={d.base}
|
|
285
|
+
colors={BASE_COLORS.map((which, i) => ({
|
|
286
|
+
color: dots[i].color,
|
|
287
|
+
name: [d.source, t.colors.preview.secondary, t.colors.preview.tertiary][i],
|
|
288
|
+
disabled: !canSetBaseColor(choice, which),
|
|
289
|
+
onChange: (color, lock) => update((c) => withBaseColor(c, which, color, lock)),
|
|
290
|
+
// Only the source colour has a lock.
|
|
291
|
+
...(which === 'source' && {
|
|
292
|
+
locked: !!choice.sourceLocked,
|
|
293
|
+
onLock: (locked: boolean) => update(({ sourceLocked: _, ...c }) => (locked ? { ...c, sourceLocked: true } : c)),
|
|
294
|
+
}),
|
|
295
|
+
}))}
|
|
296
|
+
/>
|
|
297
|
+
<Button kind="outlined" size="small" icon={Image} onClick={fromPicture}>
|
|
298
|
+
{d.fromPicture}
|
|
299
|
+
</Button>
|
|
300
|
+
</div>
|
|
301
|
+
</FormField>
|
|
302
|
+
{/* Named by the harmony chosen, else (a file with a variant) "Harmoni". */}
|
|
303
|
+
<FormField label={choice.harmony ? d.harmonies[choice.harmony] : d.harmony} hint={d.harmonyHint}>
|
|
304
|
+
<div className="space-y-4">
|
|
305
|
+
<ChipSet label={d.harmony} scroll>
|
|
306
|
+
{HARMONIES.map((h) => (
|
|
307
|
+
<FilterChip key={h} selected={choice.harmony === h} onClick={() => setDynamic({ harmony: h, variant: undefined })}>
|
|
308
|
+
{d.harmonies[h]}
|
|
309
|
+
</FilterChip>
|
|
310
|
+
))}
|
|
311
|
+
</ChipSet>
|
|
312
|
+
<ColorWheel
|
|
313
|
+
edge={wheel.edge}
|
|
314
|
+
middle={wheel.middle}
|
|
315
|
+
dots={dots.map((dot, i) => ({ ...dot, locked: !i && !!choice.sourceLocked }))}
|
|
316
|
+
label={d.wheel}
|
|
317
|
+
valueText={choice.source}
|
|
318
|
+
disabled={choice.sourceLocked}
|
|
319
|
+
onMove={(x, y) =>
|
|
320
|
+
// A harmony from the wheel on, as the dots are its (a variant's palettes do not follow the chroma).
|
|
321
|
+
update((c) => ({ ...c, source: wheelColor(x, y), harmony: c.harmony ?? HARMONIES[0], variant: undefined }))
|
|
322
|
+
}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
</FormField>
|
|
326
|
+
<FormField label={d.contrast} hint={d.contrastHint}>
|
|
327
|
+
<SegmentedButton<Contrast>
|
|
328
|
+
label={d.contrast}
|
|
329
|
+
value={choice.contrast}
|
|
330
|
+
options={(Object.keys(CONTRASTS) as Contrast[]).map((c) => ({ value: c, label: d.contrasts[c] }))}
|
|
331
|
+
onChange={(contrast) => setDynamic({ contrast })}
|
|
332
|
+
/>
|
|
333
|
+
</FormField>
|
|
334
|
+
</section>
|
|
335
|
+
}
|
|
336
|
+
side={
|
|
337
|
+
<div className="space-y-6">
|
|
338
|
+
{MODES.map((m) => (
|
|
339
|
+
<div key={m}>{preview(m)}</div>
|
|
340
|
+
))}
|
|
341
|
+
</div>
|
|
342
|
+
}
|
|
343
|
+
/>
|
|
344
|
+
</>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// A mode's colours: the preview and the fields (a function, not a component, so the fields keep their focus).
|
|
349
|
+
function modeColors() {
|
|
350
|
+
return (
|
|
351
|
+
<>
|
|
352
|
+
<p className="text-muted">{changing.length ? t.colors.pages.fallbackIntro(appNames) : t.colors.intro}</p>
|
|
353
|
+
{loaded.unreadable && file && <ProblemNote>{t.colors.unreadable}</ProblemNote>}
|
|
354
|
+
{/* On a wide computer screen the colours in the wide column, and the preview beside them, in view
|
|
355
|
+
while they are changed. */}
|
|
356
|
+
<FormColumns
|
|
357
|
+
cards={false}
|
|
358
|
+
sticky
|
|
359
|
+
first={preview(mode)}
|
|
360
|
+
main={
|
|
361
|
+
<>
|
|
362
|
+
<div className="grid gap-5 sm:grid-cols-2">
|
|
363
|
+
{colorFields.map((field) => {
|
|
364
|
+
const id = `color-${mode}-${field.name}`;
|
|
365
|
+
return (
|
|
366
|
+
<div key={field.name}>
|
|
367
|
+
<Label htmlFor={id} hint={field.hint}>
|
|
368
|
+
{field.label}
|
|
369
|
+
</Label>
|
|
370
|
+
<ColorField
|
|
371
|
+
id={id}
|
|
372
|
+
label={field.label}
|
|
373
|
+
value={colors[mode][field.name]}
|
|
374
|
+
swatches={swatches}
|
|
375
|
+
onChange={(v) => set(mode, field.name, v)}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
})}
|
|
380
|
+
</div>
|
|
381
|
+
<section className="space-y-2">
|
|
382
|
+
<Button kind="outlined" size="small" disabled={isDefault || saving} onClick={() => setColors(defaultColors)}>
|
|
383
|
+
{t.colors.standard}
|
|
384
|
+
</Button>
|
|
385
|
+
<SupportingText>{t.colors.standardHint}</SupportingText>
|
|
386
|
+
<SupportingText>{t.colors.icons}</SupportingText>
|
|
387
|
+
</section>
|
|
388
|
+
</>
|
|
389
|
+
}
|
|
390
|
+
/>
|
|
391
|
+
</>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** A page's colours as the site was last built (packages/editor/scripts/page-colors.mjs): its address, its colours in both modes, and,
|
|
397
|
+
* when an app changed them, the app (its id) and the picture they came from. */
|
|
398
|
+
type BuiltPage = { path: string; light: Record<string, string>; dark: Record<string, string>; app?: string; picture?: string };
|
|
399
|
+
|
|
400
|
+
// The colours shown of a page, by their names: the accent colour, the light colour, the background and the text.
|
|
401
|
+
const SHOWN = ['accent', 'tint', 'bg', 'ink'];
|
|
402
|
+
|
|
403
|
+
/** "Sider": the apps that change the pages' colours, each to its settings, and each page's colours as the site was last
|
|
404
|
+
* built, those from a picture first. */
|
|
405
|
+
function PageColors({ env, files, apps, mode }: { env: Env; files: RepoFile[]; apps: SiteApp[]; mode: Mode }) {
|
|
406
|
+
const p = t.colors.pages;
|
|
407
|
+
// undefined while they are fetched, null when the site has none (not built since, or not built at all).
|
|
408
|
+
const [pages, setPages] = useState<BuiltPage[] | null>();
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
let current = true;
|
|
411
|
+
fetch('/editor/page-colors.json', { cache: 'no-store' })
|
|
412
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
413
|
+
.then((json) => (Array.isArray(json?.pages) ? (json.pages as BuiltPage[]) : null))
|
|
414
|
+
.catch(() => null)
|
|
415
|
+
.then((found) => current && setPages(found));
|
|
416
|
+
return () => {
|
|
417
|
+
current = false;
|
|
418
|
+
};
|
|
419
|
+
}, []);
|
|
420
|
+
const settings = useMemo(() => appSettings(files, apps), [files, apps]);
|
|
421
|
+
const { relations } = env;
|
|
422
|
+
// A page's name, as the editor has it, else its address.
|
|
423
|
+
const nameAt = (path: string) =>
|
|
424
|
+
[...relations.fixed, ...relations.pages].find((o) => o.value === path)?.label ??
|
|
425
|
+
relations.blog.find((o) => `/${o.value}/` === path)?.label ??
|
|
426
|
+
relations.categories.find((o) => `/blog/kategori/${o.value}/` === path)?.label ??
|
|
427
|
+
path;
|
|
428
|
+
// An app's settings chosen in a list ("Kontrast: Standard · Variant: Som indholdet").
|
|
429
|
+
const chosen = (app: SiteApp) =>
|
|
430
|
+
(app.settings?.fields ?? [])
|
|
431
|
+
.flatMap((f) => {
|
|
432
|
+
const option = f.options?.find((o) => o.value === settings[app.id]?.[f.name]);
|
|
433
|
+
return option ? [`${f.label}: ${option.label}`] : [];
|
|
434
|
+
})
|
|
435
|
+
.join(' · ');
|
|
436
|
+
const fromApp = pages?.filter((page) => page.app) ?? [];
|
|
437
|
+
const own = pages?.filter((page) => !page.app) ?? [];
|
|
438
|
+
const row = (page: BuiltPage) => (
|
|
439
|
+
<ListItem
|
|
440
|
+
key={page.path}
|
|
441
|
+
leading={page.picture && <LeadingImage src={shownAt(splitPicture(page.picture).src)} />}
|
|
442
|
+
headline={nameAt(page.path)}
|
|
443
|
+
supporting={page.path}
|
|
444
|
+
truncate
|
|
445
|
+
trailing={<ColorSwatches colors={SHOWN.map((n) => page[mode][n])} label={p.swatches(t.colors[mode])} />}
|
|
446
|
+
/>
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<>
|
|
451
|
+
<p className="text-muted">{p.intro}</p>
|
|
452
|
+
<Group title={p.apps}>
|
|
453
|
+
{apps.map((app) => (
|
|
454
|
+
<ListItem
|
|
455
|
+
key={app.id}
|
|
456
|
+
href={`#${settingsRoute(app) ?? `apps/${app.id}`}`}
|
|
457
|
+
icon={app.icon}
|
|
458
|
+
headline={app.label}
|
|
459
|
+
supporting={chosen(app)}
|
|
460
|
+
arrow
|
|
461
|
+
/>
|
|
462
|
+
))}
|
|
463
|
+
</Group>
|
|
464
|
+
{pages === undefined ? (
|
|
465
|
+
<EmptyNote busy>{p.loading}</EmptyNote>
|
|
466
|
+
) : pages === null ? (
|
|
467
|
+
<EmptyNote>{p.missing}</EmptyNote>
|
|
468
|
+
) : (
|
|
469
|
+
<>
|
|
470
|
+
{fromApp.length > 0 && <Group title={p.fromApp}>{fromApp.map(row)}</Group>}
|
|
471
|
+
{own.length > 0 && <Group title={p.own}>{own.map(row)}</Group>}
|
|
472
|
+
</>
|
|
473
|
+
)}
|
|
474
|
+
</>
|
|
475
|
+
);
|
|
476
|
+
}
|