@jant/core 0.6.15 → 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 +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- 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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- 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__/mise-config.test.ts +22 -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 +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- 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 +88 -1
- 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__/compose-format-convert.test.ts +0 -19
- 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 +79 -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 +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- 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 +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- 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 +1142 -268
- 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 +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- 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/post-refresh.ts +135 -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/__tests__/link-input-rules.test.ts +121 -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-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- 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__/backfill-thread-activity.test.ts +234 -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/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -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 +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -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 +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- 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 +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- 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 +1691 -12
- 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__/markdown.test.ts +16 -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 +88 -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__/translit.test.ts +87 -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 +93 -34
- 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 +1021 -66
- 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 +21 -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/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- 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-display.ts +42 -9
- 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 +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -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__/palette.test.ts +44 -0
- 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 +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -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 +11 -2
- 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/__tests__/sitemap.test.ts +68 -1
- 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 +657 -1
- 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__/featured.test.ts +6 -2
- 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__/preview.test.ts +3 -1
- 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 +507 -357
- 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 +183 -19
- 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 +139 -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-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -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 +330 -47
- 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 +281 -24
- 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 +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -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 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- 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 +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- 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 +401 -301
- 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 +122 -3
- 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/DraftPreviewBar.tsx +25 -9
- 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 +55 -23
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.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-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- 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
|
@@ -16,8 +16,17 @@ import {
|
|
|
16
16
|
FEATURED_SPARKLE_PATH,
|
|
17
17
|
} from "../../lib/featured-icons.js";
|
|
18
18
|
import { showConfirmDialog } from "../confirm.js";
|
|
19
|
+
import { refreshArticleView } from "../post-refresh.js";
|
|
19
20
|
import { showToast } from "../toast.js";
|
|
21
|
+
import { readErrorMessage } from "../json.js";
|
|
20
22
|
import { publicPath } from "../runtime-paths.js";
|
|
23
|
+
import { looksLikeAddress } from "../../lib/url.js";
|
|
24
|
+
import { pickPost } from "../post-picker.js";
|
|
25
|
+
import {
|
|
26
|
+
ensureComposeDialog,
|
|
27
|
+
readComposeDialogLabels,
|
|
28
|
+
} from "../compose-launch.js";
|
|
29
|
+
import type { PostPickerResult } from "./jant-post-picker.js";
|
|
21
30
|
import {
|
|
22
31
|
applyItemOrder,
|
|
23
32
|
filterCollectionsBySearch,
|
|
@@ -33,6 +42,52 @@ interface PostMenuData {
|
|
|
33
42
|
featured: boolean;
|
|
34
43
|
visibility: string;
|
|
35
44
|
isReply: boolean;
|
|
45
|
+
isDraft: boolean;
|
|
46
|
+
/** Content language of the Thread, when the site publishes more than one. */
|
|
47
|
+
language: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One language the site publishes. */
|
|
51
|
+
interface MenuLanguage {
|
|
52
|
+
tag: string;
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What the server made of an address pasted into the translation picker: the
|
|
58
|
+
* Thread it names, or why that Thread cannot be linked to this one.
|
|
59
|
+
*/
|
|
60
|
+
type TranslationAddressResolution =
|
|
61
|
+
| {
|
|
62
|
+
kind: "ok";
|
|
63
|
+
address: string;
|
|
64
|
+
candidate: { id: string; label: string; language: string | null };
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind:
|
|
68
|
+
| "external"
|
|
69
|
+
| "not_found"
|
|
70
|
+
| "not_a_post"
|
|
71
|
+
| "same_thread"
|
|
72
|
+
| "unpublished"
|
|
73
|
+
| "no_language"
|
|
74
|
+
| "same_language"
|
|
75
|
+
| "group_conflict";
|
|
76
|
+
address: string;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
kind: "language_taken" | "group_language_taken";
|
|
80
|
+
address: string;
|
|
81
|
+
language: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** A Thread already linked to this one as a translation. */
|
|
85
|
+
interface TranslationItem {
|
|
86
|
+
id: string;
|
|
87
|
+
slug: string;
|
|
88
|
+
/** What to call it — its title, or something derived when it has none. */
|
|
89
|
+
label: string;
|
|
90
|
+
language: string | null;
|
|
36
91
|
}
|
|
37
92
|
|
|
38
93
|
interface CollectionItem {
|
|
@@ -49,7 +104,16 @@ interface ThreadCollectionsResponse {
|
|
|
49
104
|
collectionIds?: string[];
|
|
50
105
|
}
|
|
51
106
|
|
|
52
|
-
type PostMenuView =
|
|
107
|
+
type PostMenuView =
|
|
108
|
+
"menu" | "collections" | "visibility" | "language" | "language-switch";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Where the language panel puts focus, in document order rather than in the
|
|
112
|
+
* order written here — `querySelector` picks the first match on the page, so
|
|
113
|
+
* this reads as "the topmost real action in the panel", whichever it is.
|
|
114
|
+
*/
|
|
115
|
+
const LANGUAGE_PANEL_FOCUS =
|
|
116
|
+
"[data-post-menu-translation] a, [data-post-menu-translation-first], [data-post-menu-open-language-switch]";
|
|
53
117
|
|
|
54
118
|
interface MenuTriggerRect {
|
|
55
119
|
top: number;
|
|
@@ -104,11 +168,15 @@ function readPostMenuData(article: HTMLElement): PostMenuData | null {
|
|
|
104
168
|
featured: article.hasAttribute("data-post-featured"),
|
|
105
169
|
visibility: article.dataset.postVisibility ?? "public",
|
|
106
170
|
isReply: threadId !== id || article.hasAttribute("data-post-reply"),
|
|
171
|
+
isDraft: article.hasAttribute("data-post-draft"),
|
|
172
|
+
// Carried on the Thread root, so a reply reads it from there.
|
|
173
|
+
language: threadRootArticle.dataset.postLanguage ?? null,
|
|
107
174
|
};
|
|
108
175
|
}
|
|
109
176
|
|
|
110
177
|
export class JantPostMenu extends LitElement {
|
|
111
178
|
static properties = {
|
|
179
|
+
languages: { type: Array },
|
|
112
180
|
_open: { state: true },
|
|
113
181
|
_data: { state: true },
|
|
114
182
|
_x: { state: true },
|
|
@@ -120,8 +188,12 @@ export class JantPostMenu extends LitElement {
|
|
|
120
188
|
_collectionSearch: { state: true },
|
|
121
189
|
_threadCollectionIds: { state: true },
|
|
122
190
|
_addCollectionPanelOpen: { state: true },
|
|
191
|
+
_translations: { state: true },
|
|
192
|
+
_translationsLoading: { state: true },
|
|
193
|
+
_translationBusy: { state: true },
|
|
123
194
|
};
|
|
124
195
|
|
|
196
|
+
declare languages: MenuLanguage[];
|
|
125
197
|
declare _open: boolean;
|
|
126
198
|
declare _data: PostMenuData | null;
|
|
127
199
|
declare _x: number;
|
|
@@ -133,6 +205,9 @@ export class JantPostMenu extends LitElement {
|
|
|
133
205
|
declare _collectionSearch: string;
|
|
134
206
|
declare _threadCollectionIds: string[];
|
|
135
207
|
declare _addCollectionPanelOpen: boolean;
|
|
208
|
+
declare _translations: TranslationItem[] | null;
|
|
209
|
+
declare _translationsLoading: boolean;
|
|
210
|
+
declare _translationBusy: boolean;
|
|
136
211
|
declare _triggerEl: HTMLElement | null;
|
|
137
212
|
|
|
138
213
|
/** Whether collections were modified during this session (triggers page reload on close) */
|
|
@@ -147,6 +222,7 @@ export class JantPostMenu extends LitElement {
|
|
|
147
222
|
|
|
148
223
|
constructor() {
|
|
149
224
|
super();
|
|
225
|
+
this.languages = [];
|
|
150
226
|
this._open = false;
|
|
151
227
|
this._data = null;
|
|
152
228
|
this._x = 0;
|
|
@@ -158,6 +234,9 @@ export class JantPostMenu extends LitElement {
|
|
|
158
234
|
this._collectionSearch = "";
|
|
159
235
|
this._threadCollectionIds = [];
|
|
160
236
|
this._addCollectionPanelOpen = false;
|
|
237
|
+
this._translations = null;
|
|
238
|
+
this._translationsLoading = false;
|
|
239
|
+
this._translationBusy = false;
|
|
161
240
|
this._triggerEl = null;
|
|
162
241
|
}
|
|
163
242
|
|
|
@@ -215,6 +294,12 @@ export class JantPostMenu extends LitElement {
|
|
|
215
294
|
return;
|
|
216
295
|
}
|
|
217
296
|
if (this._open) {
|
|
297
|
+
// One step back per press, not one step out: the language picker is a
|
|
298
|
+
// level deeper than the panel that opens it.
|
|
299
|
+
if (this._view === "language-switch") {
|
|
300
|
+
this.#showLanguagePanel();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
218
303
|
if (this._view !== "menu") {
|
|
219
304
|
this.#showMainMenu();
|
|
220
305
|
return;
|
|
@@ -297,11 +382,27 @@ export class JantPostMenu extends LitElement {
|
|
|
297
382
|
return;
|
|
298
383
|
}
|
|
299
384
|
|
|
300
|
-
// Clicking inside the dropdown — don't close (menu or
|
|
385
|
+
// Clicking inside the dropdown — don't close (menu or any of its panels).
|
|
386
|
+
//
|
|
387
|
+
// `.post-menu-view` — the root of whichever panel is on screen — and not
|
|
388
|
+
// the two obvious alternatives, for two different reasons.
|
|
389
|
+
//
|
|
390
|
+
// Not `[role="menu"]`: that is on the *list*, and a panel's header sits
|
|
391
|
+
// outside it, so every back button read as a click outside the menu and
|
|
392
|
+
// closed the whole thing.
|
|
393
|
+
//
|
|
394
|
+
// Not `.post-menu-panel`, the container the views render into, even though
|
|
395
|
+
// it reads as the more honest "anywhere in the dropdown". On a *real*
|
|
396
|
+
// click the browser runs a microtask checkpoint between event listeners,
|
|
397
|
+
// so a menu item that switches panels has already re-rendered by the time
|
|
398
|
+
// this handler runs: `target` is a detached node whose subtree ends at the
|
|
399
|
+
// view root, and `closest` can no longer reach the container. Clicking
|
|
400
|
+
// Language would open the panel and close the menu in the same event.
|
|
401
|
+
// (`el.click()` from a script never shows this — the stack never empties,
|
|
402
|
+
// so nothing re-renders mid-dispatch. Hence the `performUpdate()` in the
|
|
403
|
+
// test that covers it.)
|
|
301
404
|
if (this._open) {
|
|
302
|
-
const inside = target.closest?.(
|
|
303
|
-
"[role='menu'], [data-collection-picker], [data-visibility-panel]",
|
|
304
|
-
);
|
|
405
|
+
const inside = target.closest?.(".post-menu-view, .post-menu-panel");
|
|
305
406
|
if (inside) return;
|
|
306
407
|
}
|
|
307
408
|
|
|
@@ -462,6 +563,402 @@ export class JantPostMenu extends LitElement {
|
|
|
462
563
|
);
|
|
463
564
|
}
|
|
464
565
|
|
|
566
|
+
// --- Language and translations ---
|
|
567
|
+
|
|
568
|
+
/** Whether this site publishes enough languages for any of this to matter. */
|
|
569
|
+
get #multilingual(): boolean {
|
|
570
|
+
return this.languages.length > 1;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#languageLabel(tag: string | null): string {
|
|
574
|
+
if (!tag) return "Not set";
|
|
575
|
+
return this.languages.find((l) => l.tag === tag)?.label ?? tag;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async #openLanguagePanel() {
|
|
579
|
+
if (this._data?.isReply) return;
|
|
580
|
+
this._view = "language";
|
|
581
|
+
this.#focusAfterUpdate(LANGUAGE_PANEL_FOCUS);
|
|
582
|
+
// The panel is mostly about them, and the picker one level down needs them
|
|
583
|
+
// too: a language is unavailable exactly when another post in the group
|
|
584
|
+
// already holds it.
|
|
585
|
+
await this.#loadTranslations();
|
|
586
|
+
// A Thread whose every other language is already spoken for has nothing to
|
|
587
|
+
// put on screen until this resolves, so the first attempt found nothing to
|
|
588
|
+
// land on. Anywhere else, focus is already inside and must stay put.
|
|
589
|
+
if (!this.contains(document.activeElement)) {
|
|
590
|
+
this.#focusAfterUpdate(LANGUAGE_PANEL_FOCUS);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
#showLanguagePanel(focusSelector = "[data-post-menu-open-language-switch]") {
|
|
595
|
+
this._view = "language";
|
|
596
|
+
this.#focusAfterUpdate(focusSelector);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The one place the language picker lives.
|
|
601
|
+
*
|
|
602
|
+
* Behind a row of its own rather than at the top of the language panel:
|
|
603
|
+
* switching a Thread's language is a correction, made once if ever, while
|
|
604
|
+
* reading and adding other versions is the daily work. Left inline it also
|
|
605
|
+
* degenerated — on a two-language site whose other version is already linked,
|
|
606
|
+
* the radio group rendered a single unclickable row for the current language.
|
|
607
|
+
*/
|
|
608
|
+
#openLanguageSwitch() {
|
|
609
|
+
if (this._data?.isReply) return;
|
|
610
|
+
this._view = "language-switch";
|
|
611
|
+
this.#focusAfterUpdate(
|
|
612
|
+
"[data-post-menu-language-current='true'], [data-post-menu-language-option]",
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Split the site's languages by what this Thread can do with each.
|
|
618
|
+
*
|
|
619
|
+
* A language another version already holds is neither selectable nor free:
|
|
620
|
+
* the group allows one post per language, so it is spoken for either way.
|
|
621
|
+
*/
|
|
622
|
+
#languageChoices() {
|
|
623
|
+
const current = this._data?.language ?? null;
|
|
624
|
+
const linked = this._translations ?? [];
|
|
625
|
+
const held = new Set(
|
|
626
|
+
linked.flatMap((translation) =>
|
|
627
|
+
translation.language ? [translation.language] : [],
|
|
628
|
+
),
|
|
629
|
+
);
|
|
630
|
+
return {
|
|
631
|
+
current,
|
|
632
|
+
linked,
|
|
633
|
+
selectable: this.languages.filter(
|
|
634
|
+
(language) => language.tag === current || !held.has(language.tag),
|
|
635
|
+
),
|
|
636
|
+
free: this.languages.filter(
|
|
637
|
+
(language) => language.tag !== current && !held.has(language.tag),
|
|
638
|
+
),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Set the language of this whole Thread.
|
|
644
|
+
*
|
|
645
|
+
* Not optimistic: unlike visibility, this can be refused server-side — the
|
|
646
|
+
* Thread's translation group may already hold that language — and showing the
|
|
647
|
+
* new language before the server agrees would be a lie the author acts on.
|
|
648
|
+
*/
|
|
649
|
+
async #setLanguage(tag: string) {
|
|
650
|
+
const data = this._data;
|
|
651
|
+
if (!data || this._translationBusy) return;
|
|
652
|
+
this._translationBusy = true;
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const response = await fetch(
|
|
656
|
+
publicPath(`/api/posts/${data.threadId}/language`),
|
|
657
|
+
{
|
|
658
|
+
method: "PUT",
|
|
659
|
+
headers: { "Content-Type": "application/json" },
|
|
660
|
+
credentials: "same-origin",
|
|
661
|
+
body: JSON.stringify({ language: tag }),
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
if (!response.ok) {
|
|
665
|
+
showToast(
|
|
666
|
+
await readErrorMessage(
|
|
667
|
+
response,
|
|
668
|
+
"Could not change the language. Try again.",
|
|
669
|
+
),
|
|
670
|
+
"error",
|
|
671
|
+
);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const article = document.querySelector<HTMLElement>(
|
|
676
|
+
`article[data-post-id="${data.threadId}"]`,
|
|
677
|
+
);
|
|
678
|
+
if (article) article.dataset.postLanguage = tag;
|
|
679
|
+
this._data = { ...data, language: tag };
|
|
680
|
+
showToast(`Thread is now in ${this.#languageLabel(tag)}.`);
|
|
681
|
+
this.#close();
|
|
682
|
+
} catch {
|
|
683
|
+
showToast("Could not change the language. Try again.", "error");
|
|
684
|
+
} finally {
|
|
685
|
+
this._translationBusy = false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async #loadTranslations() {
|
|
690
|
+
const data = this._data;
|
|
691
|
+
if (!data) return;
|
|
692
|
+
this._translationsLoading = true;
|
|
693
|
+
try {
|
|
694
|
+
const response = await fetch(
|
|
695
|
+
publicPath(`/api/posts/${data.threadId}/translations`),
|
|
696
|
+
{ credentials: "same-origin" },
|
|
697
|
+
);
|
|
698
|
+
if (!response.ok) return;
|
|
699
|
+
const body = (await response.json()) as {
|
|
700
|
+
translations?: TranslationItem[];
|
|
701
|
+
};
|
|
702
|
+
this._translations = body.translations ?? [];
|
|
703
|
+
} catch {
|
|
704
|
+
this._translations = [];
|
|
705
|
+
} finally {
|
|
706
|
+
this._translationsLoading = false;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Open the composer on a new post that translates this one.
|
|
712
|
+
*
|
|
713
|
+
* Prefers the dialog, so the author writes the translation without leaving
|
|
714
|
+
* the post they are translating — navigating away to a blank page loses that
|
|
715
|
+
* context exactly when it is most useful. Pages that render no dialog (the
|
|
716
|
+
* standalone composer among them) fall back to the URL, which carries the
|
|
717
|
+
* same two values.
|
|
718
|
+
*/
|
|
719
|
+
async #writeTranslation(tag: string) {
|
|
720
|
+
const data = this._data;
|
|
721
|
+
if (!data) return;
|
|
722
|
+
this.#close({ restoreFocus: false });
|
|
723
|
+
|
|
724
|
+
const composeEl = await ensureComposeDialog();
|
|
725
|
+
if (composeEl) {
|
|
726
|
+
await composeEl.openTranslation(data.threadId, tag);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const params = new URLSearchParams({
|
|
731
|
+
translationOf: data.threadId,
|
|
732
|
+
lang: tag,
|
|
733
|
+
});
|
|
734
|
+
window.location.assign(publicPath(`/new?${params.toString()}`));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Ask the author which post to link, in a dialog rather than in this popover.
|
|
739
|
+
*
|
|
740
|
+
* The menu is the wrong shape for a search: results need room for a real
|
|
741
|
+
* title and a keyboard. The picker is generic — the eligibility rules stay on
|
|
742
|
+
* the server, and this only supplies the copy and the request.
|
|
743
|
+
*/
|
|
744
|
+
async #openLinkPicker() {
|
|
745
|
+
const data = this._data;
|
|
746
|
+
if (!data) return;
|
|
747
|
+
this.#close({ restoreFocus: false });
|
|
748
|
+
|
|
749
|
+
const picked = await pickPost({
|
|
750
|
+
heading: "Link a translation",
|
|
751
|
+
hint: "Only posts you could actually link are listed: published, and in a language this one's group does not already have.",
|
|
752
|
+
placeholder: "Search your posts, or paste a URL…",
|
|
753
|
+
emptyHint: "Nothing matched that you could link.",
|
|
754
|
+
search: async (query) => {
|
|
755
|
+
// An author writing a translation has the other post open in a tab, so
|
|
756
|
+
// the URL is closer to hand than the title. Resolving it also lets the
|
|
757
|
+
// answer be a reason rather than an empty list.
|
|
758
|
+
if (looksLikeAddress(query)) {
|
|
759
|
+
return this.#resolveTranslationAddress(data.threadId, query);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const response = await fetch(
|
|
763
|
+
publicPath(
|
|
764
|
+
`/api/posts/${data.threadId}/translations/candidates?q=${encodeURIComponent(query)}`,
|
|
765
|
+
),
|
|
766
|
+
{ credentials: "same-origin" },
|
|
767
|
+
);
|
|
768
|
+
if (!response.ok) return { items: [] };
|
|
769
|
+
const body = (await response.json()) as {
|
|
770
|
+
candidates?: Array<{
|
|
771
|
+
id: string;
|
|
772
|
+
label: string;
|
|
773
|
+
language: string | null;
|
|
774
|
+
}>;
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
items: (body.candidates ?? []).map((candidate) => ({
|
|
778
|
+
id: candidate.id,
|
|
779
|
+
label: candidate.label,
|
|
780
|
+
meta: this.#languageLabel(candidate.language),
|
|
781
|
+
})),
|
|
782
|
+
};
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
if (!picked) return;
|
|
787
|
+
this._data = data;
|
|
788
|
+
await this.#linkTranslation(picked);
|
|
789
|
+
// The page renders the link too ("Also available in …"), so it has to catch
|
|
790
|
+
// up with the change the author just made.
|
|
791
|
+
window.location.reload();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Look up the post at a pasted address, and say what is wrong with it when
|
|
796
|
+
* it cannot be linked. The rules are the server's; this only puts the answer
|
|
797
|
+
* into words.
|
|
798
|
+
*/
|
|
799
|
+
async #resolveTranslationAddress(
|
|
800
|
+
threadId: string,
|
|
801
|
+
address: string,
|
|
802
|
+
): Promise<PostPickerResult> {
|
|
803
|
+
let resolution: TranslationAddressResolution | undefined;
|
|
804
|
+
try {
|
|
805
|
+
const response = await fetch(
|
|
806
|
+
publicPath(
|
|
807
|
+
`/api/posts/${threadId}/translations/resolve?url=${encodeURIComponent(address)}`,
|
|
808
|
+
),
|
|
809
|
+
{ credentials: "same-origin" },
|
|
810
|
+
);
|
|
811
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
812
|
+
({ resolution } = (await response.json()) as {
|
|
813
|
+
resolution?: TranslationAddressResolution;
|
|
814
|
+
});
|
|
815
|
+
} catch {
|
|
816
|
+
return {
|
|
817
|
+
items: [],
|
|
818
|
+
note: "Could not check that address. Try again in a moment.",
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!resolution) return { items: [] };
|
|
823
|
+
if (resolution.kind === "ok") {
|
|
824
|
+
return {
|
|
825
|
+
items: [
|
|
826
|
+
{
|
|
827
|
+
id: resolution.candidate.id,
|
|
828
|
+
label: resolution.candidate.label,
|
|
829
|
+
meta: this.#languageLabel(resolution.candidate.language),
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return { items: [], note: this.#translationAddressNote(resolution) };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
#translationAddressNote(resolution: TranslationAddressResolution): string {
|
|
839
|
+
const language = this.#languageLabel(
|
|
840
|
+
"language" in resolution ? resolution.language : null,
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
switch (resolution.kind) {
|
|
844
|
+
case "external":
|
|
845
|
+
return "That address is somewhere else. Only posts on this site can be linked.";
|
|
846
|
+
case "not_found":
|
|
847
|
+
return `Nothing at ${resolution.address}. Check the address, or search by title.`;
|
|
848
|
+
case "not_a_post":
|
|
849
|
+
return "That address is not a post. Translations link posts to posts.";
|
|
850
|
+
case "same_thread":
|
|
851
|
+
return "That is this post.";
|
|
852
|
+
case "unpublished":
|
|
853
|
+
return "That post is a draft. Publish it, then link it.";
|
|
854
|
+
case "no_language":
|
|
855
|
+
return "That post has no language yet. Set one on it first.";
|
|
856
|
+
case "same_language":
|
|
857
|
+
return `That post is written in ${this.#languageLabel(
|
|
858
|
+
this._data?.language ?? null,
|
|
859
|
+
)} too. A translation has to be in another language.`;
|
|
860
|
+
case "language_taken":
|
|
861
|
+
return `A ${language} version is already linked here. Unlink that one first.`;
|
|
862
|
+
case "group_conflict":
|
|
863
|
+
return "That post belongs to another translation group. Unlink it first.";
|
|
864
|
+
case "group_language_taken":
|
|
865
|
+
return `Its translation group already has a ${language} version.`;
|
|
866
|
+
default:
|
|
867
|
+
return "That post cannot be linked.";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async #linkTranslation(otherId: string) {
|
|
872
|
+
const data = this._data;
|
|
873
|
+
if (!data || this._translationBusy) return;
|
|
874
|
+
this._translationBusy = true;
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
const response = await fetch(
|
|
878
|
+
publicPath(`/api/posts/${data.threadId}/translations`),
|
|
879
|
+
{
|
|
880
|
+
method: "POST",
|
|
881
|
+
headers: { "Content-Type": "application/json" },
|
|
882
|
+
credentials: "same-origin",
|
|
883
|
+
body: JSON.stringify({ postId: otherId }),
|
|
884
|
+
},
|
|
885
|
+
);
|
|
886
|
+
if (!response.ok) {
|
|
887
|
+
showToast(
|
|
888
|
+
await readErrorMessage(
|
|
889
|
+
response,
|
|
890
|
+
"Could not link those posts. Try again.",
|
|
891
|
+
),
|
|
892
|
+
"error",
|
|
893
|
+
);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
await this.#loadTranslations();
|
|
897
|
+
showToast("Posts linked as translations.");
|
|
898
|
+
} catch {
|
|
899
|
+
showToast("Could not link those posts. Try again.", "error");
|
|
900
|
+
} finally {
|
|
901
|
+
this._translationBusy = false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Take one other version out of this Thread's translation group.
|
|
907
|
+
*
|
|
908
|
+
* Unlinking is per-version rather than "leave the group": from the author's
|
|
909
|
+
* seat the group *is* the list of other versions, so removing the row they
|
|
910
|
+
* are looking at is the action they mean. A group left with a single member
|
|
911
|
+
* collapses server-side, which makes the two-post case do the obvious thing.
|
|
912
|
+
*
|
|
913
|
+
* The confirm runs after the menu closes, the way deleting a post does — a
|
|
914
|
+
* modal opened underneath a popover fights it for focus and for the backdrop
|
|
915
|
+
* click.
|
|
916
|
+
*/
|
|
917
|
+
async #unlinkTranslation(target: TranslationItem) {
|
|
918
|
+
const data = this._data;
|
|
919
|
+
if (!data || this._translationBusy) return;
|
|
920
|
+
|
|
921
|
+
const trigger = this._triggerEl;
|
|
922
|
+
this.#close({ restoreFocus: false });
|
|
923
|
+
|
|
924
|
+
const languageLabel = this.#languageLabel(target.language);
|
|
925
|
+
const confirmed = await showConfirmDialog({
|
|
926
|
+
message: `Unlink the ${languageLabel} version, “${target.label}”? Both posts stay published — they just stop pointing at each other.`,
|
|
927
|
+
confirmLabel: "Unlink",
|
|
928
|
+
cancelLabel: "Cancel",
|
|
929
|
+
tone: "danger",
|
|
930
|
+
});
|
|
931
|
+
if (!confirmed) {
|
|
932
|
+
trigger?.focus();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
this._translationBusy = true;
|
|
937
|
+
try {
|
|
938
|
+
const response = await fetch(
|
|
939
|
+
publicPath(`/api/posts/${target.id}/translations`),
|
|
940
|
+
{ method: "DELETE", credentials: "same-origin" },
|
|
941
|
+
);
|
|
942
|
+
if (!response.ok) {
|
|
943
|
+
showToast(
|
|
944
|
+
await readErrorMessage(response, "Could not unlink. Try again."),
|
|
945
|
+
"error",
|
|
946
|
+
);
|
|
947
|
+
trigger?.focus();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
showToast("Translation link removed.");
|
|
951
|
+
// The page renders the link too ("Also available in …"), so it has to
|
|
952
|
+
// catch up with the change the author just made.
|
|
953
|
+
window.location.reload();
|
|
954
|
+
} catch {
|
|
955
|
+
showToast("Could not unlink. Try again.", "error");
|
|
956
|
+
trigger?.focus();
|
|
957
|
+
} finally {
|
|
958
|
+
this._translationBusy = false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
465
962
|
#close(options: { restoreFocus?: boolean } = {}) {
|
|
466
963
|
const restoreFocus =
|
|
467
964
|
options.restoreFocus ?? this.#restoreTriggerFocusOnClose;
|
|
@@ -473,6 +970,7 @@ export class JantPostMenu extends LitElement {
|
|
|
473
970
|
this._view = "menu";
|
|
474
971
|
this._addCollectionPanelOpen = false;
|
|
475
972
|
this._collectionSearch = "";
|
|
973
|
+
this._translations = null;
|
|
476
974
|
|
|
477
975
|
if (this.#collectionsDirty) {
|
|
478
976
|
this.#collectionsDirty = false;
|
|
@@ -518,17 +1016,42 @@ export class JantPostMenu extends LitElement {
|
|
|
518
1016
|
const postId = this._data.id;
|
|
519
1017
|
this.#close({ restoreFocus: false });
|
|
520
1018
|
|
|
521
|
-
const
|
|
522
|
-
"compose-dialog",
|
|
523
|
-
) as HTMLDialogElement | null;
|
|
524
|
-
const composeEl = dialog?.querySelector("jant-compose-dialog") as
|
|
525
|
-
| import("./jant-compose-dialog.js").JantComposeDialog
|
|
526
|
-
| null;
|
|
1019
|
+
const composeEl = await ensureComposeDialog();
|
|
527
1020
|
if (composeEl) {
|
|
528
1021
|
await composeEl.openEdit(postId);
|
|
529
1022
|
}
|
|
530
1023
|
}
|
|
531
1024
|
|
|
1025
|
+
async #publish() {
|
|
1026
|
+
if (!this._data) return;
|
|
1027
|
+
const postId = this._data.id;
|
|
1028
|
+
const trigger = this._triggerEl;
|
|
1029
|
+
this.#close({ restoreFocus: false });
|
|
1030
|
+
|
|
1031
|
+
const article = document.querySelector<HTMLElement>(
|
|
1032
|
+
`article[data-post-id="${postId}"]`,
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
try {
|
|
1036
|
+
const res = await fetch(`/api/posts/${postId}`, {
|
|
1037
|
+
method: "PUT",
|
|
1038
|
+
headers: { "Content-Type": "application/json" },
|
|
1039
|
+
body: JSON.stringify({ status: "published" }),
|
|
1040
|
+
});
|
|
1041
|
+
if (!res.ok) throw new Error();
|
|
1042
|
+
|
|
1043
|
+
showToast("Draft published.");
|
|
1044
|
+
// The badge, the timestamp, and any drafts published alongside this one
|
|
1045
|
+
// all change at once — re-render the surface rather than patching it.
|
|
1046
|
+
if (!article || !(await refreshArticleView(article))) {
|
|
1047
|
+
globalThis.location.reload();
|
|
1048
|
+
}
|
|
1049
|
+
} catch {
|
|
1050
|
+
showToast("Could not publish the draft. Try again.", "error");
|
|
1051
|
+
trigger?.focus();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
532
1055
|
#setVisibility(newVisibility: string) {
|
|
533
1056
|
if (!this._data) return;
|
|
534
1057
|
const postId = this._data.id;
|
|
@@ -711,8 +1234,10 @@ export class JantPostMenu extends LitElement {
|
|
|
711
1234
|
this.#close({ restoreFocus: false });
|
|
712
1235
|
|
|
713
1236
|
const confirmed = await showConfirmDialog({
|
|
714
|
-
message:
|
|
715
|
-
|
|
1237
|
+
message: this._data.isDraft
|
|
1238
|
+
? "Discard this draft permanently? This can't be undone."
|
|
1239
|
+
: "Delete this post permanently? This can't be undone.",
|
|
1240
|
+
confirmLabel: this._data.isDraft ? "Discard" : "Delete",
|
|
716
1241
|
cancelLabel: "Cancel",
|
|
717
1242
|
tone: "danger",
|
|
718
1243
|
});
|
|
@@ -896,8 +1421,7 @@ export class JantPostMenu extends LitElement {
|
|
|
896
1421
|
if (!detail) return;
|
|
897
1422
|
|
|
898
1423
|
const formEl = this.querySelector("jant-collection-form") as
|
|
899
|
-
|
|
900
|
-
| null;
|
|
1424
|
+
(HTMLElement & { loading: boolean }) | null;
|
|
901
1425
|
if (formEl) formEl.loading = true;
|
|
902
1426
|
|
|
903
1427
|
try {
|
|
@@ -957,26 +1481,23 @@ export class JantPostMenu extends LitElement {
|
|
|
957
1481
|
}
|
|
958
1482
|
}
|
|
959
1483
|
|
|
960
|
-
/**
|
|
1484
|
+
/** Collection form labels the server rendered onto the compose dialog. */
|
|
961
1485
|
#getCollectionFormLabels() {
|
|
962
|
-
|
|
963
|
-
| import("./jant-compose-dialog.js").JantComposeDialog
|
|
964
|
-
| null;
|
|
965
|
-
return composeEl?.labels?.collectionFormLabels ?? null;
|
|
1486
|
+
return readComposeDialogLabels()?.collectionFormLabels ?? null;
|
|
966
1487
|
}
|
|
967
1488
|
|
|
968
1489
|
#getAddCollectionLabel() {
|
|
969
|
-
|
|
970
|
-
| import("./jant-compose-dialog.js").JantComposeDialog
|
|
971
|
-
| null;
|
|
972
|
-
return composeEl?.labels?.addCollection ?? "Add Collection";
|
|
1490
|
+
return readComposeDialogLabels()?.addCollection ?? "Add Collection";
|
|
973
1491
|
}
|
|
974
1492
|
|
|
1493
|
+
/**
|
|
1494
|
+
* The composer reads its collections from server-rendered markup, so a
|
|
1495
|
+
* collection created here would be missing from it until the next page
|
|
1496
|
+
* load. Loading the composer to refresh it is the price of staying right.
|
|
1497
|
+
*/
|
|
975
1498
|
async #refreshComposeCollections() {
|
|
976
|
-
const composeEl =
|
|
977
|
-
|
|
978
|
-
} | null;
|
|
979
|
-
await composeEl?.refreshCollections?.();
|
|
1499
|
+
const composeEl = await ensureComposeDialog();
|
|
1500
|
+
await composeEl?.refreshCollections();
|
|
980
1501
|
}
|
|
981
1502
|
|
|
982
1503
|
#getVisibilityLabel(visibility: string) {
|
|
@@ -992,6 +1513,21 @@ export class JantPostMenu extends LitElement {
|
|
|
992
1513
|
|
|
993
1514
|
// --- Icons (inline SVG) ---
|
|
994
1515
|
|
|
1516
|
+
#iconPublish() {
|
|
1517
|
+
return html`<svg
|
|
1518
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1519
|
+
viewBox="0 0 24 24"
|
|
1520
|
+
fill="none"
|
|
1521
|
+
stroke="currentColor"
|
|
1522
|
+
stroke-width="1.75"
|
|
1523
|
+
stroke-linecap="round"
|
|
1524
|
+
stroke-linejoin="round"
|
|
1525
|
+
>
|
|
1526
|
+
<path d="M12 19V5" />
|
|
1527
|
+
<path d="m5 12 7-7 7 7" />
|
|
1528
|
+
</svg>`;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
995
1531
|
#iconEdit() {
|
|
996
1532
|
return html`<svg
|
|
997
1533
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -1131,6 +1667,22 @@ export class JantPostMenu extends LitElement {
|
|
|
1131
1667
|
</svg>`;
|
|
1132
1668
|
}
|
|
1133
1669
|
|
|
1670
|
+
#iconExternal() {
|
|
1671
|
+
return html`<svg
|
|
1672
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1673
|
+
viewBox="0 0 24 24"
|
|
1674
|
+
fill="none"
|
|
1675
|
+
stroke="currentColor"
|
|
1676
|
+
stroke-width="1.9"
|
|
1677
|
+
stroke-linecap="round"
|
|
1678
|
+
stroke-linejoin="round"
|
|
1679
|
+
>
|
|
1680
|
+
<path d="M14 4h6v6" />
|
|
1681
|
+
<path d="M20 4 11 13" />
|
|
1682
|
+
<path d="M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4" />
|
|
1683
|
+
</svg>`;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1134
1686
|
// --- Render ---
|
|
1135
1687
|
|
|
1136
1688
|
#renderCollectionPicker() {
|
|
@@ -1164,116 +1716,126 @@ export class JantPostMenu extends LitElement {
|
|
|
1164
1716
|
<span>Collections</span>
|
|
1165
1717
|
</div>
|
|
1166
1718
|
</div>
|
|
1167
|
-
${
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1719
|
+
${
|
|
1720
|
+
collections.length > 0
|
|
1721
|
+
? html`<div class="post-menu-picker-search">
|
|
1722
|
+
<svg
|
|
1723
|
+
width="14"
|
|
1724
|
+
height="14"
|
|
1725
|
+
viewBox="0 0 24 24"
|
|
1726
|
+
fill="none"
|
|
1727
|
+
stroke="currentColor"
|
|
1728
|
+
stroke-width="2"
|
|
1729
|
+
stroke-linecap="round"
|
|
1730
|
+
stroke-linejoin="round"
|
|
1731
|
+
>
|
|
1732
|
+
<circle cx="11" cy="11" r="8" />
|
|
1733
|
+
<path d="m21 21-4.3-4.3" />
|
|
1734
|
+
</svg>
|
|
1735
|
+
<input
|
|
1736
|
+
type="text"
|
|
1737
|
+
placeholder="Search collections..."
|
|
1738
|
+
autocomplete="off"
|
|
1739
|
+
autocorrect="off"
|
|
1740
|
+
spellcheck="false"
|
|
1741
|
+
.value=${this._collectionSearch}
|
|
1742
|
+
@keydown=${this.#handleCollectionSearchKeydown}
|
|
1743
|
+
@input=${(e: Event) => {
|
|
1744
|
+
this._collectionSearch = (
|
|
1745
|
+
e.target as HTMLInputElement
|
|
1746
|
+
).value;
|
|
1747
|
+
}}
|
|
1748
|
+
/>
|
|
1749
|
+
</div>`
|
|
1750
|
+
: nothing
|
|
1751
|
+
}
|
|
1196
1752
|
<div
|
|
1197
1753
|
class="post-menu-picker-list"
|
|
1198
1754
|
role="listbox"
|
|
1199
1755
|
aria-multiselectable="true"
|
|
1200
1756
|
>
|
|
1201
|
-
${
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1757
|
+
${
|
|
1758
|
+
this._collectionsLoading
|
|
1759
|
+
? html`<div class="post-menu-picker-empty">Loading...</div>`
|
|
1760
|
+
: filtered.length > 0
|
|
1761
|
+
? filtered.map((c) => {
|
|
1762
|
+
const selected = this._threadCollectionIds.includes(c.id);
|
|
1763
|
+
return html`
|
|
1764
|
+
<button
|
|
1765
|
+
type="button"
|
|
1766
|
+
role="option"
|
|
1767
|
+
aria-selected=${selected ? "true" : "false"}
|
|
1768
|
+
class=${`post-menu-picker-option${
|
|
1769
|
+
selected ? " post-menu-picker-option-selected" : ""
|
|
1770
|
+
}`}
|
|
1771
|
+
@keydown=${(event: globalThis.KeyboardEvent) =>
|
|
1772
|
+
this.#handleCollectionOptionKeydown(event, c.id)}
|
|
1773
|
+
@click=${(event: globalThis.MouseEvent) =>
|
|
1774
|
+
this.#handleCollectionOptionClick(event, c.id)}
|
|
1775
|
+
>
|
|
1776
|
+
<span class="post-menu-picker-title">${c.title}</span>
|
|
1777
|
+
${
|
|
1778
|
+
selected
|
|
1779
|
+
? html`<span
|
|
1780
|
+
class="post-menu-picker-marker post-menu-picker-marker-selected"
|
|
1781
|
+
>
|
|
1782
|
+
<svg
|
|
1783
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1784
|
+
viewBox="0 0 24 24"
|
|
1785
|
+
fill="none"
|
|
1786
|
+
aria-hidden="true"
|
|
1787
|
+
>
|
|
1788
|
+
<circle
|
|
1789
|
+
cx="12"
|
|
1790
|
+
cy="12"
|
|
1791
|
+
r="10"
|
|
1792
|
+
fill="currentColor"
|
|
1793
|
+
/>
|
|
1794
|
+
<path
|
|
1795
|
+
d="M8 12.5 10.7 15.2 16.4 9.5"
|
|
1796
|
+
stroke="var(--site-page-bg)"
|
|
1797
|
+
stroke-width="2.3"
|
|
1798
|
+
stroke-linecap="round"
|
|
1799
|
+
stroke-linejoin="round"
|
|
1800
|
+
/>
|
|
1801
|
+
</svg>
|
|
1802
|
+
</span>`
|
|
1803
|
+
: html`<span
|
|
1804
|
+
class="post-menu-picker-marker post-menu-picker-marker-add"
|
|
1805
|
+
>
|
|
1806
|
+
<svg
|
|
1807
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1808
|
+
viewBox="0 0 24 24"
|
|
1809
|
+
fill="none"
|
|
1810
|
+
aria-hidden="true"
|
|
1811
|
+
>
|
|
1812
|
+
<circle
|
|
1813
|
+
cx="12"
|
|
1814
|
+
cy="12"
|
|
1815
|
+
r="9"
|
|
1816
|
+
stroke="currentColor"
|
|
1817
|
+
stroke-width="1.8"
|
|
1818
|
+
/>
|
|
1819
|
+
<path
|
|
1820
|
+
d="M12 8v8M8 12h8"
|
|
1821
|
+
stroke="currentColor"
|
|
1822
|
+
stroke-width="1.9"
|
|
1823
|
+
stroke-linecap="round"
|
|
1824
|
+
/>
|
|
1825
|
+
</svg>
|
|
1826
|
+
</span>`
|
|
1827
|
+
}
|
|
1828
|
+
</button>
|
|
1829
|
+
`;
|
|
1830
|
+
})
|
|
1831
|
+
: html`<div class="post-menu-picker-empty">
|
|
1832
|
+
${
|
|
1833
|
+
hasSearch
|
|
1834
|
+
? "No matching collections"
|
|
1835
|
+
: "No collections yet"
|
|
1836
|
+
}
|
|
1837
|
+
</div>`
|
|
1838
|
+
}
|
|
1277
1839
|
</div>
|
|
1278
1840
|
<div class="post-menu-picker-footer">
|
|
1279
1841
|
<button
|
|
@@ -1302,6 +1864,236 @@ export class JantPostMenu extends LitElement {
|
|
|
1302
1864
|
`;
|
|
1303
1865
|
}
|
|
1304
1866
|
|
|
1867
|
+
/**
|
|
1868
|
+
* Everything about this Thread's language, in one place.
|
|
1869
|
+
*
|
|
1870
|
+
* The picker and the translation list belong together: a language is
|
|
1871
|
+
* unavailable here precisely because another post in the group already holds
|
|
1872
|
+
* it, and that reason is only legible next to the list of those posts. Two
|
|
1873
|
+
* top-level menu entries for one subject was one too many.
|
|
1874
|
+
*
|
|
1875
|
+
* Top to bottom, the panel answers three questions in the order they get
|
|
1876
|
+
* asked. *What language is this?* — the entry that opened this panel already
|
|
1877
|
+
* said "Language · 简体中文", so the panel opens by confirming it, and the
|
|
1878
|
+
* picker itself is one level down (see `#openLanguageSwitch`) because
|
|
1879
|
+
* changing it is a correction, made once if ever. *What can I do?* — writing
|
|
1880
|
+
* or linking a version, the recurring work, and self-describing enough to
|
|
1881
|
+
* need no section label over it. *What is already there?* — the other
|
|
1882
|
+
* versions, last, because they arrive with a fetch: any earlier and the
|
|
1883
|
+
* panel would shove itself downward under the author's cursor a moment
|
|
1884
|
+
* after opening.
|
|
1885
|
+
*/
|
|
1886
|
+
#renderLanguagePanel() {
|
|
1887
|
+
if (!this._data || this._data.isReply) return nothing;
|
|
1888
|
+
|
|
1889
|
+
const { current, linked, free } = this.#languageChoices();
|
|
1890
|
+
return html`
|
|
1891
|
+
<div class="post-menu-view post-menu-language-panel">
|
|
1892
|
+
<div class="post-menu-panel-header">
|
|
1893
|
+
<button
|
|
1894
|
+
type="button"
|
|
1895
|
+
class="post-menu-panel-back"
|
|
1896
|
+
aria-label="Back"
|
|
1897
|
+
@click=${() => this.#showMainMenu("[data-post-menu-open-language]")}
|
|
1898
|
+
>
|
|
1899
|
+
${this.#iconChevronLeft()}
|
|
1900
|
+
</button>
|
|
1901
|
+
<div class="post-menu-panel-heading">
|
|
1902
|
+
<span>Language</span>
|
|
1903
|
+
</div>
|
|
1904
|
+
</div>
|
|
1905
|
+
<div role="menu" class="post-menu-list">
|
|
1906
|
+
<!-- Both sections stand or fall together: with every other language
|
|
1907
|
+
spoken for there is nothing to switch to and nothing to add. -->
|
|
1908
|
+
${
|
|
1909
|
+
free.length > 0
|
|
1910
|
+
? html`
|
|
1911
|
+
<div class="post-menu-section">
|
|
1912
|
+
<button
|
|
1913
|
+
type="button"
|
|
1914
|
+
role="menuitem"
|
|
1915
|
+
class="post-menu-item"
|
|
1916
|
+
data-post-menu-open-language-switch
|
|
1917
|
+
?disabled=${this._translationBusy}
|
|
1918
|
+
@click=${() => this.#openLanguageSwitch()}
|
|
1919
|
+
>
|
|
1920
|
+
<span class="post-menu-item-label">Change language</span>
|
|
1921
|
+
<span class="post-menu-item-meta"
|
|
1922
|
+
>${this.#languageLabel(current)}</span
|
|
1923
|
+
>
|
|
1924
|
+
<span
|
|
1925
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
1926
|
+
>${this.#iconChevronRight()}</span
|
|
1927
|
+
>
|
|
1928
|
+
</button>
|
|
1929
|
+
</div>
|
|
1930
|
+
<div class="post-menu-section">
|
|
1931
|
+
${free.map(
|
|
1932
|
+
(language, index) => html`
|
|
1933
|
+
<button
|
|
1934
|
+
type="button"
|
|
1935
|
+
role="menuitem"
|
|
1936
|
+
class="post-menu-item"
|
|
1937
|
+
?data-post-menu-translation-first=${index === 0}
|
|
1938
|
+
@click=${() => this.#writeTranslation(language.tag)}
|
|
1939
|
+
>
|
|
1940
|
+
<span class="post-menu-item-label"
|
|
1941
|
+
>Write the ${language.label} version</span
|
|
1942
|
+
>
|
|
1943
|
+
<span
|
|
1944
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
1945
|
+
>${this.#iconChevronRight()}</span
|
|
1946
|
+
>
|
|
1947
|
+
</button>
|
|
1948
|
+
`,
|
|
1949
|
+
)}
|
|
1950
|
+
<button
|
|
1951
|
+
type="button"
|
|
1952
|
+
role="menuitem"
|
|
1953
|
+
class="post-menu-item"
|
|
1954
|
+
@click=${() => this.#openLinkPicker()}
|
|
1955
|
+
>
|
|
1956
|
+
<span class="post-menu-item-label"
|
|
1957
|
+
>Link a version you already wrote</span
|
|
1958
|
+
>
|
|
1959
|
+
<span
|
|
1960
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
1961
|
+
>${this.#iconChevronRight()}</span
|
|
1962
|
+
>
|
|
1963
|
+
</button>
|
|
1964
|
+
</div>
|
|
1965
|
+
`
|
|
1966
|
+
: nothing
|
|
1967
|
+
}
|
|
1968
|
+
${
|
|
1969
|
+
linked.length > 0
|
|
1970
|
+
? html`
|
|
1971
|
+
<div class="post-menu-section">
|
|
1972
|
+
<p class="post-menu-section-label">Other versions</p>
|
|
1973
|
+
${linked.map((translation) => {
|
|
1974
|
+
const languageLabel = this.#languageLabel(
|
|
1975
|
+
translation.language,
|
|
1976
|
+
);
|
|
1977
|
+
return html`
|
|
1978
|
+
<div class="post-menu-row" data-post-menu-translation>
|
|
1979
|
+
<a
|
|
1980
|
+
role="menuitem"
|
|
1981
|
+
class="post-menu-item post-menu-row-main"
|
|
1982
|
+
href=${publicPath(`/${translation.slug}`)}
|
|
1983
|
+
target="_blank"
|
|
1984
|
+
rel="noopener noreferrer"
|
|
1985
|
+
title=${translation.label}
|
|
1986
|
+
aria-label=${`Open the ${languageLabel} version, ${translation.label}, in a new tab`}
|
|
1987
|
+
>
|
|
1988
|
+
<span
|
|
1989
|
+
class="post-menu-item-label"
|
|
1990
|
+
lang=${translation.language ?? nothing}
|
|
1991
|
+
>${languageLabel}</span
|
|
1992
|
+
>
|
|
1993
|
+
<span
|
|
1994
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
1995
|
+
>${this.#iconExternal()}</span
|
|
1996
|
+
>
|
|
1997
|
+
</a>
|
|
1998
|
+
<button
|
|
1999
|
+
type="button"
|
|
2000
|
+
role="menuitem"
|
|
2001
|
+
class="post-menu-row-action post-menu-row-action-danger"
|
|
2002
|
+
data-post-menu-translation-unlink
|
|
2003
|
+
?disabled=${this._translationBusy}
|
|
2004
|
+
aria-label=${`Unlink the ${languageLabel} version`}
|
|
2005
|
+
@click=${() => this.#unlinkTranslation(translation)}
|
|
2006
|
+
>
|
|
2007
|
+
Unlink
|
|
2008
|
+
</button>
|
|
2009
|
+
</div>
|
|
2010
|
+
`;
|
|
2011
|
+
})}
|
|
2012
|
+
</div>
|
|
2013
|
+
`
|
|
2014
|
+
: nothing
|
|
2015
|
+
}
|
|
2016
|
+
${
|
|
2017
|
+
this._translationsLoading
|
|
2018
|
+
? html`<p class="post-menu-hint">Loading…</p>`
|
|
2019
|
+
: nothing
|
|
2020
|
+
}
|
|
2021
|
+
</div>
|
|
2022
|
+
</div>
|
|
2023
|
+
`;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* Which language this Thread is written in.
|
|
2028
|
+
*
|
|
2029
|
+
* A language another version already holds is left out entirely rather than
|
|
2030
|
+
* shown greyed out: the author cannot switch to it, and a dead row saying
|
|
2031
|
+
* "Taken" answers a question nobody asked. The version holding it is one
|
|
2032
|
+
* level up under *Other versions*, where reading it and unlinking it are one
|
|
2033
|
+
* click each.
|
|
2034
|
+
*/
|
|
2035
|
+
#renderLanguageSwitchPanel() {
|
|
2036
|
+
if (!this._data || this._data.isReply) return nothing;
|
|
2037
|
+
|
|
2038
|
+
const { current, selectable } = this.#languageChoices();
|
|
2039
|
+
return html`
|
|
2040
|
+
<div
|
|
2041
|
+
class="post-menu-view post-menu-language-panel post-menu-language-switch-panel"
|
|
2042
|
+
>
|
|
2043
|
+
<div class="post-menu-panel-header">
|
|
2044
|
+
<button
|
|
2045
|
+
type="button"
|
|
2046
|
+
class="post-menu-panel-back"
|
|
2047
|
+
aria-label="Back"
|
|
2048
|
+
@click=${() => this.#showLanguagePanel()}
|
|
2049
|
+
>
|
|
2050
|
+
${this.#iconChevronLeft()}
|
|
2051
|
+
</button>
|
|
2052
|
+
<div class="post-menu-panel-heading">
|
|
2053
|
+
<span>Change language</span>
|
|
2054
|
+
</div>
|
|
2055
|
+
</div>
|
|
2056
|
+
<div role="menu" class="post-menu-list">
|
|
2057
|
+
<div
|
|
2058
|
+
class="post-menu-section"
|
|
2059
|
+
role="radiogroup"
|
|
2060
|
+
aria-label="Language"
|
|
2061
|
+
>
|
|
2062
|
+
${selectable.map((language) => {
|
|
2063
|
+
const selected = language.tag === current;
|
|
2064
|
+
return html`
|
|
2065
|
+
<button
|
|
2066
|
+
type="button"
|
|
2067
|
+
role="menuitemradio"
|
|
2068
|
+
lang=${language.tag}
|
|
2069
|
+
aria-checked=${selected ? "true" : "false"}
|
|
2070
|
+
?disabled=${this._translationBusy}
|
|
2071
|
+
data-post-menu-language-option
|
|
2072
|
+
data-post-menu-language-current=${selected ? "true" : "false"}
|
|
2073
|
+
class=${`post-menu-item${selected ? " post-menu-item-active" : ""}`}
|
|
2074
|
+
@click=${() =>
|
|
2075
|
+
selected
|
|
2076
|
+
? this.#showLanguagePanel()
|
|
2077
|
+
: this.#setLanguage(language.tag)}
|
|
2078
|
+
>
|
|
2079
|
+
<span class="post-menu-item-label">${language.label}</span>
|
|
2080
|
+
${
|
|
2081
|
+
selected
|
|
2082
|
+
? html`<span
|
|
2083
|
+
class="post-menu-item-trailing post-menu-item-check"
|
|
2084
|
+
>${this.#iconCheck()}</span
|
|
2085
|
+
>`
|
|
2086
|
+
: nothing
|
|
2087
|
+
}
|
|
2088
|
+
</button>
|
|
2089
|
+
`;
|
|
2090
|
+
})}
|
|
2091
|
+
</div>
|
|
2092
|
+
</div>
|
|
2093
|
+
</div>
|
|
2094
|
+
`;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1305
2097
|
#renderVisibilityPanel() {
|
|
1306
2098
|
if (!this._data || this._data.isReply) return nothing;
|
|
1307
2099
|
|
|
@@ -1332,9 +2124,9 @@ export class JantPostMenu extends LitElement {
|
|
|
1332
2124
|
role="menuitemradio"
|
|
1333
2125
|
aria-checked=${visibility === "public" ? "true" : "false"}
|
|
1334
2126
|
data-post-menu-visibility-option
|
|
1335
|
-
data-post-menu-visibility-current=${
|
|
1336
|
-
? "true"
|
|
1337
|
-
|
|
2127
|
+
data-post-menu-visibility-current=${
|
|
2128
|
+
visibility === "public" ? "true" : "false"
|
|
2129
|
+
}
|
|
1338
2130
|
class=${`post-menu-item${
|
|
1339
2131
|
visibility === "public" ? " post-menu-item-active" : ""
|
|
1340
2132
|
}`}
|
|
@@ -1344,12 +2136,14 @@ export class JantPostMenu extends LitElement {
|
|
|
1344
2136
|
: this.#setVisibility("public")}
|
|
1345
2137
|
>
|
|
1346
2138
|
<span class="post-menu-item-label">Public</span>
|
|
1347
|
-
${
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
2139
|
+
${
|
|
2140
|
+
visibility === "public"
|
|
2141
|
+
? html`<span
|
|
2142
|
+
class="post-menu-item-trailing post-menu-item-check"
|
|
2143
|
+
>${this.#iconCheck()}</span
|
|
2144
|
+
>`
|
|
2145
|
+
: nothing
|
|
2146
|
+
}
|
|
1353
2147
|
</button>
|
|
1354
2148
|
|
|
1355
2149
|
<button
|
|
@@ -1357,9 +2151,9 @@ export class JantPostMenu extends LitElement {
|
|
|
1357
2151
|
role="menuitemradio"
|
|
1358
2152
|
aria-checked=${visibility === "latest_hidden" ? "true" : "false"}
|
|
1359
2153
|
data-post-menu-visibility-option
|
|
1360
|
-
data-post-menu-visibility-current=${
|
|
1361
|
-
? "true"
|
|
1362
|
-
|
|
2154
|
+
data-post-menu-visibility-current=${
|
|
2155
|
+
visibility === "latest_hidden" ? "true" : "false"
|
|
2156
|
+
}
|
|
1363
2157
|
class=${`post-menu-item${
|
|
1364
2158
|
visibility === "latest_hidden" ? " post-menu-item-active" : ""
|
|
1365
2159
|
}`}
|
|
@@ -1369,12 +2163,14 @@ export class JantPostMenu extends LitElement {
|
|
|
1369
2163
|
: this.#setVisibility("latest_hidden")}
|
|
1370
2164
|
>
|
|
1371
2165
|
<span class="post-menu-item-label">Hidden from Latest</span>
|
|
1372
|
-
${
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2166
|
+
${
|
|
2167
|
+
visibility === "latest_hidden"
|
|
2168
|
+
? html`<span
|
|
2169
|
+
class="post-menu-item-trailing post-menu-item-check"
|
|
2170
|
+
>${this.#iconCheck()}</span
|
|
2171
|
+
>`
|
|
2172
|
+
: nothing
|
|
2173
|
+
}
|
|
1378
2174
|
</button>
|
|
1379
2175
|
|
|
1380
2176
|
<button
|
|
@@ -1382,9 +2178,9 @@ export class JantPostMenu extends LitElement {
|
|
|
1382
2178
|
role="menuitemradio"
|
|
1383
2179
|
aria-checked=${visibility === "private" ? "true" : "false"}
|
|
1384
2180
|
data-post-menu-visibility-option
|
|
1385
|
-
data-post-menu-visibility-current=${
|
|
1386
|
-
? "true"
|
|
1387
|
-
|
|
2181
|
+
data-post-menu-visibility-current=${
|
|
2182
|
+
visibility === "private" ? "true" : "false"
|
|
2183
|
+
}
|
|
1388
2184
|
class=${`post-menu-item${
|
|
1389
2185
|
visibility === "private" ? " post-menu-item-active" : ""
|
|
1390
2186
|
}`}
|
|
@@ -1394,12 +2190,14 @@ export class JantPostMenu extends LitElement {
|
|
|
1394
2190
|
: this.#setVisibility("private")}
|
|
1395
2191
|
>
|
|
1396
2192
|
<span class="post-menu-item-label">Private</span>
|
|
1397
|
-
${
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
2193
|
+
${
|
|
2194
|
+
visibility === "private"
|
|
2195
|
+
? html`<span
|
|
2196
|
+
class="post-menu-item-trailing post-menu-item-check"
|
|
2197
|
+
>${this.#iconCheck()}</span
|
|
2198
|
+
>`
|
|
2199
|
+
: nothing
|
|
2200
|
+
}
|
|
1403
2201
|
</button>
|
|
1404
2202
|
</div>
|
|
1405
2203
|
</div>
|
|
@@ -1501,100 +2299,164 @@ export class JantPostMenu extends LitElement {
|
|
|
1501
2299
|
<span class="post-menu-item-trailing">${this.#iconEdit()}</span>
|
|
1502
2300
|
</button>
|
|
1503
2301
|
|
|
1504
|
-
${
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
2302
|
+
${
|
|
2303
|
+
this._data.isDraft
|
|
2304
|
+
? html`
|
|
2305
|
+
<button
|
|
2306
|
+
type="button"
|
|
2307
|
+
role="menuitem"
|
|
2308
|
+
class="post-menu-item"
|
|
2309
|
+
@click=${() => this.#publish()}
|
|
2310
|
+
>
|
|
2311
|
+
<span class="post-menu-item-label">Publish</span>
|
|
2312
|
+
<span class="post-menu-item-trailing"
|
|
2313
|
+
>${this.#iconPublish()}</span
|
|
2314
|
+
>
|
|
2315
|
+
</button>
|
|
2316
|
+
`
|
|
2317
|
+
: nothing
|
|
2318
|
+
}
|
|
2319
|
+
${
|
|
2320
|
+
this._data.isReply
|
|
2321
|
+
? nothing
|
|
2322
|
+
: html`
|
|
2323
|
+
<button
|
|
2324
|
+
type="button"
|
|
2325
|
+
role="menuitem"
|
|
2326
|
+
class="post-menu-item"
|
|
2327
|
+
data-post-menu-open-collections
|
|
2328
|
+
@click=${() => this.#openCollectionPicker()}
|
|
1517
2329
|
>
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2330
|
+
<span class="post-menu-item-label"
|
|
2331
|
+
>Add to collection</span
|
|
2332
|
+
>
|
|
2333
|
+
<span
|
|
2334
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
2335
|
+
>${this.#iconChevronRight()}</span
|
|
2336
|
+
>
|
|
2337
|
+
</button>
|
|
2338
|
+
`
|
|
2339
|
+
}
|
|
2340
|
+
${
|
|
2341
|
+
this._data.isReply
|
|
2342
|
+
? nothing
|
|
2343
|
+
: html`
|
|
2344
|
+
<button
|
|
2345
|
+
type="button"
|
|
2346
|
+
role="menuitem"
|
|
2347
|
+
class="post-menu-item"
|
|
2348
|
+
data-post-menu-open-visibility
|
|
2349
|
+
@click=${() => this.#openVisibilityPanel()}
|
|
1533
2350
|
>
|
|
1534
|
-
|
|
1535
|
-
|
|
2351
|
+
<span class="post-menu-item-label">Visibility</span>
|
|
2352
|
+
<span class="post-menu-item-meta"
|
|
2353
|
+
>${this.#getVisibilityLabel(visibility)}</span
|
|
2354
|
+
>
|
|
2355
|
+
<span
|
|
2356
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
2357
|
+
>${this.#iconChevronRight()}</span
|
|
2358
|
+
>
|
|
2359
|
+
</button>
|
|
2360
|
+
`
|
|
2361
|
+
}
|
|
2362
|
+
${
|
|
2363
|
+
this._data.isReply || !this.#multilingual
|
|
2364
|
+
? nothing
|
|
2365
|
+
: html`
|
|
2366
|
+
<button
|
|
2367
|
+
type="button"
|
|
2368
|
+
role="menuitem"
|
|
2369
|
+
class="post-menu-item"
|
|
2370
|
+
data-post-menu-open-language
|
|
2371
|
+
@click=${() => this.#openLanguagePanel()}
|
|
1536
2372
|
>
|
|
1537
|
-
|
|
1538
|
-
|
|
2373
|
+
<span class="post-menu-item-label">Language</span>
|
|
2374
|
+
<span class="post-menu-item-meta"
|
|
2375
|
+
>${this.#languageLabel(this._data.language)}</span
|
|
2376
|
+
>
|
|
2377
|
+
<span
|
|
2378
|
+
class="post-menu-item-trailing post-menu-item-chevron"
|
|
2379
|
+
>${this.#iconChevronRight()}</span
|
|
2380
|
+
>
|
|
2381
|
+
</button>
|
|
2382
|
+
`
|
|
2383
|
+
}
|
|
1539
2384
|
</div>
|
|
1540
2385
|
|
|
1541
2386
|
<div class="post-menu-section">
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
: "Add to Featured"}</span
|
|
1552
|
-
>
|
|
1553
|
-
<span class="post-menu-item-trailing"
|
|
1554
|
-
>${isFeatured
|
|
1555
|
-
? this.#iconFeaturedOff()
|
|
1556
|
-
: this.#iconFeatured()}</span
|
|
1557
|
-
>
|
|
1558
|
-
</button>
|
|
1559
|
-
|
|
1560
|
-
${this._data.isReply
|
|
1561
|
-
? nothing
|
|
1562
|
-
: html`
|
|
1563
|
-
<button
|
|
1564
|
-
type="button"
|
|
1565
|
-
role="menuitem"
|
|
1566
|
-
class="post-menu-item"
|
|
1567
|
-
@click=${() => this.#togglePin()}
|
|
1568
|
-
>
|
|
1569
|
-
<span class="post-menu-item-label"
|
|
1570
|
-
>${isPinned ? "Unpin" : "Pin this post"}</span
|
|
1571
|
-
>
|
|
1572
|
-
<span class="post-menu-item-trailing"
|
|
1573
|
-
>${isPinned ? this.#iconPinOff() : this.#iconPin()}</span
|
|
2387
|
+
${
|
|
2388
|
+
this._data.isDraft
|
|
2389
|
+
? nothing
|
|
2390
|
+
: html`
|
|
2391
|
+
<button
|
|
2392
|
+
type="button"
|
|
2393
|
+
role="menuitem"
|
|
2394
|
+
class="post-menu-item"
|
|
2395
|
+
@click=${() => this.#setFeatured(!isFeatured)}
|
|
1574
2396
|
>
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2397
|
+
<span class="post-menu-item-label"
|
|
2398
|
+
>${
|
|
2399
|
+
isFeatured
|
|
2400
|
+
? "Remove from Featured"
|
|
2401
|
+
: "Add to Featured"
|
|
2402
|
+
}</span
|
|
2403
|
+
>
|
|
2404
|
+
<span class="post-menu-item-trailing"
|
|
2405
|
+
>${
|
|
2406
|
+
isFeatured
|
|
2407
|
+
? this.#iconFeaturedOff()
|
|
2408
|
+
: this.#iconFeatured()
|
|
2409
|
+
}</span
|
|
2410
|
+
>
|
|
2411
|
+
</button>
|
|
2412
|
+
`
|
|
2413
|
+
}
|
|
2414
|
+
${
|
|
2415
|
+
this._data.isReply
|
|
2416
|
+
? nothing
|
|
2417
|
+
: html`
|
|
2418
|
+
<button
|
|
2419
|
+
type="button"
|
|
2420
|
+
role="menuitem"
|
|
2421
|
+
class="post-menu-item"
|
|
2422
|
+
@click=${() => this.#togglePin()}
|
|
1589
2423
|
>
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2424
|
+
<span class="post-menu-item-label"
|
|
2425
|
+
>${isPinned ? "Unpin" : "Pin this post"}</span
|
|
2426
|
+
>
|
|
2427
|
+
<span class="post-menu-item-trailing"
|
|
2428
|
+
>${isPinned ? this.#iconPinOff() : this.#iconPin()}</span
|
|
2429
|
+
>
|
|
2430
|
+
</button>
|
|
2431
|
+
`
|
|
2432
|
+
}
|
|
2433
|
+
${
|
|
2434
|
+
collectionId && !this._data.isReply
|
|
2435
|
+
? html`
|
|
2436
|
+
<button
|
|
2437
|
+
type="button"
|
|
2438
|
+
role="menuitem"
|
|
2439
|
+
class="post-menu-item"
|
|
2440
|
+
@click=${() => this.#toggleCollectionPin()}
|
|
1594
2441
|
>
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2442
|
+
<span class="post-menu-item-label"
|
|
2443
|
+
>${
|
|
2444
|
+
isPinnedInCollection
|
|
2445
|
+
? "Unpin from collection"
|
|
2446
|
+
: "Pin in collection"
|
|
2447
|
+
}</span
|
|
2448
|
+
>
|
|
2449
|
+
<span class="post-menu-item-trailing"
|
|
2450
|
+
>${
|
|
2451
|
+
isPinnedInCollection
|
|
2452
|
+
? this.#iconPinOff()
|
|
2453
|
+
: this.#iconPin()
|
|
2454
|
+
}</span
|
|
2455
|
+
>
|
|
2456
|
+
</button>
|
|
2457
|
+
`
|
|
2458
|
+
: nothing
|
|
2459
|
+
}
|
|
1598
2460
|
</div>
|
|
1599
2461
|
|
|
1600
2462
|
<div class="post-menu-section post-menu-section-danger">
|
|
@@ -1604,7 +2466,9 @@ export class JantPostMenu extends LitElement {
|
|
|
1604
2466
|
class="post-menu-item post-menu-item-danger"
|
|
1605
2467
|
@click=${() => this.#delete()}
|
|
1606
2468
|
>
|
|
1607
|
-
<span class="post-menu-item-label"
|
|
2469
|
+
<span class="post-menu-item-label"
|
|
2470
|
+
>${this._data.isDraft ? "Discard" : "Delete"}</span
|
|
2471
|
+
>
|
|
1608
2472
|
<span class="post-menu-item-trailing">${this.#iconTrash()}</span>
|
|
1609
2473
|
</button>
|
|
1610
2474
|
</div>
|
|
@@ -1623,30 +2487,40 @@ export class JantPostMenu extends LitElement {
|
|
|
1623
2487
|
const showMenuSurface = !this._addCollectionPanelOpen;
|
|
1624
2488
|
|
|
1625
2489
|
return html`
|
|
1626
|
-
${
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
class="post-menu-backdrop"
|
|
1630
|
-
@click=${() => this.#close({ restoreFocus: false })}
|
|
1631
|
-
></div>
|
|
1632
|
-
<div class="dropdown-menu" style=${wrapperStyle}>
|
|
2490
|
+
${
|
|
2491
|
+
showMenuSurface
|
|
2492
|
+
? html`
|
|
1633
2493
|
<div
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
>
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
2494
|
+
class="post-menu-backdrop"
|
|
2495
|
+
@click=${() => this.#close({ restoreFocus: false })}
|
|
2496
|
+
></div>
|
|
2497
|
+
<div class="dropdown-menu" style=${wrapperStyle}>
|
|
2498
|
+
<div
|
|
2499
|
+
data-popover
|
|
2500
|
+
aria-hidden="false"
|
|
2501
|
+
class="!static post-menu-panel"
|
|
2502
|
+
>
|
|
2503
|
+
${
|
|
2504
|
+
this._view === "collections"
|
|
2505
|
+
? this.#renderCollectionPicker()
|
|
2506
|
+
: this._view === "visibility"
|
|
2507
|
+
? this.#renderVisibilityPanel()
|
|
2508
|
+
: this._view === "language"
|
|
2509
|
+
? this.#renderLanguagePanel()
|
|
2510
|
+
: this._view === "language-switch"
|
|
2511
|
+
? this.#renderLanguageSwitchPanel()
|
|
2512
|
+
: this.#renderMenu()
|
|
2513
|
+
}
|
|
2514
|
+
</div>
|
|
1643
2515
|
</div>
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
${
|
|
1648
|
-
|
|
1649
|
-
|
|
2516
|
+
`
|
|
2517
|
+
: nothing
|
|
2518
|
+
}
|
|
2519
|
+
${
|
|
2520
|
+
this._addCollectionPanelOpen
|
|
2521
|
+
? this.#renderAddCollectionPanel()
|
|
2522
|
+
: nothing
|
|
2523
|
+
}
|
|
1650
2524
|
`;
|
|
1651
2525
|
}
|
|
1652
2526
|
}
|