@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
|
@@ -53,11 +53,14 @@ function renderArchivePage(
|
|
|
53
53
|
totalCount: 1,
|
|
54
54
|
currentPage: 1,
|
|
55
55
|
totalPages: 1,
|
|
56
|
-
filters: {},
|
|
56
|
+
filters: { selection: {} },
|
|
57
57
|
availableYears: [2026],
|
|
58
58
|
availableCollections: [],
|
|
59
59
|
isAuthenticated: false,
|
|
60
60
|
timeZone: "UTC",
|
|
61
|
+
// The fixture above is grid data, so these cases pin the grid layout.
|
|
62
|
+
// The list default gets its own case at the end of the file.
|
|
63
|
+
defaultLayout: "grid",
|
|
61
64
|
...props,
|
|
62
65
|
}),
|
|
63
66
|
);
|
|
@@ -93,22 +96,46 @@ describe("ArchivePage", () => {
|
|
|
93
96
|
it("serializes visibility latest_hidden as the hidden URL alias", () => {
|
|
94
97
|
const html = renderArchivePage({
|
|
95
98
|
isAuthenticated: true,
|
|
96
|
-
filters: { visibility: "latest_hidden" },
|
|
99
|
+
filters: { selection: { visibility: "latest_hidden" } },
|
|
97
100
|
});
|
|
98
101
|
|
|
99
102
|
expect(html).toContain("visibility=hidden");
|
|
100
103
|
expect(html).not.toContain("visibility=latest_hidden");
|
|
101
104
|
});
|
|
102
105
|
|
|
106
|
+
// The archive already shows a signed-out reader every non-private post, so
|
|
107
|
+
// the dimension names sets they can reach. `private` is the exception, and
|
|
108
|
+
// it is absent rather than present-and-broken.
|
|
109
|
+
it("offers the visibility filter without private to a signed-out reader", () => {
|
|
110
|
+
const html = renderArchivePage();
|
|
111
|
+
|
|
112
|
+
expect(html).toContain('id="af-visibility"');
|
|
113
|
+
expect(html).toContain("visibility=public");
|
|
114
|
+
expect(html).toContain("visibility=hidden");
|
|
115
|
+
expect(html).toContain("visibility=featured");
|
|
116
|
+
expect(html).not.toContain("visibility=private");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("offers private to the author", () => {
|
|
120
|
+
const html = renderArchivePage({ isAuthenticated: true });
|
|
121
|
+
|
|
122
|
+
expect(html).toContain('id="af-visibility"');
|
|
123
|
+
expect(html).toContain("visibility=private");
|
|
124
|
+
});
|
|
125
|
+
|
|
103
126
|
it("marks the thread filter active when filtering single posts", () => {
|
|
104
|
-
const html = renderArchivePage({
|
|
127
|
+
const html = renderArchivePage({
|
|
128
|
+
filters: { selection: { replies: false } },
|
|
129
|
+
});
|
|
105
130
|
|
|
106
131
|
expect(html).toContain("archive-chip-active");
|
|
107
132
|
expect(html).toContain("Single posts");
|
|
108
133
|
});
|
|
109
134
|
|
|
110
135
|
it("uses a recognizable quote mark for the quote format filter", () => {
|
|
111
|
-
const html = renderArchivePage({
|
|
136
|
+
const html = renderArchivePage({
|
|
137
|
+
filters: { selection: { format: "quote" } },
|
|
138
|
+
});
|
|
112
139
|
|
|
113
140
|
expect(html).toContain('class="lucide lucide-quote"');
|
|
114
141
|
expect(html).not.toContain('class="lucide lucide-text-quote"');
|
|
@@ -132,7 +159,9 @@ describe("ArchivePage", () => {
|
|
|
132
159
|
});
|
|
133
160
|
|
|
134
161
|
it("marks the updated sort active and keeps a way back", () => {
|
|
135
|
-
const html = renderArchivePage({
|
|
162
|
+
const html = renderArchivePage({
|
|
163
|
+
filters: { selection: {}, sort: "updated" },
|
|
164
|
+
});
|
|
136
165
|
|
|
137
166
|
expect(html).toContain(
|
|
138
167
|
'<a href="/archive?sort=updated" class="archive-view-btn archive-view-btn-active" role="radio" aria-checked="true" aria-label="Sort by when each thread was last added to"',
|
|
@@ -160,20 +189,47 @@ describe("ArchivePage", () => {
|
|
|
160
189
|
'<span class="archive-month-header-label">March 2026</span>',
|
|
161
190
|
);
|
|
162
191
|
|
|
163
|
-
const updated = renderArchivePage({
|
|
192
|
+
const updated = renderArchivePage({
|
|
193
|
+
filters: { selection: {}, sort: "updated" },
|
|
194
|
+
});
|
|
164
195
|
expect(updated).toContain(
|
|
165
196
|
'<span class="archive-month-header-label">Updated March 2026</span>',
|
|
166
197
|
);
|
|
167
198
|
expect(updated).toContain("newest changes first");
|
|
168
199
|
});
|
|
169
200
|
|
|
170
|
-
it("keeps the sort selection in filter and
|
|
201
|
+
it("keeps the sort selection in filter and layout links", () => {
|
|
171
202
|
const html = renderArchivePage({
|
|
172
|
-
filters: { sort: "updated" },
|
|
203
|
+
filters: { selection: {}, sort: "updated" },
|
|
173
204
|
availableCollections: [{ slug: "tech", title: "Tech" }],
|
|
174
205
|
});
|
|
175
206
|
|
|
176
207
|
expect(html).toContain("collection=tech&sort=updated");
|
|
177
|
-
expect(html).toContain("
|
|
208
|
+
expect(html).toContain("layout=list&sort=updated");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Neither layout is implied by an absent param any more, so both toggle
|
|
212
|
+
// links name themselves. A link shared with a layout chosen keeps that layout
|
|
213
|
+
// even if the site's ARCHIVE_DEFAULT_LAYOUT later changes.
|
|
214
|
+
it("writes both layout options into the URL explicitly", () => {
|
|
215
|
+
const html = renderArchivePage();
|
|
216
|
+
|
|
217
|
+
expect(html).toContain('href="/archive?layout=grid"');
|
|
218
|
+
expect(html).toContain('href="/archive?layout=list"');
|
|
219
|
+
expect(html).not.toContain("view=grid");
|
|
220
|
+
expect(html).not.toContain("view=list");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("renders the flat list when the site default layout is list", () => {
|
|
224
|
+
const html = renderArchivePage({
|
|
225
|
+
groups: [],
|
|
226
|
+
items: [],
|
|
227
|
+
defaultLayout: undefined,
|
|
228
|
+
totalCount: 0,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(html).toContain(
|
|
232
|
+
'<a href="/archive?layout=list" class="archive-view-btn archive-view-btn-active"',
|
|
233
|
+
);
|
|
178
234
|
});
|
|
179
235
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory's two kinds of row read against each other.
|
|
3
|
+
*
|
|
4
|
+
* A collection and a smart collection sit in one list, one above the other, and
|
|
5
|
+
* a reader compares them. So they carry the same two measures — how many
|
|
6
|
+
* threads, and how long ago the newest one moved — and the only thing the smart
|
|
7
|
+
* row says differently is the funnel marker naming where its members came from.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Context } from "hono";
|
|
11
|
+
import { renderToString } from "hono/jsx/dom/server";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { I18nProvider } from "../../../i18n/context.js";
|
|
14
|
+
import { createI18n } from "../../../i18n/i18n.js";
|
|
15
|
+
import { CollectionsPage } from "../CollectionsPage.js";
|
|
16
|
+
import type { CollectionDirectoryItem } from "../../../types.js";
|
|
17
|
+
|
|
18
|
+
const COLLECTION_ACTIVITY_AT = 1_700_000_000;
|
|
19
|
+
const SMART_ACTIVITY_AT = 1_700_086_400;
|
|
20
|
+
|
|
21
|
+
const items: CollectionDirectoryItem[] = [
|
|
22
|
+
{
|
|
23
|
+
id: "cdi_test00000000000000000000000",
|
|
24
|
+
type: "collection",
|
|
25
|
+
collection: {
|
|
26
|
+
id: "col_test000000000000000000000000",
|
|
27
|
+
siteId: "sit_test000000000000000000000000",
|
|
28
|
+
slug: "recipes",
|
|
29
|
+
title: "Recipes",
|
|
30
|
+
description: null,
|
|
31
|
+
sortOrder: "manual",
|
|
32
|
+
createdAt: 1_700_000_000,
|
|
33
|
+
updatedAt: 1_700_000_000,
|
|
34
|
+
threadCount: 2,
|
|
35
|
+
recentActivityAt: COLLECTION_ACTIVITY_AT,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "cdi_test00000000000000000000001",
|
|
40
|
+
type: "smart_collection",
|
|
41
|
+
smartCollection: {
|
|
42
|
+
id: "smc_test000000000000000000000000",
|
|
43
|
+
siteId: "sit_test000000000000000000000000",
|
|
44
|
+
slug: "quotes",
|
|
45
|
+
title: "Quotes",
|
|
46
|
+
description: null,
|
|
47
|
+
selection: { format: "quote" },
|
|
48
|
+
sort: "newest",
|
|
49
|
+
layout: null,
|
|
50
|
+
createdAt: 1_700_000_000,
|
|
51
|
+
updatedAt: 1_700_000_000,
|
|
52
|
+
threadCount: 5,
|
|
53
|
+
recentActivityAt: SMART_ACTIVITY_AT,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function render(isAuthenticated: boolean) {
|
|
59
|
+
const i18n = createI18n("en");
|
|
60
|
+
const c = {
|
|
61
|
+
get(key: string) {
|
|
62
|
+
if (key === "i18n") return i18n;
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
} as unknown as Context;
|
|
66
|
+
|
|
67
|
+
I18nProvider({ c, children: "" });
|
|
68
|
+
|
|
69
|
+
return renderToString(
|
|
70
|
+
CollectionsPage({
|
|
71
|
+
items,
|
|
72
|
+
isAuthenticated,
|
|
73
|
+
navigationCollectionIds: [],
|
|
74
|
+
sitePathPrefix: "",
|
|
75
|
+
basePath: "",
|
|
76
|
+
siteOrigin: "",
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("collections directory rows", () => {
|
|
82
|
+
it("dates a smart collection row the way it dates a collection row", () => {
|
|
83
|
+
const html = render(false);
|
|
84
|
+
|
|
85
|
+
expect(html.match(/class="collection-directory-updated"/g)).toHaveLength(2);
|
|
86
|
+
expect(html).toContain(
|
|
87
|
+
new Date(COLLECTION_ACTIVITY_AT * 1000).toISOString(),
|
|
88
|
+
);
|
|
89
|
+
expect(html).toContain(new Date(SMART_ACTIVITY_AT * 1000).toISOString());
|
|
90
|
+
expect(html).toContain("5 threads");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection links must keep the reader in the language view they are in.
|
|
3
|
+
*
|
|
4
|
+
* A collection page is served once per language (`langGet("/*")` in
|
|
5
|
+
* routes/pages/language.tsx), so a link from `/en/collections` that points at
|
|
6
|
+
* `/{slug}` does not just look odd — it silently moves the reader to the
|
|
7
|
+
* primary language's copy of that collection.
|
|
8
|
+
*
|
|
9
|
+
* The signed-in directory renders through `CollectionsManager`, the signed-out
|
|
10
|
+
* one directly through `CollectionDirectory`. Both are covered here because the
|
|
11
|
+
* bug this pins down existed in only one of them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Context } from "hono";
|
|
15
|
+
import { renderToString } from "hono/jsx/dom/server";
|
|
16
|
+
import { describe, expect, it } from "vitest";
|
|
17
|
+
import { I18nProvider } from "../../../i18n/context.js";
|
|
18
|
+
import { createI18n } from "../../../i18n/i18n.js";
|
|
19
|
+
import { CollectionsPage } from "../CollectionsPage.js";
|
|
20
|
+
import type { CollectionDirectoryItem } from "../../../types.js";
|
|
21
|
+
|
|
22
|
+
const items: CollectionDirectoryItem[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "cdi_test00000000000000000000000",
|
|
25
|
+
type: "collection",
|
|
26
|
+
collection: {
|
|
27
|
+
id: "col_test000000000000000000000000",
|
|
28
|
+
siteId: "sit_test000000000000000000000000",
|
|
29
|
+
slug: "recipes",
|
|
30
|
+
title: "Recipes",
|
|
31
|
+
description: null,
|
|
32
|
+
sortOrder: "manual",
|
|
33
|
+
createdAt: 1_700_000_000,
|
|
34
|
+
updatedAt: 1_700_000_000,
|
|
35
|
+
threadCount: 2,
|
|
36
|
+
recentActivityAt: 1_700_000_000,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function render(isAuthenticated: boolean, basePath: string) {
|
|
42
|
+
const i18n = createI18n("en");
|
|
43
|
+
const c = {
|
|
44
|
+
get(key: string) {
|
|
45
|
+
if (key === "i18n") return i18n;
|
|
46
|
+
return undefined;
|
|
47
|
+
},
|
|
48
|
+
} as unknown as Context;
|
|
49
|
+
|
|
50
|
+
I18nProvider({ c, children: "" });
|
|
51
|
+
|
|
52
|
+
return renderToString(
|
|
53
|
+
CollectionsPage({
|
|
54
|
+
items,
|
|
55
|
+
isAuthenticated,
|
|
56
|
+
navigationCollectionIds: [],
|
|
57
|
+
sitePathPrefix: "",
|
|
58
|
+
basePath,
|
|
59
|
+
siteOrigin: "",
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("collections directory links", () => {
|
|
65
|
+
it("keeps a signed-out reader inside the language view", () => {
|
|
66
|
+
const html = render(false, "/en");
|
|
67
|
+
expect(html).toContain('href="/en/recipes"');
|
|
68
|
+
expect(html).not.toContain('href="/recipes"');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps the signed-in author inside the language view", () => {
|
|
72
|
+
const html = render(true, "/en");
|
|
73
|
+
expect(html).toContain('href="/en/recipes"');
|
|
74
|
+
expect(html).not.toContain('href="/recipes"');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("links at the root when there is no language view", () => {
|
|
78
|
+
expect(render(false, "")).toContain('href="/recipes"');
|
|
79
|
+
expect(render(true, "")).toContain('href="/recipes"');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -2,7 +2,15 @@ import { msg } from "@lingui/core/macro";
|
|
|
2
2
|
import type { FC } from "hono/jsx";
|
|
3
3
|
import { useLingui } from "../../i18n/context.js";
|
|
4
4
|
import type { CollectionDirectoryItem } from "../../types.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getCollectionPagePath,
|
|
7
|
+
getCollectionSelectionPath,
|
|
8
|
+
} from "../../lib/collection-paths.js";
|
|
9
|
+
import { getIconSvg } from "../../lib/icons.js";
|
|
10
|
+
import {
|
|
11
|
+
describeSmartCollection,
|
|
12
|
+
getSmartCollectionLabels,
|
|
13
|
+
} from "./smart-collection-labels.js";
|
|
6
14
|
import { getDividerCollectionGroup } from "../../lib/collection-groups.js";
|
|
7
15
|
import { render as renderMarkdown } from "../../lib/markdown.js";
|
|
8
16
|
import { formatRelativeAge, toISOString } from "../../lib/time.js";
|
|
@@ -12,13 +20,35 @@ export interface CollectionDirectoryProps {
|
|
|
12
20
|
items: CollectionDirectoryItem[];
|
|
13
21
|
emptyMessage?: string;
|
|
14
22
|
sitePathPrefix?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Public path prefix for collection links, carrying the language prefix in a
|
|
25
|
+
* language view.
|
|
26
|
+
*
|
|
27
|
+
* Required rather than defaulting to `sitePathPrefix`: forgetting it does not
|
|
28
|
+
* fail, it silently links a reader in `/en` back into the primary view, which
|
|
29
|
+
* is a bug that looks like a working link. Pass `navData.basePath`.
|
|
30
|
+
*/
|
|
31
|
+
basePath: string;
|
|
15
32
|
siteOrigin?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to offer each collection's feed beside it.
|
|
35
|
+
*
|
|
36
|
+
* This is the only place a reader is handed a collection feed: `/subscribe`
|
|
37
|
+
* deliberately lists the site-wide feeds only, because wanting one collection
|
|
38
|
+
* and not the rest is a rare intent that would tax every reader's attention
|
|
39
|
+
* there. Here it sits next to the collection someone is already looking at.
|
|
40
|
+
*
|
|
41
|
+
* Defaults off so the dashboard's collection manager — where the point is
|
|
42
|
+
* organizing, not subscribing — does not grow feed icons by accident.
|
|
43
|
+
*/
|
|
44
|
+
feedsEnabled?: boolean;
|
|
16
45
|
}
|
|
17
46
|
|
|
18
47
|
const hasDirectoryContent = (items: CollectionDirectoryItem[]) =>
|
|
19
48
|
items.some(
|
|
20
49
|
(item) =>
|
|
21
50
|
(item.type === "collection" && item.collection) ||
|
|
51
|
+
(item.type === "smart_collection" && item.smartCollection) ||
|
|
22
52
|
(item.type === "link" && item.label && item.url),
|
|
23
53
|
);
|
|
24
54
|
|
|
@@ -37,6 +67,7 @@ const hasDirectoryContent = (items: CollectionDirectoryItem[]) =>
|
|
|
37
67
|
const computeSequenceLabels = (items: CollectionDirectoryItem[]): string[] => {
|
|
38
68
|
const isContentItem = (item: CollectionDirectoryItem) =>
|
|
39
69
|
(item.type === "collection" && item.collection) ||
|
|
70
|
+
(item.type === "smart_collection" && item.smartCollection) ||
|
|
40
71
|
(item.type === "link" && item.label && item.url);
|
|
41
72
|
|
|
42
73
|
// First pass: determine group sizes.
|
|
@@ -109,13 +140,43 @@ const computeSequenceLabels = (items: CollectionDirectoryItem[]): string[] => {
|
|
|
109
140
|
return labels;
|
|
110
141
|
};
|
|
111
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The feed icon that rides a directory row's summary line.
|
|
145
|
+
*
|
|
146
|
+
* Same affordance as the one on the collection's own page and on the archive:
|
|
147
|
+
* small, quiet, and recognizable to a reader who knows what it means. Readers
|
|
148
|
+
* who do not are served by /subscribe instead, which is why collection feeds
|
|
149
|
+
* are not listed there.
|
|
150
|
+
*
|
|
151
|
+
* @param props - Where the feed lives and what to call it
|
|
152
|
+
* @returns The icon link, or nothing when feeds are switched off
|
|
153
|
+
*/
|
|
154
|
+
const CollectionFeedLink: FC<{
|
|
155
|
+
href: string | null;
|
|
156
|
+
title: string;
|
|
157
|
+
}> = ({ href, title }) =>
|
|
158
|
+
href ? (
|
|
159
|
+
<a href={href} class="feed-link" title={title} rel="noopener noreferrer">
|
|
160
|
+
<span dangerouslySetInnerHTML={{ __html: getIconSvg("rss") ?? "" }} />
|
|
161
|
+
</a>
|
|
162
|
+
) : null;
|
|
163
|
+
|
|
112
164
|
export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
|
|
113
165
|
items,
|
|
114
166
|
emptyMessage,
|
|
115
167
|
sitePathPrefix = "",
|
|
168
|
+
basePath,
|
|
116
169
|
siteOrigin = "",
|
|
170
|
+
feedsEnabled = false,
|
|
117
171
|
}) => {
|
|
118
172
|
const { i18n } = useLingui();
|
|
173
|
+
const feedTitle = i18n._(
|
|
174
|
+
msg({
|
|
175
|
+
message: "RSS feed",
|
|
176
|
+
comment:
|
|
177
|
+
"@context: Tooltip for the RSS feed icon beside a collection in the collections directory",
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
119
180
|
|
|
120
181
|
if (!hasDirectoryContent(items)) {
|
|
121
182
|
return (
|
|
@@ -152,7 +213,7 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
|
|
|
152
213
|
<a
|
|
153
214
|
href={toPublicPath(
|
|
154
215
|
getCollectionSelectionPath(group.slugExpression),
|
|
155
|
-
|
|
216
|
+
basePath,
|
|
156
217
|
)}
|
|
157
218
|
class="collection-directory-divider-link collection-directory-divider-text"
|
|
158
219
|
>
|
|
@@ -254,6 +315,112 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
|
|
|
254
315
|
);
|
|
255
316
|
}
|
|
256
317
|
|
|
318
|
+
if (item.type === "smart_collection") {
|
|
319
|
+
const smartCollection = item.smartCollection;
|
|
320
|
+
if (!smartCollection) return null;
|
|
321
|
+
const sequence = sequenceLabels[index];
|
|
322
|
+
// The full sentence is the marker's tooltip; its accessible name is
|
|
323
|
+
// just the kind, because the marker sits inside the title link and a
|
|
324
|
+
// whole sentence there would swamp the link's name.
|
|
325
|
+
const conditions = describeSmartCollection(
|
|
326
|
+
smartCollection.selection,
|
|
327
|
+
i18n,
|
|
328
|
+
);
|
|
329
|
+
const smartCollectionNoun = getSmartCollectionLabels(i18n).noun;
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div
|
|
333
|
+
key={item.id}
|
|
334
|
+
class="collection-directory-item"
|
|
335
|
+
data-smart-collection={smartCollection.slug}
|
|
336
|
+
>
|
|
337
|
+
<div class="collection-directory-main">
|
|
338
|
+
<span class="collection-directory-sequence" aria-hidden="true">
|
|
339
|
+
{sequence}
|
|
340
|
+
</span>
|
|
341
|
+
<div class="collection-directory-title-row">
|
|
342
|
+
<a
|
|
343
|
+
href={toPublicPath(
|
|
344
|
+
getCollectionPagePath(smartCollection.slug),
|
|
345
|
+
basePath,
|
|
346
|
+
)}
|
|
347
|
+
class="collection-directory-title-link"
|
|
348
|
+
>
|
|
349
|
+
<span class="collection-directory-title">
|
|
350
|
+
{smartCollection.title}
|
|
351
|
+
{/* Shown to every reader, not only the author: which
|
|
352
|
+
kind of collection this is changes how the list
|
|
353
|
+
reads. It goes in the same slot the link rows put
|
|
354
|
+
their marker in, which centres it on the title
|
|
355
|
+
without any arithmetic. */}
|
|
356
|
+
<span
|
|
357
|
+
class="collection-directory-smart-icon"
|
|
358
|
+
title={conditions}
|
|
359
|
+
aria-label={smartCollectionNoun}
|
|
360
|
+
role="img"
|
|
361
|
+
dangerouslySetInnerHTML={{
|
|
362
|
+
__html: getIconSvg("funnel", "icon-fine") ?? "",
|
|
363
|
+
}}
|
|
364
|
+
/>
|
|
365
|
+
</span>
|
|
366
|
+
</a>
|
|
367
|
+
</div>
|
|
368
|
+
{smartCollection.description && (
|
|
369
|
+
<div
|
|
370
|
+
class="collection-directory-description prose"
|
|
371
|
+
dangerouslySetInnerHTML={{
|
|
372
|
+
__html: renderMarkdown(smartCollection.description, {
|
|
373
|
+
namespace: smartCollection.id,
|
|
374
|
+
}),
|
|
375
|
+
}}
|
|
376
|
+
/>
|
|
377
|
+
)}
|
|
378
|
+
<p class="collection-directory-summary">
|
|
379
|
+
<span class="collection-directory-meta">
|
|
380
|
+
{smartCollection.threadCount}{" "}
|
|
381
|
+
{smartCollection.threadCount === 1
|
|
382
|
+
? i18n._(
|
|
383
|
+
msg({
|
|
384
|
+
message: "thread",
|
|
385
|
+
comment: "@context: Singular thread count label",
|
|
386
|
+
}),
|
|
387
|
+
)
|
|
388
|
+
: i18n._(
|
|
389
|
+
msg({
|
|
390
|
+
message: "threads",
|
|
391
|
+
comment: "@context: Plural thread count label",
|
|
392
|
+
}),
|
|
393
|
+
)}
|
|
394
|
+
</span>
|
|
395
|
+
{/* The same two measures a collection row carries. The
|
|
396
|
+
conditions are what differs between the two kinds; how
|
|
397
|
+
full and how fresh the result is is not. */}
|
|
398
|
+
<span
|
|
399
|
+
class="collection-directory-meta-separator"
|
|
400
|
+
aria-hidden="true"
|
|
401
|
+
>
|
|
402
|
+
/
|
|
403
|
+
</span>
|
|
404
|
+
<time
|
|
405
|
+
class="collection-directory-updated"
|
|
406
|
+
dateTime={toISOString(smartCollection.recentActivityAt)}
|
|
407
|
+
>
|
|
408
|
+
{formatRelativeAge(smartCollection.recentActivityAt)}
|
|
409
|
+
</time>
|
|
410
|
+
<CollectionFeedLink
|
|
411
|
+
href={
|
|
412
|
+
feedsEnabled
|
|
413
|
+
? `${toPublicPath(getCollectionPagePath(smartCollection.slug), basePath)}/feed`
|
|
414
|
+
: null
|
|
415
|
+
}
|
|
416
|
+
title={feedTitle}
|
|
417
|
+
/>
|
|
418
|
+
</p>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
257
424
|
const collection = item.collection;
|
|
258
425
|
if (!collection) return null;
|
|
259
426
|
const sequence = sequenceLabels[index];
|
|
@@ -268,7 +435,7 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
|
|
|
268
435
|
<a
|
|
269
436
|
href={toPublicPath(
|
|
270
437
|
getCollectionSelectionPath(collection.slug),
|
|
271
|
-
|
|
438
|
+
basePath,
|
|
272
439
|
)}
|
|
273
440
|
class="collection-directory-title-link"
|
|
274
441
|
>
|
|
@@ -316,6 +483,14 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
|
|
|
316
483
|
>
|
|
317
484
|
{formatRelativeAge(collection.recentActivityAt)}
|
|
318
485
|
</time>
|
|
486
|
+
<CollectionFeedLink
|
|
487
|
+
href={
|
|
488
|
+
feedsEnabled
|
|
489
|
+
? `${toPublicPath(getCollectionSelectionPath(collection.slug), basePath)}/feed`
|
|
490
|
+
: null
|
|
491
|
+
}
|
|
492
|
+
title={feedTitle}
|
|
493
|
+
/>
|
|
319
494
|
</p>
|
|
320
495
|
</div>
|
|
321
496
|
</div>
|