@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
|
@@ -11,12 +11,29 @@ import { useLingui } from "../../i18n/context.js";
|
|
|
11
11
|
import type {
|
|
12
12
|
ArchivePageProps,
|
|
13
13
|
ArchiveFilters,
|
|
14
|
-
|
|
14
|
+
ArchiveLayout,
|
|
15
|
+
ArchiveSort,
|
|
15
16
|
ArchiveVisibility,
|
|
17
|
+
Format,
|
|
16
18
|
MediaKind,
|
|
17
19
|
} from "../../types.js";
|
|
18
20
|
import type { PostView } from "../../types/views.js";
|
|
19
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ARCHIVE_VISIBILITIES,
|
|
23
|
+
FORMATS,
|
|
24
|
+
MEDIA_KINDS,
|
|
25
|
+
PUBLIC_ARCHIVE_VISIBILITIES,
|
|
26
|
+
} from "../../types.js";
|
|
27
|
+
import type {
|
|
28
|
+
DimensionContext,
|
|
29
|
+
FilterDimensionValues,
|
|
30
|
+
MediaSelection,
|
|
31
|
+
} from "../../lib/filter-dimensions.js";
|
|
32
|
+
import {
|
|
33
|
+
buildCollectionVocabulary,
|
|
34
|
+
FILTER_DIMENSIONS,
|
|
35
|
+
serializePostFilterSelection,
|
|
36
|
+
} from "../../lib/filter-dimensions.js";
|
|
20
37
|
import { getFeaturedIconSvg } from "../../lib/featured-icons.js";
|
|
21
38
|
import { getIconSvg } from "../../lib/icons.js";
|
|
22
39
|
import { toPublicPath } from "../../lib/url.js";
|
|
@@ -24,49 +41,60 @@ import { toMediaKind } from "../../lib/upload.js";
|
|
|
24
41
|
import { PagePagination } from "../shared/Pagination.js";
|
|
25
42
|
import { TimelineFeedItem } from "../feed/TimelineFeed.js";
|
|
26
43
|
import { DecorativeQuoteMark } from "../shared/DecorativeQuoteMark.js";
|
|
44
|
+
import {
|
|
45
|
+
ARCHIVE_ALL_POSTS_LABEL,
|
|
46
|
+
getFormatLabelPlural as getSharedFormatLabelPlural,
|
|
47
|
+
getMediaKindLabel as getSharedMediaKindLabel,
|
|
48
|
+
getVisibilityLabel as getSharedVisibilityLabel,
|
|
49
|
+
hasActiveArchiveFilter,
|
|
50
|
+
} from "../shared/archive-labels.js";
|
|
27
51
|
|
|
28
52
|
// =============================================================================
|
|
29
53
|
// URL Builder
|
|
30
54
|
// =============================================================================
|
|
31
55
|
|
|
56
|
+
/**
|
|
57
|
+
* One chip's edit to the current view.
|
|
58
|
+
*
|
|
59
|
+
* Dimensions are named exactly as the shared registry names them, so a chip
|
|
60
|
+
* cannot spell a filter the route is unable to read back. An explicit
|
|
61
|
+
* `undefined` clears a dimension, the same way spreading it does.
|
|
62
|
+
*/
|
|
63
|
+
type FilterUpdates = Partial<FilterDimensionValues> & {
|
|
64
|
+
layout?: ArchiveLayout;
|
|
65
|
+
sort?: ArchiveSort;
|
|
66
|
+
/** Drop every selection and return to the bare archive. */
|
|
67
|
+
clear?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
32
70
|
/** Build an archive URL preserving existing filter params, overriding with updates. */
|
|
33
71
|
function buildFilterUrl(
|
|
34
72
|
current: ArchiveFilters,
|
|
35
|
-
updates:
|
|
36
|
-
|
|
73
|
+
updates: FilterUpdates,
|
|
74
|
+
ctx: DimensionContext,
|
|
75
|
+
basePath = "",
|
|
37
76
|
): string {
|
|
38
|
-
if (updates.clear) return toPublicPath("/archive",
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const params =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (merged.hasReplies !== undefined) {
|
|
55
|
-
params.set("replies", merged.hasReplies ? "any" : "none");
|
|
56
|
-
}
|
|
57
|
-
if (merged.visibility) {
|
|
58
|
-
// "hidden" is the URL spelling of the internal latest_hidden value
|
|
59
|
-
params.set(
|
|
60
|
-
"visibility",
|
|
61
|
-
merged.visibility === "latest_hidden" ? "hidden" : merged.visibility,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
if (merged.view && merged.view !== "grid") params.set("view", merged.view);
|
|
77
|
+
if (updates.clear) return toPublicPath("/archive", basePath);
|
|
78
|
+
|
|
79
|
+
const { layout, sort, clear: _clear, ...selectionUpdates } = updates;
|
|
80
|
+
const params = serializePostFilterSelection(
|
|
81
|
+
{ ...current.selection, ...selectionUpdates },
|
|
82
|
+
ctx,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Both layouts are written out in full. An absent `layout` means "whatever
|
|
86
|
+
// this site defaults to", so a link shared with a layout chosen keeps that
|
|
87
|
+
// layout even if the site default changes later. Only `layout` is ever
|
|
88
|
+
// emitted; the older `view` spelling is read-only now.
|
|
89
|
+
const mergedLayout = "layout" in updates ? layout : current.layout;
|
|
90
|
+
if (mergedLayout) params.set("layout", mergedLayout);
|
|
91
|
+
const mergedSort = "sort" in updates ? sort : current.sort;
|
|
92
|
+
if (mergedSort === "updated") params.set("sort", "updated");
|
|
65
93
|
|
|
66
94
|
const qs = params.toString();
|
|
67
95
|
return qs
|
|
68
|
-
? toPublicPath(`/archive?${qs}`,
|
|
69
|
-
: toPublicPath("/archive",
|
|
96
|
+
? toPublicPath(`/archive?${qs}`, basePath)
|
|
97
|
+
: toPublicPath("/archive", basePath);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
// =============================================================================
|
|
@@ -98,36 +126,16 @@ function getFormatLabel(format: string): string {
|
|
|
98
126
|
return labels[format] ?? format;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
function getFormatLabelPlural(format:
|
|
129
|
+
function getFormatLabelPlural(format: Format): string {
|
|
102
130
|
const { i18n } = useLingui();
|
|
103
|
-
|
|
104
|
-
note: i18n._(
|
|
105
|
-
msg({
|
|
106
|
-
message: "Notes",
|
|
107
|
-
comment: "@context: Post format label plural - notes",
|
|
108
|
-
}),
|
|
109
|
-
),
|
|
110
|
-
link: i18n._(
|
|
111
|
-
msg({
|
|
112
|
-
message: "Links",
|
|
113
|
-
comment: "@context: Post format label plural - links",
|
|
114
|
-
}),
|
|
115
|
-
),
|
|
116
|
-
quote: i18n._(
|
|
117
|
-
msg({
|
|
118
|
-
message: "Quotes",
|
|
119
|
-
comment: "@context: Post format label plural - quotes",
|
|
120
|
-
}),
|
|
121
|
-
),
|
|
122
|
-
};
|
|
123
|
-
return labels[format] ?? format + "s";
|
|
131
|
+
return getSharedFormatLabelPlural(format, i18n);
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
/** Icon name mapping for post formats. */
|
|
127
135
|
const FORMAT_ICONS: Record<string, string> = {
|
|
128
136
|
note: "notepad-text",
|
|
129
137
|
link: "external-link",
|
|
130
|
-
quote: "
|
|
138
|
+
quote: "quote",
|
|
131
139
|
};
|
|
132
140
|
|
|
133
141
|
/** Icon name mapping for media kinds. */
|
|
@@ -141,39 +149,21 @@ const MEDIA_KIND_ICONS: Record<MediaKind, string> = {
|
|
|
141
149
|
|
|
142
150
|
function getMediaKindLabel(kind: MediaKind): string {
|
|
143
151
|
const { i18n } = useLingui();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
message: "Audio",
|
|
160
|
-
comment: "@context: Archive media filter - audio",
|
|
161
|
-
}),
|
|
162
|
-
),
|
|
163
|
-
text: i18n._(
|
|
164
|
-
msg({
|
|
165
|
-
message: "Text attachment",
|
|
166
|
-
comment: "@context: Archive media filter - text file attachments",
|
|
167
|
-
}),
|
|
168
|
-
),
|
|
169
|
-
document: i18n._(
|
|
170
|
-
msg({
|
|
171
|
-
message: "Files",
|
|
172
|
-
comment: "@context: Archive media filter - files/documents",
|
|
173
|
-
}),
|
|
174
|
-
),
|
|
175
|
-
};
|
|
176
|
-
return labels[kind] ?? kind;
|
|
152
|
+
return getSharedMediaKindLabel(kind, i18n);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The kinds a media selection names, or none when it only asks about presence.
|
|
157
|
+
*
|
|
158
|
+
* `any`/`none` and a list of kinds are one value in the shared vocabulary; the
|
|
159
|
+
* chip's multi-select only has something to highlight in the list case.
|
|
160
|
+
*/
|
|
161
|
+
function selectedMediaKinds(
|
|
162
|
+
selection: MediaSelection | undefined,
|
|
163
|
+
): readonly MediaKind[] {
|
|
164
|
+
return selection === undefined || selection === "any" || selection === "none"
|
|
165
|
+
? []
|
|
166
|
+
: selection;
|
|
177
167
|
}
|
|
178
168
|
|
|
179
169
|
// =============================================================================
|
|
@@ -351,28 +341,25 @@ const ChipMediaSelect: FC<{
|
|
|
351
341
|
id: string;
|
|
352
342
|
icon: string;
|
|
353
343
|
filters: ArchiveFilters;
|
|
344
|
+
ctx: DimensionContext;
|
|
354
345
|
activeLabel?: string;
|
|
355
346
|
clearUrl: string;
|
|
356
|
-
|
|
357
|
-
}> = ({ id, icon, filters: f, activeLabel, clearUrl,
|
|
347
|
+
basePath?: string;
|
|
348
|
+
}> = ({ id, icon, filters: f, ctx, activeLabel, clearUrl, basePath = "" }) => {
|
|
358
349
|
const { i18n } = useLingui();
|
|
359
350
|
const isActive = !!activeLabel;
|
|
360
|
-
const activeKinds = f.
|
|
351
|
+
const activeKinds = selectedMediaKinds(f.selection.media);
|
|
361
352
|
|
|
362
353
|
const singleKind = activeKinds.length === 1 ? activeKinds[0] : undefined;
|
|
363
354
|
const activeMediaIcon = isActive
|
|
364
|
-
? f.
|
|
355
|
+
? f.selection.media === "none"
|
|
365
356
|
? "text"
|
|
366
357
|
: singleKind
|
|
367
358
|
? MEDIA_KIND_ICONS[singleKind]
|
|
368
359
|
: icon
|
|
369
360
|
: undefined;
|
|
370
361
|
|
|
371
|
-
const textOnlyUrl = buildFilterUrl(
|
|
372
|
-
{ ...f, mediaKinds: undefined, hasMedia: undefined },
|
|
373
|
-
{ hasMedia: false, mediaKinds: undefined },
|
|
374
|
-
sitePathPrefix,
|
|
375
|
-
);
|
|
362
|
+
const textOnlyUrl = buildFilterUrl(f, { media: "none" }, ctx, basePath);
|
|
376
363
|
const clearLabel = i18n._(
|
|
377
364
|
msg({
|
|
378
365
|
message: "Clear filter",
|
|
@@ -386,7 +373,7 @@ const ChipMediaSelect: FC<{
|
|
|
386
373
|
id={id}
|
|
387
374
|
class="archive-chip-select archive-chip-dropdown archive-chip-media select"
|
|
388
375
|
data-select-initialized
|
|
389
|
-
data-filter-key=
|
|
376
|
+
data-filter-key={FILTER_DIMENSIONS.media.url.param}
|
|
390
377
|
>
|
|
391
378
|
<button
|
|
392
379
|
type="button"
|
|
@@ -419,7 +406,7 @@ const ChipMediaSelect: FC<{
|
|
|
419
406
|
role="option"
|
|
420
407
|
data-value={textOnlyUrl}
|
|
421
408
|
data-navigate="true"
|
|
422
|
-
aria-selected={f.
|
|
409
|
+
aria-selected={f.selection.media === "none" ? "true" : undefined}
|
|
423
410
|
>
|
|
424
411
|
<span class="flex items-center gap-2">
|
|
425
412
|
<Icon name="text" class="[&>svg]:size-4 text-muted-foreground" />
|
|
@@ -463,34 +450,121 @@ const ChipMediaSelect: FC<{
|
|
|
463
450
|
// View Toggle
|
|
464
451
|
// =============================================================================
|
|
465
452
|
|
|
466
|
-
|
|
453
|
+
/**
|
|
454
|
+
* One option in a toolbar toggle. `label` is used for both the hover tooltip
|
|
455
|
+
* and the accessible name — an icon-only control has to explain itself to
|
|
456
|
+
* both audiences, and one string keeps them from drifting apart.
|
|
457
|
+
*/
|
|
458
|
+
const ToggleOption: FC<{
|
|
459
|
+
href: string;
|
|
460
|
+
icon: string;
|
|
461
|
+
label: string;
|
|
462
|
+
active: boolean;
|
|
463
|
+
}> = ({ href, icon, label, active }) => (
|
|
464
|
+
<a
|
|
465
|
+
href={href}
|
|
466
|
+
class={`archive-view-btn${active ? " archive-view-btn-active" : ""}`}
|
|
467
|
+
role="radio"
|
|
468
|
+
aria-checked={active ? "true" : "false"}
|
|
469
|
+
aria-label={label}
|
|
470
|
+
title={label}
|
|
471
|
+
>
|
|
472
|
+
<Icon name={icon} class="[&>svg]:size-4" />
|
|
473
|
+
</a>
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const LayoutToggle: FC<{
|
|
467
477
|
filters: ArchiveFilters;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
478
|
+
ctx: DimensionContext;
|
|
479
|
+
defaultLayout: ArchiveLayout;
|
|
480
|
+
basePath?: string;
|
|
481
|
+
}> = ({ filters, ctx, defaultLayout, basePath = "" }) => {
|
|
482
|
+
const { i18n } = useLingui();
|
|
483
|
+
const currentLayout: ArchiveLayout = filters.layout ?? defaultLayout;
|
|
473
484
|
|
|
474
485
|
return (
|
|
475
|
-
<div
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
<
|
|
486
|
-
href={
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
486
|
+
<div
|
|
487
|
+
class="archive-view-toggle"
|
|
488
|
+
role="radiogroup"
|
|
489
|
+
aria-label={i18n._(
|
|
490
|
+
msg({
|
|
491
|
+
message: "Layout",
|
|
492
|
+
comment: "@context: Archive grid/list toggle group label",
|
|
493
|
+
}),
|
|
494
|
+
)}
|
|
495
|
+
>
|
|
496
|
+
<ToggleOption
|
|
497
|
+
href={buildFilterUrl(filters, { layout: "grid" }, ctx, basePath)}
|
|
498
|
+
icon="layout-grid"
|
|
499
|
+
active={currentLayout === "grid"}
|
|
500
|
+
label={i18n._(
|
|
501
|
+
msg({
|
|
502
|
+
message: "Show as a grid of tiles",
|
|
503
|
+
comment: "@context: Archive view option - grid",
|
|
504
|
+
}),
|
|
505
|
+
)}
|
|
506
|
+
/>
|
|
507
|
+
<ToggleOption
|
|
508
|
+
href={buildFilterUrl(filters, { layout: "list" }, ctx, basePath)}
|
|
509
|
+
icon="list"
|
|
510
|
+
active={currentLayout === "list"}
|
|
511
|
+
label={i18n._(
|
|
512
|
+
msg({
|
|
513
|
+
message: "Show as a list with full posts",
|
|
514
|
+
comment: "@context: Archive view option - list",
|
|
515
|
+
}),
|
|
516
|
+
)}
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Time-axis toggle. Sits next to the view toggle because it changes how the
|
|
524
|
+
* page is arranged, not which threads it contains — unlike the filter chips.
|
|
525
|
+
*/
|
|
526
|
+
const SortToggle: FC<{
|
|
527
|
+
filters: ArchiveFilters;
|
|
528
|
+
ctx: DimensionContext;
|
|
529
|
+
basePath?: string;
|
|
530
|
+
}> = ({ filters, ctx, basePath = "" }) => {
|
|
531
|
+
const { i18n } = useLingui();
|
|
532
|
+
const sortsByActivity = filters.sort === "updated";
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div
|
|
536
|
+
class="archive-view-toggle"
|
|
537
|
+
role="radiogroup"
|
|
538
|
+
aria-label={i18n._(
|
|
539
|
+
msg({
|
|
540
|
+
message: "Sort order",
|
|
541
|
+
comment: "@context: Archive sort toggle group label",
|
|
542
|
+
}),
|
|
543
|
+
)}
|
|
544
|
+
>
|
|
545
|
+
<ToggleOption
|
|
546
|
+
href={buildFilterUrl(filters, { sort: undefined }, ctx, basePath)}
|
|
547
|
+
icon="clock"
|
|
548
|
+
active={!sortsByActivity}
|
|
549
|
+
label={i18n._(
|
|
550
|
+
msg({
|
|
551
|
+
message: "Sort by when each thread was published",
|
|
552
|
+
comment: "@context: Archive sort option - newest published first",
|
|
553
|
+
}),
|
|
554
|
+
)}
|
|
555
|
+
/>
|
|
556
|
+
<ToggleOption
|
|
557
|
+
href={buildFilterUrl(filters, { sort: "updated" }, ctx, basePath)}
|
|
558
|
+
icon="history"
|
|
559
|
+
active={sortsByActivity}
|
|
560
|
+
label={i18n._(
|
|
561
|
+
msg({
|
|
562
|
+
message: "Sort by when each thread was last added to",
|
|
563
|
+
comment:
|
|
564
|
+
"@context: Archive sort option - threads that recently gained a post first",
|
|
565
|
+
}),
|
|
566
|
+
)}
|
|
567
|
+
/>
|
|
494
568
|
</div>
|
|
495
569
|
);
|
|
496
570
|
};
|
|
@@ -499,43 +573,9 @@ const ViewToggle: FC<{
|
|
|
499
573
|
// Filter Bar
|
|
500
574
|
// =============================================================================
|
|
501
575
|
|
|
502
|
-
const ARCHIVE_VISIBILITIES: ArchiveVisibility[] = [
|
|
503
|
-
"public",
|
|
504
|
-
"featured",
|
|
505
|
-
"latest_hidden",
|
|
506
|
-
"private",
|
|
507
|
-
];
|
|
508
|
-
|
|
509
576
|
function getVisibilityLabel(v: ArchiveVisibility): string {
|
|
510
577
|
const { i18n } = useLingui();
|
|
511
|
-
|
|
512
|
-
public: i18n._(
|
|
513
|
-
msg({
|
|
514
|
-
message: "Public",
|
|
515
|
-
comment: "@context: Archive visibility filter - public posts",
|
|
516
|
-
}),
|
|
517
|
-
),
|
|
518
|
-
latest_hidden: i18n._(
|
|
519
|
-
msg({
|
|
520
|
-
message: "Hidden from Latest",
|
|
521
|
-
comment:
|
|
522
|
-
"@context: Archive visibility filter for posts hidden from Latest",
|
|
523
|
-
}),
|
|
524
|
-
),
|
|
525
|
-
private: i18n._(
|
|
526
|
-
msg({
|
|
527
|
-
message: "Private",
|
|
528
|
-
comment: "@context: Archive visibility filter - private posts",
|
|
529
|
-
}),
|
|
530
|
-
),
|
|
531
|
-
featured: i18n._(
|
|
532
|
-
msg({
|
|
533
|
-
message: "Featured",
|
|
534
|
-
comment: "@context: Archive visibility filter - featured posts",
|
|
535
|
-
}),
|
|
536
|
-
),
|
|
537
|
-
};
|
|
538
|
-
return labels[v];
|
|
578
|
+
return getSharedVisibilityLabel(v, i18n);
|
|
539
579
|
}
|
|
540
580
|
|
|
541
581
|
const VISIBILITY_ICONS: Record<ArchiveVisibility, string> = {
|
|
@@ -567,19 +607,27 @@ const THREAD_ICONS = {
|
|
|
567
607
|
|
|
568
608
|
const FilterBar: FC<{
|
|
569
609
|
filters: ArchiveFilters;
|
|
610
|
+
defaultLayout: ArchiveLayout;
|
|
570
611
|
availableYears: number[];
|
|
571
|
-
availableCollections: { slug: string; title: string }[];
|
|
612
|
+
availableCollections: { id: string; slug: string; title: string }[];
|
|
572
613
|
isAuthenticated: boolean;
|
|
573
|
-
|
|
614
|
+
basePath?: string;
|
|
574
615
|
}> = ({
|
|
575
616
|
filters,
|
|
617
|
+
defaultLayout,
|
|
576
618
|
availableYears,
|
|
577
619
|
availableCollections,
|
|
578
620
|
isAuthenticated,
|
|
579
|
-
|
|
621
|
+
basePath = "",
|
|
580
622
|
}) => {
|
|
581
623
|
const { i18n } = useLingui();
|
|
582
|
-
|
|
624
|
+
// Every link on this bar is this page with one dimension edited, so the bar
|
|
625
|
+
// spells them with the same serializer the route parses with.
|
|
626
|
+
const ctx: DimensionContext = {
|
|
627
|
+
collections: buildCollectionVocabulary(availableCollections),
|
|
628
|
+
};
|
|
629
|
+
const currentUrl = buildFilterUrl(filters, {}, ctx, basePath);
|
|
630
|
+
const selection = filters.selection;
|
|
583
631
|
|
|
584
632
|
// --- Year options ---------------------------------------------------------
|
|
585
633
|
|
|
@@ -592,20 +640,24 @@ const FilterBar: FC<{
|
|
|
592
640
|
}),
|
|
593
641
|
),
|
|
594
642
|
icon: FILTER_ICONS.year,
|
|
595
|
-
value: buildFilterUrl(
|
|
596
|
-
{ ...filters, year: undefined },
|
|
597
|
-
{ year: undefined },
|
|
598
|
-
sitePathPrefix,
|
|
599
|
-
),
|
|
643
|
+
value: buildFilterUrl(filters, { year: undefined }, ctx, basePath),
|
|
600
644
|
},
|
|
601
645
|
...availableYears.map((year) => ({
|
|
602
646
|
label: String(year),
|
|
603
|
-
value: buildFilterUrl(filters, { year },
|
|
647
|
+
value: buildFilterUrl(filters, { year }, ctx, basePath),
|
|
604
648
|
})),
|
|
605
649
|
];
|
|
606
650
|
|
|
607
651
|
// --- Collection options ---------------------------------------------------
|
|
608
652
|
|
|
653
|
+
// The chip picks one collection at a time. A URL may name several — the
|
|
654
|
+
// vocabulary supports it and the page renders it — but a dropdown that
|
|
655
|
+
// replaces the selection is the honest control for a single click.
|
|
656
|
+
const selectedCollectionIds = selection.collection ?? [];
|
|
657
|
+
const selectedCollectionTitles = selectedCollectionIds
|
|
658
|
+
.map((id) => ctx.collections?.titleById.get(id))
|
|
659
|
+
.filter((title): title is string => Boolean(title));
|
|
660
|
+
|
|
609
661
|
const collectionOptions: ChipSelectOption[] = [
|
|
610
662
|
{
|
|
611
663
|
label: i18n._(
|
|
@@ -615,52 +667,40 @@ const FilterBar: FC<{
|
|
|
615
667
|
}),
|
|
616
668
|
),
|
|
617
669
|
icon: FILTER_ICONS.collection,
|
|
618
|
-
value: buildFilterUrl(
|
|
619
|
-
{
|
|
620
|
-
...filters,
|
|
621
|
-
collectionSlug: undefined,
|
|
622
|
-
collectionTitle: undefined,
|
|
623
|
-
},
|
|
624
|
-
{ collectionSlug: undefined, collectionTitle: undefined },
|
|
625
|
-
sitePathPrefix,
|
|
626
|
-
),
|
|
670
|
+
value: buildFilterUrl(filters, { collection: undefined }, ctx, basePath),
|
|
627
671
|
},
|
|
628
672
|
...availableCollections.map((col) => ({
|
|
629
673
|
label: col.title,
|
|
630
|
-
value: buildFilterUrl(
|
|
631
|
-
filters,
|
|
632
|
-
{ collectionSlug: col.slug },
|
|
633
|
-
sitePathPrefix,
|
|
634
|
-
),
|
|
674
|
+
value: buildFilterUrl(filters, { collection: [col.id] }, ctx, basePath),
|
|
635
675
|
})),
|
|
636
676
|
];
|
|
637
677
|
|
|
638
678
|
// --- Format options (Notes split into All / Titled / Untitled) -----------
|
|
639
679
|
|
|
640
|
-
const formatActiveLabel =
|
|
641
|
-
?
|
|
680
|
+
const formatActiveLabel = selection.format
|
|
681
|
+
? selection.title === true
|
|
642
682
|
? i18n._(
|
|
643
683
|
msg({
|
|
644
684
|
message: "Titled",
|
|
645
685
|
comment: "@context: Archive filter - notes that have a title",
|
|
646
686
|
}),
|
|
647
687
|
)
|
|
648
|
-
:
|
|
688
|
+
: selection.title === false
|
|
649
689
|
? i18n._(
|
|
650
690
|
msg({
|
|
651
691
|
message: "Untitled",
|
|
652
692
|
comment: "@context: Archive filter - notes without a title",
|
|
653
693
|
}),
|
|
654
694
|
)
|
|
655
|
-
: getFormatLabelPlural(
|
|
695
|
+
: getFormatLabelPlural(selection.format)
|
|
656
696
|
: undefined;
|
|
657
697
|
|
|
658
|
-
const formatActiveIcon =
|
|
659
|
-
?
|
|
698
|
+
const formatActiveIcon = selection.format
|
|
699
|
+
? selection.title === true
|
|
660
700
|
? "type"
|
|
661
|
-
:
|
|
701
|
+
: selection.title === false
|
|
662
702
|
? "text"
|
|
663
|
-
: FORMAT_ICONS[
|
|
703
|
+
: FORMAT_ICONS[selection.format]
|
|
664
704
|
: undefined;
|
|
665
705
|
|
|
666
706
|
const formatOptions: ChipSelectOption[] = [
|
|
@@ -673,9 +713,10 @@ const FilterBar: FC<{
|
|
|
673
713
|
),
|
|
674
714
|
icon: FILTER_ICONS.format,
|
|
675
715
|
value: buildFilterUrl(
|
|
676
|
-
|
|
677
|
-
{ format: undefined,
|
|
678
|
-
|
|
716
|
+
filters,
|
|
717
|
+
{ format: undefined, title: undefined },
|
|
718
|
+
ctx,
|
|
719
|
+
basePath,
|
|
679
720
|
),
|
|
680
721
|
},
|
|
681
722
|
{
|
|
@@ -683,11 +724,9 @@ const FilterBar: FC<{
|
|
|
683
724
|
icon: FORMAT_ICONS.note,
|
|
684
725
|
value: buildFilterUrl(
|
|
685
726
|
filters,
|
|
686
|
-
{
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
},
|
|
690
|
-
sitePathPrefix,
|
|
727
|
+
{ format: "note", title: undefined },
|
|
728
|
+
ctx,
|
|
729
|
+
basePath,
|
|
691
730
|
),
|
|
692
731
|
},
|
|
693
732
|
{
|
|
@@ -701,11 +740,9 @@ const FilterBar: FC<{
|
|
|
701
740
|
indent: true,
|
|
702
741
|
value: buildFilterUrl(
|
|
703
742
|
filters,
|
|
704
|
-
{
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
},
|
|
708
|
-
sitePathPrefix,
|
|
743
|
+
{ format: "note", title: true },
|
|
744
|
+
ctx,
|
|
745
|
+
basePath,
|
|
709
746
|
),
|
|
710
747
|
},
|
|
711
748
|
{
|
|
@@ -719,11 +756,9 @@ const FilterBar: FC<{
|
|
|
719
756
|
indent: true,
|
|
720
757
|
value: buildFilterUrl(
|
|
721
758
|
filters,
|
|
722
|
-
{
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
},
|
|
726
|
-
sitePathPrefix,
|
|
759
|
+
{ format: "note", title: false },
|
|
760
|
+
ctx,
|
|
761
|
+
basePath,
|
|
727
762
|
),
|
|
728
763
|
},
|
|
729
764
|
...FORMATS.filter((f) => f !== "note").map((f) => ({
|
|
@@ -731,25 +766,33 @@ const FilterBar: FC<{
|
|
|
731
766
|
icon: FORMAT_ICONS[f],
|
|
732
767
|
value: buildFilterUrl(
|
|
733
768
|
filters,
|
|
734
|
-
{ format: f,
|
|
735
|
-
|
|
769
|
+
{ format: f, title: undefined },
|
|
770
|
+
ctx,
|
|
771
|
+
basePath,
|
|
736
772
|
),
|
|
737
773
|
})),
|
|
738
774
|
];
|
|
739
775
|
|
|
740
|
-
// --- Visibility options
|
|
741
|
-
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
776
|
+
// --- Visibility options ------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
// A signed-out reader gets the same dimension minus `private`: the archive
|
|
779
|
+
// already shows them Hidden-from-Latest posts, so those two values name sets
|
|
780
|
+
// they can actually reach. `private` names one they cannot, and the route
|
|
781
|
+
// redirects rather than quietly widening it.
|
|
782
|
+
const selectableVisibilities = isAuthenticated
|
|
783
|
+
? ARCHIVE_VISIBILITIES
|
|
784
|
+
: PUBLIC_ARCHIVE_VISIBILITIES;
|
|
785
|
+
|
|
786
|
+
// "All visibility" is the absence of the parameter, not a value of it. The
|
|
787
|
+
// page once emitted `?visibility=all` here, which read back as exactly the
|
|
788
|
+
// same thing — a second spelling of "nothing selected" that only the parser
|
|
789
|
+
// knew about. It is still read, for URLs already written; it is not written.
|
|
790
|
+
const allVisibilityUrl = buildFilterUrl(
|
|
791
|
+
filters,
|
|
747
792
|
{ visibility: undefined },
|
|
748
|
-
|
|
793
|
+
ctx,
|
|
794
|
+
basePath,
|
|
749
795
|
);
|
|
750
|
-
const allVisibilityUrl = allVisibilityBaseUrl.includes("?")
|
|
751
|
-
? `${allVisibilityBaseUrl}&visibility=all`
|
|
752
|
-
: `${allVisibilityBaseUrl}?visibility=all`;
|
|
753
796
|
|
|
754
797
|
const visibilityOptions: ChipSelectOption[] = [
|
|
755
798
|
{
|
|
@@ -762,21 +805,22 @@ const FilterBar: FC<{
|
|
|
762
805
|
icon: FILTER_ICONS.visibility,
|
|
763
806
|
value: allVisibilityUrl,
|
|
764
807
|
},
|
|
765
|
-
...
|
|
808
|
+
...selectableVisibilities.map((v) => ({
|
|
766
809
|
label: getVisibilityLabel(v),
|
|
767
810
|
...(v === "featured"
|
|
768
811
|
? { iconHtml: FEATURED_VISIBILITY_ICON_HTML }
|
|
769
812
|
: { icon: VISIBILITY_ICONS[v] }),
|
|
770
|
-
value: buildFilterUrl(filters, { visibility: v },
|
|
813
|
+
value: buildFilterUrl(filters, { visibility: v }, ctx, basePath),
|
|
771
814
|
})),
|
|
772
815
|
];
|
|
773
816
|
|
|
774
817
|
// --- Thread options ---------------------------------------------------------
|
|
775
818
|
|
|
776
819
|
const threadClearUrl = buildFilterUrl(
|
|
777
|
-
|
|
778
|
-
{
|
|
779
|
-
|
|
820
|
+
filters,
|
|
821
|
+
{ replies: undefined },
|
|
822
|
+
ctx,
|
|
823
|
+
basePath,
|
|
780
824
|
);
|
|
781
825
|
|
|
782
826
|
const threadsLabel = i18n._(
|
|
@@ -806,31 +850,31 @@ const FilterBar: FC<{
|
|
|
806
850
|
{
|
|
807
851
|
label: threadsLabel,
|
|
808
852
|
icon: THREAD_ICONS.threads,
|
|
809
|
-
value: buildFilterUrl(filters, {
|
|
853
|
+
value: buildFilterUrl(filters, { replies: true }, ctx, basePath),
|
|
810
854
|
},
|
|
811
855
|
{
|
|
812
856
|
label: singlePostsLabel,
|
|
813
857
|
icon: THREAD_ICONS.single,
|
|
814
|
-
value: buildFilterUrl(filters, {
|
|
858
|
+
value: buildFilterUrl(filters, { replies: false }, ctx, basePath),
|
|
815
859
|
},
|
|
816
860
|
];
|
|
817
861
|
|
|
818
862
|
const threadActiveLabel =
|
|
819
|
-
|
|
863
|
+
selection.replies === true
|
|
820
864
|
? threadsLabel
|
|
821
|
-
:
|
|
865
|
+
: selection.replies === false
|
|
822
866
|
? singlePostsLabel
|
|
823
867
|
: undefined;
|
|
824
868
|
const threadActiveIcon =
|
|
825
|
-
|
|
869
|
+
selection.replies === true
|
|
826
870
|
? THREAD_ICONS.threads
|
|
827
|
-
:
|
|
871
|
+
: selection.replies === false
|
|
828
872
|
? THREAD_ICONS.single
|
|
829
873
|
: undefined;
|
|
830
874
|
|
|
831
|
-
const activeKinds =
|
|
875
|
+
const activeKinds = selectedMediaKinds(selection.media);
|
|
832
876
|
const mediaActiveLabel =
|
|
833
|
-
|
|
877
|
+
selection.media === "none"
|
|
834
878
|
? i18n._(
|
|
835
879
|
msg({
|
|
836
880
|
message: "Text",
|
|
@@ -843,11 +887,20 @@ const FilterBar: FC<{
|
|
|
843
887
|
getMediaKindLabel(activeKinds[0]!)
|
|
844
888
|
: activeKinds.length > 1
|
|
845
889
|
? String(activeKinds.length)
|
|
846
|
-
:
|
|
890
|
+
: selection.media === "any"
|
|
891
|
+
? i18n._(
|
|
892
|
+
msg({
|
|
893
|
+
message: "With media",
|
|
894
|
+
comment:
|
|
895
|
+
"@context: Archive filter - posts carrying any media attachment",
|
|
896
|
+
}),
|
|
897
|
+
)
|
|
898
|
+
: undefined;
|
|
847
899
|
const mediaClearUrl = buildFilterUrl(
|
|
848
|
-
|
|
849
|
-
{
|
|
850
|
-
|
|
900
|
+
filters,
|
|
901
|
+
{ media: undefined },
|
|
902
|
+
ctx,
|
|
903
|
+
basePath,
|
|
851
904
|
);
|
|
852
905
|
|
|
853
906
|
return (
|
|
@@ -860,11 +913,12 @@ const FilterBar: FC<{
|
|
|
860
913
|
options={yearOptions}
|
|
861
914
|
currentValue={currentUrl}
|
|
862
915
|
clearUrl={buildFilterUrl(
|
|
863
|
-
|
|
916
|
+
filters,
|
|
864
917
|
{ year: undefined },
|
|
865
|
-
|
|
918
|
+
ctx,
|
|
919
|
+
basePath,
|
|
866
920
|
)}
|
|
867
|
-
activeLabel={
|
|
921
|
+
activeLabel={selection.year ? String(selection.year) : undefined}
|
|
868
922
|
/>
|
|
869
923
|
)}
|
|
870
924
|
{availableCollections.length > 0 && (
|
|
@@ -874,15 +928,16 @@ const FilterBar: FC<{
|
|
|
874
928
|
options={collectionOptions}
|
|
875
929
|
currentValue={currentUrl}
|
|
876
930
|
clearUrl={buildFilterUrl(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
},
|
|
882
|
-
{ collectionSlug: undefined, collectionTitle: undefined },
|
|
883
|
-
sitePathPrefix,
|
|
931
|
+
filters,
|
|
932
|
+
{ collection: undefined },
|
|
933
|
+
ctx,
|
|
934
|
+
basePath,
|
|
884
935
|
)}
|
|
885
|
-
activeLabel={
|
|
936
|
+
activeLabel={
|
|
937
|
+
selectedCollectionTitles.length > 0
|
|
938
|
+
? selectedCollectionTitles.join(", ")
|
|
939
|
+
: undefined
|
|
940
|
+
}
|
|
886
941
|
iconOnly
|
|
887
942
|
/>
|
|
888
943
|
)}
|
|
@@ -892,9 +947,10 @@ const FilterBar: FC<{
|
|
|
892
947
|
options={formatOptions}
|
|
893
948
|
currentValue={currentUrl}
|
|
894
949
|
clearUrl={buildFilterUrl(
|
|
895
|
-
|
|
896
|
-
{ format: undefined,
|
|
897
|
-
|
|
950
|
+
filters,
|
|
951
|
+
{ format: undefined, title: undefined },
|
|
952
|
+
ctx,
|
|
953
|
+
basePath,
|
|
898
954
|
)}
|
|
899
955
|
activeLabel={formatActiveLabel}
|
|
900
956
|
activeIcon={formatActiveIcon}
|
|
@@ -916,39 +972,46 @@ const FilterBar: FC<{
|
|
|
916
972
|
id="af-media"
|
|
917
973
|
icon={FILTER_ICONS.media}
|
|
918
974
|
filters={filters}
|
|
975
|
+
ctx={ctx}
|
|
919
976
|
activeLabel={mediaActiveLabel}
|
|
920
977
|
clearUrl={mediaClearUrl}
|
|
921
|
-
|
|
978
|
+
basePath={basePath}
|
|
922
979
|
/>
|
|
923
980
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
/>
|
|
948
|
-
)}
|
|
981
|
+
<ChipSelect
|
|
982
|
+
id="af-visibility"
|
|
983
|
+
icon={FILTER_ICONS.visibility}
|
|
984
|
+
options={visibilityOptions}
|
|
985
|
+
currentValue={currentUrl}
|
|
986
|
+
clearUrl={allVisibilityUrl}
|
|
987
|
+
activeLabel={
|
|
988
|
+
selection.visibility
|
|
989
|
+
? getVisibilityLabel(selection.visibility)
|
|
990
|
+
: undefined
|
|
991
|
+
}
|
|
992
|
+
activeIcon={
|
|
993
|
+
selection.visibility && selection.visibility !== "featured"
|
|
994
|
+
? VISIBILITY_ICONS[selection.visibility]
|
|
995
|
+
: undefined
|
|
996
|
+
}
|
|
997
|
+
activeIconHtml={
|
|
998
|
+
selection.visibility === "featured"
|
|
999
|
+
? FEATURED_VISIBILITY_ICON_HTML
|
|
1000
|
+
: undefined
|
|
1001
|
+
}
|
|
1002
|
+
iconOnly
|
|
1003
|
+
/>
|
|
949
1004
|
</div>
|
|
950
1005
|
|
|
951
|
-
<
|
|
1006
|
+
<div class="archive-toolbar-toggles">
|
|
1007
|
+
<SortToggle filters={filters} ctx={ctx} basePath={basePath} />
|
|
1008
|
+
<LayoutToggle
|
|
1009
|
+
filters={filters}
|
|
1010
|
+
ctx={ctx}
|
|
1011
|
+
defaultLayout={defaultLayout}
|
|
1012
|
+
basePath={basePath}
|
|
1013
|
+
/>
|
|
1014
|
+
</div>
|
|
952
1015
|
</div>
|
|
953
1016
|
);
|
|
954
1017
|
};
|
|
@@ -1245,20 +1308,26 @@ const ArchiveTile: FC<{ post: PostView; timeZone?: string }> = ({
|
|
|
1245
1308
|
export const ArchivePage: FC<ArchivePageProps> = ({
|
|
1246
1309
|
groups,
|
|
1247
1310
|
items,
|
|
1311
|
+
defaultLayout = "list",
|
|
1248
1312
|
totalCount,
|
|
1313
|
+
baselineCount,
|
|
1249
1314
|
currentPage,
|
|
1250
1315
|
totalPages,
|
|
1251
1316
|
filters,
|
|
1252
1317
|
availableYears,
|
|
1253
1318
|
availableCollections,
|
|
1254
1319
|
isAuthenticated,
|
|
1255
|
-
|
|
1320
|
+
basePath = "",
|
|
1256
1321
|
timeZone = "UTC",
|
|
1257
1322
|
feedHref,
|
|
1258
1323
|
}) => {
|
|
1259
1324
|
const { i18n } = useLingui();
|
|
1260
|
-
const
|
|
1261
|
-
const
|
|
1325
|
+
const currentLayout: ArchiveLayout = filters.layout ?? defaultLayout;
|
|
1326
|
+
const sortsByActivity = filters.sort === "updated";
|
|
1327
|
+
const dimensionCtx: DimensionContext = {
|
|
1328
|
+
collections: buildCollectionVocabulary(availableCollections),
|
|
1329
|
+
};
|
|
1330
|
+
const paginationBaseUrl = buildFilterUrl(filters, {}, dimensionCtx, basePath);
|
|
1262
1331
|
const totalCountUnit =
|
|
1263
1332
|
totalCount === 1
|
|
1264
1333
|
? i18n._(
|
|
@@ -1276,30 +1345,54 @@ export const ArchivePage: FC<ArchivePageProps> = ({
|
|
|
1276
1345
|
}),
|
|
1277
1346
|
);
|
|
1278
1347
|
|
|
1348
|
+
// A reader who arrives on a pre-filtered URL never applied the filter and has
|
|
1349
|
+
// no way to know how much it removed. Naming the seven dimensions back at
|
|
1350
|
+
// them is what the chip bar already does; what it cannot say is how much is
|
|
1351
|
+
// left, so the count carries its own baseline instead.
|
|
1352
|
+
const showsSubset =
|
|
1353
|
+
hasActiveArchiveFilter(filters.selection) &&
|
|
1354
|
+
baselineCount !== undefined &&
|
|
1355
|
+
baselineCount > totalCount;
|
|
1356
|
+
|
|
1357
|
+
const countRemainder = showsSubset
|
|
1358
|
+
? i18n._(
|
|
1359
|
+
msg({
|
|
1360
|
+
message: "of {total} {unit}",
|
|
1361
|
+
comment:
|
|
1362
|
+
"@context: Archive page count when a filter narrows the view, following the matching count — reads as '42 of 1,240 threads'",
|
|
1363
|
+
}),
|
|
1364
|
+
{ total: baselineCount, unit: totalCountUnit },
|
|
1365
|
+
)
|
|
1366
|
+
: totalCountUnit;
|
|
1367
|
+
|
|
1368
|
+
const countSummary = `${totalCount} ${countRemainder}`;
|
|
1369
|
+
|
|
1279
1370
|
return (
|
|
1280
1371
|
<div class="py-6" data-page="archive">
|
|
1281
1372
|
<header class="archive-page-header page-intro">
|
|
1282
1373
|
<div class="page-intro-title-row">
|
|
1283
|
-
<h1 class="page-intro-title">
|
|
1284
|
-
{i18n._(
|
|
1285
|
-
msg({
|
|
1286
|
-
message: "Archive",
|
|
1287
|
-
comment: "@context: Archive page title",
|
|
1288
|
-
}),
|
|
1289
|
-
)}
|
|
1290
|
-
</h1>
|
|
1374
|
+
<h1 class="page-intro-title">{i18n._(ARCHIVE_ALL_POSTS_LABEL)}</h1>
|
|
1291
1375
|
</div>
|
|
1292
|
-
<p
|
|
1293
|
-
class="page-intro-meta archive-page-meta"
|
|
1294
|
-
aria-label={`${totalCount} ${totalCountUnit}`}
|
|
1295
|
-
>
|
|
1376
|
+
<p class="page-intro-meta archive-page-meta" aria-label={countSummary}>
|
|
1296
1377
|
<span class="archive-page-summary-count">{totalCount}</span>{" "}
|
|
1297
|
-
{
|
|
1378
|
+
{countRemainder}
|
|
1379
|
+
{sortsByActivity && (
|
|
1380
|
+
<>
|
|
1381
|
+
{" · "}
|
|
1382
|
+
{i18n._(
|
|
1383
|
+
msg({
|
|
1384
|
+
message: "newest changes first",
|
|
1385
|
+
comment:
|
|
1386
|
+
"@context: Archive page meta note explaining the active sort order",
|
|
1387
|
+
}),
|
|
1388
|
+
)}
|
|
1389
|
+
</>
|
|
1390
|
+
)}
|
|
1298
1391
|
{feedHref && (
|
|
1299
1392
|
<>
|
|
1300
1393
|
{" "}
|
|
1301
1394
|
<a
|
|
1302
|
-
href={toPublicPath(feedHref,
|
|
1395
|
+
href={toPublicPath(feedHref, basePath)}
|
|
1303
1396
|
class="feed-link"
|
|
1304
1397
|
title={i18n._(
|
|
1305
1398
|
msg({
|
|
@@ -1322,10 +1415,11 @@ export const ArchivePage: FC<ArchivePageProps> = ({
|
|
|
1322
1415
|
|
|
1323
1416
|
<FilterBar
|
|
1324
1417
|
filters={filters}
|
|
1418
|
+
defaultLayout={defaultLayout}
|
|
1325
1419
|
availableYears={availableYears}
|
|
1326
1420
|
availableCollections={availableCollections}
|
|
1327
1421
|
isAuthenticated={isAuthenticated}
|
|
1328
|
-
|
|
1422
|
+
basePath={basePath}
|
|
1329
1423
|
/>
|
|
1330
1424
|
</header>
|
|
1331
1425
|
|
|
@@ -1340,14 +1434,20 @@ export const ArchivePage: FC<ArchivePageProps> = ({
|
|
|
1340
1434
|
}),
|
|
1341
1435
|
)}
|
|
1342
1436
|
</p>
|
|
1343
|
-
) :
|
|
1437
|
+
) : currentLayout === "grid" ? (
|
|
1344
1438
|
<div class="archive-grid-wrapper">
|
|
1345
1439
|
<div class="archive-grid">
|
|
1346
1440
|
{groups.map((group, groupIndex) => (
|
|
1347
1441
|
<div key={`grid-${group.year}-${group.month}`} class="contents">
|
|
1348
1442
|
<ArchiveMonthHeader
|
|
1349
1443
|
class={`archive-month-header${groupIndex > 0 ? " archive-month-header-spaced" : ""}`}
|
|
1350
|
-
|
|
1444
|
+
// Not translated, on purpose: `group.label` is built with
|
|
1445
|
+
// a hardcoded en-US month name, so a translated wrapper
|
|
1446
|
+
// would only produce "更新于 August 2026". Both halves
|
|
1447
|
+
// become translatable together, or neither does.
|
|
1448
|
+
label={
|
|
1449
|
+
sortsByActivity ? `Updated ${group.label}` : group.label
|
|
1450
|
+
}
|
|
1351
1451
|
count={group.totalCount}
|
|
1352
1452
|
/>
|
|
1353
1453
|
{group.posts.map((post) => (
|