@jant/core 0.6.16 → 0.7.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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Settings Component
|
|
3
|
+
*
|
|
4
|
+
* The single home for every language setting: the site's content language, the
|
|
5
|
+
* dashboard's UI language, and multilingual content.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the General settings page, this component talks to the server itself
|
|
8
|
+
* rather than through the settings bridge. Its operations are not "save this
|
|
9
|
+
* form" — turning multilingual on stamps every existing post, removing a
|
|
10
|
+
* language can be refused, changing the primary language rewrites two settings
|
|
11
|
+
* at once — so each needs its own endpoint, its own confirmation, and its own
|
|
12
|
+
* error handling.
|
|
13
|
+
*
|
|
14
|
+
* Light DOM only — BaseCoat and Tailwind classes apply directly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { LitElement, html, nothing } from "lit";
|
|
18
|
+
import { showConfirmDialog } from "../confirm.js";
|
|
19
|
+
import { queueToastForNextPage, showToast } from "../toast.js";
|
|
20
|
+
import { getJsonString, readJsonObject } from "../json.js";
|
|
21
|
+
import { getJantDocsUrl } from "../../lib/jant-docs.js";
|
|
22
|
+
import {
|
|
23
|
+
LOCALE_PICKER_TRIGGER_CLASS,
|
|
24
|
+
type LocaleOption,
|
|
25
|
+
type LocalePickerLabels,
|
|
26
|
+
} from "./jant-locale-picker.js";
|
|
27
|
+
|
|
28
|
+
interface LanguageLabels {
|
|
29
|
+
siteSection: string;
|
|
30
|
+
dashboardSection: string;
|
|
31
|
+
contentLanguage: string;
|
|
32
|
+
contentLanguageHelp: string;
|
|
33
|
+
primaryLanguage: string;
|
|
34
|
+
primaryLanguageHelp: string;
|
|
35
|
+
languagesLabel: string;
|
|
36
|
+
primaryBadge: string;
|
|
37
|
+
makePrimary: string;
|
|
38
|
+
dashboardLanguage: string;
|
|
39
|
+
dashboardLanguageHelp: string;
|
|
40
|
+
followContent: string;
|
|
41
|
+
multilingual: string;
|
|
42
|
+
multilingualHelp: string;
|
|
43
|
+
multilingualDocs: string;
|
|
44
|
+
multilingualDocsHelp: string;
|
|
45
|
+
statusOn: string;
|
|
46
|
+
turnOn: string;
|
|
47
|
+
addMissingLanguage: string;
|
|
48
|
+
viewPosts: string;
|
|
49
|
+
otherLanguages: string;
|
|
50
|
+
addLanguage: string;
|
|
51
|
+
removeLanguage: string;
|
|
52
|
+
languageMenu: string;
|
|
53
|
+
enableTitle: string;
|
|
54
|
+
enableReassurance: string;
|
|
55
|
+
enableMarkTitle: string;
|
|
56
|
+
enableMarkWarning: string;
|
|
57
|
+
enableFixHint: string;
|
|
58
|
+
enableNeedsLanguage: string;
|
|
59
|
+
changePrimaryTitle: string;
|
|
60
|
+
changePrimaryBody: string;
|
|
61
|
+
changePrimaryConfirm: string;
|
|
62
|
+
disableTitle: string;
|
|
63
|
+
disableBody: string;
|
|
64
|
+
disableConfirm: string;
|
|
65
|
+
removeLastTitle: string;
|
|
66
|
+
removeLastBody: string;
|
|
67
|
+
removeLastConfirm: string;
|
|
68
|
+
cancel: string;
|
|
69
|
+
save: string;
|
|
70
|
+
saving: string;
|
|
71
|
+
searchPlaceholder: string;
|
|
72
|
+
noMatches: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface LanguageInitialState {
|
|
76
|
+
contentLanguage: string;
|
|
77
|
+
dashboardLanguage: string;
|
|
78
|
+
multilingualEnabled: boolean;
|
|
79
|
+
additionalLanguages: string[];
|
|
80
|
+
unmarkedPostCount: number;
|
|
81
|
+
sitePathPrefix: string;
|
|
82
|
+
/**
|
|
83
|
+
* Docs link, resolved by the server. Vite's `define` does not reach modules
|
|
84
|
+
* the dev server hands the browser, so the environment's docs host is only
|
|
85
|
+
* known server-side.
|
|
86
|
+
*/
|
|
87
|
+
multilingualDocsUrl: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Catalog locales the dashboard is translated into. */
|
|
91
|
+
const DASHBOARD_LOCALES = ["", "en", "zh-Hans", "zh-Hant"] as const;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The reference for everything this page cannot say in two lines: URL
|
|
95
|
+
* structure, per-language feeds, and how translation groups work. Same shape
|
|
96
|
+
* as the theming and API links elsewhere in settings. Used only if the server
|
|
97
|
+
* sent no link — it resolves the docs host for the deployed build, not for a
|
|
98
|
+
* dev server.
|
|
99
|
+
*/
|
|
100
|
+
const MULTILINGUAL_DOCS_FALLBACK_URL = getJantDocsUrl("multilingual");
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fill the `{name}` slots the server deliberately left intact.
|
|
104
|
+
*
|
|
105
|
+
* Plurals and every other value are already resolved by Lingui server-side —
|
|
106
|
+
* only values that are not known until the author interacts (the language they
|
|
107
|
+
* picked, the prefix it maps to) reach here.
|
|
108
|
+
*/
|
|
109
|
+
function interpolate(template: string, values: Record<string, string>): string {
|
|
110
|
+
// An unknown placeholder is left as-is rather than blanked: a copy change
|
|
111
|
+
// that renames a slot should read oddly, not silently drop a fact.
|
|
112
|
+
return template.replace(
|
|
113
|
+
/\{(\w+)\}/g,
|
|
114
|
+
(match, key: string) => values[key] ?? match,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class JantSettingsLanguage extends LitElement {
|
|
119
|
+
static properties = {
|
|
120
|
+
labels: { type: Object },
|
|
121
|
+
locales: { type: Array },
|
|
122
|
+
_contentLanguage: { state: true },
|
|
123
|
+
_dashboardLanguage: { state: true },
|
|
124
|
+
_multilingualEnabled: { state: true },
|
|
125
|
+
_additional: { state: true },
|
|
126
|
+
_unmarkedPostCount: { state: true },
|
|
127
|
+
_sitePathPrefix: { state: true },
|
|
128
|
+
_savingSite: { state: true },
|
|
129
|
+
_savingDashboard: { state: true },
|
|
130
|
+
_removeError: { state: true },
|
|
131
|
+
_rowMenuOpen: { state: true },
|
|
132
|
+
_enableError: { state: true },
|
|
133
|
+
_enableOpen: { state: true },
|
|
134
|
+
_enableAdditional: { state: true },
|
|
135
|
+
_enablePrimary: { state: true },
|
|
136
|
+
_enableBusy: { state: true },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
declare labels: LanguageLabels;
|
|
140
|
+
declare locales: LocaleOption[];
|
|
141
|
+
declare _contentLanguage: string;
|
|
142
|
+
declare _dashboardLanguage: string;
|
|
143
|
+
declare _multilingualEnabled: boolean;
|
|
144
|
+
declare _additional: string[];
|
|
145
|
+
declare _unmarkedPostCount: number;
|
|
146
|
+
declare _sitePathPrefix: string;
|
|
147
|
+
declare _savingSite: boolean;
|
|
148
|
+
declare _savingDashboard: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* The refusal to remove a language, shown under that language's own row.
|
|
151
|
+
* Inline rather than a toast: the way out is a link to the posts, and a
|
|
152
|
+
* link belongs where the refusal happened.
|
|
153
|
+
*/
|
|
154
|
+
declare _removeError: { tag: string; message: string } | null;
|
|
155
|
+
/** Language tag whose row menu (make primary / remove) is open. */
|
|
156
|
+
declare _rowMenuOpen: string | null;
|
|
157
|
+
/**
|
|
158
|
+
* Refusal from the enable dialog's save, shown inside the dialog. When the
|
|
159
|
+
* server names the language whose posts blocked the save, putting it back
|
|
160
|
+
* on the list is offered as one click.
|
|
161
|
+
*/
|
|
162
|
+
declare _enableError: { message: string; language?: string } | null;
|
|
163
|
+
declare _enableOpen: boolean;
|
|
164
|
+
declare _enableAdditional: string[];
|
|
165
|
+
declare _enablePrimary: string;
|
|
166
|
+
declare _enableBusy: boolean;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Docs link for this page. Set from the server's initial state before the
|
|
170
|
+
* first render and never changed after, so it stays outside reactive state.
|
|
171
|
+
*/
|
|
172
|
+
#multilingualDocsUrl = MULTILINGUAL_DOCS_FALLBACK_URL;
|
|
173
|
+
|
|
174
|
+
createRenderRoot() {
|
|
175
|
+
// Drop the server-rendered skeleton first. lit-html appends its parts
|
|
176
|
+
// rather than replacing existing children, so leaving it would stack the
|
|
177
|
+
// placeholder block above the real form.
|
|
178
|
+
this.innerHTML = "";
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
constructor() {
|
|
183
|
+
super();
|
|
184
|
+
this.labels = {} as LanguageLabels;
|
|
185
|
+
this.locales = [];
|
|
186
|
+
this._contentLanguage = "en";
|
|
187
|
+
this._dashboardLanguage = "";
|
|
188
|
+
this._multilingualEnabled = false;
|
|
189
|
+
this._additional = [];
|
|
190
|
+
this._unmarkedPostCount = 0;
|
|
191
|
+
this._sitePathPrefix = "";
|
|
192
|
+
this._savingSite = false;
|
|
193
|
+
this._savingDashboard = false;
|
|
194
|
+
this._removeError = null;
|
|
195
|
+
this._rowMenuOpen = null;
|
|
196
|
+
this._enableError = null;
|
|
197
|
+
this._enableOpen = false;
|
|
198
|
+
this._enableAdditional = [];
|
|
199
|
+
this._enablePrimary = "en";
|
|
200
|
+
this._enableBusy = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
connectedCallback() {
|
|
204
|
+
super.connectedCallback();
|
|
205
|
+
this.#loadInitialState();
|
|
206
|
+
document.addEventListener("click", this.#onDocumentClick);
|
|
207
|
+
document.addEventListener("keydown", this.#onDocumentKeydown);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
disconnectedCallback() {
|
|
211
|
+
super.disconnectedCallback();
|
|
212
|
+
document.removeEventListener("click", this.#onDocumentClick);
|
|
213
|
+
document.removeEventListener("keydown", this.#onDocumentKeydown);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#loadInitialState() {
|
|
217
|
+
const node = document.getElementById("language-settings-initial-data");
|
|
218
|
+
if (!node?.textContent) return;
|
|
219
|
+
|
|
220
|
+
let parsed: unknown;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(node.textContent);
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const state = parsed as Partial<LanguageInitialState>;
|
|
227
|
+
|
|
228
|
+
this._contentLanguage = state.contentLanguage ?? "en";
|
|
229
|
+
this._dashboardLanguage = state.dashboardLanguage ?? "";
|
|
230
|
+
this._multilingualEnabled = state.multilingualEnabled ?? false;
|
|
231
|
+
this._additional = Array.isArray(state.additionalLanguages)
|
|
232
|
+
? [...state.additionalLanguages]
|
|
233
|
+
: [];
|
|
234
|
+
this._unmarkedPostCount = state.unmarkedPostCount ?? 0;
|
|
235
|
+
this._sitePathPrefix = state.sitePathPrefix ?? "";
|
|
236
|
+
this.#multilingualDocsUrl =
|
|
237
|
+
state.multilingualDocsUrl || MULTILINGUAL_DOCS_FALLBACK_URL;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Presentation helpers ──────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
#entry(tag: string): LocaleOption {
|
|
243
|
+
return (
|
|
244
|
+
this.locales.find((locale) => locale.tag === tag) ?? {
|
|
245
|
+
tag,
|
|
246
|
+
native: tag,
|
|
247
|
+
english: tag,
|
|
248
|
+
coverage: 0,
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#displayName(tag: string): string {
|
|
254
|
+
const entry = this.#entry(tag);
|
|
255
|
+
return entry.native || entry.tag;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#prefixFor(tag: string): string {
|
|
259
|
+
return `${this._sitePathPrefix}/${tag.toLowerCase()}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#endpoint(path: string): string {
|
|
263
|
+
return `${this._sitePathPrefix}/settings/language${path}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Dismissal ─────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
/** True while any locale picker on this page is showing its list. */
|
|
269
|
+
#pickerIsOpen(): boolean {
|
|
270
|
+
return this.querySelector("jant-locale-picker[open]") !== null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#onDocumentClick = (event: Event) => {
|
|
274
|
+
if (!this._rowMenuOpen) return;
|
|
275
|
+
const target = event.target as globalThis.Element | null;
|
|
276
|
+
const inMenu =
|
|
277
|
+
target && this.contains(target) && target.closest?.("[data-row-menu]");
|
|
278
|
+
if (!inMenu) this._rowMenuOpen = null;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
#onDocumentKeydown = (event: globalThis.KeyboardEvent) => {
|
|
282
|
+
if (event.key !== "Escape") return;
|
|
283
|
+
// An open picker handles its own Escape and stops the event there, so
|
|
284
|
+
// reaching this handler means no picker consumed it.
|
|
285
|
+
if (this._rowMenuOpen) {
|
|
286
|
+
event.stopPropagation();
|
|
287
|
+
this._rowMenuOpen = null;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (this._enableOpen) {
|
|
291
|
+
event.stopPropagation();
|
|
292
|
+
this.#closeEnableDialog();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
#toggleRowMenu(tag: string) {
|
|
297
|
+
this._rowMenuOpen = this._rowMenuOpen === tag ? null : tag;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Server calls ──────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
async #post(
|
|
303
|
+
path: string,
|
|
304
|
+
body: Record<string, unknown>,
|
|
305
|
+
): Promise<{ ok: boolean; message?: string; language?: string }> {
|
|
306
|
+
try {
|
|
307
|
+
const res = await fetch(this.#endpoint(path), {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: {
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
Accept: "application/json",
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify(body),
|
|
314
|
+
});
|
|
315
|
+
const json = await readJsonObject(res);
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
return {
|
|
318
|
+
ok: false,
|
|
319
|
+
message: getJsonString(json, "error") ?? `HTTP ${res.status}`,
|
|
320
|
+
// A language-in-use refusal names the language, so the dialog can
|
|
321
|
+
// offer putting it back as one click.
|
|
322
|
+
language: getJsonString(json, "language"),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { ok: true, message: getJsonString(json, "toast") };
|
|
326
|
+
} catch {
|
|
327
|
+
return { ok: false, message: "Couldn't reach the server. Try again." };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async #selectPrimary(tag: string) {
|
|
332
|
+
this._removeError = null;
|
|
333
|
+
if (tag === this._contentLanguage) return;
|
|
334
|
+
|
|
335
|
+
// With multilingual off this is just "what language is this site in". With
|
|
336
|
+
// it on, the root URLs change hands, which subscribers notice — so say so.
|
|
337
|
+
if (this._multilingualEnabled) {
|
|
338
|
+
const previous = this._contentLanguage;
|
|
339
|
+
const confirmed = await showConfirmDialog({
|
|
340
|
+
title: this.labels.changePrimaryTitle,
|
|
341
|
+
message: interpolate(this.labels.changePrimaryBody, {
|
|
342
|
+
next: this.#displayName(tag),
|
|
343
|
+
previous: this.#displayName(previous),
|
|
344
|
+
prefix: this.#prefixFor(previous),
|
|
345
|
+
}),
|
|
346
|
+
confirmLabel: this.labels.changePrimaryConfirm,
|
|
347
|
+
cancelLabel: this.labels.cancel,
|
|
348
|
+
});
|
|
349
|
+
if (!confirmed) return;
|
|
350
|
+
|
|
351
|
+
this._savingSite = true;
|
|
352
|
+
const result = await this.#post("/primary", { language: tag });
|
|
353
|
+
this._savingSite = false;
|
|
354
|
+
if (!result.ok) {
|
|
355
|
+
showToast(result.message ?? "", "error");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// The two lists swap in one server-side step; mirror that here so the
|
|
359
|
+
// page reads correctly without a reload.
|
|
360
|
+
this._additional = [
|
|
361
|
+
...this._additional.filter((entry) => entry !== tag),
|
|
362
|
+
previous,
|
|
363
|
+
];
|
|
364
|
+
this._contentLanguage = tag;
|
|
365
|
+
if (result.message) showToast(result.message);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this._savingSite = true;
|
|
370
|
+
const previous = this._contentLanguage;
|
|
371
|
+
this._contentLanguage = tag;
|
|
372
|
+
const result = await this.#post("", { contentLanguage: tag });
|
|
373
|
+
this._savingSite = false;
|
|
374
|
+
if (!result.ok) {
|
|
375
|
+
this._contentLanguage = previous;
|
|
376
|
+
showToast(result.message ?? "", "error");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (result.message) showToast(result.message);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async #selectDashboardLanguage(value: string) {
|
|
383
|
+
const previous = this._dashboardLanguage;
|
|
384
|
+
this._dashboardLanguage = value;
|
|
385
|
+
this._savingDashboard = true;
|
|
386
|
+
const result = await this.#post("", { dashboardLanguage: value });
|
|
387
|
+
this._savingDashboard = false;
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
this._dashboardLanguage = previous;
|
|
390
|
+
showToast(result.message ?? "", "error");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// The dashboard renders in the catalog that was active when the page
|
|
394
|
+
// loaded, so a reload is what actually applies the change.
|
|
395
|
+
window.location.reload();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async #addLanguage(tag: string) {
|
|
399
|
+
this._removeError = null;
|
|
400
|
+
const result = await this.#post("/add", { language: tag });
|
|
401
|
+
if (!result.ok) {
|
|
402
|
+
showToast(result.message ?? "", "error");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this._additional = [...this._additional, tag];
|
|
406
|
+
if (result.message) showToast(result.message);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Remove a language, and with the last one the feature itself.
|
|
411
|
+
*
|
|
412
|
+
* Per-language views need a second language to mean anything, so the server
|
|
413
|
+
* turns multilingual content off when this empties the list. That is a bigger
|
|
414
|
+
* change than the menu item promises, hence the confirmation — asked before
|
|
415
|
+
* the request, which means it is also asked in the case the server then
|
|
416
|
+
* refuses (posts still written in that language). The refusal lands under the
|
|
417
|
+
* row as it always has; the alternative is a preflight round-trip on every
|
|
418
|
+
* removal to avoid one dialog in the rarer path.
|
|
419
|
+
*/
|
|
420
|
+
async #removeLanguage(tag: string) {
|
|
421
|
+
this._removeError = null;
|
|
422
|
+
const isLast =
|
|
423
|
+
this._multilingualEnabled &&
|
|
424
|
+
this._additional.length === 1 &&
|
|
425
|
+
this._additional[0] === tag;
|
|
426
|
+
|
|
427
|
+
if (isLast) {
|
|
428
|
+
const confirmed = await showConfirmDialog({
|
|
429
|
+
title: this.labels.removeLastTitle,
|
|
430
|
+
message: interpolate(this.labels.removeLastBody, {
|
|
431
|
+
language: this.#displayName(tag),
|
|
432
|
+
prefix: this.#prefixFor(tag),
|
|
433
|
+
}),
|
|
434
|
+
confirmLabel: this.labels.removeLastConfirm,
|
|
435
|
+
cancelLabel: this.labels.cancel,
|
|
436
|
+
});
|
|
437
|
+
if (!confirmed) return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const result = await this.#post("/remove", { language: tag });
|
|
441
|
+
if (!result.ok) {
|
|
442
|
+
this._removeError = { tag, message: result.message ?? "" };
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
this._additional = this._additional.filter((entry) => entry !== tag);
|
|
446
|
+
|
|
447
|
+
if (isLast) {
|
|
448
|
+
this._multilingualEnabled = false;
|
|
449
|
+
// Same reload as turning it off directly: the header switcher and the
|
|
450
|
+
// composer's language control are server-rendered.
|
|
451
|
+
if (result.message) queueToastForNextPage(result.message);
|
|
452
|
+
window.location.reload();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (result.message) showToast(result.message);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async #turnOffMultilingual() {
|
|
459
|
+
this._removeError = null;
|
|
460
|
+
const confirmed = await showConfirmDialog({
|
|
461
|
+
title: this.labels.disableTitle,
|
|
462
|
+
message: interpolate(this.labels.disableBody, {
|
|
463
|
+
prefix: this.#prefixFor(this._additional[0] ?? "en"),
|
|
464
|
+
}),
|
|
465
|
+
confirmLabel: this.labels.disableConfirm,
|
|
466
|
+
cancelLabel: this.labels.cancel,
|
|
467
|
+
});
|
|
468
|
+
if (!confirmed) return;
|
|
469
|
+
|
|
470
|
+
const result = await this.#post("/disable", {});
|
|
471
|
+
if (!result.ok) {
|
|
472
|
+
showToast(result.message ?? "", "error");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this._multilingualEnabled = false;
|
|
476
|
+
// Queued for the same reason as the enable path: the reload below happens
|
|
477
|
+
// before a toast raised here could be read.
|
|
478
|
+
if (result.message) queueToastForNextPage(result.message);
|
|
479
|
+
// The header's language switcher has to go away with the views.
|
|
480
|
+
window.location.reload();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── Enable dialog ─────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
#openEnableDialog() {
|
|
486
|
+
this._removeError = null;
|
|
487
|
+
this._enableError = null;
|
|
488
|
+
this._enablePrimary = this._contentLanguage;
|
|
489
|
+
this._enableAdditional = [...this._additional];
|
|
490
|
+
this._enableOpen = true;
|
|
491
|
+
void this.updateComplete.then(() => {
|
|
492
|
+
const dialog = this.querySelector<HTMLDialogElement>(
|
|
493
|
+
"[data-enable-dialog]",
|
|
494
|
+
);
|
|
495
|
+
if (dialog && !dialog.open) dialog.showModal();
|
|
496
|
+
dialog?.querySelector<HTMLElement>("[data-enable-panel]")?.focus();
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#closeEnableDialog() {
|
|
501
|
+
const dialog = this.querySelector<HTMLDialogElement>(
|
|
502
|
+
"[data-enable-dialog]",
|
|
503
|
+
);
|
|
504
|
+
if (dialog?.open) dialog.close();
|
|
505
|
+
this._enableOpen = false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async #confirmEnable() {
|
|
509
|
+
if (this._enableAdditional.length === 0 || this._enableBusy) return;
|
|
510
|
+
|
|
511
|
+
this._enableBusy = true;
|
|
512
|
+
this._enableError = null;
|
|
513
|
+
const result = await this.#post("/enable", {
|
|
514
|
+
primary: this._enablePrimary,
|
|
515
|
+
additional: this._enableAdditional,
|
|
516
|
+
});
|
|
517
|
+
this._enableBusy = false;
|
|
518
|
+
if (!result.ok) {
|
|
519
|
+
// Inside the dialog, where the refusal happened — a toast would sit
|
|
520
|
+
// beneath the modal's top layer and never be seen.
|
|
521
|
+
this._enableError = {
|
|
522
|
+
message: result.message ?? "",
|
|
523
|
+
...(result.language ? { language: result.language } : {}),
|
|
524
|
+
};
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this._contentLanguage = this._enablePrimary;
|
|
529
|
+
this._additional = [...this._enableAdditional];
|
|
530
|
+
this._multilingualEnabled = true;
|
|
531
|
+
this._unmarkedPostCount = 0;
|
|
532
|
+
this.#closeEnableDialog();
|
|
533
|
+
// Queued rather than shown: the reload below would wipe a toast raised
|
|
534
|
+
// here, and this is the one confirmation that says how many posts were
|
|
535
|
+
// stamped — the number is gone from the page by the time it lands.
|
|
536
|
+
if (result.message) queueToastForNextPage(result.message);
|
|
537
|
+
// The page's own chrome changed with the setting — the language switcher
|
|
538
|
+
// exists now — and only the server can render it. Same pattern as the
|
|
539
|
+
// dashboard-language change above.
|
|
540
|
+
window.location.reload();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ── Render ────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Labels handed to every picker on the page.
|
|
547
|
+
*
|
|
548
|
+
* Built once: the picker takes them as a property, and a fresh object each
|
|
549
|
+
* render would make Lit see a changed property on every keystroke elsewhere
|
|
550
|
+
* in the form.
|
|
551
|
+
*/
|
|
552
|
+
get #pickerLabels(): LocalePickerLabels {
|
|
553
|
+
this.#pickerLabelsCache ??= {
|
|
554
|
+
search: this.labels.searchPlaceholder,
|
|
555
|
+
empty: this.labels.noMatches,
|
|
556
|
+
};
|
|
557
|
+
return this.#pickerLabelsCache;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
#pickerLabelsCache: LocalePickerLabels | null = null;
|
|
561
|
+
|
|
562
|
+
#renderLanguagePicker(options: {
|
|
563
|
+
labelId: string;
|
|
564
|
+
current?: string;
|
|
565
|
+
exclude: readonly string[];
|
|
566
|
+
triggerLabel?: string;
|
|
567
|
+
triggerClass: string;
|
|
568
|
+
onSelect: (tag: string) => void;
|
|
569
|
+
}) {
|
|
570
|
+
return html`
|
|
571
|
+
<jant-locale-picker
|
|
572
|
+
.locales=${this.locales}
|
|
573
|
+
.labels=${this.#pickerLabels}
|
|
574
|
+
.value=${options.current ?? ""}
|
|
575
|
+
.exclude=${[...options.exclude]}
|
|
576
|
+
.triggerLabel=${options.triggerLabel ?? ""}
|
|
577
|
+
.triggerClass=${options.triggerClass}
|
|
578
|
+
.labelledby=${options.labelId}
|
|
579
|
+
@locale-select=${(e: CustomEvent<{ tag: string }>) =>
|
|
580
|
+
options.onSelect(e.detail.tag)}
|
|
581
|
+
></jant-locale-picker>
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* The save refusal, rendered inside the dialog with its one-click fix.
|
|
587
|
+
*
|
|
588
|
+
* When the server names the language whose posts blocked the save, an
|
|
589
|
+
* "Add {language}" button puts it straight back on the list — the reader
|
|
590
|
+
* of the error should not have to find that language in a picker.
|
|
591
|
+
*/
|
|
592
|
+
#renderEnableError(error: { message: string; language?: string }) {
|
|
593
|
+
return html`
|
|
594
|
+
<div class="flex flex-wrap items-center gap-2" role="alert">
|
|
595
|
+
<p class="text-sm text-destructive">${error.message}</p>
|
|
596
|
+
${
|
|
597
|
+
error.language
|
|
598
|
+
? html`
|
|
599
|
+
<button
|
|
600
|
+
type="button"
|
|
601
|
+
class="btn-sm-outline"
|
|
602
|
+
data-enable-add-back
|
|
603
|
+
@click=${() => this.#addBackLanguage(error.language ?? "")}
|
|
604
|
+
>
|
|
605
|
+
${interpolate(this.labels.addMissingLanguage, {
|
|
606
|
+
language: this.#displayName(error.language),
|
|
607
|
+
})}
|
|
608
|
+
</button>
|
|
609
|
+
`
|
|
610
|
+
: nothing
|
|
611
|
+
}
|
|
612
|
+
</div>
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Put the language the refusal named back on the dialog's list. */
|
|
617
|
+
#addBackLanguage(tag: string) {
|
|
618
|
+
if (!tag) return;
|
|
619
|
+
this._enableError = null;
|
|
620
|
+
if (tag === this._enablePrimary || this._enableAdditional.includes(tag)) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
this._enableAdditional = [...this._enableAdditional, tag];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The confirmation shown before multilingual content is switched on.
|
|
628
|
+
*
|
|
629
|
+
* BaseCoat's `.alert` is a two-column grid where only a title element and a
|
|
630
|
+
* `<section>` are placed in the readable column, so the warning uses that
|
|
631
|
+
* structure rather than bare paragraphs.
|
|
632
|
+
*/
|
|
633
|
+
#renderEnableDialog() {
|
|
634
|
+
if (!this._enableOpen) return nothing;
|
|
635
|
+
|
|
636
|
+
// With nothing to stamp there is nothing to warn about — the alert only
|
|
637
|
+
// exists for the one-time marking of posts that predate the feature.
|
|
638
|
+
const warning =
|
|
639
|
+
this._unmarkedPostCount > 0
|
|
640
|
+
? interpolate(this.labels.enableMarkWarning, {
|
|
641
|
+
language: this.#displayName(this._enablePrimary),
|
|
642
|
+
})
|
|
643
|
+
: null;
|
|
644
|
+
|
|
645
|
+
return html`
|
|
646
|
+
<dialog
|
|
647
|
+
class="dialog confirm-dialog"
|
|
648
|
+
data-enable-dialog
|
|
649
|
+
@cancel=${(e: Event) => {
|
|
650
|
+
e.preventDefault();
|
|
651
|
+
this.#closeEnableDialog();
|
|
652
|
+
}}
|
|
653
|
+
@click=${(e: Event) => {
|
|
654
|
+
if (e.target === e.currentTarget) this.#closeEnableDialog();
|
|
655
|
+
}}
|
|
656
|
+
@keydown=${(e: globalThis.KeyboardEvent) => {
|
|
657
|
+
if (e.key === "Enter" && !e.shiftKey && !this.#pickerIsOpen()) {
|
|
658
|
+
e.preventDefault();
|
|
659
|
+
void this.#confirmEnable();
|
|
660
|
+
}
|
|
661
|
+
}}
|
|
662
|
+
>
|
|
663
|
+
<div
|
|
664
|
+
class="confirm-dialog-panel"
|
|
665
|
+
data-enable-panel
|
|
666
|
+
tabindex="-1"
|
|
667
|
+
role="document"
|
|
668
|
+
aria-labelledby="language-enable-title"
|
|
669
|
+
>
|
|
670
|
+
<header class="confirm-dialog-header">
|
|
671
|
+
<h2 id="language-enable-title" class="confirm-dialog-title">
|
|
672
|
+
${this.labels.enableTitle}
|
|
673
|
+
</h2>
|
|
674
|
+
</header>
|
|
675
|
+
|
|
676
|
+
<div class="flex flex-col gap-4 py-2">
|
|
677
|
+
<div class="field">
|
|
678
|
+
<span id="language-enable-primary-label" class="label"
|
|
679
|
+
>${this.labels.primaryLanguage}</span
|
|
680
|
+
>
|
|
681
|
+
${this.#renderLanguagePicker({
|
|
682
|
+
labelId: "language-enable-primary-label",
|
|
683
|
+
current: this._enablePrimary,
|
|
684
|
+
exclude: this._enableAdditional,
|
|
685
|
+
triggerLabel: this.#displayName(this._enablePrimary),
|
|
686
|
+
triggerClass: LOCALE_PICKER_TRIGGER_CLASS,
|
|
687
|
+
onSelect: (tag) => {
|
|
688
|
+
this._enablePrimary = tag;
|
|
689
|
+
this._enableAdditional = this._enableAdditional.filter(
|
|
690
|
+
(entry) => entry !== tag,
|
|
691
|
+
);
|
|
692
|
+
},
|
|
693
|
+
})}
|
|
694
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
695
|
+
${this.labels.primaryLanguageHelp}
|
|
696
|
+
</p>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div class="field">
|
|
700
|
+
<span id="language-enable-add-label" class="label"
|
|
701
|
+
>${this.labels.otherLanguages}</span
|
|
702
|
+
>
|
|
703
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
704
|
+
${this._enableAdditional.map(
|
|
705
|
+
(tag) => html`
|
|
706
|
+
<span class="badge-secondary gap-1">
|
|
707
|
+
${this.#displayName(tag)}
|
|
708
|
+
<button
|
|
709
|
+
type="button"
|
|
710
|
+
class="cursor-pointer opacity-70 hover:opacity-100"
|
|
711
|
+
aria-label=${interpolate(this.labels.removeLanguage, {
|
|
712
|
+
language: this.#displayName(tag),
|
|
713
|
+
})}
|
|
714
|
+
@click=${() => {
|
|
715
|
+
this._enableAdditional =
|
|
716
|
+
this._enableAdditional.filter(
|
|
717
|
+
(entry) => entry !== tag,
|
|
718
|
+
);
|
|
719
|
+
}}
|
|
720
|
+
>
|
|
721
|
+
×
|
|
722
|
+
</button>
|
|
723
|
+
</span>
|
|
724
|
+
`,
|
|
725
|
+
)}
|
|
726
|
+
${this.#renderLanguagePicker({
|
|
727
|
+
labelId: "language-enable-add-label",
|
|
728
|
+
exclude: [this._enablePrimary, ...this._enableAdditional],
|
|
729
|
+
triggerLabel: `+ ${this.labels.addLanguage}`,
|
|
730
|
+
triggerClass: "btn-sm-outline",
|
|
731
|
+
onSelect: (tag) => {
|
|
732
|
+
this._enableAdditional = [...this._enableAdditional, tag];
|
|
733
|
+
},
|
|
734
|
+
})}
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
|
|
738
|
+
${
|
|
739
|
+
warning
|
|
740
|
+
? html`
|
|
741
|
+
<div class="alert">
|
|
742
|
+
<svg
|
|
743
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
744
|
+
viewBox="0 0 24 24"
|
|
745
|
+
fill="none"
|
|
746
|
+
stroke="currentColor"
|
|
747
|
+
stroke-width="2"
|
|
748
|
+
stroke-linecap="round"
|
|
749
|
+
stroke-linejoin="round"
|
|
750
|
+
aria-hidden="true"
|
|
751
|
+
>
|
|
752
|
+
<path
|
|
753
|
+
d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"
|
|
754
|
+
/>
|
|
755
|
+
<path d="M12 9v4" />
|
|
756
|
+
<path d="M12 17h.01" />
|
|
757
|
+
</svg>
|
|
758
|
+
<strong>${this.labels.enableMarkTitle}</strong>
|
|
759
|
+
<section>
|
|
760
|
+
<p>${warning}</p>
|
|
761
|
+
<p>${this.labels.enableFixHint}</p>
|
|
762
|
+
</section>
|
|
763
|
+
</div>
|
|
764
|
+
`
|
|
765
|
+
: nothing
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
<p class="text-sm text-muted-foreground">
|
|
769
|
+
${this.labels.enableReassurance}
|
|
770
|
+
</p>
|
|
771
|
+
|
|
772
|
+
${
|
|
773
|
+
this._enableAdditional.length === 0
|
|
774
|
+
? html`<p class="text-sm text-muted-foreground">
|
|
775
|
+
${this.labels.enableNeedsLanguage}
|
|
776
|
+
</p>`
|
|
777
|
+
: nothing
|
|
778
|
+
}
|
|
779
|
+
${
|
|
780
|
+
this._enableError
|
|
781
|
+
? this.#renderEnableError(this._enableError)
|
|
782
|
+
: nothing
|
|
783
|
+
}
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<footer class="confirm-dialog-actions">
|
|
787
|
+
<button
|
|
788
|
+
type="button"
|
|
789
|
+
class="btn-outline"
|
|
790
|
+
@click=${() => this.#closeEnableDialog()}
|
|
791
|
+
>
|
|
792
|
+
${this.labels.cancel}
|
|
793
|
+
</button>
|
|
794
|
+
<button
|
|
795
|
+
type="button"
|
|
796
|
+
class="btn"
|
|
797
|
+
data-enable-confirm
|
|
798
|
+
?disabled=${
|
|
799
|
+
this._enableAdditional.length === 0 || this._enableBusy
|
|
800
|
+
}
|
|
801
|
+
@click=${() => void this.#confirmEnable()}
|
|
802
|
+
>
|
|
803
|
+
${this._enableBusy ? this.labels.saving : this.labels.save}
|
|
804
|
+
</button>
|
|
805
|
+
</footer>
|
|
806
|
+
</div>
|
|
807
|
+
</dialog>
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The folded actions of one non-primary language row.
|
|
813
|
+
*
|
|
814
|
+
* Making a language primary or removing it are once-a-year moves; a row of
|
|
815
|
+
* always-visible buttons would give them daily-driver prominence. A "⋯"
|
|
816
|
+
* menu keeps the list to what matters — language and address — and follows
|
|
817
|
+
* the dismissal rules every popover here follows.
|
|
818
|
+
*/
|
|
819
|
+
#renderRowMenu(tag: string) {
|
|
820
|
+
const open = this._rowMenuOpen === tag;
|
|
821
|
+
const name = this.#displayName(tag);
|
|
822
|
+
|
|
823
|
+
return html`
|
|
824
|
+
<span class="relative ml-auto flex items-center" data-row-menu>
|
|
825
|
+
<button
|
|
826
|
+
type="button"
|
|
827
|
+
class="btn-sm-ghost text-muted-foreground"
|
|
828
|
+
aria-haspopup="menu"
|
|
829
|
+
aria-expanded=${open ? "true" : "false"}
|
|
830
|
+
aria-label=${interpolate(this.labels.languageMenu, {
|
|
831
|
+
language: name,
|
|
832
|
+
})}
|
|
833
|
+
data-language-menu
|
|
834
|
+
@click=${() => this.#toggleRowMenu(tag)}
|
|
835
|
+
>
|
|
836
|
+
⋯
|
|
837
|
+
</button>
|
|
838
|
+
${
|
|
839
|
+
open
|
|
840
|
+
? html`
|
|
841
|
+
<div
|
|
842
|
+
role="menu"
|
|
843
|
+
class="absolute right-0 top-full z-20 mt-1 min-w-40 rounded-md border bg-popover py-1 text-popover-foreground shadow-md"
|
|
844
|
+
>
|
|
845
|
+
<button
|
|
846
|
+
type="button"
|
|
847
|
+
role="menuitem"
|
|
848
|
+
class="block w-full cursor-pointer px-3 py-2 text-left text-sm hover:bg-accent"
|
|
849
|
+
?disabled=${this._savingSite}
|
|
850
|
+
@click=${() => {
|
|
851
|
+
this._rowMenuOpen = null;
|
|
852
|
+
void this.#selectPrimary(tag);
|
|
853
|
+
}}
|
|
854
|
+
>
|
|
855
|
+
${this.labels.makePrimary}
|
|
856
|
+
</button>
|
|
857
|
+
<button
|
|
858
|
+
type="button"
|
|
859
|
+
role="menuitem"
|
|
860
|
+
class="block w-full cursor-pointer px-3 py-2 text-left text-sm text-destructive hover:bg-accent"
|
|
861
|
+
@click=${() => {
|
|
862
|
+
this._rowMenuOpen = null;
|
|
863
|
+
void this.#removeLanguage(tag);
|
|
864
|
+
}}
|
|
865
|
+
>
|
|
866
|
+
${interpolate(this.labels.removeLanguage, {
|
|
867
|
+
language: name,
|
|
868
|
+
})}
|
|
869
|
+
</button>
|
|
870
|
+
</div>
|
|
871
|
+
`
|
|
872
|
+
: nothing
|
|
873
|
+
}
|
|
874
|
+
</span>
|
|
875
|
+
`;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The site's languages as one list, each with the address it is served at.
|
|
880
|
+
*
|
|
881
|
+
* One row per language keeps every fact and action in the same place —
|
|
882
|
+
* which language, which URL, which one owns the root, make another primary,
|
|
883
|
+
* remove one — instead of a picker, a chip row and a URL legend apart.
|
|
884
|
+
*/
|
|
885
|
+
#renderLanguagesList() {
|
|
886
|
+
const languages = [this._contentLanguage, ...this._additional];
|
|
887
|
+
|
|
888
|
+
return html`
|
|
889
|
+
<div class="field">
|
|
890
|
+
<span id="language-list-label" class="label"
|
|
891
|
+
>${this.labels.languagesLabel}${
|
|
892
|
+
this._savingSite ? ` ${this.labels.saving}` : ""
|
|
893
|
+
}</span
|
|
894
|
+
>
|
|
895
|
+
<ul
|
|
896
|
+
class="flex flex-col rounded-md border divide-y"
|
|
897
|
+
aria-labelledby="language-list-label"
|
|
898
|
+
>
|
|
899
|
+
${languages.map((tag) => {
|
|
900
|
+
const isPrimary = tag === this._contentLanguage;
|
|
901
|
+
const url = isPrimary
|
|
902
|
+
? `${this._sitePathPrefix}/`
|
|
903
|
+
: this.#prefixFor(tag);
|
|
904
|
+
return html`
|
|
905
|
+
<li class="flex flex-col px-3 py-1.5 text-sm">
|
|
906
|
+
<div class="flex min-h-8 items-center gap-3">
|
|
907
|
+
<span class="min-w-0 truncate" lang=${tag}
|
|
908
|
+
>${this.#displayName(tag)}</span
|
|
909
|
+
>
|
|
910
|
+
<a
|
|
911
|
+
href=${url}
|
|
912
|
+
target="_blank"
|
|
913
|
+
rel="noopener noreferrer"
|
|
914
|
+
class="shrink-0"
|
|
915
|
+
>
|
|
916
|
+
<code
|
|
917
|
+
class="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
918
|
+
>${url}</code
|
|
919
|
+
>
|
|
920
|
+
</a>
|
|
921
|
+
${
|
|
922
|
+
isPrimary
|
|
923
|
+
? html`<span class="badge-secondary ml-auto"
|
|
924
|
+
>${this.labels.primaryBadge}</span
|
|
925
|
+
>`
|
|
926
|
+
: this.#renderRowMenu(tag)
|
|
927
|
+
}
|
|
928
|
+
</div>
|
|
929
|
+
${
|
|
930
|
+
this._removeError?.tag === tag
|
|
931
|
+
? html`
|
|
932
|
+
<p class="pb-1 text-sm text-destructive" role="alert">
|
|
933
|
+
${this._removeError.message}${
|
|
934
|
+
this._multilingualEnabled
|
|
935
|
+
? html`
|
|
936
|
+
<a
|
|
937
|
+
class="underline underline-offset-2"
|
|
938
|
+
href=${`${this.#prefixFor(tag)}/archive`}
|
|
939
|
+
target="_blank"
|
|
940
|
+
rel="noopener noreferrer"
|
|
941
|
+
>${this.labels.viewPosts}</a
|
|
942
|
+
>
|
|
943
|
+
`
|
|
944
|
+
: nothing
|
|
945
|
+
}
|
|
946
|
+
</p>
|
|
947
|
+
`
|
|
948
|
+
: nothing
|
|
949
|
+
}
|
|
950
|
+
</li>
|
|
951
|
+
`;
|
|
952
|
+
})}
|
|
953
|
+
</ul>
|
|
954
|
+
<div class="mt-2">
|
|
955
|
+
${this.#renderLanguagePicker({
|
|
956
|
+
labelId: "language-list-label",
|
|
957
|
+
exclude: [this._contentLanguage, ...this._additional],
|
|
958
|
+
triggerLabel: `+ ${this.labels.addLanguage}`,
|
|
959
|
+
triggerClass: "btn-sm-outline",
|
|
960
|
+
onSelect: (tag) => void this.#addLanguage(tag),
|
|
961
|
+
})}
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
render() {
|
|
968
|
+
if (!this.labels.contentLanguage) return nothing;
|
|
969
|
+
|
|
970
|
+
return html`
|
|
971
|
+
<div class="flex flex-col gap-8">
|
|
972
|
+
<section class="flex flex-col gap-4">
|
|
973
|
+
<h2 class="text-lg font-medium">${this.labels.siteSection}</h2>
|
|
974
|
+
|
|
975
|
+
${
|
|
976
|
+
this._multilingualEnabled
|
|
977
|
+
? this.#renderLanguagesList()
|
|
978
|
+
: html`
|
|
979
|
+
<div class="field">
|
|
980
|
+
<span id="language-primary-label" class="label"
|
|
981
|
+
>${this.labels.contentLanguage}</span
|
|
982
|
+
>
|
|
983
|
+
${this.#renderLanguagePicker({
|
|
984
|
+
labelId: "language-primary-label",
|
|
985
|
+
current: this._contentLanguage,
|
|
986
|
+
exclude: [],
|
|
987
|
+
triggerLabel: this.#displayName(this._contentLanguage),
|
|
988
|
+
triggerClass: LOCALE_PICKER_TRIGGER_CLASS,
|
|
989
|
+
onSelect: (tag) => void this.#selectPrimary(tag),
|
|
990
|
+
})}
|
|
991
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
992
|
+
${this.labels.contentLanguageHelp}${
|
|
993
|
+
this._savingSite ? ` ${this.labels.saving}` : ""
|
|
994
|
+
}
|
|
995
|
+
</p>
|
|
996
|
+
</div>
|
|
997
|
+
`
|
|
998
|
+
}
|
|
999
|
+
</section>
|
|
1000
|
+
|
|
1001
|
+
<section class="flex flex-col gap-3 border-t pt-8">
|
|
1002
|
+
<h2 class="text-lg font-medium">${this.labels.multilingual}</h2>
|
|
1003
|
+
<p class="text-sm text-muted-foreground">
|
|
1004
|
+
${this.labels.multilingualHelp}
|
|
1005
|
+
</p>
|
|
1006
|
+
<p class="text-sm text-muted-foreground">
|
|
1007
|
+
<a
|
|
1008
|
+
href=${this.#multilingualDocsUrl}
|
|
1009
|
+
target="_blank"
|
|
1010
|
+
rel="noopener noreferrer"
|
|
1011
|
+
class="underline hover:text-foreground transition-colors"
|
|
1012
|
+
>${this.labels.multilingualDocs}</a
|
|
1013
|
+
>
|
|
1014
|
+
— ${this.labels.multilingualDocsHelp}
|
|
1015
|
+
</p>
|
|
1016
|
+
<div class="flex items-center gap-3">
|
|
1017
|
+
${
|
|
1018
|
+
this._multilingualEnabled
|
|
1019
|
+
? html`
|
|
1020
|
+
<span class="badge-secondary">${this.labels.statusOn}</span>
|
|
1021
|
+
<button
|
|
1022
|
+
type="button"
|
|
1023
|
+
class="btn-link h-auto p-0 text-sm text-muted-foreground"
|
|
1024
|
+
data-multilingual-off
|
|
1025
|
+
@click=${() => void this.#turnOffMultilingual()}
|
|
1026
|
+
>
|
|
1027
|
+
${this.labels.disableConfirm}
|
|
1028
|
+
</button>
|
|
1029
|
+
`
|
|
1030
|
+
: html`
|
|
1031
|
+
<button
|
|
1032
|
+
type="button"
|
|
1033
|
+
class="btn-sm-outline"
|
|
1034
|
+
data-multilingual-setup
|
|
1035
|
+
@click=${() => this.#openEnableDialog()}
|
|
1036
|
+
>
|
|
1037
|
+
${this.labels.turnOn}
|
|
1038
|
+
</button>
|
|
1039
|
+
`
|
|
1040
|
+
}
|
|
1041
|
+
</div>
|
|
1042
|
+
</section>
|
|
1043
|
+
|
|
1044
|
+
<section class="flex flex-col gap-4 border-t pt-8">
|
|
1045
|
+
<h2 class="text-lg font-medium">${this.labels.dashboardSection}</h2>
|
|
1046
|
+
|
|
1047
|
+
<div class="field">
|
|
1048
|
+
<label id="dashboard-language-label" class="label"
|
|
1049
|
+
>${this.labels.dashboardLanguage}</label
|
|
1050
|
+
>
|
|
1051
|
+
<select
|
|
1052
|
+
class="select"
|
|
1053
|
+
aria-labelledby="dashboard-language-label"
|
|
1054
|
+
?disabled=${this._savingDashboard}
|
|
1055
|
+
@change=${(e: Event) =>
|
|
1056
|
+
void this.#selectDashboardLanguage(
|
|
1057
|
+
(e.target as HTMLSelectElement).value,
|
|
1058
|
+
)}
|
|
1059
|
+
>
|
|
1060
|
+
${DASHBOARD_LOCALES.map(
|
|
1061
|
+
(tag) => html`
|
|
1062
|
+
<option
|
|
1063
|
+
value=${tag}
|
|
1064
|
+
?selected=${this._dashboardLanguage === tag}
|
|
1065
|
+
>
|
|
1066
|
+
${
|
|
1067
|
+
tag === ""
|
|
1068
|
+
? this.labels.followContent
|
|
1069
|
+
: this.#displayName(tag)
|
|
1070
|
+
}
|
|
1071
|
+
</option>
|
|
1072
|
+
`,
|
|
1073
|
+
)}
|
|
1074
|
+
</select>
|
|
1075
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
1076
|
+
${this.labels.dashboardLanguageHelp}
|
|
1077
|
+
</p>
|
|
1078
|
+
</div>
|
|
1079
|
+
</section>
|
|
1080
|
+
</div>
|
|
1081
|
+
|
|
1082
|
+
${this.#renderEnableDialog()}
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
customElements.define("jant-settings-language", JantSettingsLanguage);
|