@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
package/src/ui/feed/LinkCard.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Link Card
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The link reference (domain, title, preview) comes first, then the author's
|
|
5
|
+
* commentary as first-class prose (same level as a Note body) — the reader
|
|
6
|
+
* sees what is being referenced before what is said about it. Feed, compact
|
|
7
|
+
* and detail differ only in heading level, never in that order.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { FC } from "hono/jsx";
|
|
10
11
|
import type { TimelineCardProps } from "../../types.js";
|
|
12
|
+
import { EmptyPostContent } from "../shared/EmptyPostContent.js";
|
|
11
13
|
import { StarRating } from "../shared/StarRating.js";
|
|
12
14
|
import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
|
|
13
15
|
import { PostFooter } from "../shared/PostFooter.js";
|
|
@@ -32,6 +34,14 @@ export const LinkCard: FC<TimelineCardProps> = ({
|
|
|
32
34
|
const safeUrl = post.url ? sanitizeUrl(post.url) : "";
|
|
33
35
|
const domain = safeUrl ? extractDisplayDomain(safeUrl) : null;
|
|
34
36
|
|
|
37
|
+
/* The h-entry's own `u-url` is the permalink, emitted once by PostFooter.
|
|
38
|
+
The title points at the referenced article instead, which is what
|
|
39
|
+
`u-bookmark-of` means — and only when there is one: without a link the
|
|
40
|
+
href falls back to the permalink, and a post cannot bookmark itself. */
|
|
41
|
+
const titleLinkClass = safeUrl
|
|
42
|
+
? "u-bookmark-of feed-link-title-link"
|
|
43
|
+
: "feed-link-title-link";
|
|
44
|
+
|
|
35
45
|
const domainEl =
|
|
36
46
|
domain &&
|
|
37
47
|
(safeUrl ? (
|
|
@@ -51,6 +61,33 @@ export const LinkCard: FC<TimelineCardProps> = ({
|
|
|
51
61
|
</div>
|
|
52
62
|
));
|
|
53
63
|
|
|
64
|
+
const titleLinkEl = post.title && (
|
|
65
|
+
<a
|
|
66
|
+
href={safeUrl || post.permalink}
|
|
67
|
+
class={titleLinkClass}
|
|
68
|
+
target={safeUrl ? "_blank" : undefined}
|
|
69
|
+
rel={safeUrl ? "noopener noreferrer" : undefined}
|
|
70
|
+
>
|
|
71
|
+
{post.title}
|
|
72
|
+
</a>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/* Detail promotes the title to the page heading and carries the rating with
|
|
76
|
+
it; feed and compact keep it a card heading. Only the wrapper differs —
|
|
77
|
+
the link itself, and where the title sits in the reference, do not. */
|
|
78
|
+
const titleEl =
|
|
79
|
+
post.title &&
|
|
80
|
+
(isDetail ? (
|
|
81
|
+
<div class="post-header-block">
|
|
82
|
+
<h1 class="p-name post-detail-title feed-link-title">{titleLinkEl}</h1>
|
|
83
|
+
{showHeaderRating && <StarRating rating={post.rating} />}
|
|
84
|
+
</div>
|
|
85
|
+
) : (
|
|
86
|
+
<h2 class={`p-name feed-link-title ${isCompact ? "text-sm" : ""}`}>
|
|
87
|
+
{titleLinkEl}
|
|
88
|
+
</h2>
|
|
89
|
+
));
|
|
90
|
+
|
|
54
91
|
const previewEl = !isCompact && post.previewImageUrl && (
|
|
55
92
|
<LinkPreview
|
|
56
93
|
imageUrl={post.previewImageUrl}
|
|
@@ -68,6 +105,11 @@ export const LinkCard: FC<TimelineCardProps> = ({
|
|
|
68
105
|
/>
|
|
69
106
|
);
|
|
70
107
|
|
|
108
|
+
/* No title and no commentary leaves the entry with neither `p-name` nor
|
|
109
|
+
`e-content`, which is the shape a parser fills in by guessing. The title
|
|
110
|
+
is the `p-name`, so this only ever renders when there is none. */
|
|
111
|
+
const emptyContentEl = !post.title && !bodyEl && <EmptyPostContent />;
|
|
112
|
+
|
|
71
113
|
const mediaEl = !isCompact && post.media.length > 0 && (
|
|
72
114
|
<div class="mt-3" data-post-media>
|
|
73
115
|
<MediaGallery attachments={post.media} postPermalink={post.permalink} />
|
|
@@ -78,22 +120,14 @@ export const LinkCard: FC<TimelineCardProps> = ({
|
|
|
78
120
|
<StarRating rating={post.rating} />
|
|
79
121
|
);
|
|
80
122
|
|
|
81
|
-
/* -- Link reference: domain + title + preview
|
|
123
|
+
/* -- Link reference: domain + title + preview, in that order in every mode.
|
|
124
|
+
The preview is part of what is being referenced, so the author's
|
|
125
|
+
commentary always follows it — a detail page that led with the body read
|
|
126
|
+
as a comment on something the reader had not seen yet. -- */
|
|
82
127
|
const linkRef = (
|
|
83
128
|
<>
|
|
84
129
|
{domainEl}
|
|
85
|
-
{
|
|
86
|
-
<h2 class={`p-name feed-link-title ${isCompact ? "text-sm" : ""}`}>
|
|
87
|
-
<a
|
|
88
|
-
href={safeUrl || post.permalink}
|
|
89
|
-
class="u-url feed-link-title-link"
|
|
90
|
-
target={safeUrl ? "_blank" : undefined}
|
|
91
|
-
rel={safeUrl ? "noopener noreferrer" : undefined}
|
|
92
|
-
>
|
|
93
|
-
{post.title}
|
|
94
|
-
</a>
|
|
95
|
-
</h2>
|
|
96
|
-
)}
|
|
130
|
+
{titleEl}
|
|
97
131
|
{previewEl}
|
|
98
132
|
</>
|
|
99
133
|
);
|
|
@@ -104,39 +138,15 @@ export const LinkCard: FC<TimelineCardProps> = ({
|
|
|
104
138
|
{...(isDetail ? { "data-page": "post" } : {})}
|
|
105
139
|
{...getPostArticleAttributes(post)}
|
|
106
140
|
>
|
|
107
|
-
{!isCompact && !display?.hideStatusBadges &&
|
|
108
|
-
|
|
109
|
-
<>
|
|
110
|
-
{domainEl}
|
|
111
|
-
{post.title && (
|
|
112
|
-
<div class="post-header-block">
|
|
113
|
-
<h1 class="p-name post-detail-title feed-link-title">
|
|
114
|
-
<a
|
|
115
|
-
href={safeUrl || post.permalink}
|
|
116
|
-
class="u-url feed-link-title-link"
|
|
117
|
-
target={safeUrl ? "_blank" : undefined}
|
|
118
|
-
rel={safeUrl ? "noopener noreferrer" : undefined}
|
|
119
|
-
>
|
|
120
|
-
{post.title}
|
|
121
|
-
</a>
|
|
122
|
-
</h1>
|
|
123
|
-
{showHeaderRating && <StarRating rating={post.rating} />}
|
|
124
|
-
</div>
|
|
125
|
-
)}
|
|
126
|
-
{bodyEl}
|
|
127
|
-
{previewEl}
|
|
128
|
-
</>
|
|
129
|
-
) : isCompact ? (
|
|
130
|
-
linkRef
|
|
131
|
-
) : (
|
|
132
|
-
<>
|
|
133
|
-
{linkRef}
|
|
134
|
-
{bodyEl}
|
|
135
|
-
</>
|
|
141
|
+
{!isCompact && !display?.hideStatusBadges && (
|
|
142
|
+
<PostStatusBadges post={post} />
|
|
136
143
|
)}
|
|
144
|
+
{linkRef}
|
|
145
|
+
{bodyEl}
|
|
137
146
|
{(() => {
|
|
138
147
|
const tail = (
|
|
139
148
|
<>
|
|
149
|
+
{emptyContentEl}
|
|
140
150
|
{mediaEl}
|
|
141
151
|
{ratingEl}
|
|
142
152
|
<PostFooter
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
+
import { getLinkPreviewProviderLabel } from "../../lib/link-preview.js";
|
|
9
10
|
import { Icon } from "../shared/Icon.js";
|
|
10
11
|
|
|
11
12
|
interface LinkPreviewProps {
|
|
@@ -15,12 +16,6 @@ interface LinkPreviewProps {
|
|
|
15
16
|
provider?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const PROVIDER_LABELS: Record<string, string> = {
|
|
19
|
-
youtube: "YouTube",
|
|
20
|
-
vimeo: "Vimeo",
|
|
21
|
-
bilibili: "Bilibili",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
19
|
export const LinkPreview: FC<LinkPreviewProps> = ({
|
|
25
20
|
imageUrl,
|
|
26
21
|
linkUrl,
|
|
@@ -28,7 +23,7 @@ export const LinkPreview: FC<LinkPreviewProps> = ({
|
|
|
28
23
|
provider,
|
|
29
24
|
}) => {
|
|
30
25
|
const isVideo = kind === "video";
|
|
31
|
-
const providerLabel = provider
|
|
26
|
+
const providerLabel = getLinkPreviewProviderLabel(provider);
|
|
32
27
|
|
|
33
28
|
return (
|
|
34
29
|
<a
|
package/src/ui/feed/NoteCard.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { msg } from "@lingui/core/macro";
|
|
|
9
9
|
import type { FC } from "hono/jsx";
|
|
10
10
|
import { useLingui } from "../../i18n/context.js";
|
|
11
11
|
import type { TimelineCardProps } from "../../types.js";
|
|
12
|
+
import { EmptyPostContent } from "../shared/EmptyPostContent.js";
|
|
12
13
|
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
13
14
|
import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
|
|
14
15
|
import { StarRating } from "../shared/StarRating.js";
|
|
@@ -89,7 +90,9 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
89
90
|
{...(isDetail ? { "data-page": "post" } : {})}
|
|
90
91
|
{...getPostArticleAttributes(post)}
|
|
91
92
|
>
|
|
92
|
-
{!isCompact && !display?.hideStatusBadges &&
|
|
93
|
+
{!isCompact && !display?.hideStatusBadges && (
|
|
94
|
+
<PostStatusBadges post={post} />
|
|
95
|
+
)}
|
|
93
96
|
{isArticle &&
|
|
94
97
|
(isDetail ? (
|
|
95
98
|
<div class="post-header-block post-header-block-detail">
|
|
@@ -104,20 +107,22 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
104
107
|
</div>
|
|
105
108
|
) : (
|
|
106
109
|
<h2
|
|
107
|
-
class={`p-name ${isCompact ? "text-sm mb-1" : "feed-note-title"}`}
|
|
110
|
+
class={`p-name ${isCompact ? "feed-compact-title text-sm mb-1" : "feed-note-title"}`}
|
|
108
111
|
>
|
|
109
|
-
<a href={post.permalink} class="
|
|
112
|
+
<a href={post.permalink} class="hover:underline">
|
|
110
113
|
{post.title}
|
|
111
114
|
</a>
|
|
112
115
|
</h2>
|
|
113
116
|
))}
|
|
114
|
-
{displayHtml
|
|
117
|
+
{displayHtml ? (
|
|
115
118
|
<div
|
|
116
119
|
class={`e-content prose ${isCompact ? "prose-sm" : isDetail || showFullBody ? "post-detail-body" : isArticle ? "post-body-summary" : ""}`}
|
|
117
120
|
data-post-body
|
|
118
121
|
{...(clampNote ? { "data-note-clamp": "" } : {})}
|
|
119
122
|
dangerouslySetInnerHTML={{ __html: displayHtml }}
|
|
120
123
|
/>
|
|
124
|
+
) : (
|
|
125
|
+
!isArticle && <EmptyPostContent />
|
|
121
126
|
)}
|
|
122
127
|
{(() => {
|
|
123
128
|
const tail = (
|
|
@@ -2,30 +2,116 @@
|
|
|
2
2
|
* Post Status Badges
|
|
3
3
|
*
|
|
4
4
|
* Renders top-of-card status indicators that should stay visually prominent.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Visibility is driven by CSS selectors on the parent article's data
|
|
6
|
+
* attributes, so for the signed-in author every badge ships in the DOM whether
|
|
7
|
+
* or not it currently applies: the post menu toggles `data-post-pinned` and
|
|
8
|
+
* friends on the article, and the matching badge appears without a reload.
|
|
9
|
+
*
|
|
10
|
+
* An anonymous reader has no menu and cannot change any of that, and never
|
|
11
|
+
* receives a draft or private post to begin with, so the same card would carry
|
|
12
|
+
* four permanently invisible badges. There we emit only the badges that
|
|
13
|
+
* actually apply — usually none at all. Featured is rendered in the footer
|
|
14
|
+
* meta instead.
|
|
9
15
|
*/
|
|
10
16
|
|
|
17
|
+
import { msg } from "@lingui/core/macro";
|
|
11
18
|
import type { FC } from "hono/jsx";
|
|
19
|
+
import { useLingui } from "../../i18n/context.js";
|
|
20
|
+
import { useViewer } from "../../lib/viewer-context.js";
|
|
21
|
+
import type { PostView } from "../../types.js";
|
|
12
22
|
import { Icon } from "../shared/Icon.js";
|
|
13
23
|
|
|
14
|
-
|
|
24
|
+
type PostStatusBadgeSource = Pick<
|
|
25
|
+
PostView,
|
|
26
|
+
"status" | "visibility" | "pinned" | "pinnedInCollection"
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
export const PostStatusBadges: FC<{ post: PostStatusBadgeSource }> = ({
|
|
30
|
+
post,
|
|
31
|
+
}) => {
|
|
32
|
+
const { i18n } = useLingui();
|
|
33
|
+
const { isAuthor } = useViewer();
|
|
34
|
+
|
|
35
|
+
const isPinned = post.pinned;
|
|
36
|
+
const isPinnedInCollection = Boolean(post.pinnedInCollection);
|
|
37
|
+
const isPrivate = post.visibility === "private";
|
|
38
|
+
const isDraft = post.status === "draft";
|
|
39
|
+
|
|
40
|
+
// The author's card keeps every badge so the menu can reveal one instantly.
|
|
41
|
+
// A reader's card carries only what is true right now, and nothing when the
|
|
42
|
+
// post has no status to announce.
|
|
43
|
+
if (
|
|
44
|
+
!isAuthor &&
|
|
45
|
+
!isPinned &&
|
|
46
|
+
!isPinnedInCollection &&
|
|
47
|
+
!isPrivate &&
|
|
48
|
+
!isDraft
|
|
49
|
+
) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const showPinned = isAuthor || isPinned;
|
|
54
|
+
const showPinnedInCollection = isAuthor || isPinnedInCollection;
|
|
55
|
+
const showPrivate = isAuthor || isPrivate;
|
|
56
|
+
const showDraft = isAuthor || isDraft;
|
|
57
|
+
|
|
58
|
+
const pinnedLabel = i18n._(
|
|
59
|
+
msg({
|
|
60
|
+
message: "Pinned",
|
|
61
|
+
comment: "@context: Post status badge for a pinned post",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
15
65
|
return (
|
|
16
66
|
<div class="post-status-badges">
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
67
|
+
{showPinned && (
|
|
68
|
+
<span class="post-status-badge post-status-pinned">
|
|
69
|
+
<Icon name="post-status-pin" />
|
|
70
|
+
{pinnedLabel}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
{showPinnedInCollection && (
|
|
74
|
+
<span class="post-status-badge post-status-pinned-in-collection">
|
|
75
|
+
<Icon name="post-status-pin" />
|
|
76
|
+
{pinnedLabel}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
{showPrivate && (
|
|
80
|
+
<span class="post-status-badge post-status-private">
|
|
81
|
+
<Icon name="post-status-private" />
|
|
82
|
+
{i18n._(
|
|
83
|
+
msg({
|
|
84
|
+
message: "Private",
|
|
85
|
+
comment: "@context: Post status badge for a private post",
|
|
86
|
+
}),
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
{/* The one badge that is also a control: a draft's most likely next
|
|
91
|
+
action is "finish it", so the label itself opens the editor. */}
|
|
92
|
+
{showDraft && (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="post-status-badge post-status-draft"
|
|
96
|
+
data-draft-continue
|
|
97
|
+
title={i18n._(
|
|
98
|
+
msg({
|
|
99
|
+
message: "Continue writing this draft",
|
|
100
|
+
comment:
|
|
101
|
+
"@context: Tooltip on the draft badge, which opens the editor",
|
|
102
|
+
}),
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
<Icon name="post-status-draft" />
|
|
106
|
+
{i18n._(
|
|
107
|
+
msg({
|
|
108
|
+
message: "Draft",
|
|
109
|
+
comment:
|
|
110
|
+
"@context: Post status badge for an unpublished draft, shown to the author only",
|
|
111
|
+
}),
|
|
112
|
+
)}
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
29
115
|
</div>
|
|
30
116
|
);
|
|
31
117
|
};
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Left-border accent blockquote with full date in footer.
|
|
5
5
|
*
|
|
6
|
-
* Fields:
|
|
7
|
-
* - quoteText: the quoted text
|
|
8
|
-
* - title: attribution (who said it)
|
|
9
|
-
* - url: source link
|
|
10
|
-
* - bodyHtml: commentary
|
|
6
|
+
* Fields, and the microformats2 property each maps to:
|
|
7
|
+
* - quoteText: the quoted text — the h-cite's `p-content`
|
|
8
|
+
* - title: attribution (who said it) — the h-cite's `p-name`
|
|
9
|
+
* - url: source link — the h-cite's `u-url`
|
|
10
|
+
* - bodyHtml: commentary — the h-entry's `e-content`
|
|
11
|
+
*
|
|
12
|
+
* `p-name` for the attribution is deliberately loose: the field may hold a
|
|
13
|
+
* person, a work, or a site, and a person would strictly be a `p-author` with
|
|
14
|
+
* a nested h-card. `p-name` covers all three without inventing that h-card.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import type { FC } from "hono/jsx";
|
|
@@ -17,6 +21,7 @@ import { PostFooter } from "../shared/PostFooter.js";
|
|
|
17
21
|
import { PostStatusBadges } from "./PostStatusBadges.js";
|
|
18
22
|
import { sanitizeUrl, extractDisplayDomain } from "../../lib/url.js";
|
|
19
23
|
import { DecorativeQuoteMark } from "../shared/DecorativeQuoteMark.js";
|
|
24
|
+
import { EmptyPostContent } from "../shared/EmptyPostContent.js";
|
|
20
25
|
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
21
26
|
import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
|
|
22
27
|
|
|
@@ -30,6 +35,9 @@ export const QuoteCard: FC<TimelineCardProps> = ({
|
|
|
30
35
|
const articleClass = `h-entry post-menu-target${isCompact ? " feed-compact" : isDetail ? " py-6" : " feed-quote-post"}`;
|
|
31
36
|
const safeUrl = post.url ? sanitizeUrl(post.url) : "";
|
|
32
37
|
const commentaryHtml = post.bodyHtml ?? null;
|
|
38
|
+
const showCite =
|
|
39
|
+
!!post.quoteText || (!isCompact && !!(post.title || safeUrl));
|
|
40
|
+
const showCommentary = !isCompact && !!commentaryHtml;
|
|
33
41
|
|
|
34
42
|
return (
|
|
35
43
|
<article
|
|
@@ -37,36 +45,53 @@ export const QuoteCard: FC<TimelineCardProps> = ({
|
|
|
37
45
|
{...(isDetail ? { "data-page": "post" } : {})}
|
|
38
46
|
{...getPostArticleAttributes(post)}
|
|
39
47
|
>
|
|
40
|
-
{!isCompact && !display?.hideStatusBadges &&
|
|
41
|
-
|
|
42
|
-
<blockquote class={`feed-quote${isCompact ? "" : " feed-quote-card"}`}>
|
|
43
|
-
{!isCompact && <DecorativeQuoteMark class="feed-quote-mark" />}
|
|
44
|
-
<div
|
|
45
|
-
class={`e-content feed-quote-content${isCompact ? " text-sm" : isDetail ? " post-detail-quote" : ""}`}
|
|
46
|
-
>
|
|
47
|
-
{post.quoteText}
|
|
48
|
-
</div>
|
|
49
|
-
</blockquote>
|
|
48
|
+
{!isCompact && !display?.hideStatusBadges && (
|
|
49
|
+
<PostStatusBadges post={post} />
|
|
50
50
|
)}
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
{showCite && (
|
|
52
|
+
/* The h-cite spans both the quoted text and its attribution, because
|
|
53
|
+
mf2 properties bind to descendants: a `u-url` on the source link
|
|
54
|
+
left outside would attach to the h-entry and shadow the permalink,
|
|
55
|
+
and `p-name` would become this post's own name. `<figure>` with a
|
|
56
|
+
`<figcaption>` is also how HTML pairs a blockquote with its source. */
|
|
57
|
+
<figure class="feed-quote-cite h-cite">
|
|
58
|
+
{post.quoteText && (
|
|
59
|
+
<blockquote
|
|
60
|
+
class={`feed-quote${isCompact ? "" : " feed-quote-card"}`}
|
|
59
61
|
>
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
{!isCompact && <DecorativeQuoteMark class="feed-quote-mark" />}
|
|
63
|
+
<div
|
|
64
|
+
class={`p-content feed-quote-content${isCompact ? " text-sm" : isDetail ? " post-detail-quote" : ""}`}
|
|
65
|
+
>
|
|
66
|
+
{post.quoteText}
|
|
67
|
+
</div>
|
|
68
|
+
</blockquote>
|
|
69
|
+
)}
|
|
70
|
+
{!isCompact && (post.title || safeUrl) && (
|
|
71
|
+
<figcaption class="feed-quote-attribution">
|
|
72
|
+
{safeUrl ? (
|
|
73
|
+
<a
|
|
74
|
+
href={safeUrl}
|
|
75
|
+
class="u-url p-name feed-quote-source"
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener noreferrer"
|
|
78
|
+
>
|
|
79
|
+
{post.title || extractDisplayDomain(safeUrl) || safeUrl}
|
|
80
|
+
</a>
|
|
81
|
+
) : (
|
|
82
|
+
<span class="p-name">{post.title}</span>
|
|
83
|
+
)}
|
|
84
|
+
</figcaption>
|
|
64
85
|
)}
|
|
65
|
-
</
|
|
86
|
+
</figure>
|
|
66
87
|
)}
|
|
67
|
-
{
|
|
88
|
+
{/* Nothing quoted and nothing said leaves the entry with no `p-name`,
|
|
89
|
+
no `e-content`, and no nested h-cite — the shape a parser fills in by
|
|
90
|
+
guessing. */}
|
|
91
|
+
{!showCite && !showCommentary && <EmptyPostContent />}
|
|
92
|
+
{showCommentary && (
|
|
68
93
|
<div
|
|
69
|
-
class={`feed-quote-commentary prose${isDetail ? " post-detail-body" : " text-muted-foreground"}`}
|
|
94
|
+
class={`e-content feed-quote-commentary prose${isDetail ? " post-detail-body" : " text-muted-foreground"}`}
|
|
70
95
|
data-post-body
|
|
71
96
|
dangerouslySetInnerHTML={{ __html: commentaryHtml }}
|
|
72
97
|
/>
|
|
@@ -36,6 +36,7 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
36
36
|
leadingReplies,
|
|
37
37
|
trailingReplies,
|
|
38
38
|
latestReply,
|
|
39
|
+
gapHref,
|
|
39
40
|
totalReplyCount,
|
|
40
41
|
}) => {
|
|
41
42
|
const { i18n } = useLingui();
|
|
@@ -81,7 +82,10 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
81
82
|
});
|
|
82
83
|
const renderedLeadingReplies = dedupeReplies(leadingReplies);
|
|
83
84
|
const renderedTrailingReplies = dedupeReplies(trailingReplies);
|
|
84
|
-
|
|
85
|
+
// The gap opens the first post it hides — the rule in `lib/thread-fold.ts`,
|
|
86
|
+
// which the feed follows too. The fallback only covers a gap target that
|
|
87
|
+
// went unpublished between the count and the fetch.
|
|
88
|
+
const gapLinkHref = gapHref ?? latestReply.permalink;
|
|
85
89
|
|
|
86
90
|
// Always render the collapsible shell + toggle: the cap and fade are a
|
|
87
91
|
// constant "this is context" affordance. The toggle's *initial* visibility
|
|
@@ -116,7 +120,7 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
116
120
|
const gapItem =
|
|
117
121
|
hiddenCount > 0 ? (
|
|
118
122
|
<div class="thread-item thread-item-gap">
|
|
119
|
-
<a href={
|
|
123
|
+
<a href={gapLinkHref} class="thread-gap-link">
|
|
120
124
|
{hiddenPostsLabel}
|
|
121
125
|
</a>
|
|
122
126
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import type { Context } from "hono";
|
|
3
2
|
import { renderToString } from "hono/jsx/dom/server";
|
|
4
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { readComponentCss } from "../../../__tests__/helpers/component-css.js";
|
|
5
5
|
import { I18nProvider } from "../../../i18n/context.js";
|
|
6
6
|
import { createI18n } from "../../../i18n/i18n.js";
|
|
7
7
|
import type { PostView, TimelineItemView } from "../../../types.js";
|
|
@@ -36,8 +36,7 @@ function createPostView(overrides: Partial<PostView> = {}): PostView {
|
|
|
36
36
|
|
|
37
37
|
function renderWithI18n(
|
|
38
38
|
render: () =>
|
|
39
|
-
| ReturnType<typeof
|
|
40
|
-
| ReturnType<typeof CuratedThreadPreview>,
|
|
39
|
+
ReturnType<typeof ThreadPreview> | ReturnType<typeof CuratedThreadPreview>,
|
|
41
40
|
) {
|
|
42
41
|
const i18n = createI18n("en");
|
|
43
42
|
const c = {
|
|
@@ -157,10 +156,7 @@ describe("getThreadPreviewState", () => {
|
|
|
157
156
|
});
|
|
158
157
|
|
|
159
158
|
it("keeps thread preview items shrinkable within the grid track", () => {
|
|
160
|
-
const css =
|
|
161
|
-
new URL("../../../styles/ui.css", import.meta.url),
|
|
162
|
-
"utf8",
|
|
163
|
-
);
|
|
159
|
+
const css = readComponentCss();
|
|
164
160
|
|
|
165
161
|
expect(css).toMatch(
|
|
166
162
|
/\.thread-item\s*\{[\s\S]*min-width:\s*0;[\s\S]*max-width:\s*100%;/,
|
|
@@ -168,17 +164,14 @@ describe("getThreadPreviewState", () => {
|
|
|
168
164
|
});
|
|
169
165
|
|
|
170
166
|
it("adds extra mobile inset before the thread rail reaches the viewport edge", () => {
|
|
171
|
-
const css =
|
|
172
|
-
new URL("../../../styles/ui.css", import.meta.url),
|
|
173
|
-
"utf8",
|
|
174
|
-
);
|
|
167
|
+
const css = readComponentCss();
|
|
175
168
|
|
|
176
169
|
expect(css).toMatch(
|
|
177
|
-
/@media\s*\(max-width:\s*
|
|
170
|
+
/@media\s*\(max-width:\s*699px\)\s*\{[\s\S]*\.thread-group-preview,\s*\.thread-group-detail\s*\{[\s\S]*--site-thread-rail-indent:\s*8px;[\s\S]*--site-thread-rail-line-left:\s*-11px;/,
|
|
178
171
|
);
|
|
179
172
|
// dot-left is no longer hardcoded — derived via calc() from line-left
|
|
180
173
|
expect(css).not.toMatch(
|
|
181
|
-
/@media\s*\(max-width:\s*
|
|
174
|
+
/@media\s*\(max-width:\s*699px\)\s*\{[\s\S]*--site-thread-rail-dot-left:/,
|
|
182
175
|
);
|
|
183
176
|
});
|
|
184
177
|
|
|
@@ -400,7 +393,7 @@ describe("getThreadPreviewState", () => {
|
|
|
400
393
|
expect(toggleTag(html)).not.toContain("hidden");
|
|
401
394
|
});
|
|
402
395
|
|
|
403
|
-
it("points the hidden-posts gap link
|
|
396
|
+
it("points the hidden-posts gap link at the first post it hides", () => {
|
|
404
397
|
const html = renderWithI18n(() =>
|
|
405
398
|
ThreadPreview({
|
|
406
399
|
rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),
|
|
@@ -427,16 +420,20 @@ describe("getThreadPreviewState", () => {
|
|
|
427
420
|
bodyHtml: "<p>Latest</p>",
|
|
428
421
|
isLastInThread: true,
|
|
429
422
|
}),
|
|
423
|
+
gapHref: "/post-3",
|
|
430
424
|
totalReplyCount: 4,
|
|
431
425
|
}),
|
|
432
426
|
);
|
|
433
427
|
|
|
434
428
|
expect(html).toMatch(
|
|
435
|
-
/<a[^>]*\bhref="\/post-
|
|
429
|
+
/<a[^>]*\bhref="\/post-3"[^>]*\bclass="thread-gap-link"|<a[^>]*\bclass="thread-gap-link"[^>]*\bhref="\/post-3"/,
|
|
436
430
|
);
|
|
437
431
|
});
|
|
438
432
|
|
|
439
|
-
|
|
433
|
+
// The gap target comes from the service, one rank past the leading window.
|
|
434
|
+
// It can be missing where the count and the fetch disagree — a reply
|
|
435
|
+
// unpublished between them — and the link still has to go somewhere.
|
|
436
|
+
it("falls back to the latest reply when no gap target arrives", () => {
|
|
440
437
|
const html = renderWithI18n(() =>
|
|
441
438
|
ThreadPreview({
|
|
442
439
|
rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),
|