@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
|
@@ -7,11 +7,23 @@ const { showConfirmDialogMock, showToastMock } = vi.hoisted(() => ({
|
|
|
7
7
|
showToastMock: vi.fn(),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Paths are relative to the *component*, which lives one level up from this
|
|
11
|
+
// folder — `../confirm.js` from here names a file that does not exist, so the
|
|
12
|
+
// mock silently never applied and the real dialog ran instead.
|
|
13
|
+
// The composer's bundle loads on first use; the stub dialog below stands in
|
|
14
|
+
// for the upgraded element, so loading is a no-op here.
|
|
15
|
+
vi.mock("../../lazy-entries.js", () => ({
|
|
16
|
+
ensureCompose: async () => {},
|
|
17
|
+
ensureSettings: async () => {},
|
|
18
|
+
ensureManage: async () => {},
|
|
19
|
+
loadEntriesForPage: () => {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("../../confirm.js", () => ({
|
|
11
23
|
showConfirmDialog: showConfirmDialogMock,
|
|
12
24
|
}));
|
|
13
25
|
|
|
14
|
-
vi.mock("
|
|
26
|
+
vi.mock("../../toast.js", () => ({
|
|
15
27
|
showToast: showToastMock,
|
|
16
28
|
}));
|
|
17
29
|
|
|
@@ -715,6 +727,405 @@ describe("JantPostMenu", () => {
|
|
|
715
727
|
await Promise.resolve();
|
|
716
728
|
});
|
|
717
729
|
|
|
730
|
+
describe("language panel", () => {
|
|
731
|
+
/** Open the menu on a Thread that already has an English version. */
|
|
732
|
+
async function openLanguagePanel(
|
|
733
|
+
translations: Array<Record<string, unknown>> = [
|
|
734
|
+
{
|
|
735
|
+
id: "post-en",
|
|
736
|
+
slug: "coffee-notes",
|
|
737
|
+
label: "Coffee notes",
|
|
738
|
+
language: "en",
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
languages: Array<{ tag: string; label: string }> = [
|
|
742
|
+
{ tag: "zh-Hans", label: "简体中文" },
|
|
743
|
+
{ tag: "en", label: "English" },
|
|
744
|
+
{ tag: "ja", label: "日本語" },
|
|
745
|
+
],
|
|
746
|
+
) {
|
|
747
|
+
const { menu, trigger } = await createMenu();
|
|
748
|
+
const article = requireElement(
|
|
749
|
+
document.querySelector<HTMLElement>("article[data-post-id]"),
|
|
750
|
+
"expected the post article",
|
|
751
|
+
);
|
|
752
|
+
article.dataset.postLanguage = "zh-Hans";
|
|
753
|
+
|
|
754
|
+
menu.languages = languages;
|
|
755
|
+
await menu.updateComplete;
|
|
756
|
+
|
|
757
|
+
const existingFetch = globalThis.fetch as (
|
|
758
|
+
input: unknown,
|
|
759
|
+
init?: globalThis.RequestInit,
|
|
760
|
+
) => Promise<Response>;
|
|
761
|
+
vi.stubGlobal(
|
|
762
|
+
"fetch",
|
|
763
|
+
vi.fn((input: unknown, init?: globalThis.RequestInit) => {
|
|
764
|
+
const url = String(input);
|
|
765
|
+
if (url === "/api/posts/post-1/translations") {
|
|
766
|
+
return Promise.resolve(jsonResponse({ translations }));
|
|
767
|
+
}
|
|
768
|
+
if (
|
|
769
|
+
url.endsWith("/translations") &&
|
|
770
|
+
(init?.method ?? "GET") === "DELETE"
|
|
771
|
+
) {
|
|
772
|
+
return Promise.resolve(new Response(null, { status: 204 }));
|
|
773
|
+
}
|
|
774
|
+
return existingFetch(input, init);
|
|
775
|
+
}),
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
click(trigger);
|
|
779
|
+
await menu.updateComplete;
|
|
780
|
+
click(
|
|
781
|
+
requireElement(
|
|
782
|
+
menu.querySelector<HTMLElement>("[data-post-menu-open-language]"),
|
|
783
|
+
"expected the language entry",
|
|
784
|
+
),
|
|
785
|
+
);
|
|
786
|
+
await menu.updateComplete;
|
|
787
|
+
await Promise.resolve();
|
|
788
|
+
await Promise.resolve();
|
|
789
|
+
await menu.updateComplete;
|
|
790
|
+
|
|
791
|
+
return { menu, trigger };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function radioLabels(menu: JantPostMenu): string[] {
|
|
795
|
+
return Array.from(
|
|
796
|
+
menu.querySelectorAll<HTMLElement>("[data-post-menu-language-option]"),
|
|
797
|
+
).map(
|
|
798
|
+
(option) =>
|
|
799
|
+
option.querySelector(".post-menu-item-label")?.textContent?.trim() ??
|
|
800
|
+
"",
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function openLanguageSwitch(menu: JantPostMenu) {
|
|
805
|
+
click(
|
|
806
|
+
requireElement(
|
|
807
|
+
menu.querySelector<HTMLElement>(
|
|
808
|
+
"[data-post-menu-open-language-switch]",
|
|
809
|
+
),
|
|
810
|
+
"expected the change-language entry",
|
|
811
|
+
),
|
|
812
|
+
);
|
|
813
|
+
await menu.updateComplete;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
it("keeps the picker one level down, behind the language it is on", async () => {
|
|
817
|
+
// Switching a Thread's language is a correction made once if ever, while
|
|
818
|
+
// reading and adding other versions is the daily work.
|
|
819
|
+
const { menu } = await openLanguagePanel();
|
|
820
|
+
|
|
821
|
+
expect(radioLabels(menu)).toEqual([]);
|
|
822
|
+
const entry = requireElement(
|
|
823
|
+
menu.querySelector<HTMLElement>(
|
|
824
|
+
"[data-post-menu-open-language-switch]",
|
|
825
|
+
),
|
|
826
|
+
"expected the change-language entry",
|
|
827
|
+
);
|
|
828
|
+
expect(
|
|
829
|
+
entry.querySelector(".post-menu-item-meta")?.textContent,
|
|
830
|
+
).toContain("简体中文");
|
|
831
|
+
|
|
832
|
+
await openLanguageSwitch(menu);
|
|
833
|
+
expect(menu.textContent).toContain("Change language");
|
|
834
|
+
expect(radioLabels(menu)).toEqual(["简体中文", "日本語"]);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("leaves a language another version holds out of the picker", async () => {
|
|
838
|
+
// The author cannot switch to it, so a dead row saying "Taken" answers a
|
|
839
|
+
// question nobody asked. The version holding it is one level up.
|
|
840
|
+
const { menu } = await openLanguagePanel();
|
|
841
|
+
await openLanguageSwitch(menu);
|
|
842
|
+
|
|
843
|
+
expect(radioLabels(menu)).not.toContain("English");
|
|
844
|
+
expect(menu.textContent).not.toContain("Taken");
|
|
845
|
+
expect(menu.textContent).not.toContain("Applies to the whole thread");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it("drops the change-language row when there is nothing to change to", async () => {
|
|
849
|
+
// A two-language site whose other version is already linked: the picker
|
|
850
|
+
// would hold a single unclickable row for the language it is already in.
|
|
851
|
+
const { menu } = await openLanguagePanel(undefined, [
|
|
852
|
+
{ tag: "zh-Hans", label: "简体中文" },
|
|
853
|
+
{ tag: "en", label: "English" },
|
|
854
|
+
]);
|
|
855
|
+
|
|
856
|
+
expect(
|
|
857
|
+
menu.querySelector("[data-post-menu-open-language-switch]"),
|
|
858
|
+
).toBeNull();
|
|
859
|
+
expect(menu.textContent).toContain("Other versions");
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it("lands focus in the panel even when its only row arrives with the fetch", async () => {
|
|
863
|
+
// Nothing is on screen until the translations resolve in that case, so
|
|
864
|
+
// the first focus attempt has nothing to land on.
|
|
865
|
+
const { menu } = await openLanguagePanel(undefined, [
|
|
866
|
+
{ tag: "zh-Hans", label: "简体中文" },
|
|
867
|
+
{ tag: "en", label: "English" },
|
|
868
|
+
]);
|
|
869
|
+
await menu.updateComplete;
|
|
870
|
+
await Promise.resolve();
|
|
871
|
+
|
|
872
|
+
expect(document.activeElement).toBe(
|
|
873
|
+
menu.querySelector("[data-post-menu-translation] a"),
|
|
874
|
+
);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("gives each other version a way to read it and a way to unlink it", async () => {
|
|
878
|
+
const { menu } = await openLanguagePanel();
|
|
879
|
+
|
|
880
|
+
const row = requireElement(
|
|
881
|
+
menu.querySelector<HTMLElement>("[data-post-menu-translation]"),
|
|
882
|
+
"expected a row for the English version",
|
|
883
|
+
);
|
|
884
|
+
// The language identifies the version; the title would only be clipped,
|
|
885
|
+
// so it rides on the link instead of spending the row's width.
|
|
886
|
+
expect(row.querySelector(".post-menu-item-label")?.textContent).toContain(
|
|
887
|
+
"English",
|
|
888
|
+
);
|
|
889
|
+
expect(row.querySelector(".post-menu-item-meta")).toBeNull();
|
|
890
|
+
|
|
891
|
+
const open = requireElement(
|
|
892
|
+
row.querySelector<HTMLAnchorElement>("a"),
|
|
893
|
+
"expected a link to the English version",
|
|
894
|
+
);
|
|
895
|
+
expect(open.getAttribute("href")).toBe("/coffee-notes");
|
|
896
|
+
expect(open.getAttribute("target")).toBe("_blank");
|
|
897
|
+
expect(open.getAttribute("rel")).toBe("noopener noreferrer");
|
|
898
|
+
expect(open.getAttribute("title")).toBe("Coffee notes");
|
|
899
|
+
|
|
900
|
+
const unlink = requireElement(
|
|
901
|
+
row.querySelector<HTMLElement>("[data-post-menu-translation-unlink]"),
|
|
902
|
+
"expected an unlink button",
|
|
903
|
+
);
|
|
904
|
+
expect(unlink.textContent?.trim()).toBe("Unlink");
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it("unlinks the version whose row was clicked, once confirmed", async () => {
|
|
908
|
+
// `DELETE` on the *other* post takes that one out of the group — which is
|
|
909
|
+
// what "unlink the English version" means from this row.
|
|
910
|
+
const { menu } = await openLanguagePanel();
|
|
911
|
+
showConfirmDialogMock.mockResolvedValue(true);
|
|
912
|
+
const reload = vi.fn();
|
|
913
|
+
Object.defineProperty(window, "location", {
|
|
914
|
+
configurable: true,
|
|
915
|
+
value: { ...window.location, reload, assign: vi.fn() },
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
click(
|
|
919
|
+
requireElement(
|
|
920
|
+
menu.querySelector<HTMLElement>(
|
|
921
|
+
"[data-post-menu-translation-unlink]",
|
|
922
|
+
),
|
|
923
|
+
"expected an unlink button",
|
|
924
|
+
),
|
|
925
|
+
);
|
|
926
|
+
await Promise.resolve();
|
|
927
|
+
await Promise.resolve();
|
|
928
|
+
await Promise.resolve();
|
|
929
|
+
|
|
930
|
+
expect(showConfirmDialogMock).toHaveBeenCalledWith(
|
|
931
|
+
expect.objectContaining({ confirmLabel: "Unlink", tone: "danger" }),
|
|
932
|
+
);
|
|
933
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
934
|
+
"/api/posts/post-en/translations",
|
|
935
|
+
expect.objectContaining({ method: "DELETE" }),
|
|
936
|
+
);
|
|
937
|
+
expect(reload).toHaveBeenCalled();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it("keeps the link when the confirm is dismissed", async () => {
|
|
941
|
+
const { menu, trigger } = await openLanguagePanel();
|
|
942
|
+
showConfirmDialogMock.mockResolvedValue(false);
|
|
943
|
+
|
|
944
|
+
click(
|
|
945
|
+
requireElement(
|
|
946
|
+
menu.querySelector<HTMLElement>(
|
|
947
|
+
"[data-post-menu-translation-unlink]",
|
|
948
|
+
),
|
|
949
|
+
"expected an unlink button",
|
|
950
|
+
),
|
|
951
|
+
);
|
|
952
|
+
await Promise.resolve();
|
|
953
|
+
await Promise.resolve();
|
|
954
|
+
|
|
955
|
+
expect(globalThis.fetch).not.toHaveBeenCalledWith(
|
|
956
|
+
"/api/posts/post-en/translations",
|
|
957
|
+
expect.objectContaining({ method: "DELETE" }),
|
|
958
|
+
);
|
|
959
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it("offers every free language for a new version", async () => {
|
|
963
|
+
const { menu } = await openLanguagePanel();
|
|
964
|
+
|
|
965
|
+
const addSection = requireElement(
|
|
966
|
+
menu.querySelector<HTMLElement>("[data-post-menu-translation-first]")
|
|
967
|
+
?.parentElement ?? null,
|
|
968
|
+
"expected the add-a-translation section",
|
|
969
|
+
);
|
|
970
|
+
expect(addSection.textContent).toContain("Write the 日本語 version");
|
|
971
|
+
expect(addSection.textContent).not.toContain("Write the English");
|
|
972
|
+
// Self-describing, so the section carries no label of its own.
|
|
973
|
+
expect(addSection.textContent).toContain(
|
|
974
|
+
"Link a version you already wrote",
|
|
975
|
+
);
|
|
976
|
+
expect(addSection.querySelector(".post-menu-section-label")).toBeNull();
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it("resolves an address pasted into the link picker, and says what is wrong", async () => {
|
|
980
|
+
// happy-dom has no modal dialog methods; the picker renders in light DOM
|
|
981
|
+
// either way.
|
|
982
|
+
Object.defineProperty(HTMLDialogElement.prototype, "showModal", {
|
|
983
|
+
configurable: true,
|
|
984
|
+
value(this: HTMLDialogElement) {
|
|
985
|
+
this.setAttribute("open", "");
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const { menu } = await openLanguagePanel();
|
|
990
|
+
const existingFetch = globalThis.fetch as (
|
|
991
|
+
input: unknown,
|
|
992
|
+
init?: globalThis.RequestInit,
|
|
993
|
+
) => Promise<Response>;
|
|
994
|
+
const fetchMock = vi.fn(
|
|
995
|
+
(input: unknown, init?: globalThis.RequestInit) => {
|
|
996
|
+
const url = String(input);
|
|
997
|
+
if (url.includes("/translations/resolve?")) {
|
|
998
|
+
return Promise.resolve(
|
|
999
|
+
jsonResponse({
|
|
1000
|
+
resolution: { kind: "unpublished", address: "/coffee-notes" },
|
|
1001
|
+
}),
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return existingFetch(input, init);
|
|
1005
|
+
},
|
|
1006
|
+
);
|
|
1007
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1008
|
+
|
|
1009
|
+
click(
|
|
1010
|
+
requireElement(
|
|
1011
|
+
Array.from(
|
|
1012
|
+
menu.querySelectorAll<HTMLElement>(".post-menu-item-label"),
|
|
1013
|
+
).find((label) =>
|
|
1014
|
+
label.textContent?.includes("Link a version you already wrote"),
|
|
1015
|
+
) ?? null,
|
|
1016
|
+
"expected the link-a-translation entry",
|
|
1017
|
+
),
|
|
1018
|
+
);
|
|
1019
|
+
await menu.updateComplete;
|
|
1020
|
+
await Promise.resolve();
|
|
1021
|
+
|
|
1022
|
+
const picker = requireElement(
|
|
1023
|
+
document.querySelector("jant-post-picker"),
|
|
1024
|
+
"expected the post picker",
|
|
1025
|
+
);
|
|
1026
|
+
const input = requireElement(
|
|
1027
|
+
picker.querySelector<HTMLInputElement>(".picker-dialog-input"),
|
|
1028
|
+
"expected the picker input",
|
|
1029
|
+
);
|
|
1030
|
+
input.value = "https://example.com/coffee-notes";
|
|
1031
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1032
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1033
|
+
await Promise.resolve();
|
|
1034
|
+
await Promise.resolve();
|
|
1035
|
+
|
|
1036
|
+
// A URL goes to the resolver, never to the title search.
|
|
1037
|
+
expect(
|
|
1038
|
+
fetchMock.mock.calls.some(([url]) =>
|
|
1039
|
+
String(url).includes("/api/posts/post-1/translations/resolve?url="),
|
|
1040
|
+
),
|
|
1041
|
+
).toBe(true);
|
|
1042
|
+
expect(
|
|
1043
|
+
fetchMock.mock.calls.some(([url]) =>
|
|
1044
|
+
String(url).includes("/translations/candidates"),
|
|
1045
|
+
),
|
|
1046
|
+
).toBe(false);
|
|
1047
|
+
expect(picker.textContent).toContain("That post is a draft");
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it("answers in the order the questions get asked", async () => {
|
|
1051
|
+
// The entry that opened this panel said "Language · 简体中文", so landing
|
|
1052
|
+
// on a panel whose last row is that same value read as a mismatch.
|
|
1053
|
+
const { menu } = await openLanguagePanel();
|
|
1054
|
+
|
|
1055
|
+
const rows = Array.from(
|
|
1056
|
+
menu.querySelectorAll<HTMLElement>(
|
|
1057
|
+
".post-menu-item-label, .post-menu-section-label",
|
|
1058
|
+
),
|
|
1059
|
+
).map((row) => row.textContent?.trim());
|
|
1060
|
+
|
|
1061
|
+
expect(rows).toEqual([
|
|
1062
|
+
"Change language",
|
|
1063
|
+
"Write the 日本語 version",
|
|
1064
|
+
"Link a version you already wrote",
|
|
1065
|
+
"Other versions",
|
|
1066
|
+
"English",
|
|
1067
|
+
]);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it("stays open when the clicked item re-renders the panel out from under itself", async () => {
|
|
1071
|
+
// A real browser runs a microtask checkpoint between event listeners, so
|
|
1072
|
+
// by the time the document-level handler sees the click, the item that
|
|
1073
|
+
// switched panels is already detached — the re-render swapped the whole
|
|
1074
|
+
// `.post-menu-view` out from under it, leaving the container behind.
|
|
1075
|
+
// `click()` from a script never reproduces that (the stack never empties,
|
|
1076
|
+
// so nothing re-renders mid-dispatch), so do the swap by hand in a second
|
|
1077
|
+
// listener on the item, which is where the checkpoint would fall.
|
|
1078
|
+
const { menu, trigger } = await createMenu();
|
|
1079
|
+
menu.languages = [
|
|
1080
|
+
{ tag: "zh-Hans", label: "简体中文" },
|
|
1081
|
+
{ tag: "en", label: "English" },
|
|
1082
|
+
];
|
|
1083
|
+
click(trigger);
|
|
1084
|
+
await menu.updateComplete;
|
|
1085
|
+
|
|
1086
|
+
const entry = requireElement(
|
|
1087
|
+
menu.querySelector<HTMLElement>("[data-post-menu-open-language]"),
|
|
1088
|
+
"expected the language entry",
|
|
1089
|
+
);
|
|
1090
|
+
entry.addEventListener("click", () =>
|
|
1091
|
+
menu.querySelector(".post-menu-view")?.remove(),
|
|
1092
|
+
);
|
|
1093
|
+
click(entry);
|
|
1094
|
+
await menu.updateComplete;
|
|
1095
|
+
|
|
1096
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
1097
|
+
expect(menu.textContent).toContain("Language");
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("walks back one panel per press of the back button", async () => {
|
|
1101
|
+
// The back button sits in the panel header, outside the `role="menu"`
|
|
1102
|
+
// list — matching only the list made every one of them read as a click
|
|
1103
|
+
// outside the menu and close the whole thing.
|
|
1104
|
+
const { menu } = await openLanguagePanel();
|
|
1105
|
+
await openLanguageSwitch(menu);
|
|
1106
|
+
|
|
1107
|
+
const back = () =>
|
|
1108
|
+
click(
|
|
1109
|
+
requireElement(
|
|
1110
|
+
menu.querySelector<HTMLElement>(".post-menu-panel-back"),
|
|
1111
|
+
"expected a back button",
|
|
1112
|
+
),
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
back();
|
|
1116
|
+
await menu.updateComplete;
|
|
1117
|
+
expect(
|
|
1118
|
+
menu.querySelector("[data-post-menu-open-language-switch]"),
|
|
1119
|
+
).not.toBeNull();
|
|
1120
|
+
|
|
1121
|
+
back();
|
|
1122
|
+
await menu.updateComplete;
|
|
1123
|
+
expect(
|
|
1124
|
+
menu.querySelector("[data-post-menu-open-language]"),
|
|
1125
|
+
).not.toBeNull();
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
718
1129
|
it("removes the leading feed divider from the first remaining timeline item", async () => {
|
|
719
1130
|
const host = document.createElement("div");
|
|
720
1131
|
host.innerHTML = `
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shared post picker.
|
|
5
|
+
*
|
|
6
|
+
* It is deliberately ignorant of what it is picking for — the caller supplies
|
|
7
|
+
* the copy and the search — so these tests are about the contract: it resolves
|
|
8
|
+
* exactly once, it never lets a slow search overwrite a newer one, and it can
|
|
9
|
+
* always be backed out of.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
13
|
+
import "../jant-post-picker.js";
|
|
14
|
+
import type { JantPostPicker } from "../jant-post-picker.js";
|
|
15
|
+
|
|
16
|
+
function mountPicker(): JantPostPicker {
|
|
17
|
+
const el = document.createElement("jant-post-picker") as JantPostPicker;
|
|
18
|
+
document.body.appendChild(el);
|
|
19
|
+
return el;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function flush(el: JantPostPicker) {
|
|
23
|
+
await el.updateComplete;
|
|
24
|
+
await Promise.resolve();
|
|
25
|
+
await el.updateComplete;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function type(el: JantPostPicker, value: string) {
|
|
29
|
+
const input = el.querySelector<HTMLInputElement>(".picker-dialog-input");
|
|
30
|
+
if (!input) throw new Error("expected the search input");
|
|
31
|
+
input.value = value;
|
|
32
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function optionLabels(el: JantPostPicker): string[] {
|
|
36
|
+
return Array.from(
|
|
37
|
+
el.querySelectorAll<HTMLElement>(".picker-dialog-option-label"),
|
|
38
|
+
).map((node) => node.textContent?.trim() ?? "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BASE = {
|
|
42
|
+
heading: "Link a translation",
|
|
43
|
+
placeholder: "Search your posts…",
|
|
44
|
+
emptyHint: "Nothing matched that you could link.",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe("JantPostPicker", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
vi.useRealTimers();
|
|
51
|
+
// happy-dom does not implement the modal dialog methods.
|
|
52
|
+
Object.defineProperty(HTMLDialogElement.prototype, "showModal", {
|
|
53
|
+
configurable: true,
|
|
54
|
+
value(this: HTMLDialogElement) {
|
|
55
|
+
this.setAttribute("open", "");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(HTMLDialogElement.prototype, "close", {
|
|
59
|
+
configurable: true,
|
|
60
|
+
value(this: HTMLDialogElement) {
|
|
61
|
+
this.removeAttribute("open");
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("shows the caller's copy", async () => {
|
|
67
|
+
const el = mountPicker();
|
|
68
|
+
void el.pick({ ...BASE, search: async () => ({ items: [] }) });
|
|
69
|
+
await flush(el);
|
|
70
|
+
|
|
71
|
+
expect(el.querySelector(".picker-dialog-title")?.textContent).toContain(
|
|
72
|
+
"Link a translation",
|
|
73
|
+
);
|
|
74
|
+
expect(
|
|
75
|
+
el.querySelector<HTMLInputElement>(".picker-dialog-input")?.placeholder,
|
|
76
|
+
).toBe("Search your posts…");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("resolves with the picked post", async () => {
|
|
80
|
+
const el = mountPicker();
|
|
81
|
+
const picked = el.pick({
|
|
82
|
+
...BASE,
|
|
83
|
+
minQueryLength: 1,
|
|
84
|
+
search: async () => ({
|
|
85
|
+
items: [{ id: "pst_one", label: "Coffee notes", meta: "English" }],
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
await flush(el);
|
|
89
|
+
|
|
90
|
+
type(el, "coffee");
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
92
|
+
await flush(el);
|
|
93
|
+
|
|
94
|
+
expect(optionLabels(el)).toEqual(["Coffee notes"]);
|
|
95
|
+
el.querySelector<HTMLButtonElement>(".picker-dialog-option")?.click();
|
|
96
|
+
|
|
97
|
+
expect(await picked).toBe("pst_one");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("resolves to null when dismissed", async () => {
|
|
101
|
+
const el = mountPicker();
|
|
102
|
+
const picked = el.pick({ ...BASE, search: async () => ({ items: [] }) });
|
|
103
|
+
await flush(el);
|
|
104
|
+
|
|
105
|
+
el.querySelector<HTMLDialogElement>(".picker-dialog")?.dispatchEvent(
|
|
106
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(await picked).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("says nothing matched rather than looking empty", async () => {
|
|
113
|
+
const el = mountPicker();
|
|
114
|
+
void el.pick({
|
|
115
|
+
...BASE,
|
|
116
|
+
minQueryLength: 1,
|
|
117
|
+
search: async () => ({ items: [] }),
|
|
118
|
+
});
|
|
119
|
+
await flush(el);
|
|
120
|
+
|
|
121
|
+
type(el, "nothing");
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
123
|
+
await flush(el);
|
|
124
|
+
|
|
125
|
+
expect(el.textContent).toContain("Nothing matched that you could link.");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("lets a search explain itself instead of saying nothing matched", async () => {
|
|
129
|
+
const el = mountPicker();
|
|
130
|
+
void el.pick({
|
|
131
|
+
...BASE,
|
|
132
|
+
minQueryLength: 1,
|
|
133
|
+
search: async () => ({
|
|
134
|
+
items: [],
|
|
135
|
+
note: "That post is a draft. Publish it, then link it.",
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
await flush(el);
|
|
139
|
+
|
|
140
|
+
type(el, "/coffee");
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
142
|
+
await flush(el);
|
|
143
|
+
|
|
144
|
+
expect(el.textContent).toContain("That post is a draft");
|
|
145
|
+
expect(el.textContent).not.toContain("Nothing matched");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("keeps quiet until the query is worth searching", async () => {
|
|
149
|
+
const search = vi.fn(async () => ({ items: [] }));
|
|
150
|
+
const el = mountPicker();
|
|
151
|
+
void el.pick({ ...BASE, search });
|
|
152
|
+
await flush(el);
|
|
153
|
+
|
|
154
|
+
type(el, "a");
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
156
|
+
await flush(el);
|
|
157
|
+
|
|
158
|
+
expect(search).not.toHaveBeenCalled();
|
|
159
|
+
expect(el.textContent).not.toContain("Nothing matched");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("does not let a slow search overwrite a newer one", async () => {
|
|
163
|
+
const el = mountPicker();
|
|
164
|
+
void el.pick({
|
|
165
|
+
...BASE,
|
|
166
|
+
minQueryLength: 1,
|
|
167
|
+
search: async (query) => {
|
|
168
|
+
// The first query is slower than the second, which is exactly the race
|
|
169
|
+
// a debounce alone does not fix.
|
|
170
|
+
if (query === "slow") {
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
172
|
+
return { items: [{ id: "pst_stale", label: "Stale result" }] };
|
|
173
|
+
}
|
|
174
|
+
return { items: [{ id: "pst_fresh", label: "Fresh result" }] };
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
await flush(el);
|
|
178
|
+
|
|
179
|
+
type(el, "slow");
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, 210));
|
|
181
|
+
type(el, "fast");
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
183
|
+
await flush(el);
|
|
184
|
+
|
|
185
|
+
expect(optionLabels(el)).toEqual(["Fresh result"]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("abandons an earlier request when reopened", async () => {
|
|
189
|
+
const el = mountPicker();
|
|
190
|
+
const first = el.pick({ ...BASE, search: async () => ({ items: [] }) });
|
|
191
|
+
await flush(el);
|
|
192
|
+
|
|
193
|
+
void el.pick({ ...BASE, search: async () => ({ items: [] }) });
|
|
194
|
+
|
|
195
|
+
expect(await first).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -38,22 +38,12 @@ const labels: SettingsLabels = {
|
|
|
38
38
|
"/about is already used. Rename that item before creating an About page.",
|
|
39
39
|
createAboutPage: "Create About page",
|
|
40
40
|
editAboutPage: "Edit About page",
|
|
41
|
-
|
|
41
|
+
timeSection: "Time",
|
|
42
42
|
home: "Home",
|
|
43
|
-
|
|
43
|
+
siteVisibility: "Site visibility",
|
|
44
44
|
siteName: "Site Name",
|
|
45
45
|
aboutBlog: "About this blog",
|
|
46
46
|
aboutBlogHelp: "Displayed above your blog posts.",
|
|
47
|
-
siteLanguage: "Content language",
|
|
48
|
-
siteLanguageHelp: "The language your posts are written in.",
|
|
49
|
-
siteLanguageSearchPlaceholder: "Search…",
|
|
50
|
-
siteLanguageNoMatches: "No matches.",
|
|
51
|
-
contentLanguagePreview: "Readers and search engines see",
|
|
52
|
-
dashboardLanguage: "Dashboard language",
|
|
53
|
-
dashboardLanguageHelp: "The language this admin dashboard shows in.",
|
|
54
|
-
cjkFont: "CJK fallback",
|
|
55
|
-
cjkFontHelp:
|
|
56
|
-
"Used when the content language has no built-in font profile. Your font theme still controls serif and sans styling.",
|
|
57
47
|
timeZone: "Time Zone",
|
|
58
48
|
feeds: "Feeds",
|
|
59
49
|
mainRssFeed: "Main RSS feed",
|
|
@@ -79,11 +69,24 @@ const labels: SettingsLabels = {
|
|
|
79
69
|
'Show "Build with Jant" at the bottom of the home page',
|
|
80
70
|
allowIndexing: "Allow search engines to index my site",
|
|
81
71
|
demoSeoLocked: "Demo sites always stay hidden from search engines.",
|
|
72
|
+
discoverName: "Jant Discover",
|
|
73
|
+
discoverRules: "Discover community rules",
|
|
74
|
+
discoverEnabled: "Allow Jant Discover to list my site",
|
|
75
|
+
discoverIntro:
|
|
76
|
+
"Jant Discover is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts. A post you mark Featured appears on the Discover home page 24 hours later, and link and quote posts appear on the Links and Quotes lists 24 hours after they are published. You can keep editing them in the meantime. See the Discover community rules.",
|
|
77
|
+
discoverDemoLocked: "Demo sites are never listed in Discover.",
|
|
78
|
+
discoverFeedsOffLocked:
|
|
79
|
+
"Discover reads your Atom feed, so it needs feeds turned on.",
|
|
80
|
+
discoverSearchOff:
|
|
81
|
+
"Search engine indexing is off, so this site is not listed by default. Ticking the box above lists it anyway.",
|
|
82
|
+
discoverAnnounce: "Announce my site",
|
|
83
|
+
discoverAnnounceManual: "Or submit your address by hand",
|
|
82
84
|
save: "Save",
|
|
83
85
|
cancel: "Cancel",
|
|
84
86
|
copy: "Copy",
|
|
85
87
|
copyFailed: "Could not copy. Try again.",
|
|
86
88
|
feedUrlCopied: "Feed URL copied.",
|
|
89
|
+
feedsDocs: "All feed addresses",
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
async function createElement(
|